pax_global_header00006660000000000000000000000064151254046170014517gustar00rootroot0000000000000052 comment=92bfe5d930c07dd4672b148f811305aa294d6e6f gbdev-rgbds-92bfe5d/000077500000000000000000000000001512540461700144075ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/.clang-format000066400000000000000000000056731512540461700167750ustar00rootroot00000000000000AccessModifierOffset: -4 AlignAfterOpenBracket: BlockIndent AlignArrayOfStructures: Left AlignConsecutiveAssignments: None AlignConsecutiveBitFields: Consecutive AlignConsecutiveDeclarations: None AlignConsecutiveMacros: Consecutive AlignEscapedNewlines: DontAlign AlignOperands: Align AlignTrailingComments: true AllowShortBlocksOnASingleLine: Empty AllowShortCaseLabelsOnASingleLine: false AllowShortEnumsOnASingleLine: true AllowShortFunctionsOnASingleLine: InlineOnly AllowShortIfStatementsOnASingleLine: Never AllowShortLambdasOnASingleLine: All AllowShortLoopsOnASingleLine: false AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: Yes AttributeMacros: - format_ - attr_ BinPackArguments: false BinPackParameters: false BitFieldColonSpacing: Both BreakAfterAttributes: Always BreakBeforeBinaryOperators: NonAssignment BreakBeforeBraces: Attach BreakBeforeConceptDeclarations: true BreakBeforeTernaryOperators: true BreakConstructorInitializers: BeforeColon BreakInheritanceList: AfterComma BreakStringLiterals: true ColumnLimit: 100 CompactNamespaces: false ContinuationIndentWidth: 4 Cpp11BracedListStyle: true DeriveLineEnding: true DerivePointerAlignment: false EmptyLineBeforeAccessModifier: Leave FixNamespaceComments: false IncludeBlocks: Regroup IncludeCategories: - Regex: '^&2 exit 1 fi ## Extract sources and patch them tar -xvf libpng-$pngver.tar.xz ## Start building! mkdir -p build cd build ../libpng-$pngver/configure --disable-shared --enable-static \ CFLAGS="-O3 -flto -DNDEBUG -mmacosx-version-min=10.9 -arch x86_64 -arch arm64 -fno-exceptions" make -kj make install prefix="$PWD/../libpng-staging" gbdev-rgbds-92bfe5d/.github/scripts/get_win_deps.ps1000066400000000000000000000020241512540461700225300ustar00rootroot00000000000000function getlibrary ([string] $URI, [string] $filename, [string] $hash, [string] $destdir) { $wc = New-Object Net.WebClient [string] $downloadhash = $null try { $wc.DownloadFile($URI, $filename) $downloadhash = $(Get-FileHash $filename -Algorithm SHA256).Hash } catch { Write-Host "${filename}: failed to download" exit 1 } if ($hash -ne $downloadhash) { Write-Host "${filename}: SHA256 mismatch ($downloadhash)" exit 1 } Expand-Archive -DestinationPath $destdir $filename } getlibrary 'https://www.zlib.net/zlib131.zip' 'zlib.zip' '72af66d44fcc14c22013b46b814d5d2514673dda3d115e64b690c1ad636e7b17' . getlibrary 'https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.53.zip' 'libpng.zip' '9fb99118ec4523d9a9dab652ce7c2472ec76f6ccd69d1aba3ab873bb8cf84b98' . getlibrary 'https://github.com/lexxmark/winflexbison/releases/download/v2.5.25/win_flex_bison-2.5.25.zip' 'winflexbison.zip' '8d324b62be33604b2c45ad1dd34ab93d722534448f55a16ca7292de32b6ac135' install_dir Move-Item zlib-1.3.1 zlib Move-Item libpng-1.6.53 libpng gbdev-rgbds-92bfe5d/.github/scripts/install.sh000077500000000000000000000006331512540461700214450ustar00rootroot00000000000000#!/usr/bin/env bash install -d /usr/local/bin/ /usr/local/share/man/man1/ /usr/local/share/man/man5/ /usr/local/share/man/man7/ install -s -m 755 rgbasm rgblink rgbfix rgbgfx /usr/local/bin/ install -m 644 rgbasm.1 rgblink.1 rgbfix.1 rgbgfx.1 /usr/local/share/man/man1/ install -m 644 rgbds.5 rgbasm.5 rgblink.5 rgbasm-old.5 /usr/local/share/man/man5/ install -m 644 rgbds.7 gbz80.7 /usr/local/share/man/man7/ gbdev-rgbds-92bfe5d/.github/scripts/install_deps.sh000077500000000000000000000011511512540461700224540ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail case "${1%-*}" in ubuntu) sudo apt-get -qq update sudo apt-get install -yq bison libpng-dev pkg-config ;; macos) brew install bison sha2 md5sha1sum # Export `bison` to allow using the version we install from Homebrew, # instead of the outdated one preinstalled on macOS (which doesn't even support `-Wall`...) export PATH="/opt/homebrew/opt/bison/bin:$PATH" printf 'PATH=%s\n' "$PATH" >>"$GITHUB_ENV" # Make it available to later CI steps too ;; *) echo "WARNING: Cannot install deps for OS '$1'" ;; esac bison --version make --version cmake --version gbdev-rgbds-92bfe5d/.github/scripts/mingw-w64-libpng-dev.sh000077500000000000000000000013231512540461700235600ustar00rootroot00000000000000#!/bin/bash set -euo pipefail pngver=1.6.53 arch="$1" ## Grab sources and check them wget http://downloads.sourceforge.net/project/libpng/libpng16/$pngver/libpng-$pngver.tar.xz echo 1d3fb8ccc2932d04aa3663e22ef5ef490244370f4e568d7850165068778d98d4 libpng-$pngver.tar.xz | sha256sum -c - ## Extract sources and patch them tar -xf libpng-$pngver.tar.xz ## Start building! mkdir -p build cd build ../libpng-$pngver/configure \ --host="$arch" --target="$arch" \ --prefix="/usr/$arch" \ --enable-shared --disable-static \ CPPFLAGS="-D_FORTIFY_SOURCE=2" \ CFLAGS="-O2 -pipe -fno-plt -fno-exceptions --param=ssp-buffer-size=4" \ LDFLAGS="-Wl,-O1,--sort-common,--as-needed -fstack-protector" make -kj sudo make install gbdev-rgbds-92bfe5d/.github/workflows/000077500000000000000000000000001512540461700200045ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/.github/workflows/analysis.yml000066400000000000000000000010501512540461700223460ustar00rootroot00000000000000name: Static analysis on: - push - pull_request jobs: analysis: runs-on: ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@v4 - name: Install deps shell: bash run: | ./.github/scripts/install_deps.sh ubuntu-latest - name: Static analysis run: | # Silence warnings with too many false positives (https://stackoverflow.com/a/73913076) make -kj CXX=g++-14 CXXFLAGS="-fanalyzer -fanalyzer-verbosity=0 -Wno-analyzer-use-of-uninitialized-value -DNDEBUG" Q= gbdev-rgbds-92bfe5d/.github/workflows/build-container.yml000066400000000000000000000035761512540461700236210ustar00rootroot00000000000000name: Build container image on: push: branches: - master tags: - '*' jobs: publish-docker-image: if: github.repository_owner == 'gbdev' runs-on: ubuntu-latest permissions: packages: write steps: - name: Checkout repo uses: actions/checkout@v4 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push the master container image if: github.ref == 'refs/heads/master' run: | COMMIT_HASH=$(git rev-parse --short HEAD) sed -i "2i LABEL org.opencontainers.image.description=\"RGBDS container image, containing the git version master:$COMMIT_HASH\"" Dockerfile docker build . --tag ghcr.io/gbdev/rgbds:master docker push ghcr.io/gbdev/rgbds:master - name: Build and push the version-tagged container image if: startsWith(github.ref, 'refs/tags/') run: | TAG_NAME=${GITHUB_REF#refs/tags/} sed -i "2i LABEL org.opencontainers.image.description=\"RGBDS container image for the release version $TAG_NAME\"" Dockerfile docker build . --tag ghcr.io/gbdev/rgbds:$TAG_NAME docker tag ghcr.io/gbdev/rgbds:$TAG_NAME ghcr.io/gbdev/rgbds:latest docker push ghcr.io/gbdev/rgbds:$TAG_NAME docker push ghcr.io/gbdev/rgbds:latest - name: Delete untagged container images if: github.repository_owner == 'gbdev' uses: Chizkiyahu/delete-untagged-ghcr-action@v5 with: # Requires a personal access token with delete:packages permissions token: ${{ secrets.PAT_TOKEN }} package_name: 'rgbds' untagged_only: true except_untagged_multiplatform: true owner_type: 'org' gbdev-rgbds-92bfe5d/.github/workflows/checkdiff.yml000066400000000000000000000010771512540461700224420ustar00rootroot00000000000000name: Diff completeness check on: pull_request jobs: checkdiff: runs-on: ubuntu-latest steps: - name: Set up repo run: | git clone -b "${{ github.event.pull_request.head.ref }}" "${{ github.event.pull_request.head.repo.clone_url }}" rgbds cd rgbds git remote add upstream "${{ github.event.pull_request.base.repo.clone_url }}" git fetch upstream - name: Check diff working-directory: rgbds run: | make checkdiff "BASE_REF=${{ github.event.pull_request.base.sha }}" Q= | tee log gbdev-rgbds-92bfe5d/.github/workflows/checkformat.yml000066400000000000000000000003631512540461700230170ustar00rootroot00000000000000name: Code format checking on: pull_request jobs: checkformat: runs-on: ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@v4 - name: Check format run: | contrib/checkformat.bash gbdev-rgbds-92bfe5d/.github/workflows/coverage.yml000066400000000000000000000016711512540461700223270ustar00rootroot00000000000000name: Code coverage report on: - push - pull_request jobs: coverage: runs-on: ubuntu-24.04 steps: - name: Checkout repo uses: actions/checkout@v4 - name: Install deps shell: bash run: | ./.github/scripts/install_deps.sh ubuntu - name: Install LCOV run: | sudo apt-get install lcov - name: Install test dependency dependencies shell: bash run: | test/fetch-test-deps.sh --get-deps ubuntu - name: Generate coverage report run: | contrib/coverage.bash ubuntu-ci - name: Upload coverage report uses: actions/upload-artifact@v4 with: name: coverage-report # Workaround for keeping the top-level coverage/ directory # https://github.com/actions/upload-artifact/issues/174 path: | coverage dummy-file-to-keep-directory-structure.txt gbdev-rgbds-92bfe5d/.github/workflows/create-release-artifacts.yml000066400000000000000000000140011512540461700253620ustar00rootroot00000000000000name: Create release artifacts on: push: tags: - v[0-9]* jobs: windows: runs-on: windows-2022 strategy: matrix: bits: [32, 64] include: - bits: 32 arch: x86 platform: Win32 - bits: 64 arch: x86_x64 platform: x64 fail-fast: false steps: - name: Get version from tag shell: bash run: | # Turn "vX.Y.Z" into "X.Y.Z" VERSION="${{ github.ref_name }}" echo "version=${VERSION#v}" >> $GITHUB_ENV - name: Checkout repo uses: actions/checkout@v4 - name: Install deps run: .github/scripts/get_win_deps.ps1 - name: Check libraries cache id: cache uses: actions/cache@v4 with: path: | zbuild pngbuild key: ${{ matrix.arch }}-${{ hashFiles('zlib/**', 'libpng/**') }} - name: Build zlib if: steps.cache.outputs.cache-hit != 'true' run: | # BUILD_SHARED_LIBS causes the output DLL to be correctly called `zlib1.dll` cmake -S zlib -B zbuild -A ${{ matrix.platform }} -Wno-dev -DCMAKE_INSTALL_PREFIX=install_dir -DBUILD_SHARED_LIBS=ON cmake --build zbuild --config Release -j - name: Install zlib run: | cmake --install zbuild - name: Build libpng if: steps.cache.outputs.cache-hit != 'true' shell: bash run: | cmake -S libpng -B pngbuild -A ${{ matrix.platform }} -Wno-dev -DCMAKE_INSTALL_PREFIX=install_dir -DPNG_SHARED=ON -DPNG_STATIC=OFF -DPNG_TESTS=OFF cmake --build pngbuild --config Release -j - name: Install libpng run: | cmake --install pngbuild - name: Build Windows binaries shell: bash run: | cmake -S . -B build -A ${{ matrix.platform }} -DCMAKE_INSTALL_PREFIX=install_dir -DCMAKE_BUILD_TYPE=Release cmake --build build --config Release -j --verbose cmake --install build --verbose --prefix install_dir --strip - name: Package binaries run: | Compress-Archive -LiteralPath @("install_dir/bin/rgbasm.exe", "install_dir/bin/rgblink.exe", "install_dir/bin/rgbfix.exe", "install_dir/bin/rgbgfx.exe", "install_dir/bin/zlib1.dll", "install_dir/bin/libpng16.dll") "rgbds-win${{ matrix.bits }}.zip" - name: Upload Windows binaries uses: actions/upload-artifact@v4 with: name: win${{ matrix.bits }} path: rgbds-win${{ matrix.bits }}.zip macos: runs-on: macos-14 steps: - name: Get version from tag shell: bash run: | # Turn "refs/tags/vX.Y.Z" into "X.Y.Z" VERSION="${{ github.ref_name }}" echo "version=${VERSION#v}" >> $GITHUB_ENV - name: Checkout repo uses: actions/checkout@v4 - name: Install deps shell: bash run: | ./.github/scripts/install_deps.sh macos - name: Build libpng run: | ./.github/scripts/build_libpng.sh # We force linking libpng statically; the other libs are provided by macOS itself - name: Build binaries run: | make -kj CXXFLAGS="-O3 -flto -DNDEBUG -mmacosx-version-min=10.9 -arch x86_64 -arch arm64" PNGCFLAGS="-I libpng-staging/include" PNGLDLIBS="libpng-staging/lib/libpng.a -lz" Q= strip rgb{asm,link,fix,gfx} - name: Package binaries run: | zip --junk-paths rgbds-macos.zip rgb{asm,link,fix,gfx} man/* .github/scripts/install.sh - name: Upload macOS binaries uses: actions/upload-artifact@v4 with: name: macos path: rgbds-macos.zip linux: runs-on: ubuntu-22.04 # Oldest supported, for best glibc compatibility. steps: - name: Get version from tag shell: bash run: | # Turn "refs/tags/vX.Y.Z" into "X.Y.Z" VERSION="${{ github.ref_name }}" echo "version=${VERSION#v}" >> $GITHUB_ENV - name: Checkout repo uses: actions/checkout@v4 - name: Install deps shell: bash run: | ./.github/scripts/install_deps.sh ubuntu-22.04 - name: Build binaries run: | make -kj WARNFLAGS="-Wall -Wextra -pedantic -static" PKG_CONFIG="pkg-config --static" Q= strip rgb{asm,link,fix,gfx} - name: Package binaries run: | tar caf rgbds-linux-x86_64.tar.xz --transform='s#.*/##' rgb{asm,link,fix,gfx} man/* .github/scripts/install.sh - name: Upload Linux binaries uses: actions/upload-artifact@v4 with: name: linux path: rgbds-linux-x86_64.tar.xz release: runs-on: ubuntu-latest needs: [windows, macos, linux] permissions: contents: write steps: - name: Get version from tag shell: bash run: | # Turn "refs/tags/vX.Y.Z" into "X.Y.Z" VERSION="${{ github.ref_name }}" echo "version=${VERSION#v}" >> $GITHUB_ENV - name: Checkout repo uses: actions/checkout@v4 - name: Package sources run: | make dist Q= ls - name: Download Linux binaries uses: actions/download-artifact@v4 - name: Release uses: softprops/action-gh-release@v2 with: body: | Please ensure that the packages below work properly. Once that's done, replace this text with the changelog, un-draft the release, and update the `release` branch. By the way, if you forgot to update `include/version.hpp`, RGBASM's version test is going to fail in the tag's regression testing! (Use `git push --delete origin ` to delete it) draft: true # Don't publish the release quite yet... prerelease: ${{ contains(github.ref, '-rc') }} files: | win32/rgbds-win32.zip win64/rgbds-win64.zip macos/rgbds-macos.zip linux/rgbds-linux-x86_64.tar.xz rgbds-source.tar.gz fail_on_unmatched_files: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} gbdev-rgbds-92bfe5d/.github/workflows/create-release-docs.yml000066400000000000000000000032721512540461700243420ustar00rootroot00000000000000name: Create release docs on: release: types: - released # This avoids triggering on pre-releases jobs: build: if: github.repository_owner == 'gbdev' runs-on: ubuntu-latest steps: - name: Checkout rgbds@release uses: actions/checkout@v4 with: path: rgbds - name: Checkout rgbds-www@master uses: actions/checkout@v4 with: repository: ${{ github.repository_owner }}/rgbds-www path: rgbds-www - name: Install groff and mandoc run: | sudo apt-get -qq update sudo apt-get install -yq groff mandoc - name: Update pages working-directory: rgbds/man run: | # The ref appears to be in the format "refs/tags/", so strip that ../../rgbds-www/maintainer/man_to_html.sh ${GITHUB_REF##*/} * ../../rgbds-www/maintainer/new_release.sh ${GITHUB_REF##*/} - name: Push new pages working-directory: rgbds-www run: | mkdir -p -m 700 ~/.ssh cat > ~/.ssh/id_ed25519 <<<"${{ secrets.SSH_KEY_SECRET }}" chmod 0600 ~/.ssh/id_ed25519 eval $(ssh-agent -s) ssh-add ~/.ssh/id_ed25519 git config --global user.name "GitHub Action" git config --global user.email "community@gbdev.io" git add -A git commit -m "Create RGBDS ${GITHUB_REF##*/} documentation" if git remote | grep -q origin; then git remote set-url origin git@github.com:${{ github.repository_owner }}/rgbds-www.git else git remote add origin git@github.com:${{ github.repository_owner }}/rgbds-www.git fi git push origin master gbdev-rgbds-92bfe5d/.github/workflows/testing.yml000066400000000000000000000323401512540461700222060ustar00rootroot00000000000000name: Regression testing on: - push - pull_request jobs: unix: strategy: matrix: os: [ubuntu-22.04, macos-14] cxx: [g++, clang++] buildsys: [make, cmake] exclude: # Don't use `g++` on macOS; it's just an alias to `clang++`. - os: macos-14 cxx: g++ fail-fast: false runs-on: ${{ matrix.os }} steps: - name: Checkout repo uses: actions/checkout@v4 - name: Install deps shell: bash run: | ./.github/scripts/install_deps.sh ${{ matrix.os }} - name: Build & install using Make if: matrix.buildsys == 'make' run: | make develop -kj Q= CXX=${{ matrix.cxx }} sudo make install -j Q= - name: Build & install using CMake if: matrix.buildsys == 'cmake' run: | cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=${{ matrix.cxx }} -DSANITIZERS=ON -DMORE_WARNINGS=ON cmake --build build -j --verbose sudo cmake --install build --verbose - name: Package binaries run: | mkdir bins cp rgb{asm,link,fix,gfx} bins - name: Upload binaries uses: actions/upload-artifact@v4 with: name: rgbds-canary-${{ matrix.os }}-${{ matrix.cxx }}-${{ matrix.buildsys }} path: bins - name: Compute test dependency cache params id: test-deps-cache-params shell: bash run: | paths=$(test/fetch-test-deps.sh --get-paths) hash=$(test/fetch-test-deps.sh --get-hash) tee -a <<<"paths=\"${paths//,/\\n}\"" $GITHUB_OUTPUT tee -a <<<"hash=${hash%-}" $GITHUB_OUTPUT - name: Check test dependency repositories cache id: test-deps-cache uses: actions/cache@v4 with: path: ${{ fromJSON(steps.test-deps-cache-params.outputs.paths) }} key: ${{ matrix.os }}-${{ steps.test-deps-cache-params.outputs.hash }} - name: Fetch test dependency repositories if: steps.test-deps-cache.outputs.cache-hit != 'true' continue-on-error: true run: | test/fetch-test-deps.sh - name: Install test dependency dependencies shell: bash run: | test/fetch-test-deps.sh --get-deps ${{ matrix.os }} - name: Run tests shell: bash run: | CXX=${{ matrix.cxx }} test/run-tests.sh --os ${{ matrix.os }} macos-static: runs-on: macos-14 steps: - name: Checkout repo uses: actions/checkout@v4 - name: Install deps shell: bash run: | ./.github/scripts/install_deps.sh macos - name: Build libpng run: | ./.github/scripts/build_libpng.sh - name: Build & install run: | make -kj CXXFLAGS="-O3 -flto -DNDEBUG -mmacosx-version-min=10.9 -arch x86_64 -arch arm64" PNGCFLAGS="-I libpng-staging/include" PNGLDLIBS="libpng-staging/lib/libpng.a -lz" Q= - name: Package binaries run: | mkdir bins cp rgb{asm,link,fix,gfx} bins - name: Upload binaries uses: actions/upload-artifact@v4 with: name: rgbds-canary-macos-static path: bins - name: Compute test dependency cache params id: test-deps-cache-params shell: bash run: | paths=$(test/fetch-test-deps.sh --get-paths) hash=$(test/fetch-test-deps.sh --get-hash) tee -a <<<"paths=\"${paths//,/\\n}\"" $GITHUB_OUTPUT tee -a <<<"hash=${hash%-}" $GITHUB_OUTPUT - name: Check test dependency repositories cache id: test-deps-cache uses: actions/cache@v4 with: path: ${{ fromJSON(steps.test-deps-cache-params.outputs.paths) }} key: ${{ matrix.os }}-${{ steps.test-deps-cache-params.outputs.hash }} - name: Fetch test dependency repositories if: steps.test-deps-cache.outputs.cache-hit != 'true' continue-on-error: true run: | test/fetch-test-deps.sh - name: Install test dependency dependencies shell: bash run: | test/fetch-test-deps.sh --get-deps macos - name: Run tests shell: bash run: | test/run-tests.sh --os macos windows: strategy: matrix: bits: [32, 64] os: [windows-2022, windows-2025] include: - bits: 32 arch: x86 platform: Win32 - bits: 64 arch: x86_x64 platform: x64 fail-fast: false runs-on: ${{ matrix.os }} steps: - name: Checkout repo uses: actions/checkout@v4 - name: Install deps run: .github/scripts/get_win_deps.ps1 - name: Check libraries cache id: cache uses: actions/cache@v4 with: path: | zbuild pngbuild key: ${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('zlib/**', 'libpng/**') }} - name: Build zlib if: steps.cache.outputs.cache-hit != 'true' shell: bash run: | # BUILD_SHARED_LIBS causes the output DLL to be correctly called `zlib1.dll` cmake -S zlib -B zbuild -A ${{ matrix.platform }} -Wno-dev -DCMAKE_INSTALL_PREFIX=install_dir -DBUILD_SHARED_LIBS=ON cmake --build zbuild --config Release -j - name: Install zlib run: | cmake --install zbuild - name: Build libpng if: steps.cache.outputs.cache-hit != 'true' shell: bash run: | cmake -S libpng -B pngbuild -A ${{ matrix.platform }} -Wno-dev -DCMAKE_INSTALL_PREFIX=install_dir -DPNG_SHARED=ON -DPNG_STATIC=OFF -DPNG_TESTS=OFF cmake --build pngbuild --config Release -j - name: Install libpng run: | cmake --install pngbuild - name: Build Windows binaries shell: bash run: | cmake -S . -B build -A ${{ matrix.platform }} -DCMAKE_INSTALL_PREFIX=install_dir -DCMAKE_BUILD_TYPE=Release cmake --build build --config Release -j --verbose cmake --install build --verbose --prefix install_dir - name: Package binaries shell: bash run: | mkdir bins cp install_dir/bin/{rgbasm.exe,rgblink.exe,rgbfix.exe,rgbgfx.exe,zlib1.dll,libpng16.dll} bins - name: Upload Windows binaries uses: actions/upload-artifact@v4 with: name: rgbds-canary-w${{ matrix.bits }}-${{ matrix.os }} path: bins - name: Compute test dependency cache params id: test-deps-cache-params shell: bash run: | paths=$(test/fetch-test-deps.sh --get-paths) hash=$(test/fetch-test-deps.sh --get-hash) tee -a <<<"paths=\"${paths//,/\\n}\"" $GITHUB_OUTPUT tee -a <<<"hash=${hash%-}" $GITHUB_OUTPUT - name: Check test dependency repositories cache id: test-deps-cache uses: actions/cache@v4 with: path: ${{ fromJSON(steps.test-deps-cache-params.outputs.paths) }} key: ${{ matrix.os }}-${{ matrix.bits }}-${{ steps.test-deps-cache-params.outputs.hash }} - name: Fetch test dependency repositories if: steps.test-deps-cache.outputs.cache-hit != 'true' shell: bash continue-on-error: true run: | test/fetch-test-deps.sh - name: Install test dependency dependencies shell: bash run: | test/fetch-test-deps.sh --get-deps ${{ matrix.os }} - name: Run tests shell: bash run: | cp bins/* . cp bins/*.dll test/gfx test/run-tests.sh --os ${{ matrix.os }} windows-mingw-build: strategy: matrix: bits: [32, 64] include: - bits: 32 arch: i686 triplet: i686-w64-mingw32 - bits: 64 arch: x86-64 triplet: x86_64-w64-mingw32 fail-fast: false runs-on: ubuntu-22.04 env: DIST_DIR: win${{ matrix.bits }} steps: - name: Checkout repo uses: actions/checkout@v4 - name: Install deps shell: bash run: | ./.github/scripts/install_deps.sh ubuntu - name: Install MinGW run: | # dpkg-dev is apparently required for pkg-config for cross-building sudo apt-get install g++-mingw-w64-${{ matrix.arch }}-win32 mingw-w64-tools libz-mingw-w64-dev dpkg-dev - name: Install libpng dev headers for MinGW run: | ./.github/scripts/mingw-w64-libpng-dev.sh ${{ matrix.triplet }} - name: Cross-build Windows binaries run: | make mingw${{ matrix.bits }} -kj Q= - name: Package binaries run: | # DLL dependencies can be figured out using e.g. Dependency Walker or objdump -p mkdir bins mv -v rgb{asm,link,fix,gfx}.exe bins/ cp -v /usr/${{ matrix.triplet }}/lib/zlib1.dll bins cp -v /usr/${{ matrix.triplet }}/bin/libpng16-16.dll bins cp -v /usr/lib/gcc/${{ matrix.triplet }}/10-win32/lib{ssp-0,stdc++-6}.dll bins [ "${{ matrix.bits }}" -ne 32 ] || cp -v /usr/lib/gcc/${{ matrix.triplet }}/10-win32/libgcc_s_dw2-1.dll bins - name: Upload Windows binaries uses: actions/upload-artifact@v4 with: name: rgbds-canary-mingw-win${{ matrix.bits }} path: bins - name: Upload Windows test binaries uses: actions/upload-artifact@v4 with: name: testing-programs-mingw-win${{ matrix.bits }} path: | test/gfx/randtilegen.exe test/gfx/rgbgfx_test.exe windows-mingw-testing: needs: windows-mingw-build strategy: matrix: os: [windows-2022, windows-2025] bits: [32, 64] fail-fast: false runs-on: ${{ matrix.os }} steps: - name: Checkout repo uses: actions/checkout@v4 - name: Retrieve binaries uses: actions/download-artifact@v4 with: name: rgbds-canary-mingw-win${{ matrix.bits }} path: bins - name: Retrieve test binaries uses: actions/download-artifact@v4 with: name: testing-programs-mingw-win${{ matrix.bits }} path: test/gfx - name: Extract binaries shell: bash run: | cp bins/* . cp bins/*.dll test/gfx - name: Compute test dependency cache params id: test-deps-cache-params shell: bash run: | paths=$(test/fetch-test-deps.sh --get-paths) hash=$(test/fetch-test-deps.sh --get-hash) tee -a <<<"paths=\"${paths//,/\\n}\"" $GITHUB_OUTPUT tee -a <<<"hash=${hash%-}" $GITHUB_OUTPUT - name: Check test dependency repositories cache id: test-deps-cache uses: actions/cache@v4 with: path: ${{ fromJSON(steps.test-deps-cache-params.outputs.paths) }} key: mingw-${{ matrix.bits }}-${{ steps.test-deps-cache-params.outputs.hash }} - name: Fetch test dependency repositories if: steps.test-deps-cache.outputs.cache-hit != 'true' shell: bash continue-on-error: true run: | test/fetch-test-deps.sh - name: Install test dependency dependencies shell: bash run: | test/fetch-test-deps.sh --get-deps ${{ matrix.os }} - name: Run tests shell: bash run: | test/run-tests.sh --os ${{ matrix.os }} cygwin: strategy: matrix: bits: [32, 64] include: - bits: 32 arch: x86 - bits: 64 arch: x86_64 fail-fast: false runs-on: windows-2022 timeout-minutes: 30 steps: - name: Checkout repo uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Setup Cygwin uses: cygwin/cygwin-install-action@v4 with: platform: ${{ matrix.arch }} packages: >- bison gcc-g++ git libpng-devel make pkg-config - name: Build & install using Make shell: C:\cygwin\bin\env.exe CYGWIN_NOWINPATH=1 CHERE_INVOKING=1 C:\cygwin\bin\bash.exe -o igncr '{0}' run: | # Cygwin does not support `make develop` sanitizers ASan or UBSan make -kj Q= make install -j Q= - name: Run tests shell: C:\cygwin\bin\env.exe CYGWIN_NOWINPATH=1 CHERE_INVOKING=1 C:\cygwin\bin\bash.exe -o igncr '{0}' run: | test/run-tests.sh --only-internal freebsd: runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repo uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Build & test using CMake on FreeBSD uses: vmactions/freebsd-vm@v1 with: release: "14.3" usesh: true prepare: | pkg install -y \ bash \ bison \ cmake \ git \ png run: | # FreeBSD `c++` compiler does not support `make develop` sanitizers ASan or UBSan cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=c++ -DUSE_EXTERNAL_TESTS=OFF -DOS=bsd cmake --build build -j4 --verbose cmake --install build --verbose cmake --build build --target test gbdev-rgbds-92bfe5d/.github/workflows/update-master-docs.yml000066400000000000000000000033071512540461700242330ustar00rootroot00000000000000name: Update master docs on: push: branches: - master paths: - man/gbz80.7 - man/rgbds.5 - man/rgbds.7 - man/rgbasm.1 - man/rgbasm.5 - man/rgbasm-old.5 - man/rgblink.1 - man/rgblink.5 - man/rgbfix.1 - man/rgbgfx.1 jobs: build: if: github.repository_owner == 'gbdev' runs-on: ubuntu-latest steps: - name: Checkout rgbds@master uses: actions/checkout@v4 with: repository: gbdev/rgbds ref: master path: rgbds - name: Checkout rgbds-www@master uses: actions/checkout@v4 with: repository: gbdev/rgbds-www ref: master path: rgbds-www - name: Install groff and mandoc run: | sudo apt-get -qq update sudo apt-get install -yq groff mandoc - name: Update pages working-directory: rgbds/man run: | ../../rgbds-www/maintainer/man_to_html.sh master * - name: Push new pages working-directory: rgbds-www run: | mkdir -p -m 700 ~/.ssh echo "${{ secrets.SSH_KEY_SECRET }}" > ~/.ssh/id_ed25519 chmod 0600 ~/.ssh/id_ed25519 eval $(ssh-agent -s) ssh-add ~/.ssh/id_ed25519 git config --global user.name "GitHub Action" git config --global user.email "community@gbdev.io" git add -A git commit -m "Update RGBDS master documentation" if git remote | grep -q origin; then git remote set-url origin git@github.com:gbdev/rgbds-www.git else git remote add origin git@github.com:gbdev/rgbds-www.git fi git push origin master gbdev-rgbds-92bfe5d/.gitignore000066400000000000000000000002531512540461700163770ustar00rootroot00000000000000/rgbasm /rgblink /rgbfix /rgbgfx /rgbshim.sh /coverage/ *.o *.exe *.dll *.gcno *.gcda *.gcov CMakeCache.txt CMakeFiles/ cmake_install.cmake build/ *.dSYM/ callgrind.out.* gbdev-rgbds-92bfe5d/ARCHITECTURE.md000066400000000000000000000463111512540461700166200ustar00rootroot00000000000000# RGBDS Architecture The RGBDS package consists of four programs: RGBASM, RGBLINK, RGBFIX, and RGBGFX. - RGBASM is the assembler. It takes assembly code as input, and produces an RGB object file as output (and optionally a state file, logging the final state of variables and constants). - RGBLINK is the linker. It takes object files as input, and produces a ROM file as output (and optionally a symbol and/or map file, logging where the assembly declarations got placed in the ROM). - RGBFIX is the checksum/header fixer. It takes a ROM file as input, and outputs the same ROM file (or modifies it in-place) with the cartridge header's checksum and other metadata fixed for consistency. - RGBGFX is the graphics converter. It takes a PNG image file as input, and outputs the tile data, palettes, tilemap, attribute map, and/or palette map in formats that the Game Boy can use. In the simplest case, a single pipeline can turn an assembly file into a ROM: ```console (rgbasm -o - - | rgblink -o - - | rgbfix -v -p 0) < game.asm > game.gb ``` This document describes how these four programs are structured. It goes over each source code file, noting which data is *global* (and thus scoped in all files), *owned* by that file (i.e. that is where the data's memory is managed, via [RAII](https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization)) or *referenced* by that file (i.e. there are non-owning pointers to some data, and care must be taken to not dereference those pointers after the data's owner has moved or deleted the data). We assume that the programs are single-threaded; data structures and operations may not be thread-safe. ## Folder Organization The RGBDS source code file structure is as follows: ``` rgbds/ ├── .github/ │ ├── scripts/ │ │ └── ... │ └── workflows/ │ └── ... ├── contrib/ │ ├── bash_compl/ │ ├── zsh_compl/ │ │ └── ... │ └── ... ├── include/ │ └── ... ├── man/ │ └── ... ├── src/ │ ├── asm/ │ │ └── ... │ ├── extern/ │ │ └── ... │ ├── fix/ │ │ └── ... │ ├── gfx/ │ │ └── ... │ ├── link/ │ │ └── ... │ ├── CMakeLists.txt │ ├── bison.sh │ └── ... ├── test/ │ ├── fetch-test-deps.sh │ ├── run-tests.sh │ └── ... ├── .clang-format ├── .clang-tidy ├── CMakeLists.txt ├── compile_flags.txt ├── Dockerfile └── Makefile ``` - **`.github/`:** Files related to the integration of the RGBDS codebase with GitHub features. * **`scripts/`:** Scripts used by GitHub Actions workflow files. * **`workflows/`:** GitHub Actions CI workflow description files. Used for automated testing, deployment, etc. - **`contrib/`:** Scripts and other resources which may be useful to RGBDS users and developers. * **`bash_compl/`:** Tab completion scripts for use with `bash`. Run them with `source` somewhere in your `.bashrc`, and they should auto-load when you open a shell. * **`zsh_compl/`:** Tab completion scripts for use with `zsh`. Put them somewhere in your `fpath`, and they should auto-load when you open a shell. - **`include/`:** Header files for the respective source files in `src`. - **`man/`:** Manual pages to be read with `man`, written in the [`mandoc`](https://mandoc.bsd.lv) dialect. - **`src/`:** Source code of RGBDS. * **`asm/`:** Source code of RGBASM. * **`extern/`:** Source code copied from external sources. * **`fix/`:** Source code of RGBFIX. * **`gfx/`:** Source code of RGBGFX. * **`link/`:** Source code of RGBLINK. * **`CMakeLists.txt`:** Defines how to build individual RGBDS programs with CMake, including the source files that each program depends on. * **`bison.sh`:** Script used to run the Bison parser generator with the latest flags that the user's version supports. - **`test/`:** Testing framework used to verify that changes to the code don't break or modify the behavior of RGBDS. * **`fetch-test-deps.sh`:** Script used to fetch dependencies for building external repositories. `fetch-test-deps.sh --help` describes its options. * **`run-tests.sh`:** Script used to run tests, including internal test cases and external repositories. `run-tests.sh --help` describes its options. - **`.clang-format`:** Code style for automated C++ formatting with [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html) (for which we define the shortcut `make format`). - **`.clang-tidy`:** Configuration for C++ static analysis with [`clang-tidy`](https://clang.llvm.org/extra/clang-tidy/) (for which we define the shortcut `make tidy`). - **`CMakeLists.txt`:** Defines how to build RGBDS with CMake. - **`compile_flags.txt`:** Compiler flags for `clang-tidy`. - **`Dockerfile`:** Defines how to build RGBDS with Docker (which we do in CI to provide a [container image](https://github.com/gbdev/rgbds/pkgs/container/rgbds)). - **`Makefile`:** Defines how to build RGBDS with `make`, including the source files that each program depends on. ## RGBDS These files in the `src/` directory are shared across multiple programs: often all four (RGBASM, RGBLINK, RGBFIX, and RGBGFX), sometimes only RGBASM and RGBLINK. - **`backtrace.cpp`:** Generic printing of location backtraces for RGBASM and RGBLINK. Allows configuring backtrace styles with a command-line flag (conventionally `-B/--backtrace`). Renders warnings in yellow, errors in red, and locations in cyan. - **`cli.cpp`:** A function for parsing command-line options, including RGBDS-specific "at-files" (a filename containing more options, prepended with an "`@`"). This is the only file to use the extern/getopt.cpp variables and functions. - **`diagnostics.cpp`:** Generic warning/error diagnostic support for all programs. Allows command-line flags (conventionally `-W`) to have `no-`, `error=`, or `no-error=` prefixes, and `=` level suffixes; allows "meta" flags to affect groups of individual flags; and counts how many total errors there have been. Every program has its own `warning.cpp` file that uses this. - **`linkdefs.cpp`:** Constants, data, and functions related to RGBDS object files, which are used for RGBASM output and RGBLINK input. This file defines two *global* variables, `sectionTypeInfo` (metadata about each section type) and `sectionModNames` (names of section modifiers, for error reporting). RGBLINK may change some values in `sectionTypeInfo` depending on its command-line options (this only affects RGBLINK; `sectionTypeInfo` is immutable in RGBASM). - **`opmath.cpp`:** Functions for mathematical operations in RGBASM and RGBLINK that aren't trivially equivalent to built-in C++ ones, such as division and modulo with well-defined results for negative values. - **`style.cpp`:** Generic printing of cross-platform colored or bold text. Obeys the [`FORCE_COLOR`](https://force-color.org/) and [`NO_COLOR`](https://no-color.org/) environment variables, and allows configuring with a command-line flag (conventionally `--color`). - **`usage.cpp`:** Generic printing of usage information. Renders headings in green, flags in cyan, and URLs in blue. Every program has its own `main.cpp` file that uses this. - **`util.cpp`:** Utility functions applicable to most programs, mostly dealing with text strings, such as locale-independent character checks. - **`verbosity.cpp`:** Generic printing of messages conditionally at different verbosity levels. Allows configuring with a command-line flag (conventionally `-v/--verbose`). - **`version.cpp`:** RGBDS version number and string for all the programs. ## External These files have been copied ("vendored") from external authors and adapted for use with RGBDS. Both of our vendored dependencies use the same MIT license as RGBDS. - **`getopt.cpp`:** Functions for parsing command-line options, including conventional single-dash and double-dash options. This file defines some *global* `musl_opt*` variables, including `musl_optarg` (the argument given after an option flag) and `musl_optind` (the index of the next option in `argv`). Copied from [musl libc](https://musl.libc.org/). - **`utf8decoder.cpp`:** Function for decoding UTF-8 bytes into Unicode code points. Copied from [Björn Höhrmann](https://bjoern.hoehrmann.de/utf-8/decoder/dfa/). ## RGBASM - **`actions.cpp`:** Actions taken by the assembly language parser, to avoid large amounts of code going in the parser.y file. - **`charmap.cpp`:** Functions and data related to charmaps. This file *owns* the `Charmap`s in its `charmaps` collection. It also maintains a static `currentCharmap` pointer, and a `charmapStack` stack of pointers to `Charmap`s within `charmaps` (which is affected by `PUSHC` and `POPC` directives). - **`fixpoint.cpp`:** Functions for fixed-point math, with configurable [Q*m*.*n*](https://en.wikipedia.org/wiki/Q_(number_format)) precision. - **`format.cpp`:** `FormatSpec` methods for parsing and applying format specs, as used by `{interpolations}` and `STRFMT`. - **`fstack.cpp`:** Functions and data related to "fstack" nodes (the contents of top-level or `INCLUDE`d files, macro expansions, or `REPT`/`FOR` loop iterations) and their "contexts" (metadata that is only relevant while a node's content is being lexed and parsed). This file *owns* the `Context`s in its `contextStack` collection. Each of those `Context`s *owns* its `LexerState`, and *refers* to its `FileStackNode`, `uniqueIDStr`, and `macroArgs`. Each `FileStackNode` also *references* its `parent`. - **`lexer.cpp`:** Functions and data related to [lexing](https://en.wikipedia.org/wiki/Lexical_analysis) assembly source code into tokens, which can then be parsed. This file maintains static `lexerState` and `lexerStateEOL` pointers to `LexerState`s from the `Context`s in `fstack.cpp`. Each `LexerState` *owns* its `content` and its `expansions`' content. Each `Expansion` (the contents of an `{interpolation}` or macro argument) in turn *owns* its `contents`. The lexer and parser are interdependent: when the parser reaches certain tokens, it changes the lexer's mode, which affects how characters get lexed into tokens. For example, when the parser reaches a macro name, it changes the lexer to "raw" mode, which lexes the rest of the line as a sequence of string arguments to the macro. - **`macro.cpp`:** `MacroArgs` methods related to macro arguments. Each `MacroArgs` *references* its arguments' contents. - **`main.cpp`:** The `main` function for running RGBASM, including the initial handling of command-line options. This file defines a *global* `options` variable with the parsed CLI options. - **`opt.cpp`:** Functions for parsing options specified by `OPT` or by certain command-line options. This file *owns* the `OptStackEntry`s in its `stack` collection (which is affected by `PUSHO` and `POPO` directives). - **`output.cpp`:** Functions and data related to outputting object files (with `-o/--output`) and state files (with `-s/--state`). This file *owns* its `assertions` (created by `ASSERT` and `STATIC_ASSERT` directives). Every assertion gets output in the object file. This file also *references* some `fileStackNodes`, and maintains static pointers to `Symbol`s in `objectSymbols`. Only the "registered" symbols and fstack nodes get output in the object file. The `fileStackNodes` and `objectSymbols` collections keep track of which nodes and symbols have been registered for output. - **`parser.y`:** Grammar for the RGBASM assembly language, which Bison preprocesses into a [LALR(1) parser](https://en.wikipedia.org/wiki/LALR_parser). The Bison-generated parser calls `yylex` (defined in `lexer.cpp`) to get the next token, and calls `yywrap` (defined in `fstack.cpp`) when the current context is out of tokens and returns `EOF`. - **`rpn.cpp`:** `Expression` methods and data related to "[RPN](https://en.wikipedia.org/wiki/Reverse_Polish_notation)" expressions. When a numeric expression is parsed, if its value cannot be calculated at assembly time, it is built up into a buffer of RPN-encoded operations to do so at link time by RGBLINK. The valid RPN operations are defined in [man/rgbds.5](man/rgbds.5). - **`section.cpp`:** Functions and data related to `SECTION`s. This file *owns* the `Section`s in its `sections` collection. It also maintains various static pointers to those sections, including the `currentSection`, `currentLoadSection`, and `sectionStack` (which is affected by `PUSHS` and `POPS` directives). (Note that sections cannot be deleted.) - **`symbol.cpp`:** Functions and data related to symbols (labels, constants, variables, string constants, macros, etc). This file *owns* the `Symbol`s in its `symbols` collection, and the various built-in ones outside that collection (`PCSymbol` for "`@`", `NARGSymbol` for "`_NARG`", etc). It also maintains a static `purgedSymbols` collection to remember which symbol names have been `PURGE`d from `symbols`, for error reporting purposes. - **`warning.cpp`:** Functions and data for warning and error output. This file defines a *global* `warnings` variable using the `diagnostics.cpp` code for RGBASM-specific warning flags. ## RGBFIX - **`fix.cpp`:** Functions for fixing the ROM header. - **`main.cpp`:** The `main` function for running RGBFIX, including the initial handling of command-line options. This file defines a *global* `options` variable with the parsed CLI options. - **`mbc.cpp`:** Functions and data related to [MBCs](https://gbdev.io/pandocs/MBCs.html), including the names of known MBC values. - **`warning.cpp`:** Functions and data for warning and error output. This file defines a *global* `warnings` variable using the `diagnostics.cpp` code for RGBFIX-specific warning flags. ## RGBGFX - **`color_set.cpp`:** `ColorSet` methods for creating and comparing sets of colors. A color set includes the unique colors used by a single tile, and these sets are then packed into palettes. - **`main.cpp`:** The `main` function for running RGBGFX, including the initial handling of command-line options. This file defines a *global* `options` variable with the parsed CLI options. - **`pal_packing.cpp`:** Functions for packing color sets into palettes. This is done with an ["overload-and-remove" heuristic](https://arxiv.org/abs/1605.00558) for a pagination algorithm. - **`pal_sorting.cpp`:** Functions for sorting colors within palettes, which works differently for grayscale, RGB, or indexed-color palettes. - **`pal_spec.cpp`:** Functions for parsing various formats of palette specifications (from `-c/--colors`). - **`palette.cpp`:** `Palette` methods for working with up to four GBC-native (RGB555) colors. - **`png.cpp`:** `Png` methods for reading PNG image files, standardizing them to 8-bit RGBA pixels while also reading their indexed palette if there is one. - **`process.cpp`:** Functions related to generating and outputting files (tile data, palettes, tilemap, attribute map, and/or palette map). - **`reverse.cpp`:** Functions related to reverse-generating RGBGFX outputs into a PNG file (for `-r/--reverse`). - **`rgba.cpp`:** `Rgba` methods related to RGBA colors and their 8-bit or 5-bit representations. - **`warning.cpp`:** Functions and data for warning and error output. This file defines a *global* `warnings` variable using the `diagnostics.cpp` code for RGBGFX-specific warning flags. ## RGBLINK - **`assign.cpp`:** Functions and data for assigning `SECTION`s to specific banks and addresses. This file *owns* the `memory` table of free space: each section type is associated with a list of each bank's free address ranges, which are allocated to sections using a [first-fit decreasing](https://en.wikipedia.org/wiki/Bin_packing_problem#First-fit_algorithm) bin-packing algorithm. - **`fstack.cpp`:** Functions related to "fstack" nodes (the contents of top-level or `INCLUDE`d files, macro expansions, or `REPT`/`FOR` loop iterations) read from the object files. At link time, these nodes are only needed for printing of location backtraces. - **`layout.cpp`:** Actions taken by the linker script parser, to avoid large amounts of code going in the script.y file. This file maintains some static data about the current bank and address layout, which get checked and updated for consistency as the linker script is parsed. - **`lexer.cpp`:** Functions and data related to [lexing](https://en.wikipedia.org/wiki/Lexical_analysis) linker script files into tokens, which can then be parsed. This file *owns* the `LexerStackEntry`s in its `lexerStack` collection. Each of those `LexerStackEntry`s *owns* its `file`. The stack is updated as linker scripts can `INCLUDE` other linker script pieces. The linker script lexer is simpler than the RGBASM one, and does not have modes. - **`main.cpp`:** The `main` function for running RGBLINK, including the initial handling of command-line options. This file defines a *global* `options` variable with the parsed CLI options. - **`object.cpp`:** Functions and data for reading object files generated by RGBASM. This file *owns* the `Symbol`s in its `symbolLists` collection, and the `FileStackNode`s in its `nodes` collection. - **`output.cpp`:** Functions and data related to outputting ROM files (with `-o/--output`), symbol files (with `-n/--sym`), and map files (with `-m/--map`). This file *references* some `Symbol`s and `Section`s, in collections that keep them sorted by address and name, which allows the symbol and map output to be in order. - **`patch.cpp`:** Functions and data related to "[RPN](https://en.wikipedia.org/wiki/Reverse_Polish_notation)" expression patches read from the object files, including the ones for `ASSERT` conditions. After sections have been assigned specific locations, the RPN patches can have their values calculated and applied to the ROM. The valid RPN operations are defined in [man/rgbds.5](man/rgbds.5). This file *owns* the `Assertion`s in its `assertions` collection, and the `RPNStackEntry`s in its `rpnStack` collection. - **`script.y`:** Grammar for the linker script language, which Bison preprocesses into a [LALR(1) parser](https://en.wikipedia.org/wiki/LALR_parser). The Bison-generated parser calls `yylex` (defined in `lexer.cpp`) to get the next token, and calls `yywrap` (also defined in `lexer.cpp`) when the current context is out of tokens and returns `EOF`. - **`sdas_obj.cpp`:** Functions and data for reading object files generated by [GBDK with SDCC](https://gbdk.org/). RGBLINK support for these object files is incomplete. - **`section.cpp`:** Functions and data related to `SECTION`s read from the object files. This file *owns* the `Section`s in its `sections` collection. - **`symbol.cpp`:** Functions and data related to symbols read from the object files. This file *references* the `Symbol`s in its `symbols` and `localSymbols` collections, which allow accessing symbols by name. - **`warning.cpp`:** Functions and data for warning and error output. This file defines a *global* `warnings` variable using the `diagnostics.cpp` code for RGBLINK-specific warning flags. gbdev-rgbds-92bfe5d/CMakeLists.txt000066400000000000000000000121111512540461700171430ustar00rootroot00000000000000# SPDX-License-Identifier: MIT # 3.9 required for LTO checks # 3.17 optional for CMAKE_CTEST_ARGUMENTS cmake_minimum_required(VERSION 3.9..3.17 FATAL_ERROR) project(rgbds LANGUAGES CXX) include(CTest) # get real path of source and binary directories get_filename_component(srcdir "${CMAKE_SOURCE_DIR}" REALPATH) get_filename_component(bindir "${CMAKE_BINARY_DIR}" REALPATH) # reject in-source builds, may conflict with Makefile if(srcdir STREQUAL bindir) message("RGBDS should not be built in the source directory.") message("Instead, create a separate build directory and specify to CMake the path to the source directory.") message(FATAL_ERROR "Terminating configuration") endif() option(SANITIZERS "Build with sanitizers enabled" OFF) # Ignored on MSVC option(MORE_WARNINGS "Turn on more warnings" OFF) # Ignored on MSVC if(MSVC) # MSVC's own standard library triggers warning C5105, # "macro expansion producing 'defined' has undefined behavior". # Warning C5030 is about unknown attributes (`[[gnu::ATTR]]`), none of ours being load-bearing. # Warning C4996 is about using POSIX names, which we want to do for portability. # We also opt into the C++20-conformant preprocessor. add_compile_options(/MP /wd5105 /wd5030 /wd4996 /Zc:preprocessor) add_definitions(/D_CRT_SECURE_NO_WARNINGS) if(SANITIZERS) set(SAN_FLAGS /fsanitize=address) add_compile_options(${SAN_FLAGS}) add_link_options(${SAN_FLAGS}) endif() else() add_compile_options(-Wall -pedantic) if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") # C++20 allows macros to take zero variadic arguments, but Clang (aka AppleClang on macOS) # does not recognize this yet. add_compile_options(-Wno-gnu-zero-variadic-macro-arguments) endif() if(SANITIZERS) set(SAN_FLAGS -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero) add_compile_options(${SAN_FLAGS}) add_link_options(${SAN_FLAGS}) add_definitions(-D_GLIBCXX_ASSERTIONS -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG) # A non-zero optimization level is desired in debug mode, but allow overriding it nonetheless set(CMAKE_CXX_FLAGS_DEBUG "-g -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls ${CMAKE_CXX_FLAGS_DEBUG}" CACHE STRING "" FORCE) endif() if(MORE_WARNINGS) add_compile_options(-Werror -Wextra -Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond -Wfloat-equal -Wlogical-op -Wnull-dereference -Wold-style-cast -Wshift-overflow=2 -Wstringop-overflow=4 -Wtrampolines -Wundef -Wuninitialized -Wunused -Wshadow -Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1 -Wno-format-nonliteral -Wno-strict-overflow -Wno-unused-but-set-variable # bison's `yynerrs_` is incremented but unused -Wno-type-limits -Wno-tautological-constant-out-of-range-compare -Wvla # MSVC does not support VLAs -Wno-unknown-warning-option) # Clang shouldn't diagnose unknown warnings endif() endif() # Use versioning consistent with Makefile # the git revision is used but uses the fallback in an archive find_program(GIT git) if(GIT) execute_process(COMMAND ${GIT} --git-dir=.git -c safe.directory='*' describe --tags --dirty --always WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_REV OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) message(STATUS "RGBDS version: ${GIT_REV}") else(GIT) message(STATUS "Cannot determine RGBDS version (Git not installed), falling back") endif(GIT) find_package(PkgConfig) if(MSVC OR NOT PKG_CONFIG_FOUND) # fallback to find_package # cmake's FindPNG is very fragile; it breaks when multiple versions are installed # this is most evident on macOS but can occur on Linux too find_package(PNG REQUIRED) else() pkg_check_modules(LIBPNG REQUIRED libpng) endif() include_directories("${PROJECT_SOURCE_DIR}/include") set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED True) add_subdirectory(src) set(CMAKE_CTEST_ARGUMENTS "--verbose") add_subdirectory(test) # By default, build in Release mode; Debug mode must be explicitly requested # (You may want to augment it with the options above) if(CMAKE_BUILD_TYPE STREQUAL "Release") message(CHECK_START "Checking if LTO is supported") include(CheckIPOSupported) check_ipo_supported(RESULT enable_lto) if(enable_lto) message(CHECK_PASS "yes") set_property(TARGET rgbasm rgblink rgbfix rgbgfx PROPERTY INTERPROCEDURAL_OPTIMIZATION ON) else() message(CHECK_FAIL "no") endif() endif() set(MANDIR "share/man") set(man1 "man/rgbasm.1" "man/rgbfix.1" "man/rgbgfx.1" "man/rgblink.1") set(man5 "man/rgbasm.5" "man/rgbasm-old.5" "man/rgblink.5" "man/rgbds.5") set(man7 "man/gbz80.7" "man/rgbds.7") foreach(SECTION "man1" "man5" "man7") set(DEST "${MANDIR}/${SECTION}") install(FILES ${${SECTION}} DESTINATION ${DEST}) endforeach() gbdev-rgbds-92bfe5d/CONTRIBUTING.md000066400000000000000000000236231512540461700166460ustar00rootroot00000000000000# Contributing RGBDS was created in the late '90s and has received contributions from several developers since then. It wouldn't have been possible to get to this point without their work, and it is always open to the contributions of other people. ## Reporting Bugs Bug reports are essential to improve RGBDS and they are always welcome. If you want to report a bug: 1. Make sure that there isn't a similar issue [already reported](https://github.com/gbdev/rgbds/issues). 2. Figure out a way of reproducing it reliably. 3. If there is a piece of code that triggers the bug, try to reduce it to the smallest file you can. 4. Create a new [issue](https://github.com/gbdev/rgbds/issues). Of course, it may not always be possible to give an accurate bug report, but it always helps to fix it. ## Requesting new features If you come up with a good idea that could be implemented, you can propose it to be done. 1. Create a new [issue](https://github.com/gbdev/rgbds/issues). 2. Try to be as accurate as possible. Describe what you need and why you need it, maybe with examples. Please understand that the contributors are doing it in their free time, so simple requests are more likely to catch the interest of a contributor than complicated ones. If you really need something to be done, and you think you can implement it yourself, you can always contribute to RGBDS with your own code. ## Contributing code If you want to contribute with your own code, whether it is to fix a current issue or to add something that nobody had requested, you should first consider if your change is going to be small (and likely to be accepted as-is) or big (and will have to go through some rework). Big changes will most likely require some discussion, so open an [issue](https://github.com/gbdev/rgbds/issues) and explain what you want to do and how you intend to do it. If you already have a prototype, it's always a good idea to show it. Tests help, too. If you are going to work on a specific issue that involves a lot of work, it is always a good idea to leave a message, just in case someone else is interested but doesn't know that there's someone working on it. Note that you must contribute all your changes under the MIT License. If you are just modifying a file, you don't need to do anything (maybe update the copyright years). If you are adding new files, you need to use the `SPDX-License-Identifier: MIT` header. 1. Fork this repository. 2. Checkout the `master` branch. 3. Create a new branch to work on. You could still work on `master`, but it's easier that way. 4. Compile your changes with `make develop` instead of just `make`. This target checks for additional warnings. Your patches shouldn't introduce any new warning (but it may be possible to remove some warning checks if it makes the code much easier). 5. Test your changes by running `./run-tests.sh` in the `test` directory. (You must run `./fetch-test-deps.sh` first; if you forget to, the test suite will fail and remind you mid-way.) 5. Format your changes according to `clang-format`, which will reformat the coding style according to our standards defined in `.clang-format`. 6. Create a pull request against the branch `master`. 7. Check the results of the GitHub Actions CI jobs for your pull request. The "Code format checking" and "Regression testing" jobs should all succeed. The "Diff completeness check" and "Static analysis" jobs should be manually checked, as they may output warnings which do not count as failure errors. The "Code coverage report" provides an [LCOV](https://github.com/linux-test-project/lcov)-generated report which can be downloaded and checked to see if your new code has full test coverage. 8. Be prepared to get some comments about your code and to modify it. Tip: Use `git rebase -i origin/master` to modify chains of commits. ## Adding a test The test suite is a little ad-hoc, so the way tests work is different for each program being tested. Feel free to modify how the test scripts work, if the thing you want to test doesn't fit the existing scheme(s). ### RGBASM There are two kinds of test. #### Simple tests Each `.asm` file corresponds to one test. RGBASM will be invoked on the `.asm` file with all warnings enabled. If a `.flags` file exists, its first line contains flags to pass to RGBASM. (There may be more lines, which will be ignored; they can serve as comments to explain what the test is about.) If a `.out` file exists, RGBASM's output (`print`, `println`, etc.) must match its contents. If a `.err` file exists, RGBASM's error output (`warn`, errors, etc.) must match its contents. If a `.out.bin` file exists, the object file will be linked, and the generated ROM truncated to the length of the `.out.bin` file. After that, the ROM must match the `.out.bin` file. #### CLI tests Each `.flags` file in `cli/` corresponds to one test. RGBASM will be invoked, passing it the first line of the `.flags` file. (There may be more lines, which will be ignored; they can serve as comments to explain what the test is about.) If a `.out` file exists, RGBASM's output (`print`, `println`, etc.) must match its contents. If a `.err` file exists, RGBASM's error output (`warn`, errors, etc.) must match its contents. ### RGBLINK Each `.asm` file corresponds to one test, or one *set* of tests. All tests begin by assembling the `.asm` file into an object file, which will be linked in various ways depending on the test. #### Simple tests These simply check that RGBLINK's output matches some expected output. A `.out` file **must** exist, and RGBLINK's total output must match that file's contents. Additionally, if a `.out.bin` file exists, the `.gb` file generated by RGBLINK must match it. #### Linker script tests These allow applying various linker scripts to the same object file. If one or more `.link` files exist, whose names start the same as the `.asm` file, then each of those files correspond to one test. Each `.link` linker script **must** be accompanied by a `.out` file, and RGBLINK's total output must match that file's contents when passed the corresponding linker script. #### Variant tests These allow testing RGBLINK's `-d`, `-t`, and `-w` flags. If one or more -<flag>.out or -no-<flag>.out files exist, then each of them corresponds to one test. The object file will be linked with and without said flag, respectively; and in each case, RGBLINK's total output must match the `.out` file's contents. ### RGBFIX Each `.flags` file corresponds to one test. Each one is a text file whose first line contains flags to pass to RGBFIX. (There may be more lines, which will be ignored; they can serve as comments to explain what the test is about.) RGBFIX will be invoked on the `.bin` file if it exists, or else on default-input.bin. If no `.out` file exist, RGBFIX is not expected to output anything. If one *does* exist, RGBFIX's output **must** match the `.out` file's contents. If no `.err` file exists, RGBFIX is simply expected to be able to process the file normally. If one *does* exist, RGBFIX's return status is ignored, but its error output **must** match the `.err` file's contents. Additionally, if a `.gb` file exists, the output of RGBFIX must match the `.gb`. ### RGBGFX There are three kinds of test. #### Simple tests Each `.png` file corresponds to one test. RGBGFX will be invoked on the file. If a `.flags` file exists, it will be used as part of the RGBGFX invocation (@<file>.flags). If `.out.1bpp`, `.out.2bpp`, `.out.pal`, `.out.tilemap`, `.out.attrmap`, or `.out.palmap` files exist, RGBGFX will create the corresponding kind of output, which must match the file's contents. Multiple kinds of output may be tested for the same input. If no `.err` file exists, RGBGFX is simply expected to be able to process the file normally. If one *does* exist, RGBGFX's return status is ignored, but its output **must** match the `.err` file's contents. #### Reverse tests Each `.1bpp` or `.2bpp` file corresponds to one test. RGBGFX will be invoked on the file with `-r 1` for reverse mode, then invoked on the output without `-r 1`. The round-trip output must match the input file's contents. If a `.flags` file exists, it will be used as part of the RGBGFX invocation (@<file>.flags). #### Random seed tests Each `seed*.bin` file corresponds to one test. Each one is a binary RNG file which is passed to the `rgbgfx_test` program. ### Downstream projects 1. Make sure the downstream project supports make <target> RGBDS=<path/to/RGBDS/>. While the test suite supports any Make target name, only [Make](//gnu.org/software/make) is currently supported, and the Makefile must support a `RGBDS` variable to use a non-system RGBDS directory. Also, only projects hosted on GitHub are currently supported. 2. Add the project to `test/fetch-test-deps.sh`: add a new `action` line at the bottom, following the existing pattern: ```sh action ``` (The date is used to avoid fetching too much history when cloning the repositories.) 3. Add the project to `test/run-tests.sh`: add a new `test_downstream` line at the bottom, following the existing pattern: ```sh test_downstream ``` ## Container images The CI will [take care](https://github.com/gbdev/rgbds/blob/master/.github/workflows/build-container.yml) of updating the [rgbds container](https://github.com/gbdev/rgbds/pkgs/container/rgbds) image tagged `master`. When a git tag is pushed, the image is also tagged with that tag. The image can be built locally and pushed to the GitHub container registry by manually running: ```bash # e.g. to build and tag as 'master' docker build . --tag ghcr.io/gbdev/rgbds:master docker push ghcr.io/gbdev/rgbds:master ```gbdev-rgbds-92bfe5d/CONTRIBUTORS.md000066400000000000000000000034241512540461700166710ustar00rootroot00000000000000# Contributors to RGBDS ## Original author - Carsten Elton Sørensen <csoren@gmail.com> ## Main contributors - Justin Lloyd <jlloyd@imf.la> - Vegard Nossum <vegard.nossum@gmail.com> - Anthony J. Bentley <anthony@anjbe.name> - stag019 <stag019@gmail.com> - Antonio Niño Díaz <antonio_nd@outlook.com> - Eldred "ISSOtm" Habert <me@eldred.fr> - Sylvie "Rangi" Oukaour <https://github.com/Rangi42> ## Other contributors - Ben Hetherington <dev@ben-h.uk> - Björn Höhrmann <bjoern@hoehrmann.de> - Christophe Staïesse <chastai@skynet.be> - David Brotz <dbrotz007@gmail.com> - Evie <https://evie.gbdev.io> - James "JL2210" Larrowe <https://github.com/JL2210> - Maja Kądziołka <github@compilercrim.es> - The Musl C library <https://musl.libc.org/> - obskyr <powpowd@gmail.com> - The OpenBSD Project <http://www.openbsd.org> - Quint Guvernator <quint@guvernator.net> - Sanqui <gsanky@gmail.com> - YamaArashi <shadow962@live.com> - yenatch <yenatch@gmail.com> - phs <phil@philhsmith.com> - jidoc01 <jidoc01@naver.com> [](https://github.com/gbdev/rgbds/graphs/contributors) Contributor image made with [contrib.rocks](https://contrib.rocks). ## Acknowledgements RGBGFX generates palettes using algorithms found in the paper ["Algorithms for the Pagination Problem, a Bin Packing with Overlapping Items"](https://arxiv.org/abs/1605.00558) ([GitHub](https://github.com/pagination-problem/pagination), MIT license), by Aristide Grange, Imed Kacem, and Sébastien Martin. RGBGFX's color palette was taken from [SameBoy](https://sameboy.github.io), with permission and help by [LIJI](https://github.com/LIJI32). gbdev-rgbds-92bfe5d/Dockerfile000066400000000000000000000014701512540461700164030ustar00rootroot00000000000000FROM debian:12-slim LABEL org.opencontainers.image.source=https://github.com/gbdev/rgbds ARG version=1.0.1 WORKDIR /rgbds COPY . . RUN apt-get update && \ apt-get install sudo make cmake gcc build-essential -y # Install dependencies and compile RGBDS RUN ./.github/scripts/install_deps.sh ubuntu-22.04 RUN make -j CXXFLAGS="-O3 -flto -DNDEBUG -static" PKG_CONFIG="pkg-config --static" Q= # Create an archive with the compiled executables and all the necessary to install it, # so it can be copied outside of the container and installed/used in another system RUN tar caf rgbds-linux-x86_64.tar.xz --transform='s#.*/##' rgbasm rgblink rgbfix rgbgfx man/* .github/scripts/install.sh # Install RGBDS on the container so all the executables will be available in the PATH RUN cp man/* . RUN ./.github/scripts/install.sh gbdev-rgbds-92bfe5d/LICENSE000066400000000000000000000021241512540461700154130ustar00rootroot00000000000000The MIT License Copyright (c) 1996-2025, Carsten Sørensen and RGBDS contributors. 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. gbdev-rgbds-92bfe5d/Makefile000066400000000000000000000221641512540461700160540ustar00rootroot00000000000000# SPDX-License-Identifier: MIT .SUFFIXES: .SUFFIXES: .cpp .y .o .PHONY: all clean install checkdiff develop debug profile coverage format tidy iwyu mingw32 mingw64 wine-shim dist # User-defined variables Q := @ PREFIX := /usr/local bindir := ${PREFIX}/bin mandir := ${PREFIX}/share/man SUFFIX := STRIP := -s BINMODE := 755 MANMODE := 644 # Other variables PKG_CONFIG := pkg-config PNGCFLAGS := `${PKG_CONFIG} --cflags libpng` PNGLDFLAGS := `${PKG_CONFIG} --libs-only-L libpng` PNGLDLIBS := `${PKG_CONFIG} --libs-only-l libpng` # Note: if this comes up empty, `version.cpp` will automatically fall back to last release number VERSION_STRING := `git --git-dir=.git -c safe.directory='*' describe --tags --dirty --always 2>/dev/null` WARNFLAGS := -Wall -pedantic -Wno-unknown-warning-option -Wno-gnu-zero-variadic-macro-arguments # Overridable CXXFLAGS CXXFLAGS ?= -O3 -flto -DNDEBUG # Non-overridable CXXFLAGS REALCXXFLAGS := ${CXXFLAGS} ${WARNFLAGS} -std=c++20 -I include -fno-exceptions -fno-rtti # Overridable LDFLAGS LDFLAGS ?= # Non-overridable LDFLAGS REALLDFLAGS := ${LDFLAGS} ${WARNFLAGS} -DBUILD_VERSION_STRING=\"${VERSION_STRING}\" # Wrapper around bison that passes flags depending on what the version supports BISON := src/bison.sh RM := rm -rf # Used for checking pull requests BASE_REF := origin/master # Rules to build the RGBDS binaries all: rgbasm rgblink rgbfix rgbgfx common_obj := \ src/extern/getopt.o \ src/cli.o \ src/diagnostics.o \ src/style.o \ src/usage.o \ src/util.o rgbasm_obj := \ ${common_obj} \ src/asm/actions.o \ src/asm/charmap.o \ src/asm/fixpoint.o \ src/asm/format.o \ src/asm/fstack.o \ src/asm/lexer.o \ src/asm/macro.o \ src/asm/main.o \ src/asm/opt.o \ src/asm/output.o \ src/asm/parser.o \ src/asm/rpn.o \ src/asm/section.o \ src/asm/symbol.o \ src/asm/warning.o \ src/extern/utf8decoder.o \ src/backtrace.o \ src/linkdefs.o \ src/opmath.o \ src/verbosity.o src/asm/lexer.o src/asm/main.o: src/asm/parser.hpp rgblink_obj := \ ${common_obj} \ src/link/assign.o \ src/link/fstack.o \ src/link/lexer.o \ src/link/layout.o \ src/link/main.o \ src/link/object.o \ src/link/output.o \ src/link/patch.o \ src/link/script.o \ src/link/sdas_obj.o \ src/link/section.o \ src/link/symbol.o \ src/link/warning.o \ src/extern/utf8decoder.o \ src/backtrace.o \ src/linkdefs.o \ src/opmath.o \ src/verbosity.o src/link/lexer.o src/link/main.o: src/link/script.hpp rgbfix_obj := \ ${common_obj} \ src/fix/fix.o \ src/fix/main.o \ src/fix/mbc.o \ src/fix/warning.o rgbgfx_obj := \ ${common_obj} \ src/gfx/color_set.o \ src/gfx/main.o \ src/gfx/pal_packing.o \ src/gfx/pal_sorting.o \ src/gfx/pal_spec.o \ src/gfx/palette.o \ src/gfx/png.o \ src/gfx/process.o \ src/gfx/reverse.o \ src/gfx/rgba.o \ src/gfx/warning.o \ src/verbosity.o rgbasm: ${rgbasm_obj} $Q${CXX} ${REALLDFLAGS} -o $@ ${rgbasm_obj} ${REALCXXFLAGS} src/version.cpp rgblink: ${rgblink_obj} $Q${CXX} ${REALLDFLAGS} -o $@ ${rgblink_obj} ${REALCXXFLAGS} src/version.cpp rgbfix: ${rgbfix_obj} $Q${CXX} ${REALLDFLAGS} -o $@ ${rgbfix_obj} ${REALCXXFLAGS} src/version.cpp rgbgfx: ${rgbgfx_obj} $Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ ${rgbgfx_obj} ${REALCXXFLAGS} ${PNGLDLIBS} src/version.cpp test/gfx/randtilegen: test/gfx/randtilegen.cpp $Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCXXFLAGS} ${PNGCFLAGS} ${PNGLDLIBS} test/gfx/rgbgfx_test: test/gfx/rgbgfx_test.cpp $Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCXXFLAGS} ${PNGCFLAGS} ${PNGLDLIBS} # Rules to process files # We want the Bison invocation to pass through our rules, not default ones .y.o: .y.cpp: $Q${BISON} $@ $< # Bison-generated C++ files have an accompanying header src/asm/parser.hpp: src/asm/parser.cpp $Qtouch $@ src/link/script.hpp: src/link/script.cpp $Qtouch $@ # Only RGBGFX uses libpng (POSIX make doesn't support pattern rules to cover all these) src/gfx/color_set.o: src/gfx/color_set.cpp $Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $< src/gfx/main.o: src/gfx/main.cpp $Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $< src/gfx/pal_packing.o: src/gfx/pal_packing.cpp $Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $< src/gfx/pal_sorting.o: src/gfx/pal_sorting.cpp $Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $< src/gfx/pal_spec.o: src/gfx/pal_spec.cpp $Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $< src/gfx/png.o: src/gfx/png.cpp $Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $< src/gfx/process.o: src/gfx/process.cpp $Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $< src/gfx/reverse.o: src/gfx/reverse.cpp $Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $< src/gfx/rgba.o: src/gfx/rgba.cpp $Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $< .cpp.o: $Q${CXX} ${REALCXXFLAGS} -c -o $@ $< # Target used to remove all files generated by other Makefile targets clean: $Q${RM} rgbasm rgbasm.exe $Q${RM} rgblink rgblink.exe $Q${RM} rgbfix rgbfix.exe $Q${RM} rgbgfx rgbgfx.exe $Qfind src/ -name "*.o" -exec rm {} \; $Qfind . -type f \( -name "*.gcno" -o -name "*.gcda" -o -name "*.gcov" \) -exec rm {} \; $Q${RM} rgbshim.sh $Q${RM} src/asm/parser.cpp src/asm/parser.hpp src/asm/stack.hh $Q${RM} src/link/script.cpp src/link/script.hpp src/link/stack.hh $Q${RM} test/gfx/randtilegen test/gfx/rgbgfx_test # Target used to install the binaries and man pages. install: all $Qinstall -d ${DESTDIR}${bindir}/ ${DESTDIR}${mandir}/man1/ ${DESTDIR}${mandir}/man5/ ${DESTDIR}${mandir}/man7/ $Qinstall ${STRIP} -m ${BINMODE} rgbasm ${DESTDIR}${bindir}/rgbasm${SUFFIX} $Qinstall ${STRIP} -m ${BINMODE} rgblink ${DESTDIR}${bindir}/rgblink${SUFFIX} $Qinstall ${STRIP} -m ${BINMODE} rgbfix ${DESTDIR}${bindir}/rgbfix${SUFFIX} $Qinstall ${STRIP} -m ${BINMODE} rgbgfx ${DESTDIR}${bindir}/rgbgfx${SUFFIX} $Qinstall -m ${MANMODE} man/rgbasm.1 man/rgblink.1 man/rgbfix.1 man/rgbgfx.1 ${DESTDIR}${mandir}/man1/ $Qinstall -m ${MANMODE} man/rgbds.5 man/rgbasm.5 man/rgbasm-old.5 man/rgblink.5 ${DESTDIR}${mandir}/man5/ $Qinstall -m ${MANMODE} man/rgbds.7 man/gbz80.7 ${DESTDIR}${mandir}/man7/ # Target used to check for suspiciously missing changed files. checkdiff: $Qcontrib/checkdiff.bash `git merge-base HEAD ${BASE_REF}` # Target used in development to prevent adding new issues to the source code. # All warnings are treated as errors to block the compilation and make the # continous integration infrastructure return failure. # The rationale for some of the flags is documented in the CMakeLists. develop: $Q${MAKE} WARNFLAGS="${WARNFLAGS} -Werror -Wextra \ -Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond \ -Wfloat-equal -Wlogical-op -Wnull-dereference -Wold-style-cast -Wshift-overflow=2 \ -Wstringop-overflow=4 -Wtrampolines -Wundef -Wuninitialized -Wunused -Wshadow \ -Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1 \ -Wno-format-nonliteral -Wno-strict-overflow -Wno-unused-but-set-variable \ -Wno-type-limits -Wno-tautological-constant-out-of-range-compare -Wvla \ -D_GLIBCXX_ASSERTIONS -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG \ -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero" \ CXXFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" # Target used in development to debug with gdb. debug: $Qenv ${MAKE} \ CXXFLAGS="-ggdb3 -O0 -fno-omit-frame-pointer -fno-optimize-sibling-calls" # Target used in development to profile with callgrind. profile: $Qenv ${MAKE} \ CXXFLAGS="-ggdb3 -O3 -fno-omit-frame-pointer -fno-optimize-sibling-calls" # Target used in development to inspect code coverage with gcov. coverage: $Qenv ${MAKE} \ CXXFLAGS="-ggdb3 -Og --coverage -fno-omit-frame-pointer -fno-optimize-sibling-calls" # Target used in development to format source code with clang-format. format: $Qclang-format -i $$(git ls-files '*.hpp' '*.cpp') # Target used in development to check code with clang-tidy. # Requires Bison-generated header files to exist. tidy: src/asm/parser.hpp src/link/script.hpp $Qclang-tidy -p . $$(git ls-files '*.hpp' '*.cpp') # Target used in development to remove unused `#include` headers. iwyu: $Qenv ${MAKE} \ CXX="include-what-you-use" \ REALCXXFLAGS="-std=c++20 -I include" # Targets for the project maintainer to easily create Windows exes. # This is not for Windows users! # If you're building on Windows with Cygwin or MinGW, just follow the Unix # install instructions instead. mingw32: $Q${MAKE} all test/gfx/randtilegen test/gfx/rgbgfx_test \ CXX=i686-w64-mingw32-g++ \ CXXFLAGS="-O3 -flto -DNDEBUG -static-libgcc -static-libstdc++" \ PKG_CONFIG="PKG_CONFIG_SYSROOT_DIR=/usr/i686-w64-mingw32 pkg-config" mingw64: $Q${MAKE} all test/gfx/randtilegen test/gfx/rgbgfx_test \ CXX=x86_64-w64-mingw32-g++ \ PKG_CONFIG="PKG_CONFIG_SYSROOT_DIR=/usr/x86_64-w64-mingw32 pkg-config" wine-shim: $Qecho '#!/usr/bin/env bash' > rgbshim.sh $Qecho 'WINEDEBUG=-all wine $$0.exe "$${@:1}"' >> rgbshim.sh $Qchmod +x rgbshim.sh $Qln -s rgbshim.sh rgbasm $Qln -s rgbshim.sh rgblink $Qln -s rgbshim.sh rgbfix $Qln -s rgbshim.sh rgbgfx # Target for the project maintainer to produce distributable release tarballs # of the source code. dist: $Qgit ls-files | sed s~^~$${PWD##*/}/~ \ | tar -czf rgbds-source.tar.gz -C .. -T - gbdev-rgbds-92bfe5d/README.md000066400000000000000000000036741512540461700157000ustar00rootroot00000000000000# RGBDS RGBDS (Rednex Game Boy Development System) is a free assembler/linker package for the Game Boy and Game Boy Color. It consists of: - RGBASM (assembler) - RGBLINK (linker) - RGBFIX (checksum/header fixer) - RGBGFX (PNG-to-Game Boy graphics converter) This is a fork of the original RGBDS which aims to make the programs more like other UNIX tools. This toolchain is maintained [on GitHub](https://github.com/gbdev/rgbds). The documentation of this toolchain can be [viewed online](https://rgbds.gbdev.io/docs/), including its [basic usage and development history](https://rgbds.gbdev.io/docs/rgbds.7). It is generated from the man pages found in this repository. The source code of the website itself is on GitHub as well under the repository [rgbds-www](https://github.com/gbdev/rgbds-www). If you want to contribute or maintain RGBDS, read [CONTRIBUTING.md](CONTRIBUTING.md). If you have questions regarding the code, its organization, etc. you can find the maintainers [on the GBDev community channels](https://gbdev.io/chat) or via mail at `rgbds at gbdev dot io`. ## Installing RGBDS The [installation procedure](https://rgbds.gbdev.io/install) is available online for various platforms. [Building from source](https://rgbds.gbdev.io/install/source) is possible using `make` or `cmake`; follow the link for more detailed instructions. ```sh make sudo make install ``` ```sh cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build cmake --install build ``` Two parameters available when building are a prefix (e.g. to put the executables in a directory) and a suffix (e.g. to append the version number or commit ID). ```sh make sudo make install PREFIX=install_dir/ SUFFIX=-$(git rev-parse --short HEAD) ``` ```sh cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DSUFFIX=-$(git rev-parse --short HEAD) cmake --build build cmake --install build --prefix install_dir ``` (If you set a `SUFFIX`, it should include the `.exe` extension on Windows.) gbdev-rgbds-92bfe5d/RELEASE.md000066400000000000000000000106451512540461700160170ustar00rootroot00000000000000# Releasing This describes for the maintainers of RGBDS how to publish a new release on GitHub. 1. Update the following files, then commit and push. You can use git commit -m "Release <version>" and `git push origin master`. - [include/version.hpp](include/version.hpp): set appropriate values for `PACKAGE_VERSION_MAJOR`, `PACKAGE_VERSION_MINOR`, `PACKAGE_VERSION_PATCH`, and `PACKAGE_VERSION_RC`. **Only** define `PACKAGE_VERSION_RC` if you are publishing a release candidate! - [Dockerfile](Dockerfile): update `ARG version`. - [test/fetch-test-deps.sh](test/fetch-test-deps.sh): update test dependency commits (preferably, use the latest available). - [man/\*](man/): update dates and authors. 2. Create a Git tag formatted as v<MAJOR>.<MINOR>.<PATCH>, or v<MAJOR>.<MINOR>.<PATCH>-rc<RC> for a release candidate. MAJOR, MINOR, PATCH, and RC should match their values from [include/version.hpp](include/version.hpp). You can use git tag <tag>. 3. Push the tag to GitHub. You can use git push origin <tag>. GitHub Actions will run the [create-release-artifacts.yaml](.github/workflows/create-release-artifacts.yaml) workflow to detect the tag starting with "`v[0-9]`" and automatically do the following: 1. Build 32-bit and 64-bit RGBDS binaries for Windows with `cmake`. 2. Package the binaries into zip files. 3. Package the source code into a tar.gz file with `make dist`. 4. Create a draft GitHub release for the tag, attaching the three packaged files. It will be a prerelease if the tag contains "`-rc`". If an error occurred in the above steps, delete the tag and restart the procedure. You can use git push --delete origin <tag> and git tag --delete <tag>. 4. GitHub Actions will run the [create-release-docs.yml](.github/workflows/create-release-docs.yml) workflow to add the release documentation to [rgbds-www](https://github.com/gbdev/rgbds-www). This is not done automatically for prereleases, since we do not normally publish documentation for them. If you want to manually publish prerelease documentation, such as for an April Fools joke prerelease, 1. Clone [rgbds-www](https://github.com/gbdev/rgbds-www). You can use `git clone https://github.com/gbdev/rgbds-www.git`. 2. Make sure that you have installed `groff` and `mandoc`. You will need `mandoc` 1.14.5 or later to support `-O toc`. 3. Inside of the `man` directory, run <path/to/rgbds-www>/maintainer/man_to_html.sh <tag> * then <path/to/rgbds-www>/maintainer/new_release.sh <tag>. This will render the RGBDS documentation as HTML and PDF and copy it to `rgbds-www`. If you do not have `groff` installed, you can change `groff -Tpdf -mdoc -wall` to `mandoc -Tpdf -I os=Linux` in [maintainer/man_to_html.sh](https://github.com/gbdev/rgbds-www/blob/master/maintainer/man_to_html.sh) and it will suffice. 4. Commit and push the documentation. You can use git commit -m "Create RGBDS <tag> documentation" and `git push origin master` (within the `rgbds-www` directory, not RGBDS). 5. Write a changelog in the GitHub draft release. 6. Click the "Publish release" button to publish it! 7. Update the `release` branch. You can use `git push origin master:release`. 8. Update the following related projects. 1. [rgbds-www](https://github.com/gbdev/rgbds-www): update [src/pages/versions.mdx](https://github.com/gbdev/rgbds-www/blob/master/src/pages/versions.mdx) to list the new release. 2. [rgbds-live](https://github.com/gbdev/rgbds-live): update the `rgbds` submodule (and [patches/rgbds.patch](https://github.com/gbdev/rgbds-live/blob/master/patches/rgbds.patch) if necessary) to use the new release. 3. [rgbobj](https://github.com/gbdev/rgbobj) and [rgbds-obj](https://github.com/gbdev/rgbds-obj): make sure that object files created by the latest RGBASM can be parsed and displayed. If the object file revision has been updated, `rgbobj` will need a corresponding release. gbdev-rgbds-92bfe5d/compile_flags.txt000066400000000000000000000001071512540461700177520ustar00rootroot00000000000000-std=c++20 -I include -fno-exceptions -fno-rtti -fno-caret-diagnostics gbdev-rgbds-92bfe5d/contrib/000077500000000000000000000000001512540461700160475ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/contrib/bash_compl/000077500000000000000000000000001512540461700201565ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/contrib/bash_compl/_rgbasm.bash000077500000000000000000000163311512540461700224360ustar00rootroot00000000000000#!/usr/bin/env bash # Known bugs: # - Newlines in file/directory names break this script # This is because we rely on `compgen -A`, which is broken like this. # A fix would require implementing it ourselves, and no thanks! # - `rgbasm --binary-digits=a` is treated the same as `rgbasm --binary-digits=` (for example) # This is not our fault, Bash passes both of these identically. # Maybe it could be worked around, but such a fix would likely be involved. # The user can work around it by typing `--binary-digits ''` instead, for example. # - Directories are not completed as such in "coalesced" short-opt arguments. For example, # `rgbasm -M d` can autocomplete to `rgbasm -M dir/` (no space), but # `rgbasm -Md` would autocomplete to `rgbasm -Mdir ` (trailing space) instead. # This is because directory handling is performed by Readline, whom we can't tell about the short # opt kerfuffle. The user can work around by separating the argument, as shown above. # (Also, there might be more possible bugs if `-Mdir` is actually a directory. Ugh.) # Something to note: # `rgbasm --binary-digits=a` gets passed to us as ('rgbasm' '--binary-digits' '=' 'a') # Thus, we don't need to do much to handle that form of argument passing: skip '=' after long opts. _rgbasm_completions() { # Format: "long_opt:state_after" # Empty long opt = it doesn't exit # See the `state` variable below for info about `state_after` declare -A opts=( [h]="help:normal" [V]="version:normal" [W]="warning:warning" [w]=":normal" [B]="backtrace:unk" [b]="binary-digits:unk" [D]="define:unk" [E]="export-all:normal" [g]="gfx-chars:unk" [I]="include:dir" [M]="dependfile:glob-*.mk *.d" [o]="output:glob-*.o" [P]="preinclude:glob-*.asm *.inc" [p]="pad-value:unk" [Q]="q-precision:unk" [r]="recursion-depth:unk" [s]="state:unk" [v]="verbose:normal" [X]="max-errors:unk" ) # Parse command-line up to current word local opt_ena=true # Possible states: # - normal = Well, normal. Options are parsed normally. # - unk = An argument that can't be completed, and should just be skipped. # - warning = A warning flag. # - dir = A directory path # - glob-* = A glob, after the dash is a whitespace-separated list of file globs to use local state=normal # The length of the option, used as a return value by the function below local optlen=0 # $1: a short option word # `state` will be set to the parsing state after the last option character in the word. If # "normal" is not returned, `optlen` will be set to the length (dash included) of the "option" # part of the argument. parse_short_opt() { # These options act like a long option (= takes up the entire word), but only use a single dash # So, they need some special handling if [[ "$1" = "-M"[CGP] ]]; then state=normal optlen=${#1} return; elif [[ "$1" = "-M"[QT] ]]; then state='glob-*.d *.mk *.o' optlen=${#1} return; fi for (( i = 1; i < "${#1}"; i++ )); do # If the option is not known, assume it doesn't take an argument local opt="${opts["${1:$i:1}"]:-":normal"}" state="${opt#*:}" # If the option takes an argument, record the length and exit if [[ "$state" != 'normal' ]]; then let optlen="$i + 1" return fi done optlen=0 } for (( i = 1; i < COMP_CWORD; i++ )); do local word="${COMP_WORDS[$i]}" # If currently processing an argument, skip this word if [[ "$state" != 'normal' ]]; then state=normal continue fi if [[ "$word" = '--' ]]; then # Options stop being parsed after this opt_ena=false break fi # Check if it's a long option if [[ "$word" = '--'* ]]; then # If the option is unknown, assume it takes no arguments: keep the state at "normal" for long_opt in "${opts[@]}"; do if [[ "$word" = "--${long_opt%%:*}" ]]; then state="${long_opt#*:}" # Check if the next word is just '='; if so, skip it, the argument must follow # (See "known bugs" at the top of this script) let i++ if [[ "${COMP_WORDS[$i]}" != '=' ]]; then let i-- fi optlen=0 break fi done # Check if it's a short option elif [[ "$word" = '-'* ]]; then parse_short_opt "$word" # The last option takes an argument... if [[ "$state" != 'normal' ]]; then if [[ "$optlen" -ne "${#word}" ]]; then # If it's contained within the word, we won't complete it, revert to "normal" state=normal else # Otherwise, complete it, but start at the beginning of *that* word optlen=0 fi fi fi done # Parse current word # Careful that it might look like an option, so use `--` aggressively! local cur_word="${COMP_WORDS[$i]}" # Process options, as short ones may change the state if $opt_ena && [[ "$state" = 'normal' && "$cur_word" = '-'* ]]; then # We might want to complete to an option or an arg to that option # Parse the option word to check # There's no whitespace in the option names, so we can ride a little dirty... # Is this a long option? if [[ "$cur_word" = '--'* ]]; then # It is, try to complete one mapfile -t COMPREPLY < <(compgen -W "${opts[*]%%:*}" -P '--' -- "${cur_word#--}") return 0 elif [[ "$cur_word" = '-M'[CGPQT] ]]; then # These options act like long opts with no arguments, so return them and exactly them COMPREPLY=( "$cur_word" ) return 0 else # Short options may be grouped, parse them to determine what to complete parse_short_opt "$cur_word" if [[ "$state" = 'normal' ]]; then mapfile -t COMPREPLY < <(compgen -W "${!opts[*]}" -P "$cur_word" ''; compgen -W '-MC -MG -MP -MQ -MT' "$cur_word") return 0 elif [[ "$optlen" = "${#cur_word}" && "$state" != "warning" ]]; then # This short option group only awaits its argument! # Post the option group as-is as a reply so that Readline inserts a space, # so that the next completion request switches to the argument # An exception is made for warnings, since it's idiomatic to stick them to the # `-W`, and it doesn't break anything. COMPREPLY=( "$cur_word" ) return 0 fi fi fi COMPREPLY=() case "$state" in unk) # Return with no replies: no idea what to complete! ;; warning) mapfile -t COMPREPLY < <(compgen -W " assert backwards-for builtin-args charmap-redef div empty-data-directive empty-macro-arg empty-strrpl export-undefined large-constant macro-shift nested-comment numeric-string obsolete purge shift shift-amount truncation unmapped-char unmatched-directive unterminated-load user all extra everything error" -P "${cur_word:0:$optlen}" -- "${cur_word:$optlen}") ;; normal) # Acts like a glob... state="glob-*.asm *.inc *.sm83" ;& glob-*) while read -r word; do COMPREPLY+=("${cur_word:0:$optlen}$word") done < <(for glob in ${state#glob-}; do compgen -A file -X \!"$glob" -- "${cur_word:$optlen}"; done) # Also complete directories ;& dir) while read -r word; do COMPREPLY+=("${cur_word:0:$optlen}$word") done < <(compgen -A directory -- "${cur_word:$optlen}") compopt -o filenames ;; *) echo >&2 "Internal completion error: invalid state \"$state\", please report this bug" return 1 ;; esac } complete -F _rgbasm_completions rgbasm gbdev-rgbds-92bfe5d/contrib/bash_compl/_rgbfix.bash000077500000000000000000000137431512540461700224500ustar00rootroot00000000000000#!/usr/bin/env bash # Same notes as RGBASM _rgbfix_completions() { # Format: "long_opt:state_after" # Empty long opt = it doesn't exit # See the `state` variable below for info about `state_after` declare -A opts=( [h]="help:normal" [V]="version:normal" [W]="warning:warning" [w]=":normal" [C]="color-only:normal" [c]="color-compatible:normal" [f]="fix-spec:fix-spec" [i]="game-id:unk" [j]="non-japanese:normal" [k]="new-licensee:unk" [L]="custom-logo:glob-*.1bpp" [l]="old-licensee:unk" [m]="mbc-type:mbc" [n]="rom-version:unk" [o]="output:glob-*.gb *.gbc *.sgb" [p]="pad-value:unk" [r]="ram-size:unk" [s]="sgb-compatible:normal" [t]="title:unk" [v]="validate:normal" ) # Parse command-line up to current word local opt_ena=true # Possible states: # - normal = Well, normal. Options are parsed normally. # - unk = An argument that can't be completed, and should just be skipped. # - warning = A warning flag. # - dir = A directory path # - glob-* = A glob, after the dash is a whitespace-separated list of file globs to use local state=normal # The length of the option, used as a return value by the function below local optlen=0 # $1: a short option word # `state` will be set to the parsing state after the last option character in the word. If # "normal" is not returned, `optlen` will be set to the length (dash included) of the "option" # part of the argument. parse_short_opt() { for (( i = 1; i < "${#1}"; i++ )); do # If the option is not known, assume it doesn't take an argument local opt="${opts["${1:$i:1}"]:-":normal"}" state="${opt#*:}" # If the option takes an argument, record the length and exit if [[ "$state" != 'normal' ]]; then let optlen="$i + 1" return fi done optlen=0 } for (( i = 1; i < COMP_CWORD; i++ )); do local word="${COMP_WORDS[$i]}" # If currently processing an argument, skip this word if [[ "$state" != 'normal' ]]; then state=normal continue fi if [[ "$word" = '--' ]]; then # Options stop being parsed after this opt_ena=false break fi # Check if it's a long option if [[ "$word" = '--'* ]]; then # If the option is unknown, assume it takes no arguments: keep the state at "normal" for long_opt in "${opts[@]}"; do if [[ "$word" = "--${long_opt%%:*}" ]]; then state="${long_opt#*:}" # Check if the next word is just '='; if so, skip it, the argument must follow # (See "known bugs" at the top of this script) let i++ if [[ "${COMP_WORDS[$i]}" != '=' ]]; then let i-- fi optlen=0 break fi done # Check if it's a short option elif [[ "$word" = '-'* ]]; then parse_short_opt "$word" # The last option takes an argument... if [[ "$state" != 'normal' ]]; then if [[ "$optlen" -ne "${#word}" ]]; then # If it's contained within the word, we won't complete it, revert to "normal" state=normal else # Otherwise, complete it, but start at the beginning of *that* word optlen=0 fi fi fi done # Parse current word # Careful that it might look like an option, so use `--` aggressively! local cur_word="${COMP_WORDS[$i]}" # Process options, as short ones may change the state if $opt_ena && [[ "$state" = 'normal' && "$cur_word" = '-'* ]]; then # We might want to complete to an option or an arg to that option # Parse the option word to check # There's no whitespace in the option names, so we can ride a little dirty... # Is this a long option? if [[ "$cur_word" = '--'* ]]; then # It is, try to complete one mapfile -t COMPREPLY < <(compgen -W "${opts[*]%%:*}" -P '--' -- "${cur_word#--}") return 0 else # Short options may be grouped, parse them to determine what to complete parse_short_opt "$cur_word" if [[ "$state" = 'normal' ]]; then mapfile -t COMPREPLY < <(compgen -W "${!opts[*]}" -P "$cur_word" '') return 0 elif [[ "$optlen" = "${#cur_word}" && "$state" != "warning" ]]; then # This short option group only awaits its argument! # Post the option group as-is as a reply so that Readline inserts a space, # so that the next completion request switches to the argument # An exception is made for warnings, since it's idiomatic to stick them to the # `-W`, and it doesn't break anything. COMPREPLY=( "$cur_word" ) return 0 fi fi fi COMPREPLY=() case "$state" in unk) # Return with no replies: no idea what to complete! ;; warning) mapfile -t COMPREPLY < <(compgen -W " mbc obsolete overwrite sgb truncation all everything error" -P "${cur_word:0:$optlen}" -- "${cur_word:$optlen}") ;; fix-spec) COMPREPLY=( "${cur_word}"{l,h,g,L,H,G} ) ;; mbc) local cur_arg="${cur_word:$optlen}" cur_arg="${cur_arg@U}" compopt -o nosort # Keep `help` first in the list, mainly mapfile -t COMPREPLY < <(compgen -W "help" -P "${cur_word:0:$optlen}" -- "${cur_word:$optlen}") mapfile -t COMPREPLY -O ${#COMPREPLY} < <(compgen -W " ROM_ONLY MBC1{,+RAM,+RAM+BATTERY} MBC2{,+BATTERY} MMM01{,+RAM} MBC3{+TIMER+BATTERY,+TIMER+RAM+BATTERY,,+RAM,+RAM+BATTERY} MBC5{,+RAM,+RAM+BATTERY,+RUMBLE,+RUMBLE+RAM,+RUMBLE+RAM+BATTERY} MBC6 MBC7+SENSOR+RUMBLE+RAM+BATTERY POCKET_CAMERA BANDAI_TAMA5 HUC3 HUC1+RAM+BATTERY TPP1_1.0{,+BATTERY}{,+RTC}{,+RUMBLE,+MULTIRUMBLE}" -P "${cur_word:0:$optlen}" -- "${cur_word/ /_}") ;; normal) # Acts like a glob... state="glob-*.gb *.gbc *.sgb" ;& glob-*) while read -r word; do COMPREPLY+=("${cur_word:0:$optlen}$word") done < <(for glob in ${state#glob-}; do compgen -A file -X \!"$glob" -- "${cur_word:$optlen}"; done) # Also complete directories ;& dir) while read -r word; do COMPREPLY+=("${cur_word:0:$optlen}$word") done < <(compgen -A directory -- "${cur_word:$optlen}") compopt -o filenames ;; *) echo >&2 "Internal completion error: invalid state \"$state\", please report this bug" return 1 ;; esac } complete -F _rgbfix_completions rgbfix gbdev-rgbds-92bfe5d/contrib/bash_compl/_rgbgfx.bash000077500000000000000000000127671512540461700224530ustar00rootroot00000000000000#!/usr/bin/env bash # Same notes as RGBASM _rgbgfx_completions() { # Format: "long_opt:state_after" # Empty long opt = it doesn't exit # See the `state` variable below for info about `state_after` declare -A opts=( [h]="help:normal" [V]="version:normal" [W]="warning:warning" [w]=":normal" [A]="auto-attr-map:normal" [a]="attr-map:glob-*.attrmap" [B]="background-color:unk" [b]="base-tiles:unk" [C]="color-curve:normal" [c]="colors:unk" [d]="depth:unk" [i]="input-tileset:glob-*.2bpp" [L]="slice:unk" [m]="mirror-tiles:normal" [N]="nb-tiles:unk" [n]="nb-palettes:unk" [O]="group-outputs:normal" [o]="output:glob-*.2bpp" [P]="auto-palette:normal" [p]="palette:glob-*.pal" [Q]="auto-palette-map:normal" [q]="palette-map:glob-*.palmap" [r]="reverse:unk" [s]="palette-size:unk" [T]="auto-tilemap:normal" [t]="tilemap:glob-*.tilemap" [u]="unique-tiles:normal" [v]="verbose:normal" [X]="mirror-x:normal" [x]="trim-end:unk" [Y]="mirror-y:normal" [Z]="columns:normal" ) # Parse command-line up to current word local opt_ena=true # Possible states: # - normal = Well, normal. Options are parsed normally. # - unk = An argument that can't be completed, and should just be skipped. # - warning = A warning flag. # - dir = A directory path # - glob-* = A glob, after the dash is a whitespace-separated list of file globs to use local state=normal # The length of the option, used as a return value by the function below local optlen=0 # $1: a short option word # `state` will be set to the parsing state after the last option character in the word. If # "normal" is not returned, `optlen` will be set to the length (dash included) of the "option" # part of the argument. parse_short_opt() { for (( i = 1; i < "${#1}"; i++ )); do # If the option is not known, assume it doesn't take an argument local opt="${opts["${1:$i:1}"]:-":normal"}" state="${opt#*:}" # If the option takes an argument, record the length and exit if [[ "$state" != 'normal' ]]; then let optlen="$i + 1" return fi done optlen=0 } for (( i = 1; i < COMP_CWORD; i++ )); do local word="${COMP_WORDS[$i]}" # If currently processing an argument, skip this word if [[ "$state" != 'normal' ]]; then state=normal continue fi if [[ "$word" = '--' ]]; then # Options stop being parsed after this opt_ena=false break fi # Check if it's a long option if [[ "$word" = '--'* ]]; then # If the option is unknown, assume it takes no arguments: keep the state at "normal" for long_opt in "${opts[@]}"; do if [[ "$word" = "--${long_opt%%:*}" ]]; then state="${long_opt#*:}" # Check if the next word is just '='; if so, skip it, the argument must follow # (See "known bugs" at the top of this script) let i++ if [[ "${COMP_WORDS[$i]}" != '=' ]]; then let i-- fi optlen=0 break fi done # Check if it's a short option elif [[ "$word" = '-'* ]]; then parse_short_opt "$word" # The last option takes an argument... if [[ "$state" != 'normal' ]]; then if [[ "$optlen" -ne "${#word}" ]]; then # If it's contained within the word, we won't complete it, revert to "normal" state=normal else # Otherwise, complete it, but start at the beginning of *that* word optlen=0 fi fi fi done # Parse current word # Careful that it might look like an option, so use `--` aggressively! local cur_word="${COMP_WORDS[$i]}" # Process options, as short ones may change the state if $opt_ena && [[ "$state" = 'normal' && "$cur_word" = '-'* ]]; then # We might want to complete to an option or an arg to that option # Parse the option word to check # There's no whitespace in the option names, so we can ride a little dirty... # Is this a long option? if [[ "$cur_word" = '--'* ]]; then # It is, try to complete one mapfile -t COMPREPLY < <(compgen -W "${opts[*]%%:*}" -P '--' -- "${cur_word#--}") return 0 else # Short options may be grouped, parse them to determine what to complete parse_short_opt "$cur_word" if [[ "$state" = 'normal' ]]; then mapfile -t COMPREPLY < <(compgen -W "${!opts[*]}" -P "$cur_word" '') return 0 elif [[ "$optlen" = "${#cur_word}" && "$state" != "warning" ]]; then # This short option group only awaits its argument! # Post the option group as-is as a reply so that Readline inserts a space, # so that the next completion request switches to the argument # An exception is made for warnings, since it's idiomatic to stick them to the # `-W`, and it doesn't break anything. COMPREPLY=( "$cur_word" ) return 0 fi fi fi COMPREPLY=() case "$state" in unk) # Return with no replies: no idea what to complete! ;; warning) mapfile -t COMPREPLY < <(compgen -W " embedded obsolete trim-nonempty all everything error" -P "${cur_word:0:$optlen}" -- "${cur_word:$optlen}") ;; normal) # Acts like a glob... state="glob-*.png" ;& glob-*) while read -r word; do COMPREPLY+=("${cur_word:0:$optlen}$word") done < <(for glob in ${state#glob-}; do compgen -A file -X \!"$glob" -- "${cur_word:$optlen}"; done) # Also complete directories ;& dir) while read -r word; do COMPREPLY+=("${cur_word:0:$optlen}$word") done < <(compgen -A directory -- "${cur_word:$optlen}") compopt -o filenames ;; *) echo >&2 "Internal completion error: invalid state \"$state\", please report this bug" return 1 ;; esac } complete -F _rgbgfx_completions rgbgfx gbdev-rgbds-92bfe5d/contrib/bash_compl/_rgblink.bash000077500000000000000000000121661512540461700226150ustar00rootroot00000000000000#!/usr/bin/env bash # Same notes as RGBASM _rgblink_completions() { # Format: "long_opt:state_after" # Empty long opt = it doesn't exit # See the `state` variable below for info about `state_after` declare -A opts=( [h]="help:normal" [V]="version:normal" [W]="warning:warning" [M]="no-sym-in-map:normal" [d]="dmg:normal" [B]="backtrace:unk" [l]="linkerscript:glob-*" [m]="map:glob-*.map" [n]="sym:glob-*.sym" [O]="overlay:glob-*.gb *.gbc *.sgb" [o]="output:glob-*.gb *.gbc *.sgb" [p]="pad:unk" [t]="tiny:normal" [v]="verbose:normal" [w]="wramx:normal" [x]="nopad:normal" ) # Parse command-line up to current word local opt_ena=true # Possible states: # - normal = Well, normal. Options are parsed normally. # - unk = An argument that can't be completed, and should just be skipped. # - warning = A warning flag. # - dir = A directory path # - glob-* = A glob, after the dash is a whitespace-separated list of file globs to use local state=normal # The length of the option, used as a return value by the function below local optlen=0 # $1: a short option word # `state` will be set to the parsing state after the last option character in the word. If # "normal" is not returned, `optlen` will be set to the length (dash included) of the "option" # part of the argument. parse_short_opt() { for (( i = 1; i < "${#1}"; i++ )); do # If the option is not known, assume it doesn't take an argument local opt="${opts["${1:$i:1}"]:-":normal"}" state="${opt#*:}" # If the option takes an argument, record the length and exit if [[ "$state" != 'normal' ]]; then let optlen="$i + 1" return fi done optlen=0 } for (( i = 1; i < COMP_CWORD; i++ )); do local word="${COMP_WORDS[$i]}" # If currently processing an argument, skip this word if [[ "$state" != 'normal' ]]; then state=normal continue fi if [[ "$word" = '--' ]]; then # Options stop being parsed after this opt_ena=false break fi # Check if it's a long option if [[ "$word" = '--'* ]]; then # If the option is unknown, assume it takes no arguments: keep the state at "normal" for long_opt in "${opts[@]}"; do if [[ "$word" = "--${long_opt%%:*}" ]]; then state="${long_opt#*:}" # Check if the next word is just '='; if so, skip it, the argument must follow # (See "known bugs" at the top of this script) let i++ if [[ "${COMP_WORDS[$i]}" != '=' ]]; then let i-- fi optlen=0 break fi done # Check if it's a short option elif [[ "$word" = '-'* ]]; then parse_short_opt "$word" # The last option takes an argument... if [[ "$state" != 'normal' ]]; then if [[ "$optlen" -ne "${#word}" ]]; then # If it's contained within the word, we won't complete it, revert to "normal" state=normal else # Otherwise, complete it, but start at the beginning of *that* word optlen=0 fi fi fi done # Parse current word # Careful that it might look like an option, so use `--` aggressively! local cur_word="${COMP_WORDS[$i]}" # Process options, as short ones may change the state if $opt_ena && [[ "$state" = 'normal' && "$cur_word" = '-'* ]]; then # We might want to complete to an option or an arg to that option # Parse the option word to check # There's no whitespace in the option names, so we can ride a little dirty... # Is this a long option? if [[ "$cur_word" = '--'* ]]; then # It is, try to complete one mapfile -t COMPREPLY < <(compgen -W "${opts[*]%%:*}" -P '--' -- "${cur_word#--}") return 0 else # Short options may be grouped, parse them to determine what to complete parse_short_opt "$cur_word" if [[ "$state" = 'normal' ]]; then mapfile -t COMPREPLY < <(compgen -W "${!opts[*]}" -P "$cur_word" '') return 0 elif [[ "$optlen" = "${#cur_word}" && "$state" != "warning" ]]; then # This short option group only awaits its argument! # Post the option group as-is as a reply so that Readline inserts a space, # so that the next completion request switches to the argument # An exception is made for warnings, since it's idiomatic to stick them to the # `-W`, and it doesn't break anything. COMPREPLY=( "$cur_word" ) return 0 fi fi fi COMPREPLY=() case "$state" in unk) # Return with no replies: no idea what to complete! ;; warning) mapfile -t COMPREPLY < <(compgen -W " assert div obsolete shift shift-amount truncation all everything error" -P "${cur_word:0:$optlen}" -- "${cur_word:$optlen}") ;; normal) # Acts like a glob... state="glob-*.o *.obj" ;& glob-*) while read -r word; do COMPREPLY+=("${cur_word:0:$optlen}$word") done < <(for glob in ${state#glob-}; do compgen -A file -X \!"$glob" -- "${cur_word:$optlen}"; done) # Also complete directories ;& dir) while read -r word; do COMPREPLY+=("${cur_word:0:$optlen}$word") done < <(compgen -A directory -- "${cur_word:$optlen}") compopt -o filenames ;; *) echo >&2 "Internal completion error: invalid state \"$state\", please report this bug" return 1 ;; esac } complete -F _rgblink_completions rgblink gbdev-rgbds-92bfe5d/contrib/checkdiff.bash000077500000000000000000000070011512540461700206150ustar00rootroot00000000000000#!/usr/bin/env bash # SPDX-License-Identifier: MIT declare -A FILES while read -r -d '' file; do FILES["$file"]="true" done < <(git diff --name-only -z "$1" HEAD) edited () { ${FILES["$1"]:-"false"} } dependency () { if edited "$1" && ! edited "$2"; then echo "'$1' was modified, but not '$2'! $3" | xargs fi } # Pull requests that edit the first file without the second may be correct, # but are suspicious enough to require review. dependency include/linkdefs.hpp man/rgbds.5 \ "Was the object file format changed?" dependency src/asm/parser.y man/rgbasm.5 \ "Was the rgbasm grammar changed?" dependency src/asm/actions.cpp man/rgbasm.5 \ "Was the rgbasm grammar changed?" dependency src/link/script.y man/rgblink.5 \ "Was the linker script grammar changed?" dependency src/link/layout.cpp man/rgblink.5 \ "Was the linker script grammar changed?" dependency include/asm/warning.hpp man/rgbasm.1 \ "Were the rgbasm warnings changed?" dependency include/link/warning.hpp man/rgblink.1 \ "Were the rgblink warnings changed?" dependency include/fix/warning.hpp man/rgbfix.1 \ "Were the rgbfix warnings changed?" dependency include/gfx/warning.hpp man/rgbgfx.1 \ "Were the rgbgfx warnings changed?" dependency src/asm/object.cpp include/linkdefs.hpp \ "Should the object file revision be bumped?" dependency src/link/object.cpp include/linkdefs.hpp \ "Should the object file revision be bumped?" dependency Makefile CMakeLists.txt \ "Did the build process change?" dependency Makefile src/CMakeLists.txt \ "Did the build process change?" dependency src/asm/main.cpp man/rgbasm.1 \ "Did the rgbasm CLI change?" dependency src/asm/main.cpp contrib/zsh_compl/_rgbasm \ "Did the rgbasm CLI change?" dependency src/asm/main.cpp contrib/bash_compl/_rgbasm.bash \ "Did the rgbasm CLI change?" dependency src/link/main.cpp man/rgblink.1 \ "Did the rgblink CLI change?" dependency src/link/main.cpp contrib/zsh_compl/_rgblink \ "Did the rgblink CLI change?" dependency src/link/main.cpp contrib/bash_compl/_rgblink.bash \ "Did the rgblink CLI change?" dependency src/fix/main.cpp man/rgbfix.1 \ "Did the rgbfix CLI change?" dependency src/fix/main.cpp contrib/zsh_compl/_rgbfix \ "Did the rgbfix CLI change?" dependency src/fix/main.cpp contrib/bash_compl/_rgbfix.bash \ "Did the rgbfix CLI change?" dependency src/gfx/main.cpp man/rgbgfx.1 \ "Did the rgbgfx CLI change?" dependency src/gfx/main.cpp contrib/zsh_compl/_rgbgfx \ "Did the rgbgfx CLI change?" dependency src/gfx/main.cpp contrib/bash_compl/_rgbgfx.bash \ "Did the rgbgfx CLI change?" dependency test/fetch-test-deps.sh CONTRIBUTING.md \ "Did the test protocol change?" dependency test/run-tests.sh CONTRIBUTING.md \ "Did the test protocol change?" dependency test/asm/test.sh CONTRIBUTING.md \ "Did the RGBASM test protocol change?" dependency test/link/test.sh CONTRIBUTING.md \ "Did the RGBLINK test protocol change?" dependency test/fix/test.sh CONTRIBUTING.md \ "Did the RGBFIX test protocol change?" dependency test/gfx/test.sh CONTRIBUTING.md \ "Did the RGBGFX test protocol change?" gbdev-rgbds-92bfe5d/contrib/checkformat.bash000077500000000000000000000004531512540461700212010ustar00rootroot00000000000000#!/usr/bin/env bash # SPDX-License-Identifier: MIT clang-format --version find . -type f \( -iname '*.hpp' -o -iname '*.cpp' \) -exec clang-format -i {} + if ! git diff-index --quiet HEAD --; then echo 'Unformatted files:' git diff-index --name-only HEAD -- echo git diff HEAD -- exit 1 fi gbdev-rgbds-92bfe5d/contrib/coverage.bash000077500000000000000000000014541512540461700205100ustar00rootroot00000000000000#!/usr/bin/env bash set -e # Build RGBDS with gcov support make coverage -j # Run the tests pushd test ./fetch-test-deps.sh if [[ $# -eq 0 ]]; then ./run-tests.sh else ./run-tests.sh --os "$1" fi popd # Generate coverage logs gcov src/**/*.cpp mkdir -p coverage # Generate coverage report, excluding Bison-generated files COVERAGE_INFO=coverage/coverage.info lcov -c --no-external -d . -o "$COVERAGE_INFO" lcov -r "$COVERAGE_INFO" src/asm/parser.{hpp,cpp} src/link/script.{hpp,cpp} -o "$COVERAGE_INFO" genhtml --dark-mode --num-spaces 4 -f -s -o coverage/ "$COVERAGE_INFO" # Check whether running from coverage.yml workflow if [ "$1" != "ubuntu-ci" ]; then # Open report in web browser if [ "$(uname)" == "Darwin" ]; then open coverage/index.html else xdg-open coverage/index.html fi fi gbdev-rgbds-92bfe5d/contrib/gbdiff.bash000077500000000000000000000031741512540461700201370ustar00rootroot00000000000000#!/usr/bin/env bash # SPDX-License-Identifier: MIT STATE=0 diff <(xxd "$1") <(xxd "$2") | while read -r LINE; do if [[ $STATE -eq 0 ]]; then # Discard first line (line info) STATE=1 elif [[ "$LINE" = '---' ]]; then # Separator between files switches states echo "$LINE" STATE=3 elif grep -Eq '^[0-9]+(,[0-9]+)?[cd][0-9]+(,[0-9]+)?' <<< "$LINE"; then # Line info resets the whole thing STATE=1 elif [[ $STATE -eq 1 || $STATE -eq 3 ]]; then # Compute the GB address from the ROM offset OFS=$(cut -d ' ' -f 2 <<< "$LINE" | tr -d ':') BANK=$((0x$OFS / 0x4000)) ADDR=$((0x$OFS % 0x4000 + (BANK != 0) * 0x4000)) # Try finding the preceding symbol closest to the diff if [[ $STATE -eq 1 ]]; then STATE=2 SYMFILE=${1%.*}.sym else STATE=4 SYMFILE=${2%.*}.sym fi EXTRA=$(if [[ -f "$SYMFILE" ]]; then # Read the sym file for such a symbol # Ignore comment lines, only pick matching bank # (The bank regex ignores comments already, make `cut` and `tr` process less lines) grep -Ei "$(printf "^%02x:" $BANK)" "$SYMFILE" | sed "s/$(printf "^%02x:" $BANK)/0x/g" | cut -d ';' -f 1 | tr -d "\r" | sort -g | while read -r SYMADDR SYM; do SYMADDR=$(($SYMADDR)) if [[ $SYMADDR -le $ADDR ]]; then printf " (%s+0x%x)\n" "$SYM" $((ADDR - SYMADDR)) fi done | tail -n 1 fi) printf "%02x:%04x %s\n" $BANK $ADDR "$EXTRA" fi if [[ $STATE -eq 2 || $STATE -eq 4 ]]; then OFS=$(cut -d ' ' -f 2 <<< "$LINE" | tr -d ':') BANK=$((0x$OFS / 0x4000)) ADDR=$((0x$OFS % 0x4000 + (BANK != 0) * 0x4000)) printf "%s %02x:%04x: %s\n" "${LINE:0:1}" $BANK $ADDR "${LINE#*: }" fi done gbdev-rgbds-92bfe5d/contrib/view_palettes.sh000077500000000000000000000011421512540461700212570ustar00rootroot00000000000000#!/bin/bash set -euo pipefail if [[ $# -ne 2 ]]; then cat <&2 Usage: $0 EOF exit 1 fi TMP=$(mktemp -d) readonly TMP trap 'rm -rf "$TMP"' EXIT tile() { for i in {0..7}; do printf "$1"; done } { tile '\x00\x00' && tile '\xFF\x00' && tile '\x00\xFF' && tile '\xFF\xFF'; } >"$TMP/tmp.2bpp" NB_BYTES=$(wc -c <"$1") (( NB_PALS = NB_BYTES / 8 )) for (( i = 0; i < NB_PALS; i++ )); do printf '\0\1\2\3' >>"$TMP/tmp.tilemap" printf $(printf '\\x%x' $i{,,,}) >> "$TMP/tmp.palmap" done "${RGBGFX:-${RGBDS+$RGBDS/}rgbgfx}" -r 4 "$2" -o "$TMP/tmp.2bpp" -OTQ -p "$1" -n "$NB_PALS" gbdev-rgbds-92bfe5d/contrib/zsh_compl/000077500000000000000000000000001512540461700200455ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/contrib/zsh_compl/_rgbasm000066400000000000000000000066531512540461700214140ustar00rootroot00000000000000#compdef rgbasm _rgbasm_warnings() { local warnings=( 'error:Turn all warnings into errors' 'all:Enable most warning messages' 'extra:Enable extra, possibly unwanted, messages' 'everything:Enable literally everything' 'assert:Warn when WARN-type asserts fail' 'backwards-for:Warn when start and stop are backwards relative to step' 'builtin-args:Report incorrect args to built-in funcs' 'charmap-redef:Warn when redefining a charmap mapping' 'div:Warn when dividing the smallest int by -1' 'empty-data-directive:Warn on arg-less d[bwl] in ROM' 'empty-macro-arg:Warn on empty macro arg' 'empty-strrpl:Warn on calling STRRPL with empty pattern' 'export-undefined:Warn on EXPORT of an undefined symbol' 'large-constant:Warn on constants too large for a signed 32-bit int' 'macro-shift:Warn when shifting macro args part their limits' 'nested-comment:Warn on "/*" inside block comments' 'numeric-string:Warn when a multi-character string is treated as a number' 'obsolete:Warn when using deprecated features' 'purge:Warn when purging exported symbols or labels' 'shift:Warn when shifting negative values' 'shift-amount:Warn when a shift'\''s operand is negative or \> 32' 'truncation:Warn when implicit truncation loses bits' 'unmapped-char:Warn on unmapped character' 'unmatched-directive:Warn on unmatched directive pair' 'unterminated-load:Warn on LOAD without ENDL' 'user:Warn when executing the WARN built-in' ) _describe warning warnings } local args=( # Arguments are listed here in the same order as in the manual, except for the version and help '(- : * options)'{-V,--version}'[Print version number and exit]' '(- : * options)'{-h,--help}'[Print help text and exit]' '(-E --export-all)'{-E,--export-all}'[Export all symbols]' '(-v --verbose)'{-v,--verbose}'[Enable verbose output]' -w'[Disable all warnings]' '(-B --backtrace)'{-B,--backtrace}'+[Set backtrace depth or style]:param:' '(-b --binary-digits)'{-b,--binary-digits}'+[Change chars for binary constants]:digit spec:' --color'[Whether to use color in output]:color:(auto always never)' '*'{-D,--define}'+[Define a string symbol]:name + value (default 1):' '(-g --gfx-chars)'{-g,--gfx-chars}'+[Change chars for gfx constants]:chars spec:' '(-I --include)'{-I,--include}'+[Add an include directory]:include path:_files -/' '(-M --dependfile)'{-M,--dependfile}"+[Write dependencies in Makefile format]:output file:_files -g '*.{d,mk}'" -MC'[Continue after missing dependencies]' -MG'[Assume missing dependencies should be generated]' -MP'[Add phony targets to all dependencies]' '*'-MT"+[Add a target to the rules]:target:_files -g '*.{d,mk,o}'" '*'-MQ"+[Add a target to the rules]:target:_files -g '*.{d,mk,o}'" '(-o --output)'{-o,--output}'+[Output file]:output file:_files' '(-P --preinclude)'{-P,--preinclude}"+[Pre-include a file]:include file:_files -g '*.{asm,inc}'" '(-p --pad-value)'{-p,--pad-value}'+[Set padding byte]:padding byte:' '(-Q --q-precision)'{-Q,--q-precision}'+[Set fixed-point precision]:precision:' '(-r --recursion-depth)'{-r,--recursion-depth}'+[Set maximum recursion depth]:depth:' '(-s --state)'{-s,--state}"+[Write features of final state]:state file:_files -g '*.dump.asm'" '(-W --warning)'{-W,--warning}'+[Toggle warning flags]:warning flag:_rgbasm_warnings' '(-X --max-errors)'{-X,--max-errors}'+[Set maximum errors before aborting]:maximum errors:' ":assembly sources:_files -g '*.asm'" ) _arguments -s -S : $args gbdev-rgbds-92bfe5d/contrib/zsh_compl/_rgbfix000066400000000000000000000056021512540461700214130ustar00rootroot00000000000000#compdef rgbfix _mbc_names() { local mbc_names=( 'ROM:$00' 'MBC1:$01' 'MBC1+RAM:$02' 'MBC1+RAM+BATTERY:$03' 'MBC2:$05' 'MBC2+BATTERY:$06' 'ROM+RAM:$08' 'ROM+RAM+BATTERY:$09' 'MMM01:$0B' 'MMM01+RAM:$0C' 'MMM01+RAM+BATTERY:$0D' 'MBC3+TIMER+BATTERY:$0F' 'MBC3+TIMER+RAM+BATTERY:$10' 'MBC3:$11' 'MBC3+RAM:$12' 'MBC3+RAM+BATTERY:$13' 'MBC5:$19' 'MBC5+RAM:$1A' 'MBC5+RAM+BATTERY:$1B' 'MBC5+RUMBLE:$1C' 'MBC5+RUMBLE+RAM:$1D' 'MBC5+RUMBLE+RAM+BATTERY:$1E' 'MBC6:$20' 'MBC7+SENSOR+RUMBLE+RAM+BATTERY:$22' 'POCKET_CAMERA:$FC' 'BANDAI_TAMA5:$FD' 'HUC3:$FE' 'HUC1+RAM+BATTERY:$FF' ) _describe "MBC name" mbc_names } _rgbfix_warnings() { local warnings=( 'error:Turn all warnings into errors' 'all:Enable most warning messages' 'everything:Enable literally everything' 'mbc:Warn about issues with MBC specs' 'obsolete:Warn when using deprecated features' 'overwrite:Warn when overwriting non-zero bytes' 'sgb:Warn when SGB flag conflicts with old licensee code' 'truncation:Warn when values are truncated to fit' ) _describe warning warnings } local args=( # Arguments are listed here in the same order as in the manual, except for the version and help '(- : * options)'{-V,--version}'[Print version number and exit]' '(- : * options)'{-h,--help}'[Print help text and exit]' '(-C --color-only -c --color-compatible)'{-C,--color-only}'[Mark ROM as GBC-only]' '(-C --color-only -c --color-compatible)'{-c,--color-compatible}'[Mark ROM as GBC-compatible]' '(-j --non-japanese)'{-j,--non-japanese}'[Set the non-Japanese region flag]' '(-O --overwrite)'{-O,--overwrite}'[Allow overwriting non-zero bytes]' '(-s --sgb-compatible)'{-s,--sgb-compatible}'[Set the SGB flag]' '(-f --fix-spec -v --validate)'{-v,--validate}'[Shorthand for -f lhg]' -w'[Disable all warnings]' --color'[Whether to use color in output]:color:(auto always never)' '(-f --fix-spec -v --validate)'{-f,--fix-spec}'+[Fix or trash some header values]:fix spec:' '(-i --game-id)'{-i,--game-id}'+[Set game ID string]:4-char game ID:' '(-k --new-licensee)'{-k,--new-licensee}'+[Set new licensee string]:2-char licensee ID:' '(-l --old-licensee)'{-l,--old-licensee}'+[Set old licensee ID]:licensee number:' '(-L --logo)'{-L,--logo}'+[Set custom logo]:1bpp image:' '(-m --mbc-type)'{-m,--mbc-type}"+[Set MBC flags]:mbc name:_mbc_names" '(-n --rom-version)'{-n,--rom-version}'+[Set ROM version]:rom version byte:' '(-o --output)'{-o,--output}"+[Output file]:output file:_files -g '*.{gb,sgb,gbc}'" '(-p --pad-value)'{-p,--pad-value}'+[Pad to next valid size using this byte as padding]:padding byte:' '(-r --ram-size)'{-r,--ram-size}'+[Set RAM size]:ram size byte:' '(-t --title)'{-t,--title}'+[Set title string]:11-char title string:' '(-W --warning)'{-W,--warning}'+[Toggle warning flags]:warning flag:_rgbfix_warnings' '*'":ROM files:_files -g '*.{gb,sgb,gbc}'" ) _arguments -s -S : $args gbdev-rgbds-92bfe5d/contrib/zsh_compl/_rgbgfx000066400000000000000000000065711512540461700214170ustar00rootroot00000000000000#compdef rgbgfx _depths() { local depths=( '1:1bpp' '2:2bpp (native)' ) _describe 'bit depth' depths } _rgbgfx_warnings() { local warnings=( 'error:Turn all warnings into errors' 'all:Enable most warning messages' 'everything:Enable literally everything' 'embedded:Warn when using embedded PLTE without "-c embedded"' 'obsolete:Warn when using deprecated features' 'trim-nonempty:Warn when "-x" trims nonempty tiles' ) _describe warning warnings } local args=( # Arguments are listed here in the same order as in the manual, except for the version and help '(- : * options)'{-V,--version}'[Print version number and exit]' '(- : * options)'{-h,--help}'[Print help text and exit]' '(-a --attr-map -A --auto-attr-map)'{-A,--auto-attr-map}'[Shortcut for -a .attrmap]' '(-C --color-curve)'{-C,--color-curve}'[Generate palettes using GBC color curve]' '(-m --mirror-tiles)'{-m,--mirror-tiles}'[Eliminate mirrored tiles from output]' '(-O --group-outputs)'{-O,--group-outputs}'[Base "shortcut" options on the output path, not input]' '(-p --palette -P --auto-palette)'{-P,--auto-palette}'[Shortcut for -p .pal]' '(-q --palette-map -Q --auto-palette-map)'{-Q,--auto-palette-map}'[Shortcut for -p .palmap]' '(-t --tilemap -T --auto-tilemap)'{-T,--auto-tilemap}'[Shortcut for -t .tilemap]' '(-u --unique-tiles)'{-u,--unique-tiles}'[Eliminate redundant tiles]' '(-v --verbose)'{-v,--verbose}'[Enable verbose output]' -w'[Disable all warnings]' '(-X --mirror-x)'{-X,--mirror-x}'[Eliminate horizontally mirrored tiles from output]' '(-Y --mirror-y)'{-Y,--mirror-y}'[Eliminate vertically mirrored tiles from output]' '(-Z --columns)'{-Z,--columns}'[Read the image in column-major order]' '(-a --attr-map -A --auto-attr-map)'{-a,--attr-map}'+[Generate a map of tile attributes (mirroring)]:attrmap file:_files' '(-B --background-color)'{-B,--background-color}'+[Ignore tiles containing only specified color]:color:' '(-b --base-tiles)'{-b,--base-tiles}'+[Base tile IDs for tile map output]:base tile IDs:' --color'[Whether to use color in output]:color:(auto always never)' '(-c --colors)'{-c,--colors}'+[Specify color palettes]:palette spec:' '(-d --depth)'{-d,--depth}'+[Set bit depth]:bit depth:_depths' '(-i --input-tileset)'{-i,--input-tileset}'+[Use specific tiles]:tileset file:_files -g "*.2bpp"' '(-L --slice)'{-L,--slice}'+[Only process a portion of the image]:input slice:' '(-N --nb-tiles)'{-N,--nb-tiles}'+[Limit number of tiles]:tile count:' '(-n --nb-palettes)'{-n,--nb-palettes}'+[Limit number of palettes]:palette count:' '(-o --output)'{-o,--output}'+[Set output file]:output file:_files' '(-p --palette -P --auto-palette)'{-p,--palette}"+[Output the image's palette in little-endian native RGB555 format]:palette file:_files" '(-q --palette-map -Q --auto-palette-map)'{-q,--palette-map}"+[Output the image's palette map]:palette map file:_files" '(-r --reverse)'{-r,--reverse}'+[Yield an image from binary data]:image width (in tiles):' '(-s --palette-size)'{-s,--palette-size}'+[Limit palette size]:palette size:' '(-t --tilemap -T --auto-tilemap)'{-t,--tilemap}'+[Generate a map of tile indices]:tilemap file:_files' '(-W --warning)'{-W,--warning}'+[Toggle warning flags]:warning flag:_rgbgfx_warnings' '(-x --trim-end)'{-x,--trim-end}'+[Trim end of output by this many tiles]:tile count:' ":input png file:_files -g '*.png'" ) _arguments -s -S : $args gbdev-rgbds-92bfe5d/contrib/zsh_compl/_rgblink000066400000000000000000000040041512540461700215550ustar00rootroot00000000000000#compdef rgblink _rgblink_warnings() { local warnings=( 'error:Turn all warnings into errors' 'all:Enable most warning messages' 'everything:Enable literally everything' 'assert:Warn when WARN-type asserts fail' 'div:Warn when dividing the smallest int by -1' 'obsolete:Warn when using deprecated features' 'shift:Warn when shifting negative values' 'shift-amount:Warn when a shift'\''s operand is negative or \> 32' 'truncation:Warn when implicit truncation loses bits' ) _describe warning warnings } local args=( # Arguments are listed here in the same order as in the manual, except for the version and help '(- : * options)'{-V,--version}'[Print version number and exit]' '(- : * options)'{-h,--help}'[Print help text and exit]' '(-d --dmg)'{-d,--dmg}'[Enable DMG mode (-w + no VRAM banking)]' '(-t --tiny)'{-t,--tiny}'[Enable tiny mode, disabling ROM banking]' '(-v --verbose)'{-v,--verbose}'[Enable verbose output]' '(-w --wramx)'{-w,--wramx}'[Disable WRAM banking]' '(-x --nopad)'{-x,--nopad}'[Disable padding the end of the final file]' '(-B --backtrace)'{-B,--backtrace}'+[Set backtrace depth or style]:param:' --color'[Whether to use color in output]:color:(auto always never)' '(-l --linkerscript)'{-l,--linkerscript}"+[Use a linker script]:linker script:_files -g '*.link'" '(-M --no-sym-in-map)'{-M,--no-sym-in-map}'[Do not output symbol names in map file]' '(-m --map)'{-m,--map}"+[Produce a map file]:map file:_files -g '*.map'" '(-n --sym)'(-n,--sym)"+[Produce a symbol file]:sym file:_files -g '*.sym'" '(-O --overlay)'{-O,--overlay}'+[Overlay sections over on top of bin file]:base overlay:_files' '(-o --output)'{-o,--output}"+[Write ROM image to this file]:rom file:_files -g '*.{gb,sgb,gbc}'" '(-p --pad-value)'{-p,--pad-value}'+[Set padding byte]:padding byte:' '(-S --scramble)'{-s,--scramble}'+[Activate scrambling]:scramble spec' '(-W --warning)'{-W,--warning}'+[Toggle warning flags]:warning flag:_rgblink_warnings' '*'":object files:_files -g '*.o'" ) _arguments -s -S : $args gbdev-rgbds-92bfe5d/include/000077500000000000000000000000001512540461700160325ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/include/asm/000077500000000000000000000000001512540461700166125ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/include/asm/actions.hpp000066400000000000000000000037421512540461700207710ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_ASM_ACTIONS_HPP #define RGBDS_ASM_ACTIONS_HPP #include #include #include #include #include #include #include #include "linkdefs.hpp" // AssertionType, RPNCommand #include "asm/rpn.hpp" // Expression struct AlignmentSpec { uint8_t alignment; uint16_t alignOfs; }; void act_If(int32_t condition); void act_Elif(int32_t condition); void act_Else(); void act_Endc(); AlignmentSpec act_Alignment(int32_t alignment, int32_t alignOfs); void act_Assert(AssertionType type, Expression const &expr, std::string const &message); void act_StaticAssert(AssertionType type, int32_t condition, std::string const &message); std::optional act_ReadFile(std::string const &name, uint32_t maxLen); uint32_t act_CharToNum(std::string const &str); uint32_t act_StringToNum(std::string const &str); int32_t act_CharVal(std::string const &str); int32_t act_CharVal(std::string const &str, int32_t negIdx); uint8_t act_StringByte(std::string const &str, int32_t negIdx); size_t act_StringLen(std::string const &str, bool printErrors); std::string act_StringSlice(std::string const &str, int32_t negStart, std::optional negStop); std::string act_StringSub(std::string const &str, int32_t negPos, std::optional optLen); size_t act_CharLen(std::string const &str); std::string act_StringChar(std::string const &str, int32_t negIdx); std::string act_CharSub(std::string const &str, int32_t negPos); int32_t act_CharCmp(std::string_view str1, std::string_view str2); std::string act_StringReplace(std::string_view str, std::string const &old, std::string const &rep); std::string act_StringFormat( std::string const &spec, std::vector> const &args ); std::string act_SectionName(std::string const &symName); void act_CompoundAssignment(std::string const &symName, RPNCommand op, int32_t constValue); #endif // RGBDS_ASM_ACTIONS_HPP gbdev-rgbds-92bfe5d/include/asm/charmap.hpp000066400000000000000000000020741512540461700207410ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_ASM_CHARMAP_HPP #define RGBDS_ASM_CHARMAP_HPP #include #include #include #include #include #include #define DEFAULT_CHARMAP_NAME "main" bool charmap_ForEach( void (*mapFunc)(std::string const &), void (*charFunc)(std::string const &, std::vector) ); void charmap_New(std::string const &name, std::string const *baseName); void charmap_Set(std::string const &name); void charmap_Push(); void charmap_Pop(); void charmap_CheckStack(); void charmap_Add(std::string const &mapping, std::vector &&value); bool charmap_HasChar(std::string const &mapping); size_t charmap_CharSize(std::string const &mapping); std::optional charmap_CharValue(std::string const &mapping, size_t idx); std::vector charmap_Convert(std::string const &input); size_t charmap_ConvertNext(std::string_view &input, std::vector *output); std::string charmap_Reverse(std::vector const &value, bool &unique); #endif // RGBDS_ASM_CHARMAP_HPP gbdev-rgbds-92bfe5d/include/asm/fixpoint.hpp000066400000000000000000000014541512540461700211670ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_ASM_FIXPOINT_HPP #define RGBDS_ASM_FIXPOINT_HPP #include int32_t fix_Sin(int32_t i, int32_t q); int32_t fix_Cos(int32_t i, int32_t q); int32_t fix_Tan(int32_t i, int32_t q); int32_t fix_ASin(int32_t i, int32_t q); int32_t fix_ACos(int32_t i, int32_t q); int32_t fix_ATan(int32_t i, int32_t q); int32_t fix_ATan2(int32_t i, int32_t j, int32_t q); int32_t fix_Mul(int32_t i, int32_t j, int32_t q); int32_t fix_Mod(int32_t i, int32_t j, int32_t q); int32_t fix_Div(int32_t i, int32_t j, int32_t q); int32_t fix_Pow(int32_t i, int32_t j, int32_t q); int32_t fix_Log(int32_t i, int32_t j, int32_t q); int32_t fix_Round(int32_t i, int32_t q); int32_t fix_Ceil(int32_t i, int32_t q); int32_t fix_Floor(int32_t i, int32_t q); #endif // RGBDS_ASM_FIXPOINT_HPP gbdev-rgbds-92bfe5d/include/asm/format.hpp000066400000000000000000000011661512540461700206170ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_ASM_FORMAT_HPP #define RGBDS_ASM_FORMAT_HPP #include #include #include class FormatSpec { int sign; bool exact; bool alignLeft; bool padZero; size_t width; bool hasFrac; size_t fracWidth; bool hasPrec; size_t precision; int type; bool parsed; public: bool isValid() const { return !!type; } bool isParsed() const { return parsed; } size_t parseSpec(char const *spec); void appendString(std::string &str, std::string const &value) const; void appendNumber(std::string &str, uint32_t value) const; }; #endif // RGBDS_ASM_FORMAT_HPP gbdev-rgbds-92bfe5d/include/asm/fstack.hpp000066400000000000000000000052221512540461700205770ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Contains some assembler-wide defines and externs #ifndef RGBDS_ASM_FSTACK_HPP #define RGBDS_ASM_FSTACK_HPP #include #include #include #include #include #include #include #include "linkdefs.hpp" #include "asm/lexer.hpp" struct FileStackNode { FileStackNodeType type; std::variant< std::vector, // NODE_REPT std::string // NODE_FILE, NODE_MACRO > data; bool isQuiet; // Whether to omit this node from error reporting std::shared_ptr parent; // Pointer to parent node, for error reporting // Line at which the parent context was exited // Meaningless at the root level, but gets written to the object file anyway, so init it uint32_t lineNo = 0; // Set only if referenced: ID within the object file, `UINT32_MAX` if not output yet uint32_t ID = UINT32_MAX; // REPT iteration counts since last named node, in reverse depth order std::vector &iters() { return std::get>(data); } std::vector const &iters() const { return std::get>(data); } // File name for files, file::macro name for macros std::string &name() { return std::get(data); } std::string const &name() const { return std::get(data); } FileStackNode( FileStackNodeType type_, std::variant, std::string> data_, bool isQuiet_ ) : type(type_), data(data_), isQuiet(isQuiet_) {} void printBacktrace(uint32_t curLineNo) const; }; struct MacroArgs; void fstk_VerboseOutputConfig(); void fstk_TraceCurrent(); std::shared_ptr fstk_GetFileStack(); std::shared_ptr fstk_GetUniqueIDStr(); MacroArgs *fstk_GetCurrentMacroArgs(); void fstk_AddIncludePath(std::string const &path); void fstk_AddPreIncludeFile(std::string const &path); std::optional fstk_FindFile(std::string const &path); bool fstk_FileError(std::string const &path, char const *description); bool fstk_FailedOnMissingInclude(); bool yywrap(); bool fstk_RunInclude(std::string const &path, bool isQuiet); void fstk_RunMacro( std::string const ¯oName, std::shared_ptr macroArgs, bool isQuiet ); void fstk_RunRept(uint32_t count, int32_t reptLineNo, ContentSpan const &span, bool isQuiet); void fstk_RunFor( std::string const &symName, int32_t start, int32_t stop, int32_t step, int32_t reptLineNo, ContentSpan const &span, bool isQuiet ); bool fstk_Break(); void fstk_NewRecursionDepth(size_t newDepth); bool fstk_Init(std::string const &mainPath); #endif // RGBDS_ASM_FSTACK_HPP gbdev-rgbds-92bfe5d/include/asm/lexer.hpp000066400000000000000000000100461512540461700204430ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_ASM_LEXER_HPP #define RGBDS_ASM_LEXER_HPP #include #include #include #include #include #include #include #include #include "platform.hpp" // SSIZE_MAX // This value is a compromise between `LexerState` allocation performance when reading the entire // file works, and buffering performance when it doesn't (e.g. when piping a file into RGBASM). static constexpr size_t LEXER_BUF_SIZE = 64; // The buffer needs to be large enough for the maximum `lexerState->peek()` lookahead distance static_assert(LEXER_BUF_SIZE > 1, "Lexer buffer size is too small"); // This caps the size of buffer reads, and according to POSIX, passing more than SSIZE_MAX is UB static_assert(LEXER_BUF_SIZE <= SSIZE_MAX, "Lexer buffer size is too large"); enum LexerMode { LEXER_NORMAL, LEXER_RAW, LEXER_SKIP_TO_ELIF, LEXER_SKIP_TO_ENDC, LEXER_SKIP_TO_ENDR, NB_LEXER_MODES }; struct Expansion { std::optional name; std::shared_ptr contents; size_t offset; // Cursor into `contents` size_t size() const { return contents->size(); } bool advance(); // Increment `offset`; return whether it then exceeds `contents` }; struct ContentSpan { std::shared_ptr ptr; size_t size; }; struct ViewedContent { ContentSpan span; // Span of chars size_t offset = 0; // Cursor into `span.ptr` ViewedContent(ContentSpan const &span_) : span(span_) {} ViewedContent(std::shared_ptr ptr, size_t size) : span({.ptr = ptr, .size = size}) {} std::shared_ptr makeSharedContentPtr() const { return std::shared_ptr(span.ptr, &span.ptr[offset]); } }; struct BufferedContent { int fd; // File from which to read chars char buf[LEXER_BUF_SIZE] = {}; // Circular buffer of chars size_t offset = 0; // Cursor into `buf` size_t size = 0; // Number of "fresh" chars in `buf` BufferedContent(int fd_) : fd(fd_) {} ~BufferedContent(); void advance(); // Increment `offset` circularly, decrement `size` void refill(); // Read from `fd` to fill `buf` private: size_t readMore(size_t startIndex, size_t nbChars); }; struct IfStackEntry { bool ranIfBlock; // Whether an IF/ELIF/ELSE block ran already bool reachedElseBlock; // Whether an ELSE block ran already }; struct LexerState { std::string path; LexerMode mode; bool atLineStart; uint32_t lineNo; int lastToken; int nextToken; std::deque ifStack; bool capturing; // Whether the text being lexed should be captured size_t captureSize; // Amount of text captured std::shared_ptr> captureBuf; // Buffer to send the captured text to if set bool disableExpansions; size_t expansionScanDistance; // Max distance already scanned for expansions bool expandStrings; std::deque expansions; // Front is the innermost current expansion std::variant content; ~LexerState(); int peekChar(); int peekCharAhead(); std::shared_ptr makeSharedCaptureBufPtr() const { return std::shared_ptr(captureBuf, captureBuf->data()); } void setAsCurrentState(); void setFileAsNextState(std::string const &filePath, bool updateStateNow); void setViewAsNextState(char const *name, ContentSpan const &span, uint32_t lineNo_); void clear(uint32_t lineNo_); }; void lexer_SetBinDigits(char const digits[2]); void lexer_SetGfxDigits(char const digits[4]); bool lexer_AtTopLevel(); void lexer_RestartRept(uint32_t lineNo); void lexer_SetMode(LexerMode mode); void lexer_ToggleStringExpansion(bool enable); uint32_t lexer_GetIFDepth(); void lexer_IncIFDepth(); void lexer_DecIFDepth(); bool lexer_RanIFBlock(); bool lexer_ReachedELSEBlock(); void lexer_RunIFBlock(); void lexer_ReachELSEBlock(); void lexer_CheckRecursionDepth(); uint32_t lexer_GetLineNo(); void lexer_TraceStringExpansions(); struct Capture { uint32_t lineNo; ContentSpan span; }; Capture lexer_CaptureRept(); Capture lexer_CaptureMacro(); #endif // RGBDS_ASM_LEXER_HPP gbdev-rgbds-92bfe5d/include/asm/macro.hpp000066400000000000000000000010231512540461700204200ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_ASM_MACRO_HPP #define RGBDS_ASM_MACRO_HPP #include #include #include #include struct MacroArgs { uint32_t shift; std::vector> args; uint32_t nbArgs() const { return args.size() - shift; } std::shared_ptr getArg(int32_t i) const; std::shared_ptr getAllArgs() const; void appendArg(std::shared_ptr arg); void shiftArgs(int32_t count); }; #endif // RGBDS_ASM_MACRO_HPP gbdev-rgbds-92bfe5d/include/asm/main.hpp000066400000000000000000000025741512540461700202570ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_ASM_MAIN_HPP #define RGBDS_ASM_MAIN_HPP #include #include #include #include enum MissingInclude { INC_ERROR, // A missing included file is an error that halts assembly GEN_EXIT, // A missing included file is assumed to be generated; exit normally GEN_CONTINUE, // A missing included file is assumed to be generated; continue assembling }; struct Options { bool exportAll = false; // -E uint8_t fixPrecision = 16; // -Q size_t maxRecursionDepth = 64; // -r char binDigits[2] = {'0', '1'}; // -b char gfxDigits[4] = {'0', '1', '2', '3'}; // -g FILE *dependFile = nullptr; // -M std::optional targetFileName{}; // -MQ, -MT MissingInclude missingIncludeState = INC_ERROR; // -MC, -MG bool generatePhonyDeps = false; // -MP std::optional objectFileName{}; // -o uint8_t padByte = 0; // -p uint64_t maxErrors = 0; // -X ~Options() { if (dependFile) { fclose(dependFile); } } void printDep(std::string const &depName) { if (dependFile) { fprintf(dependFile, "%s: %s\n", targetFileName->c_str(), depName.c_str()); } } }; extern Options options; #endif // RGBDS_ASM_MAIN_HPP gbdev-rgbds-92bfe5d/include/asm/opt.hpp000066400000000000000000000006141512540461700201260ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_ASM_OPT_HPP #define RGBDS_ASM_OPT_HPP #include void opt_B(char const binDigits[2]); void opt_G(char const gfxDigits[4]); void opt_P(uint8_t padByte); void opt_Q(uint8_t fixPrecision); void opt_W(char const *flag); void opt_Parse(char const *option); void opt_Push(); void opt_Pop(); void opt_CheckStack(); #endif // RGBDS_ASM_OPT_HPP gbdev-rgbds-92bfe5d/include/asm/output.hpp000066400000000000000000000014321512540461700206630ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_ASM_OUTPUT_HPP #define RGBDS_ASM_OUTPUT_HPP #include #include #include #include #include "linkdefs.hpp" struct Expression; struct FileStackNode; struct Symbol; enum StateFeature { STATE_EQU, STATE_VAR, STATE_EQUS, STATE_CHAR, STATE_MACRO, NB_STATE_FEATURES }; void out_RegisterNode(std::shared_ptr node); void out_RegisterSymbol(Symbol &sym); void out_CreatePatch(uint32_t type, Expression const &expr, uint32_t ofs, uint32_t pcShift); void out_CreateAssert( AssertionType type, Expression const &expr, std::string const &message, uint32_t ofs ); void out_WriteObject(); void out_WriteState(std::string name, std::vector const &features); #endif // RGBDS_ASM_OUTPUT_HPP gbdev-rgbds-92bfe5d/include/asm/rpn.hpp000066400000000000000000000036031512540461700201240ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_ASM_RPN_HPP #define RGBDS_ASM_RPN_HPP #include #include #include #include #include "linkdefs.hpp" struct Symbol; struct RPNValue { RPNCommand command; // The RPN_* command ID std::variant data; // Data after the ID, if any RPNValue(RPNCommand cmd); RPNValue(RPNCommand cmd, uint8_t val); RPNValue(RPNCommand cmd, uint32_t val); RPNValue(RPNCommand cmd, std::string const &name); void appendEncoded(std::vector &buffer) const; }; struct Expression { std::variant< int32_t, // If the expression's value is known, it's here std::string // Why the expression is not known, if it isn't > data = 0; std::vector rpn{}; // Values to be serialized into the RPN expression bool isKnown() const { return std::holds_alternative(data); } int32_t value() const { return std::get(data); } int32_t getConstVal() const; Symbol const *symbolOf() const; bool isDiffConstant(Symbol const *symName) const; void makeNumber(uint32_t value); void makeSymbol(std::string const &symName); void makeBankSymbol(std::string const &symName); void makeBankSection(std::string const §Name); void makeSizeOfSection(std::string const §Name); void makeStartOfSection(std::string const §Name); void makeSizeOfSectionType(SectionType type); void makeStartOfSectionType(SectionType type); void makeUnaryOp(RPNCommand op, Expression &&src); void makeBinaryOp(RPNCommand op, Expression &&src1, Expression const &src2); void addCheckHRAM(); void addCheckRST(); void addCheckBitIndex(uint8_t mask); void checkNBit(uint8_t n) const; void encode(std::vector &buffer) const; }; bool checkNBit(int32_t v, uint8_t n, char const *name); #endif // RGBDS_ASM_RPN_HPP gbdev-rgbds-92bfe5d/include/asm/section.hpp000066400000000000000000000055461512540461700210010ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_ASM_SECTION_HPP #define RGBDS_ASM_SECTION_HPP #include #include #include #include #include #include #include #include "linkdefs.hpp" struct Expression; struct FileStackNode; struct Section; struct Patch { std::shared_ptr src; uint32_t lineNo; uint32_t offset; Section *pcSection; uint32_t pcOffset; uint8_t type; std::vector rpn; }; struct Section { std::string name; SectionType type; SectionModifier modifier; std::shared_ptr src; // Where the section was defined uint32_t fileLine; // Line where the section was defined uint32_t size; uint32_t org; uint32_t bank; uint8_t align; // Exactly as specified in `ALIGN[]` uint16_t alignOfs; std::deque patches; std::vector data; uint32_t getID() const; // ID of the section in the object file (`UINT32_MAX` if none) bool isSizeKnown() const; }; struct SectionSpec { uint32_t bank; uint8_t alignment; uint16_t alignOfs; }; size_t sect_CountSections(); void sect_ForEach(void (*callback)(Section &)); Section *sect_FindSectionByName(std::string const &name); void sect_NewSection( std::string const &name, SectionType type, uint32_t org, SectionSpec const &attrs, SectionModifier mod ); void sect_SetLoadSection( std::string const &name, SectionType type, uint32_t org, SectionSpec const &attrs, SectionModifier mod ); void sect_EndLoadSection(char const *cause); void sect_CheckLoadClosed(); Section *sect_GetSymbolSection(); uint32_t sect_GetSymbolOffset(); uint32_t sect_GetOutputOffset(); std::optional sect_GetOutputBank(); Patch *sect_AddOutputPatch(); uint32_t sect_GetAlignBytes(uint8_t alignment, uint16_t offset); void sect_AlignPC(uint8_t alignment, uint16_t offset); void sect_CheckSizes(); void sect_StartUnion(); void sect_NextUnionMember(); void sect_EndUnion(); void sect_CheckUnionClosed(); void sect_ConstByte(uint8_t byte); void sect_ByteString(std::vector const &str); void sect_WordString(std::vector const &str); void sect_LongString(std::vector const &str); void sect_Skip(uint32_t skip, bool ds); void sect_RelByte(Expression const &expr, uint32_t pcShift); void sect_RelBytes(uint32_t n, std::vector const &exprs); void sect_RelWord(Expression const &expr, uint32_t pcShift); void sect_RelLong(Expression const &expr, uint32_t pcShift); void sect_PCRelByte(Expression const &expr, uint32_t pcShift); bool sect_BinaryFile(std::string const &name, uint32_t startPos); bool sect_BinaryFileSlice(std::string const &name, uint32_t startPos, uint32_t length); void sect_EndSection(); void sect_PushSection(); void sect_PopSection(); void sect_CheckStack(); std::string sect_PushSectionFragmentLiteral(); #endif // RGBDS_ASM_SECTION_HPP gbdev-rgbds-92bfe5d/include/asm/symbol.hpp000066400000000000000000000073531512540461700206400ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_ASM_SYMBOL_HPP #define RGBDS_ASM_SYMBOL_HPP #include #include #include #include #include #include #include "asm/lexer.hpp" #include "asm/section.hpp" enum SymbolType { SYM_LABEL, SYM_EQU, SYM_VAR, SYM_MACRO, SYM_EQUS, SYM_REF // Forward reference to a label }; struct Symbol; // Forward declaration for `sym_IsPC` bool sym_IsPC(Symbol const *sym); // Forward declaration for `getSection` struct Symbol { std::string name; SymbolType type; bool isBuiltin; bool isExported; // Not relevant for SYM_MACRO or SYM_EQUS bool isQuiet; // Only relevant for SYM_MACRO Section *section; std::shared_ptr src; // Where the symbol was defined uint32_t fileLine; // Line where the symbol was defined std::variant< int32_t, // If isNumeric() int32_t (*)(), // If isNumeric() via a callback ContentSpan, // For SYM_MACRO std::shared_ptr, // For SYM_EQUS std::shared_ptr (*)() // For SYM_EQUS via a callback > data; uint32_t ID; // ID of the symbol in the object file (`UINT32_MAX` if none) uint32_t defIndex; // Ordering of the symbol in the state file bool isDefined() const { return type != SYM_REF; } bool isNumeric() const { return type == SYM_LABEL || type == SYM_EQU || type == SYM_VAR; } bool isLabel() const { return type == SYM_LABEL || type == SYM_REF; } bool isConstant() const { if (type == SYM_LABEL) { Section const *sect = getSection(); return sect && sect->org != UINT32_MAX; } return type == SYM_EQU || type == SYM_VAR; } Section *getSection() const { return sym_IsPC(this) ? sect_GetSymbolSection() : section; } int32_t getValue() const; int32_t getOutputValue() const; ContentSpan const &getMacro() const; std::shared_ptr getEqus() const; uint32_t getConstantValue() const; }; bool sym_IsDotScope(std::string const &symName); void sym_ForEach(void (*callback)(Symbol &)); Symbol *sym_AddLocalLabel(std::string const &symName); Symbol *sym_AddLabel(std::string const &symName); Symbol *sym_AddAnonLabel(); std::string sym_MakeAnonLabelName(uint32_t ofs, bool neg); void sym_Export(std::string const &symName); Symbol *sym_AddEqu(std::string const &symName, int32_t value); Symbol *sym_RedefEqu(std::string const &symName, int32_t value); Symbol *sym_AddVar(std::string const &symName, int32_t value); int32_t sym_GetRSValue(); void sym_SetRSValue(int32_t value); // Find a symbol by exact name, bypassing expansion checks Symbol *sym_FindExactSymbol(std::string const &symName); // Find a symbol, possibly scoped, by name Symbol *sym_FindScopedSymbol(std::string const &symName); // Find a scoped symbol by name; do not return `@` or `_NARG` when they have no value Symbol *sym_FindScopedValidSymbol(std::string const &symName); Symbol const *sym_GetPC(); Symbol *sym_AddMacro( std::string const &symName, int32_t defLineNo, ContentSpan const &span, bool isQuiet ); Symbol *sym_Ref(std::string const &symName); Symbol *sym_AddString(std::string const &symName, std::shared_ptr value); Symbol *sym_RedefString(std::string const &symName, std::shared_ptr value); void sym_Purge(std::string const &symName); bool sym_IsPurgedExact(std::string const &symName); bool sym_IsPurgedScoped(std::string const &symName); void sym_Init(time_t now); // Functions to save and restore the current label scopes. std::pair sym_GetCurrentLabelScopes(); void sym_SetCurrentLabelScopes(std::pair newScopes); void sym_ResetCurrentLabelScopes(); #endif // RGBDS_ASM_SYMBOL_HPP gbdev-rgbds-92bfe5d/include/asm/warning.hpp000066400000000000000000000064051512540461700207750ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_ASM_WARNING_HPP #define RGBDS_ASM_WARNING_HPP #include #include "diagnostics.hpp" enum WarningLevel { LEVEL_DEFAULT, // Warnings that are enabled by default LEVEL_ALL, // Warnings that probably indicate an error LEVEL_EXTRA, // Warnings that are less likely to indicate an error LEVEL_EVERYTHING, // Literally every warning }; enum WarningID { WARNING_ASSERT, // Assertions WARNING_BACKWARDS_FOR, // `FOR` loop with backwards range WARNING_BUILTIN_ARG, // Invalid args to builtins WARNING_CHARMAP_REDEF, // Charmap entry re-definition WARNING_DIV, // Undefined division behavior WARNING_EMPTY_DATA_DIRECTIVE, // `db`, `dw` or `dl` directive without data in ROM WARNING_EMPTY_MACRO_ARG, // Empty macro argument WARNING_EMPTY_STRRPL, // Empty second argument in `STRRPL` WARNING_EXPORT_UNDEFINED, // `EXPORT` of an undefined symbol WARNING_LARGE_CONSTANT, // Constants too large WARNING_MACRO_SHIFT, // `SHIFT` past available arguments in macro WARNING_NESTED_COMMENT, // Comment-start delimiter in a block comment WARNING_OBSOLETE, // Obsolete/deprecated things WARNING_SHIFT, // Undefined `SHIFT` behavior WARNING_SHIFT_AMOUNT, // Strange `SHIFT` amount WARNING_UNMATCHED_DIRECTIVE, // `PUSH[C|O|S]` without `POP[C|O|S]` WARNING_UNTERMINATED_LOAD, // `LOAD` without `ENDL` WARNING_USER, // User-defined `WARN`ings NB_PLAIN_WARNINGS, // Warnings past this point are "parametric" warnings, only mapping to a single flag // Treating string as number may lose some bits WARNING_NUMERIC_STRING_1 = NB_PLAIN_WARNINGS, WARNING_NUMERIC_STRING_2, // Purging an exported symbol or label WARNING_PURGE_1, WARNING_PURGE_2, // Implicit truncation loses some bits WARNING_TRUNCATION_1, WARNING_TRUNCATION_2, // Character without charmap entry WARNING_UNMAPPED_CHAR_1, WARNING_UNMAPPED_CHAR_2, NB_WARNINGS, }; extern Diagnostics warnings; // Used to warn the user about problems that don't prevent the generation of // valid code. [[gnu::format(printf, 2, 3)]] void warning(WarningID id, char const *fmt, ...); // Used for errors that compromise the whole assembly process by affecting the // following code, potencially making the assembler generate errors caused by // the first one and unrelated to the code that the assembler complains about. // It is also used when the assembler goes into an invalid state (for example, // when it fails to allocate memory). [[gnu::format(printf, 1, 2), noreturn]] void fatal(char const *fmt, ...); // Used for errors that make it impossible to assemble correctly, but don't // affect the following code. The code will fail to assemble but the user will // get a list of all errors at the end, making it easier to fix all of them at // once. [[gnu::format(printf, 1, 2)]] void error(char const *fmt, ...); // Used for errors that handle their own backtrace output. The code will fail // to assemble but the user will get a list of all errors at the end, making it // easier to fix all of them at once. void errorNoTrace(std::function callback); void requireZeroErrors(); #endif // RGBDS_ASM_WARNING_HPP gbdev-rgbds-92bfe5d/include/backtrace.hpp000066400000000000000000000040651512540461700204670ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_BACKTRACE_HPP #define RGBDS_BACKTRACE_HPP #include #include #include #include #include #include "style.hpp" #define TRACE_SEPARATOR "<-" #define NODE_SEPARATOR "::" #define REPT_NODE_PREFIX "REPT~" struct Tracing { uint64_t depth = 0; bool collapse = false; bool loud = false; }; extern Tracing tracing; bool trace_ParseTraceDepth(char const *arg); template void trace_PrintBacktrace(std::vector const &stack, NameFnT getName, LineNoFnT getLineNo) { size_t n = stack.size(); if (n == 0) { return; // LCOV_EXCL_LINE } auto printLocation = [&](size_t i) { NodeT const &item = stack[n - i - 1]; style_Reset(stderr); if (!tracing.collapse) { fputs(" ", stderr); // Just three spaces; the fourth will be printed next } fprintf(stderr, " %s ", i == 0 ? "at" : TRACE_SEPARATOR); style_Set(stderr, STYLE_CYAN, true); fputs(getName(item), stderr); style_Set(stderr, STYLE_CYAN, false); fprintf(stderr, "(%" PRIu32 ")", getLineNo(item)); if (!tracing.collapse) { putc('\n', stderr); } }; if (tracing.collapse) { fputs(" ", stderr); // Just three spaces; the fourth will be handled by the loop } if (tracing.depth == 0 || static_cast(tracing.depth) >= n) { for (size_t i = 0; i < n; ++i) { printLocation(i); } } else { size_t last = tracing.depth / 2; size_t first = tracing.depth - last; size_t skipped = n - tracing.depth; for (size_t i = 0; i < first; ++i) { printLocation(i); } style_Reset(stderr); if (tracing.collapse) { fputs(" " TRACE_SEPARATOR, stderr); } else { fputs(" ", stderr); // Just three spaces; the fourth will be printed next } fprintf(stderr, " ...%zu more%s", skipped, last ? "..." : ""); if (!tracing.collapse) { putc('\n', stderr); } for (size_t i = n - last; i < n; ++i) { printLocation(i); } } if (tracing.collapse) { putc('\n', stderr); } style_Reset(stderr); } #endif // RGBDS_BACKTRACE_HPP gbdev-rgbds-92bfe5d/include/cli.hpp000066400000000000000000000006011512540461700173070ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_CLI_HPP #define RGBDS_CLI_HPP #include #include #include "extern/getopt.hpp" // option #include "usage.hpp" void cli_ParseArgs( int argc, char *argv[], char const *shortOpts, option const *longOpts, void (*parseArg)(int, char *), Usage usage ); #endif // RGBDS_CLI_HPP gbdev-rgbds-92bfe5d/include/diagnostics.hpp000066400000000000000000000141711512540461700210560ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_DIAGNOSTICS_HPP #define RGBDS_DIAGNOSTICS_HPP #include #include #include #include #include #include #include #include #include #include "helpers.hpp" #include "itertools.hpp" [[gnu::format(printf, 1, 2)]] void warnx(char const *fmt, ...); enum WarningAbled { WARNING_DEFAULT, WARNING_ENABLED, WARNING_DISABLED }; struct WarningState { WarningAbled state; WarningAbled error; void update(WarningState other); }; std::pair> getInitialWarningState(std::string &flag); template struct WarningFlag { char const *name; LevelEnumT level; }; enum WarningBehavior { DISABLED, ENABLED, ERROR }; template struct ParamWarning { WarningEnumT firstID; WarningEnumT lastID; uint8_t defaultLevel; }; template struct DiagnosticsState { WarningState flagStates[WarningEnumT::NB_WARNINGS]; WarningState metaStates[WarningEnumT::NB_WARNINGS]; bool warningsEnabled = true; bool warningsAreErrors = false; }; template struct Diagnostics { std::vector> metaWarnings; std::vector> warningFlags; std::vector> paramWarnings; DiagnosticsState state; uint64_t nbErrors; void incrementErrors() { if (nbErrors != UINT64_MAX) { ++nbErrors; } } WarningBehavior getWarningBehavior(WarningEnumT id) const; void processWarningFlag(char const *flag); }; template WarningBehavior Diagnostics::getWarningBehavior(WarningEnumT id) const { // Check if warnings are globally disabled if (!state.warningsEnabled) { return WarningBehavior::DISABLED; } // Get the state of this warning flag WarningState const &flagState = state.flagStates[id]; WarningState const &metaState = state.metaStates[id]; // If subsequent checks determine that the warning flag is enabled, this checks whether it has // -Werror without -Wno-error= or -Wno-error=, which makes it into an error bool warningIsError = state.warningsAreErrors && flagState.error != WARNING_DISABLED && metaState.error != WARNING_DISABLED; WarningBehavior enabledBehavior = warningIsError ? WarningBehavior::ERROR : WarningBehavior::ENABLED; // First, check the state of the specific warning flag if (flagState.state == WARNING_DISABLED) { // -Wno- return WarningBehavior::DISABLED; } if (flagState.error == WARNING_ENABLED) { // -Werror= return WarningBehavior::ERROR; } if (flagState.state == WARNING_ENABLED) { // -W return enabledBehavior; } // If no flag is specified, check the state of the "meta" flags that affect this warning flag if (metaState.state == WARNING_DISABLED) { // -Wno- return WarningBehavior::DISABLED; } if (metaState.error == WARNING_ENABLED) { // -Werror= return WarningBehavior::ERROR; } if (metaState.state == WARNING_ENABLED) { // -W return enabledBehavior; } // If no meta flag is specified, check the default state of this warning flag if (warningFlags[id].level == LevelEnumT::LEVEL_DEFAULT) { // enabled by default return enabledBehavior; } // No flag enables this warning, explicitly or implicitly return WarningBehavior::DISABLED; } template void Diagnostics::processWarningFlag(char const *flag) { std::string rootFlag = flag; // Check for `-Werror` or `-Wno-error` to return early if (rootFlag == "error") { // `-Werror` promotes warnings to errors state.warningsAreErrors = true; return; } else if (rootFlag == "no-error") { // `-Wno-error` disables promotion of warnings to errors state.warningsAreErrors = false; return; } auto [flagState, param] = getInitialWarningState(rootFlag); // Try to match the flag against a parametric warning // If there was an equals sign, it will have set `param`; if not, `param` will be 0, // which applies to all levels for (ParamWarning const ¶mWarning : paramWarnings) { WarningEnumT baseID = paramWarning.firstID; uint8_t maxParam = paramWarning.lastID - baseID + 1; assume(paramWarning.defaultLevel <= maxParam); if (rootFlag != warningFlags[baseID].name) { continue; } // If making the warning an error but param is 0, set to the maximum // This accommodates `-Werror=`, but also `-Werror==0`, which is // thus filtered out by the caller. // A param of 0 makes sense for disabling everything, but neither for // enabling nor "erroring". Use the default for those. if (!param.has_value() || *param == 0) { param = paramWarning.defaultLevel; } else if (*param > maxParam) { warnx( "Invalid warning flag parameter \"%s=%" PRIu32 "\"; capping at maximum %" PRIu8, rootFlag.c_str(), *param, maxParam ); *param = maxParam; } // Set the first to enabled/error, and disable the rest for (uint32_t ofs = 0; ofs < maxParam; ++ofs) { if (WarningState &warning = state.flagStates[baseID + ofs]; ofs < *param) { warning.update(flagState); } else { warning.state = WARNING_DISABLED; } } return; } if (param.has_value()) { warnx("Unknown warning flag parameter \"%s=%" PRIu32 "\"", rootFlag.c_str(), *param); return; } // Try to match against a "meta" warning for (WarningFlag const &metaWarning : metaWarnings) { if (rootFlag != metaWarning.name) { continue; } // Set each of the warning flags that meets this level for (WarningEnumT id : EnumSeq(WarningEnumT::NB_WARNINGS)) { if (metaWarning.level >= warningFlags[id].level) { state.metaStates[id].update(flagState); } } return; } // Try to match against a "normal" flag for (WarningEnumT id : EnumSeq(WarningEnumT::NB_PLAIN_WARNINGS)) { if (rootFlag == warningFlags[id].name) { state.flagStates[id].update(flagState); return; } } warnx("Unknown warning flag \"%s\"", rootFlag.c_str()); } #endif // RGBDS_DIAGNOSTICS_HPP gbdev-rgbds-92bfe5d/include/extern/000077500000000000000000000000001512540461700173375ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/include/extern/getopt.hpp000066400000000000000000000011711512540461700213520ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // This implementation was taken from musl and modified for RGBDS #ifndef RGBDS_EXTERN_GETOPT_HPP #define RGBDS_EXTERN_GETOPT_HPP // clang-format off: vertically align values static constexpr int no_argument = 0; static constexpr int required_argument = 1; static constexpr int optional_argument = 2; // clang-format on extern char *musl_optarg; extern int musl_optind, musl_optopt; struct option { char const *name; int has_arg; int *flag; int val; }; int musl_getopt_long_only(int argc, char **argv, char const *optstring, option const *longopts); #endif // RGBDS_EXTERN_GETOPT_HPP gbdev-rgbds-92bfe5d/include/extern/utf8decoder.hpp000066400000000000000000000004301512540461700222610ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_EXTERN_UTF8DECODER_HPP #define RGBDS_EXTERN_UTF8DECODER_HPP #include #define UTF8_ACCEPT 0 #define UTF8_REJECT 12 uint32_t decode(uint32_t *state, uint32_t *codep, uint8_t byte); #endif // RGBDS_EXTERN_UTF8DECODER_HPP gbdev-rgbds-92bfe5d/include/file.hpp000066400000000000000000000036151512540461700174670ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_FILE_HPP #define RGBDS_FILE_HPP #include #include #include #include #include #include #include #include "helpers.hpp" // assume #include "platform.hpp" class File { std::variant _file; public: File() : _file(nullptr) {} // This should only be called once, and before doing any `->` operations. // Returns `nullptr` on error, and a non-null pointer otherwise. File *open(std::string const &path, std::ios_base::openmode mode) { if (path != "-") { return _file.emplace().open(path, mode) ? this : nullptr; } else if (mode & std::ios_base::in) { assume(!(mode & std::ios_base::out)); _file.emplace(std::cin.rdbuf()); if (setmode(STDIN_FILENO, (mode & std::ios_base::binary) ? O_BINARY : O_TEXT) == -1) { return nullptr; } } else { assume(mode & std::ios_base::out); _file.emplace(std::cout.rdbuf()); } return this; } std::streambuf &operator*() { return std::holds_alternative(_file) ? std::get(_file) : *std::get(_file); } std::streambuf const &operator*() const { // The non-`const` version does not perform any modifications, so it's okay. return **const_cast(this); } std::streambuf *operator->() { return &**this; } std::streambuf const *operator->() const { // See the `operator*` equivalent. return const_cast(this)->operator->(); } char const *c_str(std::string const &path) const { return std::holds_alternative(_file) ? path.c_str() : std::get(_file) == std::cin.rdbuf() ? "" : ""; } }; #endif // RGBDS_FILE_HPP gbdev-rgbds-92bfe5d/include/fix/000077500000000000000000000000001512540461700166205ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/include/fix/fix.hpp000066400000000000000000000002631512540461700201200ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_FIX_FIX_HPP #define RGBDS_FIX_FIX_HPP bool fix_ProcessFile(char const *name, char const *outputName); #endif // RGBDS_FIX_FIX_HPP gbdev-rgbds-92bfe5d/include/fix/main.hpp000066400000000000000000000025241512540461700202600ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_FIX_MAIN_HPP #define RGBDS_FIX_MAIN_HPP #include #include #include #include "fix/mbc.hpp" // UNSPECIFIED, MbcType // clang-format off: vertically align values static constexpr uint8_t FIX_LOGO = 1 << 7; static constexpr uint8_t TRASH_LOGO = 1 << 6; static constexpr uint8_t FIX_HEADER_SUM = 1 << 5; static constexpr uint8_t TRASH_HEADER_SUM = 1 << 4; static constexpr uint8_t FIX_GLOBAL_SUM = 1 << 3; static constexpr uint8_t TRASH_GLOBAL_SUM = 1 << 2; // clang-format on enum Model { DMG, BOTH, CGB }; struct Options { uint8_t fixSpec = 0; // -f, -v Model model = DMG; // -C, -c bool japanese = true; // -j uint16_t oldLicensee = UNSPECIFIED; // -l uint16_t romVersion = UNSPECIFIED; // -n uint16_t padValue = UNSPECIFIED; // -p uint16_t ramSize = UNSPECIFIED; // -r bool sgb = false; // -s std::optional gameID; // -i uint8_t gameIDLen; std::optional newLicensee; // -k uint8_t newLicenseeLen; std::optional logoFilename; // -L uint8_t logo[48] = {}; MbcType cartridgeType = MBC_NONE; // -m uint8_t tpp1Rev[2]; std::optional title; // -t uint8_t titleLen; }; extern Options options; #endif // RGBDS_FIX_MAIN_HPP gbdev-rgbds-92bfe5d/include/fix/mbc.hpp000066400000000000000000000033571512540461700201020ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_FIX_MBC_HPP #define RGBDS_FIX_MBC_HPP #include #include constexpr uint16_t UNSPECIFIED = 0x200; static_assert(UNSPECIFIED > 0xFF, "UNSPECIFIED should not be in byte range!"); enum MbcType { ROM = 0x00, ROM_RAM = 0x08, ROM_RAM_BATTERY = 0x09, MBC1 = 0x01, MBC1_RAM = 0x02, MBC1_RAM_BATTERY = 0x03, MBC2 = 0x05, MBC2_BATTERY = 0x06, MMM01 = 0x0B, MMM01_RAM = 0x0C, MMM01_RAM_BATTERY = 0x0D, MBC3 = 0x11, MBC3_TIMER_BATTERY = 0x0F, MBC3_TIMER_RAM_BATTERY = 0x10, MBC3_RAM = 0x12, MBC3_RAM_BATTERY = 0x13, MBC5 = 0x19, MBC5_RAM = 0x1A, MBC5_RAM_BATTERY = 0x1B, MBC5_RUMBLE = 0x1C, MBC5_RUMBLE_RAM = 0x1D, MBC5_RUMBLE_RAM_BATTERY = 0x1E, MBC6 = 0x20, MBC7_SENSOR_RUMBLE_RAM_BATTERY = 0x22, POCKET_CAMERA = 0xFC, BANDAI_TAMA5 = 0xFD, HUC3 = 0xFE, HUC1_RAM_BATTERY = 0xFF, // "Extended" values (still valid, but not directly actionable) // A high byte of 0x01 means TPP1, the low byte is the requested features // This does not include SRAM, which is instead implied by a non-zero SRAM size // Note: Multiple rumble speeds imply rumble TPP1 = 0x100, TPP1_RUMBLE = 0x101, TPP1_MULTIRUMBLE_RUMBLE = 0x103, TPP1_TIMER = 0x104, TPP1_TIMER_RUMBLE = 0x105, TPP1_TIMER_MULTIRUMBLE_RUMBLE = 0x107, TPP1_BATTERY = 0x108, TPP1_BATTERY_RUMBLE = 0x109, TPP1_BATTERY_MULTIRUMBLE_RUMBLE = 0x10B, TPP1_BATTERY_TIMER = 0x10C, TPP1_BATTERY_TIMER_RUMBLE = 0x10D, TPP1_BATTERY_TIMER_MULTIRUMBLE_RUMBLE = 0x10F, // Error values MBC_NONE = UNSPECIFIED, // No MBC specified, do not act on it }; bool mbc_HasRAM(MbcType type); char const *mbc_Name(MbcType type); MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor); #endif // RGBDS_FIX_MBC_HPP gbdev-rgbds-92bfe5d/include/fix/warning.hpp000066400000000000000000000022541512540461700210010ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_FIX_WARNING_HPP #define RGBDS_FIX_WARNING_HPP #include #include "diagnostics.hpp" enum WarningLevel { LEVEL_DEFAULT, // Warnings that are enabled by default LEVEL_ALL, // Warnings that probably indicate an error LEVEL_EVERYTHING, // Literally every warning }; enum WarningID { WARNING_MBC, // Issues with MBC specs WARNING_OBSOLETE, // Obsolete/deprecated things WARNING_OVERWRITE, // Overwriting non-zero bytes WARNING_SGB, // SGB flag conflicts with old licensee code WARNING_TRUNCATION, // Truncating values to fit NB_PLAIN_WARNINGS, NB_WARNINGS = NB_PLAIN_WARNINGS, }; extern Diagnostics warnings; // Warns the user about problems that don't prevent fixing the ROM header [[gnu::format(printf, 2, 3)]] void warning(WarningID id, char const *fmt, ...); // Prints an error, and increments the error count [[gnu::format(printf, 1, 2)]] void error(char const *fmt, ...); // Prints an error, and exits with failure [[gnu::format(printf, 1, 2), noreturn]] void fatal(char const *fmt, ...); uint32_t checkErrors(char const *filename); #endif // RGBDS_FIX_WARNING_HPP gbdev-rgbds-92bfe5d/include/gfx/000077500000000000000000000000001512540461700166165ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/include/gfx/color_set.hpp000066400000000000000000000017101512540461700213170ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_GFX_COLOR_SET_HPP #define RGBDS_GFX_COLOR_SET_HPP #include #include #include class ColorSet { public: static constexpr size_t capacity = 4; private: // Up to 4 colors, sorted, and where SIZE_MAX means the slot is empty // (OK because it's not a valid color index) // Sorting is done on the raw numerical values to lessen `compare`'s complexity std::array _colorIndices{UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX}; public: // Adds the specified color to the set, or **silently drops it** if the set is full. void add(uint16_t color); enum ComparisonResult { NEITHER, WE_BIGGER, THEY_BIGGER = -1, }; ComparisonResult compare(ColorSet const &other) const; size_t size() const; bool empty() const; decltype(_colorIndices)::const_iterator begin() const; decltype(_colorIndices)::const_iterator end() const; }; #endif // RGBDS_GFX_COLOR_SET_HPP gbdev-rgbds-92bfe5d/include/gfx/flip.hpp000066400000000000000000000013531512540461700202630ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_GFX_FLIP_HPP #define RGBDS_GFX_FLIP_HPP #include #include // Flipping tends to happen fairly often, so take a bite out of dcache to speed it up static std::array flipTable = ([]() constexpr { std::array table{}; for (uint16_t i = 0; i < table.size(); ++i) { // To flip all the bits, we'll flip both nibbles, then each nibble half, etc. uint16_t byte = i; byte = (byte & 0b0000'1111) << 4 | (byte & 0b1111'0000) >> 4; byte = (byte & 0b0011'0011) << 2 | (byte & 0b1100'1100) >> 2; byte = (byte & 0b0101'0101) << 1 | (byte & 0b1010'1010) >> 1; table[i] = byte; } return table; })(); #endif // RGBDS_GFX_FLIP_HPP gbdev-rgbds-92bfe5d/include/gfx/main.hpp000066400000000000000000000042121512540461700202520ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_GFX_MAIN_HPP #define RGBDS_GFX_MAIN_HPP #include #include #include #include #include #include "helpers.hpp" // assume #include "gfx/rgba.hpp" struct Options { bool useColorCurve = false; // -C bool allowDedup = false; // -u bool allowMirroringX = false; // -X, -m bool allowMirroringY = false; // -Y, -m bool columnMajor = false; // -Z std::string attrmap{}; // -a, -A std::optional bgColor{}; // -B std::array baseTileIDs{0, 0}; // -b enum { NO_SPEC, EXPLICIT, EMBEDDED, DMG, } palSpecType = NO_SPEC; // -c std::vector, 4>> palSpec{}; uint8_t palSpecDmg = 0; uint8_t bitDepth = 2; // -d std::string inputTileset{}; // -i struct { uint16_t left; uint16_t top; uint16_t width; uint16_t height; uint32_t right() const { return left + width * 8; } uint32_t bottom() const { return top + height * 8; } } inputSlice{0, 0, 0, 0}; // -L (margins in clockwise order, like CSS) uint8_t basePalID = 0; // -l std::array maxNbTiles{UINT16_MAX, 0}; // -N uint16_t nbPalettes = 8; // -n std::string output{}; // -o std::string palettes{}; // -p, -P std::string palmap{}; // -q, -Q uint16_t reversedWidth = 0; // -r, in tiles uint8_t nbColorsPerPal = 0; // -s; 0 means "auto" = 1 << bitDepth; std::string tilemap{}; // -t, -T uint64_t trim = 0; // -x std::string input{}; // positional arg mutable bool hasTransparentPixels = false; uint8_t maxOpaqueColors() const { return nbColorsPerPal - hasTransparentPixels; } uint16_t maxNbColors() const { return nbColorsPerPal * nbPalettes; } uint8_t dmgColors[4] = {}; uint8_t dmgValue(uint8_t i) const { assume(i < 4); return (palSpecDmg >> (2 * i)) & 0b11; } }; extern Options options; #endif // RGBDS_GFX_MAIN_HPP gbdev-rgbds-92bfe5d/include/gfx/pal_packing.hpp000066400000000000000000000006351512540461700216030ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_GFX_PAL_PACKING_HPP #define RGBDS_GFX_PAL_PACKING_HPP #include #include #include struct Palette; class ColorSet; // Returns which palette each color set maps to, and how many palettes are necessary std::pair, size_t> overloadAndRemove(std::vector const &colorSets); #endif // RGBDS_GFX_PAL_PACKING_HPP gbdev-rgbds-92bfe5d/include/gfx/pal_sorting.hpp000066400000000000000000000013051512540461700216470ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_GFX_PAL_SORTING_HPP #define RGBDS_GFX_PAL_SORTING_HPP #include #include #include #include #include "gfx/rgba.hpp" // Allow a slot for every possible CGB color, plus one for transparency // 32 (1 << 5) per channel, times 3 RGB channels = 32768 CGB colors static constexpr size_t NB_COLOR_SLOTS = (1 << (5 * 3)) + 1; struct Palette; void sortIndexed(std::vector &palettes, std::vector const &embPal); void sortGrayscale( std::vector &palettes, std::array, NB_COLOR_SLOTS> const &colors ); void sortRgb(std::vector &palettes); #endif // RGBDS_GFX_PAL_SORTING_HPP gbdev-rgbds-92bfe5d/include/gfx/pal_spec.hpp000066400000000000000000000006011512540461700211120ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_GFX_PAL_SPEC_HPP #define RGBDS_GFX_PAL_SPEC_HPP #include void parseInlinePalSpec(char const * const rawArg); void parseExternalPalSpec(char const *arg); void parseDmgPalSpec(char const * const rawArg); void parseDmgPalSpec(uint8_t palSpecDmg); void parseBackgroundPalSpec(char const *arg); #endif // RGBDS_GFX_PAL_SPEC_HPP gbdev-rgbds-92bfe5d/include/gfx/palette.hpp000066400000000000000000000013701512540461700207660ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_GFX_PALETTE_HPP #define RGBDS_GFX_PALETTE_HPP #include #include #include struct Palette { // An array of 4 GBC-native (RGB555) colors std::array colors{UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX}; void addColor(uint16_t color); uint8_t indexOf(uint16_t color) const; uint16_t &operator[](size_t index) { return colors[index]; } uint16_t const &operator[](size_t index) const { return colors[index]; } decltype(colors)::iterator begin(); decltype(colors)::iterator end(); decltype(colors)::const_iterator begin() const; decltype(colors)::const_iterator end() const; uint8_t size() const; }; #endif // RGBDS_GFX_PALETTE_HPP gbdev-rgbds-92bfe5d/include/gfx/png.hpp000066400000000000000000000005511512540461700201140ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_GFX_PNG_HPP #define RGBDS_GFX_PNG_HPP #include #include #include #include "gfx/rgba.hpp" struct Png { uint32_t width, height; std::vector pixels{}; std::vector palette{}; Png() {} Png(char const *filename, std::streambuf &file); }; #endif // RGBDS_GFX_PNG_HPP gbdev-rgbds-92bfe5d/include/gfx/process.hpp000066400000000000000000000002471512540461700210100ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_GFX_PROCESS_HPP #define RGBDS_GFX_PROCESS_HPP void processPalettes(); void process(); #endif // RGBDS_GFX_PROCESS_HPP gbdev-rgbds-92bfe5d/include/gfx/reverse.hpp000066400000000000000000000002171512540461700210020ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_GFX_REVERSE_HPP #define RGBDS_GFX_REVERSE_HPP void reverse(); #endif // RGBDS_GFX_REVERSE_HPP gbdev-rgbds-92bfe5d/include/gfx/rgba.hpp000066400000000000000000000036421512540461700202470ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_GFX_RGBA_HPP #define RGBDS_GFX_RGBA_HPP #include struct Rgba { uint8_t red; uint8_t green; uint8_t blue; uint8_t alpha; constexpr Rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : red(r), green(g), blue(b), alpha(a) {} // Constructs the color from a "packed" RGBA representation (0xRRGGBBAA) explicit constexpr Rgba(uint32_t rgba = 0) : red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {} static constexpr Rgba fromCGBColor(uint16_t color) { constexpr auto _5to8 = [](uint8_t channel) -> uint8_t { channel &= 0b11111; // For caller's convenience return channel << 3 | channel >> 2; }; return { _5to8(color), _5to8(color >> 5), _5to8(color >> 10), static_cast(color & 0x8000 ? 0x00 : 0xFF), }; } // Returns this RGBA as a 32-bit number that can be printed in hex (`%08x`) to yield its CSS // representation uint32_t toCSS() const { constexpr auto shl = [](uint8_t val, unsigned shift) { return static_cast(val) << shift; }; return shl(red, 24) | shl(green, 16) | shl(blue, 8) | shl(alpha, 0); } bool operator==(Rgba const &rhs) const { return toCSS() == rhs.toCSS(); } // CGB colors are RGB555, so we use bit 15 to signify that the color is transparent instead // Since the rest of the bits don't matter then, we return 0x8000 exactly. static constexpr uint16_t transparent = 0b1'00000'00000'00000; static constexpr uint8_t transparency_threshold = 0x10; bool isTransparent() const { return alpha < transparency_threshold; } static constexpr uint8_t opacity_threshold = 0xF0; bool isOpaque() const { return alpha >= opacity_threshold; } // Computes the equivalent CGB color, respects the color curve depending on options uint16_t cgbColor() const; bool isGray() const { return red == green && green == blue; } uint8_t grayIndex() const; }; #endif // RGBDS_GFX_RGBA_HPP gbdev-rgbds-92bfe5d/include/gfx/warning.hpp000066400000000000000000000023361512540461700210000ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_GFX_WARNING_HPP #define RGBDS_GFX_WARNING_HPP #include "diagnostics.hpp" enum WarningLevel { LEVEL_DEFAULT, // Warnings that are enabled by default LEVEL_ALL, // Warnings that probably indicate an error LEVEL_EVERYTHING, // Literally every warning }; enum WarningID { WARNING_EMBEDDED, // Using an embedded PNG palette without '-c embedded' WARNING_OBSOLETE, // Obsolete/deprecated things WARNING_TRIM_NONEMPTY, // '-x' trims nonempty tiles NB_PLAIN_WARNINGS, NB_WARNINGS = NB_PLAIN_WARNINGS, }; extern Diagnostics warnings; // Warns the user about problems that don't prevent valid graphics conversion [[gnu::format(printf, 2, 3)]] void warning(WarningID id, char const *fmt, ...); // Prints the error count, and exits with failure [[noreturn]] void giveUp(); // If any error has been emitted thus far, calls `giveUp()` void requireZeroErrors(); // Prints an error, and increments the error count [[gnu::format(printf, 1, 2)]] void error(char const *fmt, ...); // Prints a fatal error, increments the error count, and gives up [[gnu::format(printf, 1, 2), noreturn]] void fatal(char const *fmt, ...); #endif // RGBDS_GFX_WARNING_HPP gbdev-rgbds-92bfe5d/include/helpers.hpp000066400000000000000000000052731512540461700202140ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_HELPERS_HPP #define RGBDS_HELPERS_HPP // Ideally we'd use `std::unreachable`, but it has insufficient compiler support #ifdef __GNUC__ // GCC or compatible #ifdef NDEBUG #define unreachable_ __builtin_unreachable #else // In release builds, define "unreachable" as such, but trap in debug builds #define unreachable_ __builtin_trap #endif #else // This seems to generate similar code to __builtin_unreachable, despite different semantics // Note that executing this is undefined behavior (declared [[noreturn]], but does return) [[noreturn]] static inline void unreachable_() { } #endif // Ideally we'd use `[[assume()]]`, but it has insufficient compiler support #ifdef NDEBUG #ifdef _MSC_VER #define assume(x) __assume(x) #else // `[[gnu::assume()]]` for GCC or compatible also has insufficient support (GCC 13+ only) #define assume(x) \ do { \ if (!(x)) { \ unreachable_(); \ } \ } while (0) #endif #else // In release builds, define "assume" as such, but `assert` in debug builds #include #define assume assert #endif // Ideally we'd use `std::bit_width`, but it has insufficient compiler support #ifdef __GNUC__ // GCC or compatible #define ctz __builtin_ctz #define clz __builtin_clz #elif defined(_MSC_VER) #include #pragma intrinsic(_BitScanReverse, _BitScanForward) static inline int ctz(unsigned int x) { unsigned long cnt; assume(x != 0); _BitScanForward(&cnt, x); return cnt; } static inline int clz(unsigned int x) { unsigned long cnt; assume(x != 0); _BitScanReverse(&cnt, x); return 31 - cnt; } #else #include static inline int ctz(unsigned int x) { int cnt = 0; while (!(x & 1)) { x >>= 1; ++cnt; } return cnt; } static inline int clz(unsigned int x) { int cnt = 0; while (x <= UINT_MAX / 2) { x <<= 1; ++cnt; } return cnt; } #endif // Macros for stringification #define STR(x) #x #define EXPAND_AND_STR(x) STR(x) // Macros for concatenation #define CAT(x, y) x##y #define EXPAND_AND_CAT(x, y) CAT(x, y) // For lack of , this adds some more brevity #define RANGE(s) std::begin(s), std::end(s) #define RRANGE(s) std::rbegin(s), std::rend(s) // MSVC does not inline `strlen()` or `.length()` of a constant string template static constexpr int literal_strlen(char const (&)[SizeOfString]) { return SizeOfString - 1; // Don't count the ending '\0' } // For ad-hoc RAII in place of a `defer` statement or cross-platform `__attribute__((cleanup))` template struct Defer { DeferredFnT deferred; Defer(DeferredFnT func) : deferred(func) {} ~Defer() { deferred(); } }; #endif // RGBDS_HELPERS_HPP gbdev-rgbds-92bfe5d/include/itertools.hpp000066400000000000000000000113361512540461700205730ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_ITERTOOLS_HPP #define RGBDS_ITERTOOLS_HPP #include #include #include #include #include #include #include #include // A wrapper around iterables to reverse their iteration order; used in `for`-each loops. template struct ReversedIterable { IterableT &_iterable; }; template auto begin(ReversedIterable r) { return std::rbegin(r._iterable); } template auto end(ReversedIterable r) { return std::rend(r._iterable); } template ReversedIterable reversed(IterableT &&_iterable) { return {_iterable}; } // A map from `std::string` keys to `ItemT` items, iterable in the order the items were inserted. template class InsertionOrderedMap { std::deque list; std::unordered_map map; // Indexes into `list` public: size_t size() const { return list.size(); } bool empty() const { return list.empty(); } bool contains(std::string const &name) const { return map.find(name) != map.end(); } ItemT &operator[](size_t i) { return list[i]; } typename decltype(list)::iterator begin() { return list.begin(); } typename decltype(list)::iterator end() { return list.end(); } typename decltype(list)::const_iterator begin() const { return list.begin(); } typename decltype(list)::const_iterator end() const { return list.end(); } ItemT &add(std::string const &name) { map[name] = list.size(); return list.emplace_back(); } ItemT &add(std::string const &name, ItemT &&value) { map[name] = list.size(); list.emplace_back(std::move(value)); return list.back(); } ItemT &addAnonymous() { // Add the new item to the list, but do not update the map return list.emplace_back(); } std::optional findIndex(std::string const &name) const { if (auto search = map.find(name); search != map.end()) { return search->second; } return std::nullopt; } }; // An iterable of `enum` values in the half-open range [start, stop). template class EnumSeq { EnumT _start; EnumT _stop; class Iterator { EnumT _value; public: explicit Iterator(EnumT value) : _value(value) {} Iterator &operator++() { _value = static_cast(_value + 1); return *this; } EnumT operator*() const { return _value; } bool operator==(Iterator const &rhs) const { return _value == rhs._value; } }; public: explicit EnumSeq(EnumT stop) : _start(static_cast(0)), _stop(stop) {} explicit EnumSeq(EnumT start, EnumT stop) : _start(start), _stop(stop) {} Iterator begin() { return Iterator(_start); } Iterator end() { return Iterator(_stop); } }; // Only needed inside `ZipContainer` below. // This is not a fully generic implementation; its current use cases only require for-loop behavior. // We also assume that all iterators have the same length. template class ZipIterator { std::tuple _iters; public: explicit ZipIterator(std::tuple &&iters) : _iters(iters) {} ZipIterator &operator++() { std::apply([](auto &&...it) { (++it, ...); }, _iters); return *this; } auto operator*() const { return std::apply( [](auto &&...it) { return std::tuple(*it...); }, _iters ); } bool operator==(ZipIterator const &rhs) const { return std::get<0>(_iters) == std::get<0>(rhs._iters); } }; // Only needed inside `zip` below. template class ZipContainer { std::tuple _containers; public: explicit ZipContainer(IterableTs &&...containers) : _containers(std::forward(containers)...) {} auto begin() { return ZipIterator(std::apply( [](auto &&...containers) { using std::begin; return std::make_tuple(begin(containers)...); }, _containers )); } auto end() { return ZipIterator(std::apply( [](auto &&...containers) { using std::end; return std::make_tuple(end(containers)...); }, _containers )); } }; // Only needed inside `zip` below. // Take ownership of objects and rvalue refs passed to us, but not lvalue refs template using ZipHolder = std::conditional_t< std::is_lvalue_reference_v, IterableT, std::remove_cv_t>>; // Iterates over N containers at once, yielding tuples of N items at a time. // Does the same number of iterations as the first container's iterator! template static constexpr auto zip(IterableTs &&...containers) { return ZipContainer...>(std::forward(containers)...); } #endif // RGBDS_ITERTOOLS_HPP gbdev-rgbds-92bfe5d/include/link/000077500000000000000000000000001512540461700167675ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/include/link/assign.hpp000066400000000000000000000003221512540461700207610ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_LINK_ASSIGN_HPP #define RGBDS_LINK_ASSIGN_HPP // Assigns all sections a slice of the address space void assign_AssignSections(); #endif // RGBDS_LINK_ASSIGN_HPP gbdev-rgbds-92bfe5d/include/link/fstack.hpp000066400000000000000000000022471512540461700207600ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_LINK_FSTACK_HPP #define RGBDS_LINK_FSTACK_HPP #include #include #include #include #include #include "linkdefs.hpp" struct FileStackNode { FileStackNodeType type; std::variant< std::monostate, // Default constructed; `.type` and `.data` must be set manually std::vector, // NODE_REPT std::string // NODE_FILE, NODE_MACRO > data; bool isQuiet; // Whether to omit this node from error reporting FileStackNode *parent; // Line at which the parent context was exited; meaningless for the root level uint32_t lineNo; // REPT iteration counts since last named node, in reverse depth order std::vector &iters() { return std::get>(data); } std::vector const &iters() const { return std::get>(data); } // File name for files, file::macro name for macros std::string &name() { return std::get(data); } std::string const &name() const { return std::get(data); } void printBacktrace(uint32_t curLineNo) const; }; #endif // RGBDS_LINK_FSTACK_HPP gbdev-rgbds-92bfe5d/include/link/layout.hpp000066400000000000000000000011061512540461700210130ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_LINK_LAYOUT_HPP #define RGBDS_LINK_LAYOUT_HPP #include #include #include "linkdefs.hpp" void layout_SetFloatingSectionType(SectionType type); void layout_SetSectionType(SectionType type); void layout_SetSectionType(SectionType type, uint32_t bank); void layout_SetAddr(uint32_t addr); void layout_MakeAddrFloating(); void layout_AlignTo(uint32_t alignment, uint32_t offset); void layout_Pad(uint32_t length); void layout_PlaceSection(std::string const &name, bool isOptional); #endif // RGBDS_LINK_LAYOUT_HPP gbdev-rgbds-92bfe5d/include/link/lexer.hpp000066400000000000000000000004461512540461700206230ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_LINK_LEXER_HPP #define RGBDS_LINK_LEXER_HPP #include void lexer_TraceCurrent(); void lexer_IncludeFile(std::string &&path); void lexer_IncLineNo(); bool lexer_Init(std::string const &linkerScriptName); #endif // RGBDS_LINK_LEXER_HPP gbdev-rgbds-92bfe5d/include/link/main.hpp000066400000000000000000000014741512540461700204320ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_LINK_MAIN_HPP #define RGBDS_LINK_MAIN_HPP #include #include #include struct Options { bool isDmgMode; // -d std::optional mapFileName; // -m bool noSymInMap; // -M std::optional symFileName; // -n std::optional overlayFileName; // -O std::optional outputFileName; // -o uint8_t padValue; // -p bool hasPadValue = false; // Setting these three to 0 disables the functionality uint16_t scrambleROMX; // -S uint16_t scrambleWRAMX; uint16_t scrambleSRAM; bool is32kMode; // -t bool isWRAM0Mode; // -w bool disablePadding; // -x }; extern Options options; #endif // RGBDS_LINK_MAIN_HPP gbdev-rgbds-92bfe5d/include/link/object.hpp000066400000000000000000000005531512540461700207510ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_LINK_OBJECT_HPP #define RGBDS_LINK_OBJECT_HPP #include #include // Read an object (.o) file, and add its info to the data structures. void obj_ReadFile(std::string const &filePath, size_t fileID); // Sets up object file reading void obj_Setup(size_t nbFiles); #endif // RGBDS_LINK_OBJECT_HPP gbdev-rgbds-92bfe5d/include/link/output.hpp000066400000000000000000000006321512540461700210410ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_LINK_OUTPUT_HPP #define RGBDS_LINK_OUTPUT_HPP struct Section; // Registers a section for output. void out_AddSection(Section const §ion); // Finds an assigned section overlapping another one. Section const *out_OverlappingSection(Section const §ion); // Writes all output (bin, sym, map) files. void out_WriteFiles(); #endif // RGBDS_LINK_OUTPUT_HPP gbdev-rgbds-92bfe5d/include/link/patch.hpp000066400000000000000000000011221512540461700205730ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_LINK_PATCH_HPP #define RGBDS_LINK_PATCH_HPP #include #include #include "link/section.hpp" struct Symbol; struct Assertion { Patch patch; // Also used for its `.type` std::string message; // This would be redundant with `patch.pcSection->fileSymbols`, but `section` is sometimes // `nullptr`! std::vector *fileSymbols; }; Assertion &patch_AddAssertion(); // Checks all assertions void patch_CheckAssertions(); // Applies all SECTIONs' patches to them void patch_ApplyPatches(); #endif // RGBDS_LINK_PATCH_HPP gbdev-rgbds-92bfe5d/include/link/sdas_obj.hpp000066400000000000000000000004601512540461700212640ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_LINK_SDAS_OBJ_HPP #define RGBDS_LINK_SDAS_OBJ_HPP #include #include struct FileStackNode; struct Symbol; void sdobj_ReadFile(FileStackNode const &where, FILE *file, std::vector &fileSymbols); #endif // RGBDS_LINK_SDAS_OBJ_HPP gbdev-rgbds-92bfe5d/include/link/section.hpp000066400000000000000000000050111512540461700211410ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_LINK_SECTION_HPP #define RGBDS_LINK_SECTION_HPP #include #include #include #include #include "linkdefs.hpp" struct FileStackNode; struct Section; struct Symbol; struct Patch { FileStackNode const *src; uint32_t lineNo; uint32_t offset; Section const *pcSection; uint32_t pcSectionID; uint32_t pcOffset; PatchType type; std::vector rpnExpression; }; struct Section { // Info contained in the object files std::string name; uint16_t size; uint16_t offset; SectionType type; SectionModifier modifier; bool isAddressFixed; // This `struct`'s address in ROM. // Importantly for fragments, this does not include `offset`! uint16_t org; bool isBankFixed; uint32_t bank; bool isAlignFixed; uint16_t alignMask; uint16_t alignOfs; FileStackNode const *src; int32_t lineNo; std::vector data; // Array of size `size`, or 0 if `type` does not have data std::vector patches; // Extra info computed during linking std::vector *fileSymbols; std::vector symbols; std::unique_ptr
nextPiece; // The next fragment or union "piece" of this section private: // Template class for both const and non-const iterators over the "pieces" of this section template class PiecesIterable { SectionT *_firstPiece; class Iterator { SectionT *_piece; public: explicit Iterator(SectionT *piece) : _piece(piece) {} Iterator &operator++() { _piece = _piece->nextPiece.get(); return *this; } SectionT &operator*() const { return *_piece; } bool operator==(Iterator const &rhs) const { return _piece == rhs._piece; } }; public: explicit PiecesIterable(SectionT *firstPiece) : _firstPiece(firstPiece) {} Iterator begin() { return Iterator(_firstPiece); } Iterator end() { return Iterator(nullptr); } }; public: PiecesIterable
pieces() { return PiecesIterable(this); } PiecesIterable
pieces() const { return PiecesIterable(this); } }; // Execute a callback for each section currently registered. // This is to avoid exposing the data structure in which sections are stored. void sect_ForEach(void (*callback)(Section &)); // Registers a section to be processed. void sect_AddSection(std::unique_ptr
&§ion); // Finds a section by its name. Section *sect_GetSection(std::string const &name); // Checks if all sections meet reasonable criteria, such as max size void sect_DoSanityChecks(); #endif // RGBDS_LINK_SECTION_HPP gbdev-rgbds-92bfe5d/include/link/symbol.hpp000066400000000000000000000020251512540461700210040ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_LINK_SYMBOL_HPP #define RGBDS_LINK_SYMBOL_HPP // GUIDELINE: external code MUST NOT BE AWARE of the data structure used! #include #include #include #include "linkdefs.hpp" struct FileStackNode; struct Section; struct Label { int32_t sectionID; int32_t offset; // Extra info computed during linking Section *section; }; struct Symbol { // Info contained in the object files std::string name; ExportLevel type; FileStackNode const *src; int32_t lineNo; std::variant< int32_t, // Constants just have a numeric value Label // Label values refer to an offset within a specific section > data; void linkToSection(Section §ion); void fixSectionOffset(); }; void sym_ForEach(void (*callback)(Symbol &)); void sym_AddSymbol(Symbol &symbol); // Finds a symbol in all the defined symbols. Symbol *sym_GetSymbol(std::string const &name); void sym_TraceLocalAliasedSymbols(std::string const &name); #endif // RGBDS_LINK_SYMBOL_HPP gbdev-rgbds-92bfe5d/include/link/warning.hpp000066400000000000000000000042271512540461700211520ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_LINK_WARNING_HPP #define RGBDS_LINK_WARNING_HPP #include #include #include "diagnostics.hpp" #define warningAt(where, ...) warning((where).src, (where).lineNo, __VA_ARGS__) #define errorAt(where, ...) error((where).src, (where).lineNo, __VA_ARGS__) #define fatalAt(where, ...) fatal((where).src, (where).lineNo, __VA_ARGS__) #define fatalTwoAt(where1, where2, ...) \ fatalTwo(*(where1).src, (where1).lineNo, *(where2).src, (where2).lineNo, __VA_ARGS__) enum WarningLevel { LEVEL_DEFAULT, // Warnings that are enabled by default LEVEL_ALL, // Warnings that probably indicate an error LEVEL_EVERYTHING, // Literally every warning }; enum WarningID { WARNING_ASSERT, // Assertions WARNING_DIV, // Undefined division behavior WARNING_OBSOLETE, // Obsolete/deprecated things WARNING_SHIFT, // Undefined `SHIFT` behavior WARNING_SHIFT_AMOUNT, // Strange `SHIFT` amount NB_PLAIN_WARNINGS, // Implicit truncation loses some bits WARNING_TRUNCATION_1 = NB_PLAIN_WARNINGS, WARNING_TRUNCATION_2, NB_WARNINGS, }; extern Diagnostics warnings; struct FileStackNode; [[gnu::format(printf, 4, 5)]] void warning(FileStackNode const *src, uint32_t lineNo, WarningID id, char const *fmt, ...); [[gnu::format(printf, 3, 4)]] void warning(FileStackNode const *src, uint32_t lineNo, char const *fmt, ...); [[gnu::format(printf, 1, 2)]] void warning(char const *fmt, ...); [[gnu::format(printf, 3, 4)]] void error(FileStackNode const *src, uint32_t lineNo, char const *fmt, ...); [[gnu::format(printf, 1, 2)]] void error(char const *fmt, ...); [[gnu::format(printf, 1, 2)]] void scriptError(char const *fmt, ...); [[gnu::format(printf, 3, 4), noreturn]] void fatal(FileStackNode const *src, uint32_t lineNo, char const *fmt, ...); [[gnu::format(printf, 1, 2), noreturn]] void fatal(char const *fmt, ...); [[gnu::format(printf, 5, 6), noreturn]] void fatalTwo( FileStackNode const &src1, uint32_t lineNo1, FileStackNode const &src2, uint32_t lineNo2, char const *fmt, ... ); void requireZeroErrors(); #endif // RGBDS_LINK_WARNING_HPP gbdev-rgbds-92bfe5d/include/linkdefs.hpp000066400000000000000000000056441512540461700203530ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_LINKDEFS_HPP #define RGBDS_LINKDEFS_HPP #include #include #include "helpers.hpp" // assume #define RGBDS_OBJECT_VERSION_STRING "RGB9" #define RGBDS_OBJECT_REV 13U enum AssertionType { ASSERT_WARN, ASSERT_ERROR, ASSERT_FATAL }; enum RPNCommand { RPN_ADD = 0x00, RPN_SUB = 0x01, RPN_MUL = 0x02, RPN_DIV = 0x03, RPN_MOD = 0x04, RPN_NEG = 0x05, RPN_EXP = 0x06, RPN_OR = 0x10, RPN_AND = 0x11, RPN_XOR = 0x12, RPN_NOT = 0x13, RPN_LOGAND = 0x21, RPN_LOGOR = 0x22, RPN_LOGNOT = 0x23, RPN_LOGEQ = 0x30, RPN_LOGNE = 0x31, RPN_LOGGT = 0x32, RPN_LOGLT = 0x33, RPN_LOGGE = 0x34, RPN_LOGLE = 0x35, RPN_SHL = 0x40, RPN_SHR = 0x41, RPN_USHR = 0x42, RPN_BANK_SYM = 0x50, RPN_BANK_SECT = 0x51, RPN_BANK_SELF = 0x52, RPN_SIZEOF_SECT = 0x53, RPN_STARTOF_SECT = 0x54, RPN_SIZEOF_SECTTYPE = 0x55, RPN_STARTOF_SECTTYPE = 0x56, RPN_HRAM = 0x60, RPN_RST = 0x61, RPN_BIT_INDEX = 0x62, RPN_HIGH = 0x70, RPN_LOW = 0x71, RPN_BITWIDTH = 0x72, RPN_TZCOUNT = 0x73, RPN_CONST = 0x80, RPN_SYM = 0x81 }; enum SectionType { SECTTYPE_WRAM0, SECTTYPE_VRAM, SECTTYPE_ROMX, SECTTYPE_ROM0, SECTTYPE_HRAM, SECTTYPE_WRAMX, SECTTYPE_SRAM, SECTTYPE_OAM, // In RGBLINK, this is used for "indeterminate" sections; this is primarily for SDCC // areas, which do not carry any section type info and must be told from the linker script SECTTYPE_INVALID }; static constexpr uint8_t SECTTYPE_TYPE_MASK = 0b111; static constexpr uint8_t SECTTYPE_UNION_BIT = 7; static constexpr uint8_t SECTTYPE_FRAGMENT_BIT = 6; enum FileStackNodeType { NODE_REPT, NODE_FILE, NODE_MACRO, }; static constexpr uint8_t FSTACKNODE_QUIET_BIT = 7; // Nont-`const` members may be patched in RGBLINK depending on CLI flags extern struct SectionTypeInfo { std::string const name; uint16_t const startAddr; uint16_t size; uint32_t const firstBank; uint32_t lastBank; } sectionTypeInfo[SECTTYPE_INVALID]; // Tells whether a section has data in its object file definition, // depending on type. static inline bool sectTypeHasData(SectionType type) { assume(type != SECTTYPE_INVALID); return type == SECTTYPE_ROM0 || type == SECTTYPE_ROMX; } // Returns a memory region's end address (last byte), e.g. 0x7FFF static inline uint16_t sectTypeEndAddr(SectionType type) { return sectionTypeInfo[type].startAddr + sectionTypeInfo[type].size - 1; } // Returns a memory region's number of banks, or 1 for regions without banking static inline uint32_t sectTypeBanks(SectionType type) { return sectionTypeInfo[type].lastBank - sectionTypeInfo[type].firstBank + 1; } enum SectionModifier { SECTION_NORMAL, SECTION_UNION, SECTION_FRAGMENT }; extern char const * const sectionModNames[]; enum ExportLevel { SYMTYPE_LOCAL, SYMTYPE_IMPORT, SYMTYPE_EXPORT, SYMTYPE_INVALID }; enum PatchType { PATCHTYPE_BYTE, PATCHTYPE_WORD, PATCHTYPE_LONG, PATCHTYPE_JR, PATCHTYPE_INVALID }; #endif // RGBDS_LINKDEFS_HPP gbdev-rgbds-92bfe5d/include/opmath.hpp000066400000000000000000000011751512540461700200370ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_OP_MATH_HPP #define RGBDS_OP_MATH_HPP #include int32_t op_divide(int32_t dividend, int32_t divisor); int32_t op_modulo(int32_t dividend, int32_t divisor); int32_t op_exponent(int32_t base, uint32_t power); int32_t op_shift_left(int32_t value, int32_t amount); int32_t op_shift_right(int32_t value, int32_t amount); int32_t op_shift_right_unsigned(int32_t value, int32_t amount); int32_t op_neg(int32_t value); int32_t op_high(int32_t value); int32_t op_low(int32_t value); int32_t op_bitwidth(int32_t value); int32_t op_tzcount(int32_t value); #endif // RGBDS_OP_MATH_HPP gbdev-rgbds-92bfe5d/include/platform.hpp000066400000000000000000000036671512540461700204030ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_PLATFORM_HPP #define RGBDS_PLATFORM_HPP // MSVC doesn't have str(n)casecmp, use a suitable replacement #ifdef _MSC_VER #include // IWYU pragma: export #define strcasecmp _stricmp #define strncasecmp _strnicmp #else #include // IWYU pragma: export #endif // MSVC prefixes the names of S_* macros with underscores, // and doesn't define any S_IS* macros; define them ourselves #ifdef _MSC_VER #define S_IFMT _S_IFMT #define S_IFDIR _S_IFDIR #define S_ISDIR(mode) (((mode) & (S_IFMT)) == S_IFDIR) #endif // MSVC doesn't use POSIX types or defines for `read` #ifdef _MSC_VER #include // IWYU pragma: export #define STDIN_FILENO 0 #define STDOUT_FILENO 1 #define STDERR_FILENO 2 #define ssize_t int #define SSIZE_MAX INT_MAX #define isatty _isatty #else #include // IWYU pragma: export #include // IWYU pragma: export #include // IWYU pragma: export #endif // MSVC uses a different name for O_RDWR, and needs an additional _O_BINARY flag #ifdef _MSC_VER #include // IWYU pragma: export #define O_RDWR _O_RDWR #define S_ISREG(field) ((field) & (_S_IFREG)) #define O_BINARY _O_BINARY #define O_TEXT _O_TEXT #elif !defined(O_BINARY) // Cross-compilers define O_BINARY #define O_BINARY 0 // POSIX says we shouldn't care! #define O_TEXT 0 // Assume that it's not defined either #endif // _MSC_VER // Windows has stdin and stdout open as text by default, which we may not want #if defined(_MSC_VER) || defined(__MINGW32__) #include // IWYU pragma: export #define setmode(fd, mode) _setmode(fd, mode) #else #define setmode(fd, mode) (0) #endif // MingGW and Cygwin need POSIX functions which are not standard C explicitly enabled, #if defined(__MINGW32__) || defined(__CYGWIN__) #define _POSIX_C_SOURCE 200809L #endif #endif // RGBDS_PLATFORM_HPP gbdev-rgbds-92bfe5d/include/style.hpp000066400000000000000000000016601512540461700177060ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_STYLE_HPP #define RGBDS_STYLE_HPP #include #if defined(_MSC_VER) || defined(__MINGW32__) || defined(__CYGWIN__) #define STYLE_ANSI 0 #else #define STYLE_ANSI 1 #endif enum StyleColor { #if STYLE_ANSI // Values analogous to ANSI foreground and background SGR colors STYLE_BLACK, STYLE_RED, STYLE_GREEN, STYLE_YELLOW, STYLE_BLUE, STYLE_MAGENTA, STYLE_CYAN, STYLE_GRAY, #else // Values analogous to `FOREGROUND_*` constants from `windows.h` STYLE_BLACK, STYLE_BLUE, // bit 0 STYLE_GREEN, // bit 1 STYLE_CYAN, // STYLE_BLUE | STYLE_GREEN STYLE_RED, // bit 2 STYLE_MAGENTA, // STYLE_BLUE | STYLE_RED STYLE_YELLOW, // STYLE_GREEN | STYLE_RED STYLE_GRAY, // STYLE_BLUE | STYLE_GREEN | STYLE_RED #endif }; bool style_Parse(char const *arg); void style_Set(FILE *file, StyleColor color, bool bold); void style_Reset(FILE *file); #endif // RGBDS_STYLE_HPP gbdev-rgbds-92bfe5d/include/usage.hpp000066400000000000000000000010101512540461700176370ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_USAGE_HPP #define RGBDS_USAGE_HPP #include #include #include #include struct Usage { std::string name; std::vector flags; std::vector, std::vector>> options; void printVersion(bool error) const; [[noreturn]] void printAndExit(int code) const; [[gnu::format(printf, 2, 3), noreturn]] void printAndExit(char const *fmt, ...) const; }; #endif // RGBDS_USAGE_HPP gbdev-rgbds-92bfe5d/include/util.hpp000066400000000000000000000034211512540461700175200ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_UTIL_HPP #define RGBDS_UTIL_HPP #include #include #include #include #include #include #include #include "helpers.hpp" enum NumberBase { BASE_AUTO = 0, BASE_2 = 2, BASE_8 = 8, BASE_10 = 10, BASE_16 = 16, }; // Locale-independent character class functions bool isNewline(int c); bool isBlankSpace(int c); bool isWhitespace(int c); bool isPrintable(int c); bool isUpper(int c); bool isLower(int c); bool isLetter(int c); bool isDigit(int c); bool isBinDigit(int c); bool isOctDigit(int c); bool isHexDigit(int c); bool isAlphanumeric(int c); // Locale-independent character transform functions char toLower(char c); char toUpper(char c); bool startsIdentifier(int c); bool continuesIdentifier(int c); uint8_t parseHexDigit(int c); std::optional parseNumber(char const *&str, NumberBase base = BASE_AUTO); std::optional parseWholeNumber(char const *str, NumberBase base = BASE_AUTO); char const *printChar(int c); struct Uppercase { // FNV-1a hash of an uppercased string constexpr size_t operator()(std::string const &str) const { return std::accumulate(RANGE(str), 0x811C9DC5, [](size_t hash, char c) { return (hash ^ toUpper(c)) * 16777619; }); } // Compare two strings without case-sensitivity (by converting to uppercase) constexpr bool operator()(std::string const &str1, std::string const &str2) const { return std::equal(RANGE(str1), RANGE(str2), [](char c1, char c2) { return toUpper(c1) == toUpper(c2); }); } }; // An unordered map from case-insensitive `std::string` keys to `ItemT` items template using UpperMap = std::unordered_map; #endif // RGBDS_UTIL_HPP gbdev-rgbds-92bfe5d/include/verbosity.hpp000066400000000000000000000016331512540461700205740ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_VERBOSITY_HPP #define RGBDS_VERBOSITY_HPP #include #include // This macro does not evaluate its arguments unless the condition is true. #define verbosePrint(level, ...) \ do { \ if (checkVerbosity(level)) { \ printVerbosely(__VA_ARGS__); \ } \ } while (0) enum Verbosity { VERB_NONE, // 0. Default, no extra output VERB_CONFIG, // 1. Basic configuration, after parsing CLI options VERB_NOTICE, // 2. Before significant actions VERB_INFO, // 3. Some intermediate action results VERB_DEBUG, // 4. Internals useful for debugging VERB_TRACE, // 5. Step-by-step algorithm details VERB_VVVVVV, // 6. What, can't I have a little fun? }; void incrementVerbosity(); bool checkVerbosity(Verbosity level); [[gnu::format(printf, 1, 2)]] void printVerbosely(char const *fmt, ...); void printVVVVVVerbosity(); #endif // RGBDS_VERBOSITY_HPP gbdev-rgbds-92bfe5d/include/version.hpp000066400000000000000000000004371512540461700202340ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #ifndef RGBDS_VERSION_HPP #define RGBDS_VERSION_HPP #define PACKAGE_VERSION_MAJOR 1 #define PACKAGE_VERSION_MINOR 0 #define PACKAGE_VERSION_PATCH 1 // #define PACKAGE_VERSION_RC 1 char const *get_package_version_string(); #endif // RGBDS_VERSION_H gbdev-rgbds-92bfe5d/man/000077500000000000000000000000001512540461700151625ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/man/gbz80.7000066400000000000000000000754041512540461700162160ustar00rootroot00000000000000.\" SPDX-License-Identifier: MIT .\" .Dd January 1, 2026 .Dt GBZ80 7 .Os .Sh NAME .Nm gbz80 .Nd Game Boy CPU instruction reference .Sh DESCRIPTION This is the list of instructions supported by .Xr rgbasm 1 , including a short description, the number of bytes needed to encode them and the number of CPU cycles at 1MHz (or 2MHz in GBC double speed mode) needed to complete them. .Pp Instructons are documented according to the syntax accepted by .Xr rgbasm 1 , which does not always match one-to-one with the way instructions are .Lk https://gbdev.io/gb-opcodes/optables/ encoded . .Pp Note: All arithmetic and logic instructions that use register .Sy A as a destination can omit the destination, since it is assumed to be register .Sy A by default. So the following two lines have the same effect: .Bd -literal -offset indent OR A,B OR B .Ed .Pp Furthermore, the .Sy CPL instruction can take an optional .Sy A destination, since it can only be register .Sy A . So the following two lines have the same effect: .Bd -literal -offset indent CPL CPL A .Ed .Sh LEGEND List of abbreviations used in this document. .Bl -tag -width Ds .It Ar r8 Any of the 8-bit registers .Pq Sy A , B , C , D , E , H , L . .It Ar r16 Any of the general-purpose 16-bit registers .Pq Sy BC , DE , HL . .It Ar n8 8-bit integer constant .Po signed or unsigned, .Sy -128 to .Sy 255 .Pc . .It Ar n16 16-bit integer constant .Po signed or unsigned, .Sy -32768 to .Sy 65535 .Pc . .It Ar e8 8-bit signed offset .Po Sy -128 to .Sy 127 .Pc . .It Ar u3 3-bit unsigned bit index .Po Sy 0 to .Sy 7 , with .Sy 0 as the least significant bit .Pc . .It Ar cc A condition code: .Bl -tag -width Ds -compact .It Sy Z Execute if Z is set. .It Sy NZ Execute if Z is not set. .It Sy C Execute if C is set. .It Sy NC Execute if C is not set. .El .It Ar vec An .Sy RST vector .Po Ad 0x00 , 0x08 , 0x10 , 0x18 , 0x20 , 0x28 , 0x30 , and .Ad 0x38 Pc . .El .Sh INSTRUCTION OVERVIEW .Ss Load instructions .Bl -inset -compact .It Sx LD r8,r8 .It Sx LD r8,n8 .It Sx LD r16,n16 .It Sx LD [HL],r8 .It Sx LD [HL],n8 .It Sx LD r8,[HL] .It Sx LD [r16],A .It Sx LD [n16],A .It Sx LDH [n16],A .It Sx LDH [C],A .It Sx LD A,[r16] .It Sx LD A,[n16] .It Sx LDH A,[n16] .It Sx LDH A,[C] .It Sx LD [HLI],A .It Sx LD [HLD],A .It Sx LD A,[HLI] .It Sx LD A,[HLD] .El .Ss 8-bit arithmetic instructions .Bl -inset -compact .It Sx ADC A,r8 .It Sx ADC A,[HL] .It Sx ADC A,n8 .It Sx ADD A,r8 .It Sx ADD A,[HL] .It Sx ADD A,n8 .It Sx CP A,r8 .It Sx CP A,[HL] .It Sx CP A,n8 .It Sx DEC r8 .It Sx DEC [HL] .It Sx INC r8 .It Sx INC [HL] .It Sx SBC A,r8 .It Sx SBC A,[HL] .It Sx SBC A,n8 .It Sx SUB A,r8 .It Sx SUB A,[HL] .It Sx SUB A,n8 .El .Ss 16-bit arithmetic instructions .Bl -inset -compact .It Sx ADD HL,r16 .It Sx DEC r16 .It Sx INC r16 .El .Ss Bitwise logic instructions .Bl -inset -compact .It Sx AND A,r8 .It Sx AND A,[HL] .It Sx AND A,n8 .It Sx CPL .It Sx OR A,r8 .It Sx OR A,[HL] .It Sx OR A,n8 .It Sx XOR A,r8 .It Sx XOR A,[HL] .It Sx XOR A,n8 .El .Ss Bit flag instructions .Bl -inset -compact .It Sx BIT u3,r8 .It Sx BIT u3,[HL] .It Sx RES u3,r8 .It Sx RES u3,[HL] .It Sx SET u3,r8 .It Sx SET u3,[HL] .El .Ss Bit shift instructions .Bl -inset -compact .It Sx RL r8 .It Sx RL [HL] .It Sx RLA .It Sx RLC r8 .It Sx RLC [HL] .It Sx RLCA .It Sx RR r8 .It Sx RR [HL] .It Sx RRA .It Sx RRC r8 .It Sx RRC [HL] .It Sx RRCA .It Sx SLA r8 .It Sx SLA [HL] .It Sx SRA r8 .It Sx SRA [HL] .It Sx SRL r8 .It Sx SRL [HL] .It Sx SWAP r8 .It Sx SWAP [HL] .El .Ss Jumps and subroutine instructions .Bl -inset -compact .It Sx CALL n16 .It Sx CALL cc,n16 .It Sx JP HL .It Sx JP n16 .It Sx JP cc,n16 .It Sx JR n16 .It Sx JR cc,n16 .It Sx RET cc .It Sx RET .It Sx RETI .It Sx RST vec .El .Ss Carry flag instructions .Bl -inset -compact .It Sx CCF .It Sx SCF .El .Ss Stack manipulation instructions .Bl -inset -compact .It Sx ADD HL,SP .It Sx ADD SP,e8 .It Sx DEC SP .It Sx INC SP .It Sx LD SP,n16 .It Sx LD [n16],SP .It Sx LD HL,SP+e8 .It Sx LD SP,HL .It Sx POP AF .It Sx POP r16 .It Sx PUSH AF .It Sx PUSH r16 .El .Ss Interrupt-related instructions .Bl -inset -compact .It Sx DI .It Sx EI .It Sx HALT .El .Ss Miscellaneous instructions .Bl -inset -compact .It Sx DAA .It Sx NOP .It Sx STOP .El .Sh INSTRUCTION REFERENCE .Ss ADC A,r8 Add the value in .Ar r8 plus the carry flag to .Sy A . .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 0 .It Sy H Set if overflow from bit 3. .It Sy C Set if overflow from bit 7. .El .Ss ADC A,[HL] Add the byte pointed to by .Sy HL plus the carry flag to .Sy A . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: See .Sx ADC A,r8 .Ss ADC A,n8 Add the value .Ar n8 plus the carry flag to .Sy A . .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: See .Sx ADC A,r8 .Ss ADD A,r8 Add the value in .Ar r8 to .Sy A . .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 0 .It Sy H Set if overflow from bit 3. .It Sy C Set if overflow from bit 7. .El .Ss ADD A,[HL] Add the byte pointed to by .Sy HL to .Sy A . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: See .Sx ADD A,r8 .Ss ADD A,n8 Add the value .Ar n8 to .Sy A . .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: See .Sx ADD A,r8 .Ss ADD HL,r16 Add the value in .Ar r16 to .Sy HL . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy N 0 .It Sy H Set if overflow from bit 11. .It Sy C Set if overflow from bit 15. .El .Ss ADD HL,SP Add the value in .Sy SP to .Sy HL . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: See .Sx ADD HL,r16 .Ss ADD SP,e8 Add the signed value .Ar e8 to .Sy SP . .Pp Cycles: 4 .Pp Bytes: 2 .Pp Flags: .Bl -tag -width Ds .It Sy Z 0 .It Sy N 0 .It Sy H Set if overflow from bit 3. .It Sy C Set if overflow from bit 7. .El .Ss AND A,r8 Set .Sy A to the bitwise AND between the value in .Ar r8 and .Sy A . .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 0 .It Sy H 1 .It Sy C 0 .El .Ss AND A,[HL] Set .Sy A to the bitwise AND between the byte pointed to by .Sy HL and .Sy A . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: See .Sx AND A,r8 .Ss AND A,n8 Set .Sy A to the bitwise AND between the value .Ar n8 and .Sy A . .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: See .Sx AND A,r8 .Ss BIT u3,r8 Test bit .Ar u3 in register .Ar r8 , set the zero flag if bit not set. .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if the selected bit is 0. .It Sy N 0 .It Sy H 1 .El .Ss BIT u3,[HL] Test bit .Ar u3 in the byte pointed by .Sy HL , set the zero flag if bit not set. .Pp Cycles: 3 .Pp Bytes: 2 .Pp Flags: See .Sx BIT u3,r8 .Ss CALL n16 Call address .Ar n16 . .Pp This pushes the address of the instruction after the .Sy CALL on the stack, such that .Sx RET can pop it later; then, it executes an implicit .Sx JP n16 . .Pp Cycles: 6 .Pp Bytes: 3 .Pp Flags: None affected. .Ss CALL cc,n16 Call address .Ar n16 if condition .Ar cc is met. .Pp Cycles: 6 taken / 3 untaken .Pp Bytes: 3 .Pp Flags: None affected. .Ss CCF Complement Carry Flag. .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy N 0 .It Sy H 0 .It Sy C Inverted. .El .Ss CP A,r8 ComPare the value in .Sy A with the value in .Ar r8 . .Pp This subtracts the value in .Ar r8 from .Sy A and sets flags accordingly, but discards the result. .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 1 .It Sy H Set if borrow from bit 4. .It Sy C Set if borrow (i.e. if .Ar r8 > .Sy A ) . .El .Ss CP A,[HL] ComPare the value in .Sy A with the byte pointed to by .Sy HL . .Pp This subtracts the byte pointed to by .Sy HL from .Sy A and sets flags accordingly, but discards the result. .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: See .Sx CP A,r8 .Ss CP A,n8 ComPare the value in .Sy A with the value .Ar n8 . .Pp This subtracts the value .Ar n8 from .Sy A and sets flags accordingly, but discards the result. .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: See .Sx CP A,r8 .Ss CPL ComPLement accumulator .Po Sy A = .Sy ~A .Pc ; also called bitwise NOT. .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy N 1 .It Sy H 1 .El .Ss DAA Decimal Adjust Accumulator. .Pp Designed to be used after performing an arithmetic instruction .Pq Sy ADD , ADC , SUB , SBC whose inputs were in Binary-Coded Decimal (BCD), adjusting the result to likewise be in BCD. .Pp The exact behavior of this instruction depends on the state of the subtract flag .Sy N : .Bl -tag -width Ds -offset indent .It If the subtract flag Sy N No is set: .Bl -enum -compact .It Initialize the adjustment to 0. .It If the half-carry flag .Sy H is set, then add .Ad $6 to the adjustment. .It If the carry flag is set, then add .Ad $60 to the adjustment. .It Subtract the adjustment from .Sy A . .El .It If the subtract flag Sy N No is not set: .Bl -enum -compact .It Initialize the adjustment to 0. .It If the half-carry flag .Sy H is set or .Sy A & .Ad $F > .Ad $9 , then add .Ad $6 to the adjustment. .It If the carry flag is set or .Sy A > .Ad $99 , then add .Ad $60 to the adjustment and set the carry flag. .It Add the adjustment to .Sy A . .El .El .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy H 0 .It Sy C Set or unaffected depending on the operation. .El .Ss DEC r8 Decrement the value in register .Ar r8 by 1. .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 1 .It Sy H Set if borrow from bit 4. .El .Ss DEC [HL] Decrement the byte pointed to by .Sy HL by 1. .Pp Cycles: 3 .Pp Bytes: 1 .Pp Flags: See .Sx DEC r8 .Ss DEC r16 Decrement the value in register .Ar r16 by 1. .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Ss DEC SP Decrement the value in register .Sy SP by 1. .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Ss DI Disable Interrupts by clearing the .Sy IME flag. .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: None affected. .Ss EI Enable Interrupts by setting the .Sy IME flag. .Pp The flag is only set .Em after the instruction following .Sy EI . .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: None affected. .Ss HALT Enter CPU low-power consumption mode until an interrupt occurs. .Pp The exact behavior of this instruction depends on the state of the .Sy IME flag, and whether interrupts are pending (i.e. whether .Ql [IE] & [IF] is non-zero): .Bl -tag -width Ds -offset indent .It If the Sy IME No flag is set: The CPU enters low-power mode until .Em after an interrupt is about to be serviced. The handler is executed normally, and the CPU resumes execution after the .Ic HALT when that returns. .It If the Sy IME No flag is not set, and no interrupts are pending: As soon as an interrupt becomes pending, the CPU resumes execution. This is like the above, except that the handler is .Em not called. .It If the Sy IME No flag is not set, and some interrupt is pending: The CPU continues execution after the .Ic HALT , but the byte after it is read twice in a row .Po .Sy PC is not incremented, due to a hardware bug .Pc . .El .Pp Cycles: - .Pp Bytes: 1 .Pp Flags: None affected. .Ss INC r8 Increment the value in register .Ar r8 by 1. .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 0 .It Sy H Set if overflow from bit 3. .El .Ss INC [HL] Increment the byte pointed to by .Sy HL by 1. .Pp Cycles: 3 .Pp Bytes: 1 .Pp Flags: See .Sx INC r8 .Ss INC r16 Increment the value in register .Ar r16 by 1. .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Ss INC SP Increment the value in register .Sy SP by 1. .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Ss JP n16 Jump to address .Ar n16 ; effectively, copy .Ar n16 into .Sy PC . .Pp Cycles: 4 .Pp Bytes: 3 .Pp Flags: None affected. .Ss JP cc,n16 Jump to address .Ar n16 if condition .Ar cc is met. .Pp Cycles: 4 taken / 3 untaken .Pp Bytes: 3 .Pp Flags: None affected. .Ss JP HL Jump to address in .Sy HL ; effectively, copy the value in register .Sy HL into .Sy PC . .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: None affected. .Ss JR n16 Relative Jump to address .Ar n16 . .Pp The target address .Ar n16 is .Em encoded as a signed 8-bit offset from the address immediately following the .Ic JR instruction, so it must be between .Sy -128 and .Sy 127 bytes away. For example: .Bd -literal -offset indent JR Label ; no-op; encoded offset of 0 Label: JR Label ; infinite loop; encoded offset of -2 .Ed .Pp Cycles: 3 .Pp Bytes: 2 .Pp Flags: None affected. .Ss JR cc,n16 Relative Jump to address .Ar n16 if condition .Ar cc is met. .Pp The target address .Ar n16 is .Em encoded as a signed 8-bit offset from the address immediately following the .Ic JR instruction, so it must be between .Sy -128 and .Sy 127 bytes away. .Pp Cycles: 3 taken / 2 untaken .Pp Bytes: 2 .Pp Flags: None affected. .Ss LD r8,r8 Copy (aka Load) the value in register on the right into the register on the left. .Pp Storing a register into itself is a no-op; however, some Game Boy emulators interpret .Sy LD B,B as a breakpoint, or .Sy LD D,D as a debug message .Po such as .Lk https://bgb.bircd.org/manual.html#expressions BGB .Pc . .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: None affected. .Ss LD r8,n8 Copy the value .Ar n8 into register .Ar r8 . .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: None affected. .Ss LD r16,n16 Copy the value .Ar n16 into register .Ar r16 . .Pp Cycles: 3 .Pp Bytes: 3 .Pp Flags: None affected. .Ss LD [HL],r8 Copy the value in register .Ar r8 into the byte pointed to by .Sy HL . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Ss LD [HL],n8 Copy the value .Ar n8 into the byte pointed to by .Sy HL . .Pp Cycles: 3 .Pp Bytes: 2 .Pp Flags: None affected. .Ss LD r8,[HL] Copy the value pointed to by .Sy HL into register .Ar r8 . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Ss LD [r16],A Copy the value in register .Sy A into the byte pointed to by .Ar r16 . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Ss LD [n16],A Copy the value in register .Sy A into the byte at address .Ar n16 . .Pp Cycles: 4 .Pp Bytes: 3 .Pp Flags: None affected. .Ss LDH [n16],A Copy the value in register .Sy A into the byte at address .Ar n16 . .Pp The destination address .Ar n16 is .Em encoded as its 8-bit low byte and assumes a high byte of .Ad $FF , so it must be between .Ad $FF00 and .Ad $FFFF . .Pp Cycles: 3 .Pp Bytes: 2 .Pp Flags: None affected. .Ss LDH [C],A Copy the value in register .Sy A into the byte at address .Ad $FF00+C . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Pp This is sometimes written as .Ql LD [$FF00+C],A . .Ss LD A,[r16] Copy the byte pointed to by .Ar r16 into register .Sy A . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Ss LD A,[n16] Copy the byte at address .Ar n16 into register .Sy A . .Pp Cycles: 4 .Pp Bytes: 3 .Pp Flags: None affected. .Ss LDH A,[n16] Copy the byte at address .Ar n16 into register .Sy A . .Pp The source address .Ar n16 is .Em encoded as its 8-bit low byte and assumes a high byte of .Ad $FF , so it must be between .Ad $FF00 and .Ad $FFFF . .Pp Cycles: 3 .Pp Bytes: 2 .Pp Flags: None affected. .Ss LDH A,[C] Copy the byte at address .Ad $FF00+C into register .Sy A . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Pp This is sometimes written as .Ql LD A,[$FF00+C] . .Ss LD [HLI],A Copy the value in register .Sy A into the byte pointed by .Sy HL and increment .Sy HL afterwards. .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Pp This is sometimes written as .Ql LD [HL+],A , or .Ql LDI [HL],A . .Ss LD [HLD],A Copy the value in register .Sy A into the byte pointed by .Sy HL and decrement .Sy HL afterwards. .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Pp This is sometimes written as .Ql LD [HL-],A , or .Ql LDD [HL],A . .Ss LD A,[HLD] Copy the byte pointed to by .Sy HL into register .Sy A , and decrement .Sy HL afterwards. .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Pp This is sometimes written as .Ql LD A,[HL-] , or .Ql LDD A,[HL] . .Ss LD A,[HLI] Copy the byte pointed to by .Sy HL into register .Sy A , and increment .Sy HL afterwards. .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Pp This is sometimes written as .Ql LD A,[HL+] , or .Ql LDI A,[HL] . .Ss LD SP,n16 Copy the value .Ar n16 into register .Sy SP . .Pp Cycles: 3 .Pp Bytes: 3 .Pp Flags: None affected. .Ss LD [n16],SP Copy .Sy SP & .Ad $FF at address .Ar n16 and .Sy SP >> 8 at address .Ar n16 + 1. .Pp Cycles: 5 .Pp Bytes: 3 .Pp Flags: None affected. .Ss LD HL,SP+e8 Add the signed value .Ar e8 to .Sy SP and copy the result in .Sy HL . .Pp Cycles: 3 .Pp Bytes: 2 .Pp Flags: .Bl -tag -width Ds .It Sy Z 0 .It Sy N 0 .It Sy H Set if overflow from bit 3. .It Sy C Set if overflow from bit 7. .El .Ss LD SP,HL Copy register .Sy HL into register .Sy SP . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: None affected. .Ss NOP No OPeration. .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: None affected. .Ss OR A,r8 Set .Sy A to the bitwise OR between the value in .Ar r8 and .Sy A . .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 0 .It Sy H 0 .It Sy C 0 .El .Ss OR A,[HL] Set .Sy A to the bitwise OR between the byte pointed to by .Sy HL and .Sy A . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: See .Sx OR A,r8 .Ss OR A,n8 Set .Sy A to the bitwise OR between the value .Ar n8 and .Sy A . .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: See .Sx OR A,r8 .Ss POP AF Pop register .Sy AF from the stack. This is roughly equivalent to the following .Em imaginary instructions: .Bd -literal -offset indent LD F, [SP] ; See below for individual flags INC SP LD A, [SP] INC SP .Ed .Pp Cycles: 3 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set from bit 7 of the popped low byte. .It Sy N Set from bit 6 of the popped low byte. .It Sy H Set from bit 5 of the popped low byte. .It Sy C Set from bit 4 of the popped low byte. .El .Ss POP r16 Pop register .Ar r16 from the stack. This is roughly equivalent to the following .Em imaginary instructions: .Bd -literal -offset indent LD LOW(r16), [SP] ; C, E or L INC SP LD HIGH(r16), [SP] ; B, D or H INC SP .Ed .Pp Cycles: 3 .Pp Bytes: 1 .Pp Flags: None affected. .Ss PUSH AF Push register .Sy AF into the stack. This is roughly equivalent to the following .Em imaginary instructions: .Bd -literal -offset indent DEC SP LD [SP], A DEC SP LD [SP], F.Z << 7 | F.N << 6 | F.H << 5 | F.C << 4 .Ed .Pp Cycles: 4 .Pp Bytes: 1 .Pp Flags: None affected. .Ss PUSH r16 Push register .Ar r16 into the stack. This is roughly equivalent to the following .Em imaginary instructions: .Bd -literal -offset indent DEC SP LD [SP], HIGH(r16) ; B, D or H DEC SP LD [SP], LOW(r16) ; C, E or L .Ed .Pp Cycles: 4 .Pp Bytes: 1 .Pp Flags: None affected. .Ss RES u3,r8 Set bit .Ar u3 in register .Ar r8 to 0. Bit 0 is the rightmost one, bit 7 the leftmost one. .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: None affected. .Ss RES u3,[HL] Set bit .Ar u3 in the byte pointed by .Sy HL to 0. Bit 0 is the rightmost one, bit 7 the leftmost one. .Pp Cycles: 4 .Pp Bytes: 2 .Pp Flags: None affected. .Ss RET Return from subroutine. This is basically a .Sy POP PC (if such an instruction existed). See .Sx POP r16 for an explanation of how .Sy POP works. .Pp Cycles: 4 .Pp Bytes: 1 .Pp Flags: None affected. .Ss RET cc Return from subroutine if condition .Ar cc is met. .Pp Cycles: 5 taken / 2 untaken .Pp Bytes: 1 .Pp Flags: None affected. .Ss RETI Return from subroutine and enable interrupts. This is basically equivalent to executing .Sx EI then .Sx RET , meaning that .Sy IME is set right after this instruction. .Pp Cycles: 4 .Pp Bytes: 1 .Pp Flags: None affected. .Ss RL r8 Rotate bits in register .Ar r8 left, through the carry flag. .Bd -literal ┏━ Flags ━┓ ┏━━━━━━━ r8 ━━━━━━┓ ┌─╂─ C ←╂─╂─ b7 ← ... ← b0 ←╂─┐ │ ┗━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━┛ │ └─────────────────────────────────┘ .Ed .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 0 .It Sy H 0 .It Sy C Set according to result. .El .Ss RL [HL] Rotate the byte pointed to by .Sy HL left, through the carry flag. .Bd -literal ┏━ Flags ━┓ ┏━━━━━━ [HL] ━━━━━┓ ┌─╂─ C ←╂─╂─ b7 ← ... ← b0 ←╂─┐ │ ┗━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━┛ │ └─────────────────────────────────┘ .Ed .Pp Cycles: 4 .Pp Bytes: 2 .Pp Flags: See .Sx RL r8 .Ss RLA Rotate register .Sy A left, through the carry flag. .Bd -literal ┏━ Flags ━┓ ┏━━━━━━━ A ━━━━━━━┓ ┌─╂─ C ←╂─╂─ b7 ← ... ← b0 ←╂─┐ │ ┗━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━┛ │ └─────────────────────────────────┘ .Ed .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z 0 .It Sy N 0 .It Sy H 0 .It Sy C Set according to result. .El .Ss RLC r8 Rotate register .Ar r8 left. .Bd -literal ┏━ Flags ━┓ ┏━━━━━━━ r8 ━━━━━━┓ ┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐ ┗━━━━━━━━━┛ │ ┗━━━━━━━━━━━━━━━━━┛ │ └─────────────────────┘ .Ed .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 0 .It Sy H 0 .It Sy C Set according to result. .El .Ss RLC [HL] Rotate the byte pointed to by .Sy HL left. .Bd -literal ┏━ Flags ━┓ ┏━━━━━━ [HL] ━━━━━┓ ┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐ ┗━━━━━━━━━┛ │ ┗━━━━━━━━━━━━━━━━━┛ │ └─────────────────────┘ .Ed .Pp Cycles: 4 .Pp Bytes: 2 .Pp Flags: See .Sx RLC r8 .Ss RLCA Rotate register .Sy A left. .Bd -literal ┏━ Flags ━┓ ┏━━━━━━━ A ━━━━━━━┓ ┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐ ┗━━━━━━━━━┛ │ ┗━━━━━━━━━━━━━━━━━┛ │ └─────────────────────┘ .Ed .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z 0 .It Sy N 0 .It Sy H 0 .It Sy C Set according to result. .El .Ss RR r8 Rotate register .Ar r8 right, through the carry flag. .Bd -literal ┏━━━━━━━ r8 ━━━━━━┓ ┏━ Flags ━┓ ┌─╂→ b7 → ... → b0 ─╂─╂→ C ─╂─┐ │ ┗━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━┛ │ └─────────────────────────────────┘ .Ed .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 0 .It Sy H 0 .It Sy C Set according to result. .El .Ss RR [HL] Rotate the byte pointed to by .Sy HL right, through the carry flag. .Bd -literal ┏━━━━━━ [HL] ━━━━━┓ ┏━ Flags ━┓ ┌─╂→ b7 → ... → b0 ─╂─╂→ C ─╂─┐ │ ┗━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━┛ │ └─────────────────────────────────┘ .Ed .Pp Cycles: 4 .Pp Bytes: 2 .Pp Flags: See .Sx RR r8 .Ss RRA Rotate register .Sy A right, through the carry flag. .Bd -literal ┏━━━━━━━ A ━━━━━━━┓ ┏━ Flags ━┓ ┌─╂→ b7 → ... → b0 ─╂─╂→ C ─╂─┐ │ ┗━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━┛ │ └─────────────────────────────────┘ .Ed .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z 0 .It Sy N 0 .It Sy H 0 .It Sy C Set according to result. .El .Ss RRC r8 Rotate register .Ar r8 right. .Bd -literal ┏━━━━━━━ r8 ━━━━━━┓ ┏━ Flags ━┓ ┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃ │ ┗━━━━━━━━━━━━━━━━━┛ │ ┗━━━━━━━━━┛ └─────────────────────┘ .Ed .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 0 .It Sy H 0 .It Sy C Set according to result. .El .Ss RRC [HL] Rotate the byte pointed to by .Sy HL right. .Bd -literal ┏━━━━━━ [HL] ━━━━━┓ ┏━ Flags ━┓ ┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃ │ ┗━━━━━━━━━━━━━━━━━┛ │ ┗━━━━━━━━━┛ └─────────────────────┘ .Ed .Pp Cycles: 4 .Pp Bytes: 2 .Pp Flags: See .Sx RRC r8 .Ss RRCA Rotate register .Sy A right. .Bd -literal ┏━━━━━━━ A ━━━━━━━┓ ┏━ Flags ━┓ ┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃ │ ┗━━━━━━━━━━━━━━━━━┛ │ ┗━━━━━━━━━┛ └─────────────────────┘ .Ed .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z 0 .It Sy N 0 .It Sy H 0 .It Sy C Set according to result. .El .Ss RST vec Call address .Ar vec . This is a shorter and faster equivalent to .Sx CALL for suitable values of .Ar vec . .Pp Cycles: 4 .Pp Bytes: 1 .Pp Flags: None affected. .Ss SBC A,r8 Subtract the value in .Ar r8 and the carry flag from .Sy A . .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 1 .It Sy H Set if borrow from bit 4. .It Sy C Set if borrow (i.e. if .Po Ar r8 + carry .Pc > .Sy A ) . .El .Ss SBC A,[HL] Subtract the byte pointed to by .Sy HL and the carry flag from .Sy A . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: See .Sx SBC A,r8 .Ss SBC A,n8 Subtract the value .Ar n8 and the carry flag from .Sy A . .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: See .Sx SBC A,r8 .Ss SCF Set Carry Flag. .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy N 0 .It Sy H 0 .It Sy C 1 .El .Ss SET u3,r8 Set bit .Ar u3 in register .Ar r8 to 1. Bit 0 is the rightmost one, bit 7 the leftmost one. .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: None affected. .Ss SET u3,[HL] Set bit .Ar u3 in the byte pointed by .Sy HL to 1. Bit 0 is the rightmost one, bit 7 the leftmost one. .Pp Cycles: 4 .Pp Bytes: 2 .Pp Flags: None affected. .Ss SLA r8 Shift Left Arithmetically register .Ar r8 . .Bd -literal ┏━ Flags ━┓ ┏━━━━━━━ r8 ━━━━━━┓ ┃ C ←╂─╂─ b7 ← ... ← b0 ←╂─ 0 ┗━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━┛ .Ed .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 0 .It Sy H 0 .It Sy C Set according to result. .El .Ss SLA [HL] Shift Left Arithmetically the byte pointed to by .Sy HL . .Bd -literal ┏━ Flags ━┓ ┏━━━━━━ [HL] ━━━━━┓ ┃ C ←╂─╂─ b7 ← ... ← b0 ←╂─ 0 ┗━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━┛ .Ed .Pp Cycles: 4 .Pp Bytes: 2 .Pp Flags: See .Sx SLA r8 .Ss SRA r8 Shift Right Arithmetically register .Ar r8 .Pq bit 7 of Ar r8 No is unchanged . .Bd -literal ┏━━━━━━ r8 ━━━━━━┓ ┏━ Flags ━┓ ┃ b7 → ... → b0 ─╂─╂→ C ┃ ┗━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━┛ .Ed .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 0 .It Sy H 0 .It Sy C Set according to result. .El .Ss SRA [HL] Shift Right Arithmetically the byte pointed to by .Sy HL .Pq bit 7 of the byte pointed to by Sy HL No is unchanged . .Bd -literal ┏━━━━━ [HL] ━━━━━┓ ┏━ Flags ━┓ ┃ b7 → ... → b0 ─╂─╂→ C ┃ ┗━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━┛ .Ed .Pp Cycles: 4 .Pp Bytes: 2 .Pp Flags: See .Sx SRA r8 .Ss SRL r8 Shift Right Logically register .Ar r8 . .Bd -literal ┏━━━━━━━ r8 ━━━━━━┓ ┏━ Flags ━┓ 0 ─╂→ b7 → ... → b0 ─╂─╂→ C ┃ ┗━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━┛ .Ed .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 0 .It Sy H 0 .It Sy C Set according to result. .El .Ss SRL [HL] Shift Right Logically the byte pointed to by .Sy HL . .Bd -literal ┏━━━━━━ [HL] ━━━━━┓ ┏━ Flags ━┓ 0 ─╂→ b7 → ... → b0 ─╂─╂→ C ┃ ┗━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━┛ .Ed .Pp Cycles: 4 .Pp Bytes: 2 .Pp Flags: See .Sx SRL r8 .Ss STOP Enter CPU very low power mode. Also used to switch between GBC double speed and normal speed CPU modes. .Pp The exact behavior of this instruction is fragile and may interpret its second byte as a separate instruction .Po see .Lk https://gbdev.io/pandocs/Reducing_Power_Consumption.html#using-the-stop-instruction the Pan Docs .Pc , which is why .Xr rgbasm 1 allows explicitly specifying the second byte .Pq Sy STOP Ar n8 to override the default of .Ad $00 .Po a .Sy NOP instruction .Pc . .Pp Cycles: - .Pp Bytes: 2 .Pp Flags: None affected. .Ss SUB A,r8 Subtract the value in .Ar r8 from .Sy A . .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 1 .It Sy H Set if borrow from bit 4. .It Sy C Set if borrow (i.e. if .Ar r8 > .Sy A ) . .El .Ss SUB A,[HL] Subtract the byte pointed to by .Sy HL from .Sy A . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: See .Sx SUB A,r8 .Ss SUB A,n8 Subtract the value .Ar n8 from .Sy A . .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: See .Sx SUB A,r8 .Ss SWAP r8 Swap the upper 4 bits in register .Ar r8 and the lower 4 ones. .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 0 .It Sy H 0 .It Sy C 0 .El .Ss SWAP [HL] Swap the upper 4 bits in the byte pointed by .Sy HL and the lower 4 ones. .Pp Cycles: 4 .Pp Bytes: 2 .Pp Flags: See .Sx SWAP r8 .Ss XOR A,r8 Set .Sy A to the bitwise XOR between the value in .Ar r8 and .Sy A . .Pp Cycles: 1 .Pp Bytes: 1 .Pp Flags: .Bl -tag -width Ds .It Sy Z Set if result is 0. .It Sy N 0 .It Sy H 0 .It Sy C 0 .El .Ss XOR A,[HL] Set .Sy A to the bitwise XOR between the byte pointed to by .Sy HL and .Sy A . .Pp Cycles: 2 .Pp Bytes: 1 .Pp Flags: See .Sx XOR A,r8 .Ss XOR A,n8 Set .Sy A to the bitwise XOR between the value .Ar n8 and .Sy A . .Pp Cycles: 2 .Pp Bytes: 2 .Pp Flags: See .Sx XOR A,r8 .Sh SEE ALSO .Xr rgbasm 1 , .Xr rgblink 1 , .Xr rgbfix 1 , .Xr rgbgfx 1 , .Xr rgbasm-old 5 , .Xr rgbds 7 .Sh HISTORY .Xr rgbasm 1 was originally written by .An Carsten S\(/orensen as part of the ASMotor package, and was later repackaged in RGBDS by .An Justin Lloyd . It is now maintained by a number of contributors at .Lk https://github.com/gbdev/rgbds . gbdev-rgbds-92bfe5d/man/rgbasm-old.5000066400000000000000000000261341512540461700173050ustar00rootroot00000000000000'\" e .\" .\" SPDX-License-Identifier: MIT .\" .Dd January 1, 2026 .Dt RGBASM-OLD 5 .Os .Sh NAME .Nm rgbasm-old .Nd obsolete language documentation .Sh DESCRIPTION This is the list of features that have been removed from the .Xr rgbasm 5 assembly language over its decades of evolution, along with their modern alternatives. Its goal is to be a reference for backwards incompatibility, when upgrading an old assembly codebase to work with the latest RGBDS release. It does .Em not attempt to list every syntax bug that was ever fixed (with some notable exceptions), nor new reserved keywords that may conflict with old identifiers. .Sh REMOVED These are features which have been completely removed, without any direct alternatives. Usually these features were limiting the addition of other features, or had awkward limits on their own intended effects. .Ss Automatic LD to LDH conversion (rgbasm -l) Deprecated in 0.7.0, removed in 0.8.0. .Pp .Xr rgbasm 1 used to automatically treat .Ql LD as .Ql LDH if the address was known to be in the .Ad $FF00-$FFFF range, with the .Fl L flag to opt out. .Xr rgbasm 1 0.6.0 added a .Fl l flag to opt in instead. .Pp Instead, use .Ql LDH , and remove the .Fl L and .Fl l flags from .Xr rgbasm 1 . .Ss Automatic NOP after HALT (rgbasm -H) Deprecated in 0.7.0, removed in 0.8.0. .Pp .Xr rgbasm 1 used to automatically insert a .Ql NOP after .Ql HALT , with the .Fl h flag to opt out. .Xr rgbasm 1 0.6.0 added a .Fl H flag to opt in instead. .Pp Instead, use an explicit .Ql NOP after .Ql HALT , and remove the .Fl h and .Fl H flags from .Xr rgbasm 1 . .Ss Nested macro definitions Removed in 0.4.2. .Pp Instead, put the nested macro definition inside a quoted string (making sure that none of its lines start with .Ic ENDM ) , then interpolate that string. For example: .Bd -literal -offset indent MACRO outer DEF definition EQUS """ MACRO inner println (\e1) - (\e\e1) \enENDM""" {definition} PURGE definition ENDM outer 10 inner 3 ; prints 7 .Ed .Ss Negative DS Removed in 0.3.2. .Pp This was used to "rewind" the value of .Ic @ in RAM sections, allowing labeled space allocations to overlap. .Pp Instead, use .Ic UNION . .Ss Section-local charmaps Deprecated in 0.3.9, removed in 0.4.0. .Pp Defining a .Ic CHARMAP inside a .Ic SECTION when the current global charmap was the .Sq main one used to only define that character mapping within that .Ic SECTION . .Pp Instead, use .Ic PUSHC and .Ic POPC and switch to a different character mapping for that section. .Ss __FILE__ and __LINE__ Deprecated in 0.6.0, removed in 0.7.0. .Pp Instead, use .Ic WARN or .Ic FAIL to print a complete trace of filenames and line numbers. .Ss _PI Deprecated in 0.5.0, removed in 0.6.0. .Pp Instead, use .Ql 3.141592653 . .Ss __DATE__ and __TIME__ Deprecated in 1.0.0. .Pp Instead, use .Ql __ISO_8601_LOCAL__ . .Ss Treating multi-character strings as numbers Deprecated in 0.9.0, removed in 1.0.0. .Pp Instead, use a multi-value .Ic CHARMAP , or explicitly combine the values of individual characters. .Ss Treating strings as numbers Deprecated in 1.0.0. .Pp Instead, use character constants or the .Ic CHARVAL function. .Ss rgbgfx -f/--fix and -F/--fix-and-save Removed in 0.6.0. .Pp Instead, use .Ql rgbgfx -c/--colors to explicitly specify a color palette. If using .Ql -c embedded , arrange the PNG's indexed palette in a separate graphics editor. .Ss rgbgfx -D/--debug Removed in 0.6.0. .Sh REPLACED These are features whose syntax has been changed without affecting functionality. They can generally be updated with a single search-and-replace. .Ss Defining constants and variables without DEF Deprecated in 0.7.0, removed in 0.8.0. .Pp .Ic EQU , EQUS , = , RB , RW , and .Ic RL definitions used to just start with the symbol name, but had to be typed in column 1. .Pp Instead, use .Ic DEF before constant and variable definitions. Note that .Ic EQUS expansion does not occur for the symbol name, so you have to use explicit .Ql {interpolation} . .Ss Defining macros like labels Deprecated in 0.6.0, removed in 0.7.0. .Pp Macros used to be defined as .Ql name: MACRO . .Pp Instead, use .Ql MACRO name . Note that .Ic EQUS expansion does not occur for the macro name, so you have to use explicit .Ql {interpolation} . .Ss Defining variables with SET Deprecated in 0.5.2, removed in 0.6.0. .Pp Variables used to be defined as .Ql name SET value . .Pp Instead, use .Ql DEF name = value . .Ss Global labels without colons Deprecated in 0.4.0, removed in 0.5.0. .Pp Labels used to be definable with just a name, but had to be typed in column 1. .Pp Instead, use explicit colons; for example, .Ql Label: or exported .Ql Label:: . .Ss '\e,' in strings within macro arguments Deprecated in 0.5.0, removed in 0.7.0. .Pp Macro arguments now handle quoted strings and parenthesized expressions as single arguments, so commas inside them are not argument separators and do not need escaping. .Pp Instead, just use commas without backslashes. .Ss '*' comments Deprecated in 0.4.1, removed in 0.5.0. .Pp These comments had to have the .Ql * typed in column 1. .Pp Instead, use .Ql \&; comments. .Ss STRIN, STRRIN, STRSUB, and CHARSUB Deprecated in 1.0.0. .Pp These functions used 1-based indexing of string characters, which was inconsistent with the 0-based indexing used more often in programming. .Pp Instead of .Ic STRIN , use .Ic STRFIND ; instead of .Ic STRRIN , use .Ic STRRFIND ; instead of .Ic STRSUB , use .Ic STRSLICE ; and instead of .Ic CHARSUB , use .Ic STRCHAR . .Pp Note that .Ic STRSLICE takes a start and end index instead of a start index and a length. .Ss PRINTT, PRINTI, PRINTV, and PRINTF Deprecated in 0.5.0, removed in 0.6.0. .Pp These directives were each specific to one type of value. .Pp Instead, use .Ic PRINT and .Ic PRINTLN , with .Ic STRFMT or .Ql {interpolation} for type-specific formatting. .Ss IMPORT and XREF Removed in 0.4.0. .Pp Symbols are now automatically resolved if they were exported from elsewhere. .Pp Instead, just remove these directives. .Ss GLOBAL and XDEF Deprecated in 0.4.2, removed in 0.5.0. .Pp Instead, use .Ic EXPORT . .Ss HOME, CODE, DATA, and BSS Deprecated in 0.3.0, removed in 0.4.0. .Pp Instead of .Ic HOME , use .Ic ROM0 ; instead of .Ic CODE and .Ic DATA , use .Ic ROMX ; and instead of .Ic BSS , use .Ic WRAM0 . .Ss JP [HL] Deprecated in 0.3.0, removed in 0.4.0. .Pp Instead, use .Ql JP HL . .Ss LDI A, HL and LDD A, HL Deprecated in 0.3.0, removed in 0.4.0. .Pp Instead, use .Ql LDI A, [HL] and .Ql LDD A, [HL] (or .Ql LD A, [HLI] and .Ql LD A, [HLD] ; or .Ql LD A, [HL+] and .Ql LD A, [HL-] ) . .Ss LDIO Deprecated in 0.9.0, removed in 1.0.0. .Pp Instead, use .Ql LDH . .Ss LD [C], A and LD A, [C] Deprecated in 0.9.0, removed in 1.0.0. .Pp Instead, use .Ql LDH [C], A and .Ql LDH A, [C] . .Pp Note that .Ql LD [$FF00+C], A and .Ql LD A, [$FF00+C] were also deprecated in 0.9.0, but were .Em undeprecated in 0.9.1. .Ss LDH [n8], A and LDH A, [n8] Deprecated in 0.9.0, removed in 1.0.0. .Pp .Ql LDH used to treat "addresses" from .Ad $00 to .Ad $FF as if they were the low byte of an address from .Ad $FF00 to .Ad $FFFF . .Pp Instead, use .Ql LDH [n16], A and .Ql LDH A, [n16] . .Ss LD HL, [SP + e8] Deprecated in 0.3.0, removed in 0.4.0. .Pp Instead, use .Ql LD HL, SP + e8 . .Ss LDHL SP, e8 Supported in ASMotor, removed in RGBDS. .Pp Instead, use .Ql LD HL, SP + e8 . .Ss OPT z Deprecated in 0.4.0, removed in 0.5.0. .Pp Instead, use .Ic OPT p . .Ss rgbasm -i Deprecated in 0.6.0, removed in 0.8.0. .Pp Instead, use .Fl I or .Fl \-include . .Ss rgbfix -O/--overwrite Deprecated in 1.0.0. .Pp Instead, use .Ql -Wno-overwrite . .Ss rgbgfx -h/--horizontal Removed in 0.6.0. .Pp Instead, use .Fl Z or .Fl \-columns . .Ss rgbgfx --output-* Deprecated in 0.7.0, removed in 0.8.0. .Pp Instead, use .Fl \-auto-* . .Sh CHANGED These are breaking changes that did not alter syntax, and so could not practically be deprecated. .Ss Trigonometry function units Changed in 0.6.0. .Pp Instead of dividing a circle into 65536.0 "binary degrees", it is now divided into 1.0 "turns". .Pp For example, previously we had: .EQ delim $$ .EN .Bl -bullet -offset indent .It .Ql SIN(0.25) == 0.00002 , because 0.25 binary degrees = $0.25 / 65536.0$ turns = $0.000004 tau$ radians = $0.000008 pi$ radians, and $sin ( 0.000008 pi ) = 0.00002$ .It .Ql SIN(16384.0) == 1.0 , because 16384.0 binary degrees = $16384.0 / 65536.0$ turns = $0.25 tau$ radians = $pi / 2$ radians, and $sin ( pi / 2 ) = 1$ .It .Ql ASIN(1.0) == 16384.0 .El .Pp Instead, now we have: .Bl -bullet -offset indent .It .Ql SIN(0.25) == 1.0 , because $0.25$ turns = $0.25 tau$ radians = $pi / 2$ radians, and $sin ( pi / 2 ) = 1$ .It .Ql SIN(16384.0) == 0.0 , because $16384$ turns = $16384 tau$ radians = $32768 pi$ radians, and $sin ( 32768 pi ) = 0$ .It .Ql ASIN(1.0) == 0.25 .El .EQ delim off .EN .Ss % operator behavior with negative dividend or divisor Changed in 0.5.0. .Pp Instead of having the same sign as the dividend (a remainder operation), .Ql % has the same sign as the divisor (a modulo operation). .Pp For example, previously we had: .Bl -bullet -offset indent .It .Ql 13 % 10 == 3 .It .Ql -13 % 10 == -3 .It .Ql 13 % -10 == 3 .It .Ql -13 % -10 == -3 .El .Pp Instead, now we have: .Bl -bullet -offset indent .It .Ql 13 % 10 == 3 .It .Ql -13 % 10 == 7 .It .Ql 13 % -10 == -7 .It .Ql -13 % -10 == -3 .El .Ss ** operator associativity Changed in 0.9.0. .Pp Instead of being left-associative, .Ql ** is now right-associative. .Pp Previously we had .Ql p ** q ** r == (p ** q) ** r . .Pp Instead, now we have .Ql p ** q ** r == p ** (q ** r) . .Sh BUGS These are misfeatures that may have been possible by mistake. They do not get deprecated, just fixed. .Ss Space between exported labels' colons Fixed in 0.7.0. .Pp Labels with two colons used to ignore a space between them; for example, .Ql Label:\ : . .Pp Instead, use .Ql Label:: . .Ss Space between label and colon Fixed in 0.9.0. .Pp Space between a label and its colon(s) used to be ignored; for example, .Ql Label\ : and .Ql Label\ :: . Now they are treated as invocations of the .Ql Label macro with .Ql \&: and .Ql :: as arguments. .Pp Instead, use .Ql Label: and .Ql Label:: . .Ss Extra underscores in integer constants Fixed in 1.0.0. .Pp Underscores, the optional digit separators in integer constants, used to allow more than one in sequence, or trailing without digits on either side. Now only one underscore is allowed between two digits, or between the base prefix and a digit, or between a digit and the .Ql q fixed-point precision suffix. .Ss ADD r16 with implicit first HL operand Fixed in 0.5.0. .Pp For example, .Ql ADD BC used to be treated as .Ql ADD HL, BC , and likewise for .Ql DE , .Ql HL , and .Ql SP . .Pp Instead, use an explicit first .Ql HL operand. .Ss = instead of SET Fixed in 0.4.0. .Pp The .Ic = operator used to be an alias for the .Ic SET keyword, which included using .Ic = for the .Ic SET .Em instruction . .Pp Instead, just use .Ic SET for the instruction. .Sh SEE ALSO .Xr rgbasm 1 , .Xr gbz80 7 , .Xr rgbds 5 , .Xr rgbds 7 .Sh HISTORY .Xr rgbasm 1 was originally written by .An Carsten S\(/orensen as part of the ASMotor package, and was later repackaged in RGBDS by .An Justin Lloyd . It is now maintained by a number of contributors at .Lk https://github.com/gbdev/rgbds . gbdev-rgbds-92bfe5d/man/rgbasm.1000066400000000000000000000401511512540461700165200ustar00rootroot00000000000000.\" SPDX-License-Identifier: MIT .\" .Dd January 1, 2026 .Dt RGBASM 1 .Os .Sh NAME .Nm rgbasm .Nd Game Boy assembler .Sh SYNOPSIS .Nm .Op Fl EhVvw .Op Fl B Ar param .Op Fl b Ar chars .Op Fl \-color Ar when .Op Fl D Ar name Ns Op = Ns Ar value .Op Fl g Ar chars .Op Fl I Ar path .Op Fl M Ar depend_file .Op Fl MG .Op Fl MC .Op Fl MP .Op Fl MT Ar target_file .Op Fl MQ Ar target_file .Op Fl o Ar out_file .Op Fl P Ar include_file .Op Fl p Ar pad_value .Op Fl Q Ar fix_precision .Op Fl r Ar recursion_depth .Op Fl s Ar features Ns : Ns Ar state_file .Op Fl W Ar warning .Op Fl X Ar max_errors .Ar asmfile .Sh DESCRIPTION The .Nm program creates an RGB object file from an assembly source file. The object file format is documented in .Xr rgbds 5 . .Sh ARGUMENTS .Nm accepts the usual short and long options, such as .Fl V and .Fl -version . Options later in the command line override those set earlier, except for when duplicate options are considered an error. Options can be abbreviated as long as the abbreviation is unambiguous: .Fl \-verb is .Fl \-verbose , but .Fl \-ver is invalid because it could also be .Fl \-version . .Pp Unless otherwise noted, passing .Ql - (a single dash) as a file name makes .Nm use standard input (for input files) or standard output (for output files). To suppress this behavior, and open a file in the current directory actually called .Ql - , pass .Ql ./- instead. Using standard input or output for more than one file in a single command may produce unexpected results. .Pp .Nm accepts decimal, hexadecimal, octal, and binary for numeric option arguments. Decimal numbers are written as usual; hexadecimal numbers must be prefixed with either .Ql $ or .Ql 0x ; octal numbers must be prefixed with either .Ql & or .Ql 0o ; and binary numbers must be prefixed with either .Ql % or .Ql 0b . (The prefixes .Ql $ and .Ql & will likely need escaping or quoting to avoid being interpreted by the shell.) Leading zeros (after the base prefix, if any) are accepted, and letters are not case-sensitive. For example, all of these are equivalent: .Ql 42 , .Ql 042 , .Ql 0x2A , .Ql 0X2A , .Ql 0x2a , .Ql &52 , .Ql 0o52 , .Ql 0O052 , .Ql 0b00101010 , .Ql 0B101010 . .Pp The following options are accepted: .Bl -tag -width Ds .It Fl B Ar param , Fl \-backtrace Ar param Configures how location backtraces are printed if warnings or errors occur. This flag may be specified multiple times with different parameters that combine meaningfully. If .Ar param is a positive number, it specifies the maximum backtrace depth, abbreviating deeper ones. Other valid parameter values are the following: .Bl -tag -width Ds .It Cm 0 Do not limit the maximum backtrace depth; this is the default. .It Cm all Force all locations to be printed, even "quiet" ones (see .Dq Excluding locations from backtraces in .Xr rgbasm 5 for details). .It Cm no-all Do not print "quieted" locations in backtraces; this is the default. .It Cm collapse Print all locations on one line. .It Cm no-collapse Print one location per line; this is the default. .El .It Fl b Ar chars , Fl \-binary-digits Ar chars Allow two characters to be used for binary constants in addition to the default .Sq 0 and .Sq 1 . Valid characters are numbers other than .Sq 0 and .Sq 1 , letters, .Sq \&. , .Sq # , or .Sq @ . .It Fl \-color Ar when Specify when to highlight warning and error messages with color: .Ql always , .Ql never , or .Ql auto . .Ql auto determines whether to use colors based on the .Ql Lk https://no-color.org/ NO_COLOR or .Ql Lk https://force-color.org/ FORCE_COLOR environment variables, or whether the output is to a TTY. .It Fl D Ar name Ns Oo = Ns Ar value Oc , Fl \-define Ar name Ns Oo = Ns Ar value Oc Add a string symbol to the compiled source code. This is equivalent to .Ql Ar name Ic EQUS No \(dq Ns Ar value Ns \(dq in code, or .Ql Ar name Ic EQUS No \(dq1\(dq if .Ar value is not specified. .It Fl E , Fl \-export-all Export all labels, including unreferenced and local labels. .It Fl g Ar chars , Fl \-gfx-chars Ar chars Allow four characters to be used for graphics constants in addition to the default .Sq 0 , .Sq 1 , .Sq 2 , and .Sq 3 . Valid characters are numbers other than .Sq 0 to .Sq 3 , letters, .Sq \&. , .Sq # , or .Sq @ . The defaults are 0123. .It Fl h , Fl \-help Print help text for the program and exit. .It Fl I Ar path , Fl \-include Ar path Add a new .Dq include path ; .Ar path must point to a directory. When any .Ic INCLUDE .Pq including the implicit one from Fl P , .Ic INCBIN , or .Ic READFILE is attempted, .Nm first looks up the provided path from its working directory; if this fails, it tries again from each of the .Dq include path directories, in the order they were provided. .It Fl M Ar depend_file , Fl \-dependfile Ar depend_file Write .Xr make 1 dependencies to .Ar depend_file . .It Fl MG To be used in conjunction with .Fl M . This makes .Nm assume that missing files are auto-generated: when any .Ic INCLUDE .Pq including the implicit one from Fl P , .Ic INCBIN , or .Ic READFILE is attempted on a non-existent file, it is added as a dependency, then .Nm exits normally or continues processing (depending on whether .Fl MC was enabled) instead of erroring out. This feature is used in automatic updating of Makefiles. .It Fl MC Implies .Fl MG . This makes .Nm continue processing after a non-existent dependency file, instead of exiting. Note that this is .Em not recommended if any non-existent dependencies would have influenced subsequent processing, e.g. by causing an .Ic IF condition to take a different branch. .It Fl MP When enabled, this adds a phony target to the rules emitted by .Fl M for each dependency other than the main file. This prevents .Xr make 1 from erroring out when dependency files are deleted. .It Fl MT Ar target_file Add a target to the rules emitted by .Fl M . The exact string provided will be written, including spaces and special characters. .Dl Fl MT No fileA Fl MT No fileB is equivalent to .Dl Fl MT No 'fileA fileB' . If neither this nor .Fl MQ is specified, the output file name is used. .It Fl MQ Ar target_file Same as .Fl MT , but additionally escapes any special .Xr make 1 characters, essentially .Sq $ . .It Fl o Ar out_file , Fl \-output Ar out_file Write an object file to the given filename. .It Fl P Ar include_file , Fl \-preinclude Ar include_file Pre-include a file. This acts as if a .Ql Ic INCLUDE Qq Ar include_file was read before the input .Ar asmfile . Multiple files can be pre-included in the order they were provided. .It Fl p Ar pad_value , Fl \-pad-value Ar pad_value Use this as the value for .Ic DS directives in ROM sections, unless overridden. The default is 0x00. .It Fl Q Ar fix_precision , Fl \-q-precision Ar fix_precision Use this as the precision of fixed-point numbers after the decimal point, unless they specify their own precision. The default is 16, so fixed-point numbers are Q16.16 (since they are 32-bit integers). The argument may start with a .Ql \&. to match the Q notation, for example, .Ql Fl Q Ar .16 . .It Fl r Ar recursion_depth , Fl \-recursion-depth Ar recursion_depth Specifies the recursion depth past which .Nm will assume being in an infinite loop. The default is 64. .It Fl s Ar features Ns : Ns Ar state_file , Fl \-state Ar features Ns : Ns Ar state_file Write the specified .Ar features to .Ar state_file , based on the final state of .Nm at the end of its input. The expected .Ar features are a comma-separated subset of the following: .Bl -tag -width Ds .It Cm equ Write all numeric constants as .Ql Ic def Ar name Ic equ Ar value . .It Cm var Write all variables as .Ql Ic def Ar name Ic = Ar value . .It Cm equs Write all string constants as .Ql Ic def Ar name Ic equs Qq Ar value . .It Cm char Write all characters as .Ql Ic charmap Ar name , Ar value . .It Cm macro Write all macros as .Ql Ic macro Ar name No ... Ic endm . .It Cm all Acts like .Cm equ,var,equs,char,macro . .El .Pp This flag may be specified multiple times with different feature subsets to write them to different files (see .Sx EXAMPLES below). .It Fl V , Fl \-version Print the version of the program and exit. .It Fl v , Fl \-verbose Be verbose. The verbosity level is increased by one each time the flag is specified, with each level including the previous: .Bl -enum -compact .It Print the .Nm configuration before taking actions. .It Print a notice before significant actions. .It Print some of the actions' intermediate results. .It Print some internal debug information. .It Print detailed internal information. .El The verbosity level does not go past 6. .Pp Note that verbose output is only intended to be consumed by humans, and may change without notice between RGBDS releases; relying on those for scripts is not advised. .It Fl W Ar warning , Fl \-warning Ar warning Set warning flag .Ar warning . A warning message will be printed if .Ar warning is an unknown warning flag. See the .Sx DIAGNOSTICS section for a list of warnings. .It Fl w Disable all warning output, even when turned into errors. .It Fl X Ar max_errors , Fl \-max-errors Ar max_errors If more than this number of errors (not warnings) occur, then abort the assembly process; .Fl X Ar 0 disables this behavior. The default is 100 if .Nm is printing errors to a terminal, and 0 otherwise. .It @ Ns Ar at_file Read more options and arguments from a file, as if its contents were given on the command line. Arguments are separated by whitespace or newlines. Lines starting with a hash sign .Pq Ql # are considered comments and ignored. .Pp No shell processing is performed, such as wildcard or variable expansion. There is no support for escaping or quoting whitespace to be included in arguments. The standard .Ql -- to stop option processing also disables at-file processing. Note that while .Ql -- can be used .Em inside an at-file, it only disables option processing within that at-file, and processing continues in the parent scope. .El .Sh DIAGNOSTICS Warnings are diagnostic messages that indicate possibly erroneous behavior that does not necessarily compromise the assembling process. The following options alter the way warnings are processed. .Bl -tag -width Ds .It Fl Werror Make all warnings into errors. This can be negated as .Fl Wno-error to prevent turning all warnings into errors. .It Fl Werror= Make the specified warning or meta warning into an error. A warning's name is appended .Pq example: Fl Werror=obsolete , and this warning is implicitly enabled and turned into an error. This can be negated as .Fl Wno-error= to prevent turning a specified warning into an error, even if .Fl Werror is in effect. .El .Pp The following warnings are .Dq meta warnings, that enable a collection of other warnings. If a specific warning is toggled via a meta flag and a specific one, the more specific one takes priority. The position on the command-line acts as a tie breaker, the last one taking effect. .Bl -tag -width Ds .It Fl Wall This enables warnings that are likely to indicate an error or undesired behavior, and that can easily be fixed. .It Fl Wextra This enables extra warnings that are less likely to pose a problem, but that may still be wanted. .It Fl Weverything Enables literally every warning. .El .Pp The following warnings are actual warning flags; with each description, the corresponding warning flag is included. Note that each of these flags also has a negation (for example, .Fl Wobsolete enables the warning that .Fl Wno-obsolete disables; and .Fl Wall enables every warning that .Fl Wno-all disables). Only the non-default flag is listed here. Ignoring the .Dq no- prefix, entries are listed alphabetically. .Bl -tag -width Ds .It Fl Wno-assert Warn when .Ic WARN Ns No -type assertions fail. (See .Dq Aborting the assembly process in .Xr rgbasm 5 for .Ic ASSERT ) . .It Fl Wbackwards-for Warn when .Ic FOR loops have their start and stop values switched according to the step value. This warning is enabled by .Fl Wall . .It Fl Wbuiltin-args Warn about incorrect arguments to built-in functions, such as .Fn STRSLICE with indexes outside of the string's bounds. This warning is enabled by .Fl Wall . .It Fl Wcharmap-redef Warn when re-defining a charmap mapping. This warning is enabled by .Fl Wall . .It Fl Wdiv Warn when dividing the smallest negative integer (-2**31) by -1, which yields itself due to integer overflow. .It Fl Wempty-data-directive Warn when .Ic DB , .Ic DW , or .Ic DL is used without an argument in a ROM section. This warning is enabled by .Fl Wall . .It Fl Wempty-macro-arg Warn when a macro argument is empty. This warning is enabled by .Fl Wextra . .It Fl Wempty-strrpl Warn when .Fn STRRPL is called with an empty string as its second argument (the substring to replace). This warning is enabled by .Fl Wall . .It Fl Wexport-undefined Warn when exporting an undefined symbol. This warning is enabled by .Fl Wall . .It Fl Wno-large-constant Warn when a constant too large to fit in a signed 32-bit integer is encountered. .It Fl Wmacro-shift Warn when shifting macro arguments past their limits. This warning is enabled by .Fl Wextra . .It Fl Wno-nested-comment Warn when the block comment start sequence .Ql /* is found inside of a block comment. Block comments cannot be nested, so the first .Ql */ will end the whole comment. .It Fl Wno-obsolete Warn when obsolete features are encountered, which have been deprecated and may later be removed. .It Fl Wnumeric-string= Warn when a multi-character string is treated as a number. .Fl Wnumeric-string=0 or .Fl Wno-numeric-string disables this warning. .Fl Wnumeric-string=1 or just .Fl Wnumeric-string warns about strings longer than four characters, since four or fewer characters fit within a 32-bit integer. .Fl Wnumeric-string=2 warns about any multi-character string. .It Fl Wpurge= Warn when purging symbols which are likely to have been necessary. .Fl Wpurge=0 or .Fl Wno-purge disables this warning. .Fl Wpurge=1 warns when purging any exported symbol (regardless of type). .Fl Wpurge=2 or just .Fl Wpurge also warns when purging any label (even if not exported). .It Fl Wshift Warn when shifting right a negative value. Use a division by 2**N instead. .It Fl Wshift-amount Warn when a shift's operand is negative or greater than 32. .It Fl Wtruncation= Warn when an implicit truncation (for example, .Ic db to an 8-bit value) loses some bits. .Fl Wtruncation=0 or .Fl Wno-truncation disables this warning. .Fl Wtruncation=1 or just .Fl Wtruncation warns when an N-bit value is 2**N or greater, or less than -2**N. .Fl Wtruncation=2 also warns when an N-bit value is less than -2**(N-1), which will not fit in two's complement encoding. .It Fl Wunmapped-char= Warn when a character goes through charmap conversion but has no defined mapping. .Fl Wunmapped-char=0 or .Fl Wno-unmapped-char disables this warning. .Fl Wunmapped-char=1 or just .Fl Wunmapped-char only warns if the active charmap is not empty. .Fl Wunmapped-char=2 warns if the active charmap is empty, and/or is not the default charmap .Sq main . .It Fl Wunmatched-directive Warn when a .Ic PUSHC , PUSHO , or .Ic PUSHS directive does not have a corresponding .Ic POPC , POPO , or .Ic POPS . This warning is enabled by .Fl Wextra . .It Fl Wunterminated-load Warn when a .Ic LOAD block is not terminated by an .Ic ENDL . This warning is enabled by .Fl Wextra . .It Fl Wno-user Warn when the .Ic WARN built-in is executed. (See .Dq Aborting the assembly process in .Xr rgbasm 5 for .Ic WARN ) . .El .Sh EXAMPLES You can assemble a source file in two ways. .Pp Straightforward way: .Dl $ rgbasm -o bar.o foo.asm .Pp Pipes way: .Dl $ cat foo.asm | rgbasm -o bar.o - .Dl $ rgbasm -o bar.o - < foo.asm .Pp The resulting object file is not yet a usable ROM image\(emit must first be run through .Xr rgblink 1 and then .Xr rgbfix 1 . .Pp Writing the final assembler state to a file: .Dl $ rgbasm -s all:state.dump.asm foo.asm .Pp Or to multiple files: .Dl $ rgbasm -s equ,var:numbers.dump.asm -s equs:strings.dump.asm foo.asm .Sh BUGS Please report bugs or mistakes in this documentation on .Lk https://github.com/gbdev/rgbds/issues GitHub . .Sh SEE ALSO .Xr rgbasm 5 , .Xr rgblink 1 , .Xr rgbfix 1 , .Xr rgbgfx 1 , .Xr gbz80 7 , .Xr rgbasm-old 5 , .Xr rgbds 5 , .Xr rgbds 7 .Sh HISTORY .Nm was originally written by .An Carsten S\(/orensen as part of the ASMotor package, and was later repackaged in RGBDS by .An Justin Lloyd . It is now maintained by a number of contributors at .Lk https://github.com/gbdev/rgbds . gbdev-rgbds-92bfe5d/man/rgbasm.5000066400000000000000000002234641512540461700165360ustar00rootroot00000000000000'\" e .\" .\" SPDX-License-Identifier: MIT .\" .Dd January 1, 2026 .Dt RGBASM 5 .Os .Sh NAME .Nm rgbasm .Nd language documentation .Sh DESCRIPTION This is the full description of the assembly language used by .Xr rgbasm 1 . For the full description of instructions in the machine language supported by the Game Boy CPU, see .Xr gbz80 7 . .Pp It is advisable to have some familiarity with the Game Boy hardware before reading this document. RGBDS is specifically targeted at the Game Boy, and thus a lot of its features tie directly to its concepts. This document is not intended to be a Game Boy hardware reference. .Pp Generally, .Dq the linker will refer to .Xr rgblink 1 , but any program that processes RGBDS object files (described in .Xr rgbds 5 ) can be used in its place. .Sh SYNTAX The syntax is line-based, just as in any other assembler. Each line may have components in either of these orders: .Bl -bullet -offset indent .It .Oo Ar label : Oc Oo Ar directive Oc Oo ;\ Ns Ar comment Oc .It .Oo Ar label : Oc Oo Ar instruction Oo :: Ar instruction ... Oc Oc Oo ;\ Ns Ar comment Oc .El .Pp Directives are commands to the assembler itself, such as .Ic PRINTLN , .Ic SECTION , or .Ic OPT . .Pp Labels tie a name to a specific location within a section (see .Sx Labels below). Labels are allowed before most directives, but not before .Ic IF , .Ic ELIF , .Ic ELSE , .Ic ENDC , .Ic REPT , .Ic FOR , .Ic ENDR , .Ic MACRO , or .Ic ENDM . .Pp Instructions are assembled into Game Boy opcodes. Multiple instructions on one line, as well as data directives (see .Sx Defining constant data in ROM below), can be separated by double colons .Ql :: . .Pp The available instructions are documented in .Xr gbz80 7 . .Pp Note that where an instruction requires an 8-bit register .Ar r8 , .Nm can interpret .Ic HIGH Ns Pq Ar r16 as the top 8-bit register of the given .Ar r16 , for example, .Ic HIGH Ns Pq Ic HL for .Ic H ; and .Ic LOW Ns Pq Ar r16 as the bottom one, for example, .Ic LOW Ns Pq Ic HL for .Ic L (except for .Ic LOW Ns Pq Ic AF , since .Ic F is not a valid register). .Pp Note also that where an instruction requires a condition code .Ar cc , .Nm can interpret .Ic ! Ns Ar cc as the opposite condition code; for example, .Ic !nz for .Ic z . .Pp All reserved keywords (directives, instructions, registers, built-in functions, etc.) are case-insensitive; all identifiers (labels, variables, etc) are case-sensitive. .Pp Comments are used to give humans information about the code, such as explanations. The assembler .Em always ignores comments and their contents. .Pp There are two kinds of comments, inline and block. Inline comments are anything that follows a semicolon .Ql \&; not inside a string, until the end of the line. Block comments, beginning with .Ql /* and ending with .Ql */ , can be split across multiple lines, or occur in the middle of an expression. .Pp An example demonstrating these syntax features: .Bd -literal -offset indent SECTION "My Code", ROM0\ \ ;\ a directive MyFunction:\ \ \ \ \ \ \ \ \ \ \ \ \ \ ;\ a label push hl\ \ \ \ \ \ \ \ \ \ \ \ \ \ ;\ an instruction /* ...and multiple instructions, with mixed case */ ld a, [hli] :: LD H, [HL] :: Ld l, a pop /*wait for it*/ hl ret .Ed .Pp Sometimes lines can be too long and it may be necessary to split them. To do so, put a backslash at the end of the line: .Bd -literal -offset indent DB 1, 2, 3,\ \e 4, 5, 6,\ \e\ ;\ Put it before any comments 7, 8, 9 DB "Hello,\ \e\ \ ;\ Space before the \e is included world!"\ \ \ \ \ \ \ \ \ \ \ ;\ Any leading space is included .Ed .Ss Symbol interpolation Symbols with string or numeric values can be .Dq interpolated by writing them inside .Ql {braces} . This will paste the symbol's contents as if they were part of the source file. If it is a string symbol, its characters are simply inserted as-is. If it is a numeric symbol, its value is converted to hexadecimal notation with a dollar sign .Sq $ prepended. .Pp Symbol interpolations can be nested, too. .Bd -literal -offset indent DEF topic EQUS "life, the universe, and \e"everything\e"" DEF meaning EQUS "answer" ;\ Defines answer = 42 DEF {meaning} = 42 ;\ Prints "The answer to life, the universe, and "everything" is $2A" PRINTLN "The {meaning} to {topic} is {{meaning}}" PURGE topic, meaning, {meaning} .Ed .Pp Symbols can be interpolated even in contexts that disable automatic expansion of string constants: that is, .Ql name will be expanded in all of .Ql DEF({name}) , .Ql DEF {name} EQU/=/EQUS/etc ... , .Ql REDEF {name} EQU/=/EQUS/etc ... , .Ql FOR {name}, ... , .Ql PURGE {name} , and .Ql MACRO {name} , even though it won't be in .Ql DEF(name) , .Ql PURGE {name} , etc. .Pp It's possible to change the way symbols are printed by specifying a print format like so: .Ql {fmt:symbol} . The .Ql fmt specifier consists of parts, which must be in the following order: .Ql . All the parts are optional except the required .Ql . These parts are: .Bl -column "" .It Sy Part Ta Sy Meaning .It Ql Ta May be .Ql + or .Ql \ . If specified, prints this character in front of non-negative numbers. .It Ql Ta May be .Ql # .Pq only allowed for non-decimal types . If specified, prints the value in an "exact" format: with a base prefix .Pq So $ Sc , So & Sc , or So % Sc for non-decimal integer types .Pq So x Sc / So X Sc , So o Sc , or So b Sc ; with a .Ql q precision suffix for fixed-point numbers; or with .Ql \e escape characters (but no enclosing quotes) for strings. .It Ql Ta May be .Ql - . If specified, aligns left instead of right. .It Ql Ta May be .Ql 0 . If specified, pads right-aligned numbers with zeros instead of spaces. .It Ql Ta May be one or more .Ql 0 \[en] .Ql 9 . If specified, pads the value to this width, right-aligned with spaces by default. .It Ql Ta May be .Ql \&. followed by zero or more .Ql 0 \[en] .Ql 9 . If specified, prints this many fractional digits of a fixed-point number. Defaults to 5 digits, maximum 255 digits. (A .Ql \&. followed by zero .Ql 0 \[en] .Ql 9 prints zero fractional digits and no decimal point.) .It Ql Ta May be .Ql q followed by one or more .Ql 0 \[en] .Ql 9 . If specified, prints a fixed-point number at this precision. Defaults to the current .Fl Q option. .It Ql Ta Specifies the type of value. .El .Pp Valid types are: .Bl -column -offset indent "Type" "Lowercase hexadecimal" "Example" .It Sy Type Ta Sy Format Ta Sy Example .It Ql d Ta Signed decimal Ta -42 .It Ql u Ta Unsigned decimal Ta 4294967254 .It Ql x Ta Lowercase hexadecimal Ta 2a .It Ql X Ta Uppercase hexadecimal Ta 2A .It Ql b Ta Binary Ta 101010 .It Ql o Ta Octal Ta 52 .It Ql f Ta Fixed-point Ta 1234.56789 .It Ql s Ta String Ta string contents .El .Pp Examples: .Bd -literal -offset indent SECTION "Test", ROM0[2] X: ;\ This works with labels **whose address is known** DEF Y = 3 ;\ This also works with variables DEF SUM EQU X + Y ;\ And likewise with numeric constants ; Prints "%0010 + $3 == 5" PRINTLN "{#05b:X} + {#x:Y} == {d:SUM}" rsset 32 DEF PERCENT rb 1 ;\ Same with offset constants DEF VALUE = 20 DEF RESULT = MUL(20.0, 0.32) ; Prints "32% of 20 = 6.40" PRINTLN "{d:PERCENT}% of {d:VALUE} = {f:RESULT}" DEF WHO EQUS STRLWR("WORLD") ; Prints "Hello world!" PRINTLN "Hello {s:WHO}!" .Ed .Pp Although, for these examples, .Ic STRFMT would be more appropriate; see .Sx String expressions below. .Sh EXPRESSIONS There are two types of expressions: numeric and string. .Pp Numeric expressions are always evaluated using signed 32-bit math. In Boolean logic contexts, zero is considered to be the only "false" number, and all non-zero numbers (including negative) are "true". .Pp An expression is said to be "constant" if .Nm knows its value. This is generally always the case, unless a label is involved, as explained in the .Sx SYMBOLS section. However, some operators can be constant even with non-constant operands, as explained in .Sx Operators below. .Pp Directives generally require constant expressions: for example, .Ic REPT requires the number of repetitions to be known at assembly time. .Ss Numeric literals .Nm supports a variety of numeric literals. .Bl -column -offset indent "Precise fixed-point" "Prefixes" "Accepted characters" .It Sy Format type Ta Sy Prefixes Ta Sy Accepted characters .It Decimal Ta none Ta 0123456789 .It Hexadecimal Ta Li $ , 0x , 0X Ta 0123456789ABCDEF .It Octal Ta Li & , 0o , 0O Ta 01234567 .It Binary Ta Li % , 0b , 0B Ta 01 .It Fixed-point Ta none Ta 01234.56789 .It Precise fixed-point Ta none Ta 12.34q8 .It Character constant Ta none Ta 'A' .It Game Boy graphics Ta Li \` Ta 0123 .El .Pp Underscores are also accepted in numbers, except at the beginning of one. This can be useful for grouping digits, like .Ql 123_456 or .Ql %1100_1001 . .Pp The "character constant" form yields the value the character maps to in the current charmap. For example, by default .Pq refer to Xr ascii 7 .Sq 'A' yields 65. A character constant must represent a single value, so it cannot include multiple characters, or characters which map to multiple values. See .Sx Character maps for information on charmaps, and .Sx String expressions for information on escape characters allowed in character constants. .Pp The last one, Game Boy graphics, expects up to eight digits between 0 and 3, corresponding to pixels' two-bit shade values. The resulting numeric value is the two bytes of tile data which would produce that row of pixels. For example, .Sq \`01012323 is equivalent to .Sq $0F55 . .Pp In place of a numeric literal, you can also use a numeric symbol's name, which is implicitly replaced with its value. .Ss Operators You can use these operators in numeric expressions (listed from highest to lowest precedence): .Bl -column -offset indent "!= == <= >= < >" .It Sy Operator Ta Sy Meaning .It Li \&( \&) Ta Grouping .It Li FUNC() Ta Built-in function call .It Li ** Ta Exponentiation .It Li + - ~ \&! Ta Unary plus, unary minus (negation), complement (bitwise negation), and Boolean negation .It Li * / % Ta Multiplication, division (rounding down), and modulo (remainder) .It Li << >> >>> Ta Bit shifts (left, sign-extended right, zero-extended right) .It Li & \&| ^ Ta Bitwise AND/OR/XOR .It Li + - Ta Addition and subtraction .It Li == != < > <= >= Ta Comparisons .It Li && Ta Boolean AND .It Li || Ta Boolean OR .El .Pp .Sq ** raises a number to a non-negative power. It is the only .Em right-associative operator, meaning that .Ql p ** q ** r is equal to .Ql p ** (q ** r) , not .Ql (p ** q) ** r . All other binary operators are left-associative. .Pp .Sq ~ complements a value by inverting all 32 of its bits. .Pp .Sq % is used to get the remainder of the corresponding division, so that .Ql x / y * y + x % y == x is always true. The result has the same sign as the divisor. This makes .Ql x % y equal to .Ql (x + y) % y or .Ql (x - y) % y . .Pp Shifting works by shifting all bits in the left operand either left .Pq Sq << or right .Pq Sq >> by the right operand's amount. When shifting left, all newly-inserted bits are reset; when shifting right, they are copies of the original most significant bit instead. This makes .Sq a << b and .Sq a >> b equivalent to multiplying and dividing by 2 to the power of b, respectively. .Pp Comparison operators return 0 if the comparison is false, and 1 otherwise. .Pp Unlike in many other languages, and for technical reasons, .Nm still evaluates both operands of .Sq && and .Sq || . .Pp The operators .Sq && and .Sq & with a zero constant as either operand will be constant 0, and .Sq || with a non-zero constant as either operand will be constant 1, even if the other operand is non-constant. .Pp .Sq \&! returns 1 if the operand was 0, and 0 otherwise. Even a non-constant operand with any non-zero bits will return 0. .Ss Integer functions Besides operators, there are also some functions which have more specialized uses: .Bl -column "BITWIDTH(n)" .It Sy Name Ta Sy Operation .It Fn HIGH n Ta Equivalent to Ql Po Ns Ar n No & $FF00 Pc >> 8 . .It Fn LOW n Ta Equivalent to Ql Ar n No & $FF . .EQ delim $$ .EN .It Fn BITWIDTH n Ta Returns the number of bits necessary to represent .Ar n . Some useful formulas: .Ic BITWIDTH Ns ( Ar n Ns )\ \-\ 1 equals $\[lf] log sub 2 ( n ) \[rf]$; .Ic BITWIDTH Ns Pq Ar n Ns \ \-\ 1 equals $\[lc] log sub 2 ( n ) \[rc]$; and .No 32\ \-\ Ns Ic BITWIDTH Ns Pq Ar n equals $roman clz ( n )$, the count of leading zero bits in the binary representation of .Ar n . .It Fn TZCOUNT n Ta Returns $roman ctz ( n )$, the count of trailing zero bits in the binary representation of .Ar n . .El .EQ delim off .EN .Ss Fixed-point expressions Fixed-point numbers are technically just integers, but conceptually they have a decimal point at a fixed location (hence the name). This gives them increased precision, at the cost of a smaller range, while remaining far cheaper to manipulate than floating-point numbers (which .Nm does not support). .Pp The default precision of all fixed-point numbers is 16 bits, meaning the lower 16 bits are used for the fractional part; so they count in 65536ths of 1.0. This precision can be changed with the .Fl Q command-line option, and/or by .Ic OPT Q .Pq see Sx Changing options while assembling . An individual fixed-point literal can specify its own precision, overriding the current default, by appending a .Dq q followed by the number of fractional bits: for example, .Ql 789.25q8 is equal to $000315_40 .EQ delim $$ .EN ($= 789.25 * 2 sup 8$). .Pp Since fixed-point values are still just integers, you can use them in normal integer expressions. You can easily truncate a fixed-point number into an integer by shifting it right by the number of fractional bits, or by dividing it by 1.0. It follows that you can convert an integer to a fixed-point number by shifting it left that same amount, or by multiplying it by 1.0. For example, .Ql 123.0 / 1.0 == 123 , and .Ql 123 * 1.0 == 123.0 . .Pp Note that the current number of fractional bits can be computed as .Ic TZCOUNT Ns Pq 1.0 . .Pp The following functions are designed to operate with fixed-point numbers (which must be known constant): .Bl -column -offset indent "ATAN2(y, x)" .It Sy Name Ta Sy Operation .It Fn DIV x y Ta Fixed-point division .It Fn MUL x y Ta Fixed-point multiplication .It Fn FMOD x y Ta Fixed-point modulo .It Fn POW x y Ta $x sup y$ .It Fn LOG x y Ta Logarithm of $x$ to the base $y$ .It Fn ROUND x Ta Round $x$ half away from zero to the nearest integer .It Fn CEIL x Ta Round $x$ up to the nearest integer .It Fn FLOOR x Ta Round $x$ down to the nearest integer .It Fn SIN x Ta Sine of $x$ .It Fn COS x Ta Cosine of $x$ .It Fn TAN x Ta Tangent of $x$ .It Fn ASIN x Ta Inverse sine of $x$ .It Fn ACOS x Ta Inverse cosine of $x$ .It Fn ATAN x Ta Inverse tangent of $x$ .It Fn ATAN2 y x Ta Angle between $( x , y )$ and $( 1 , 0 )$ .El .EQ delim off .EN .Pp There are no functions for fixed-point addition and subtraction, because the .Sq + and .Sq - operators can add and subtract pairs of fixed-point operands. .Bd -ragged -offset indent Note that some operators or functions are meaningful when combining integers and fixed-point values. For example, .Ql 2.0 * 3 is equivalent to .Ql MUL(2.0, 3.0) , and .Ql 6.0 / 2 is equivalent to .Ql DIV(6.0, 2.0) . Be careful and think about what the operations mean when doing this sort of thing. .Ed .Pp All of these fixed-point functions can take an optional final argument, which is the precision to use for that one operation. For example, .Ql MUL(6.0q8, 7.0q8, 8) will evaluate to .Ql 42.0q8 no matter what value is set as the current .Cm Q option. .Nm .Em does not check precisions for consistency , so nonsensical input like .Ql MUL(4.2q8, 6.9q12, 16) will produce a nonsensical (but technically correct) result: .Dq garbage in, garbage out . .Pp The .Ic FMOD function is used to get the remainder of the corresponding fixed-point division. The result has the same sign as the .Em dividend ; this is the opposite of how the integer modulo operator .Sq % works! .Pp The trigonometry functions .Pq Ic SIN , Ic COS , Ic TAN , No etc are defined in terms of a circle divided into 1.0 .Dq turns .EQ delim $$ .EN (equal to $2 pi$ radians, or 360 degrees). .EQ delim off .EN .Pp These functions are useful for automatic generation of various tables. For example: .Bd -literal -offset indent ; Generate a table of 128 sine values ; from sin(0.0) included to sin(0.5) excluded, ; with amplitude scaled from [-1.0, 1.0] to [0.0, 128.0], ; then divided by 1.0 to round down to integer values. FOR angle, 0.0, 0.5, 0.5 / 128 db MUL(SIN(angle) + 1.0, 128.0 / 2) / 1.0 ENDR .Ed .Ss String expressions The most basic string expression is a string literal: any number of characters contained in double quotes .Pq Ql \&"for instance" . The backslash character .Ql \e is special in that it causes the character following it to be .Dq escaped , meaning that it is treated differently from normal. There are a number of escape sequences you can use within a string: .Bl -column -offset indent "Sequence" .It Sy Sequence Ta Sy Meaning .It Ql \e\e Ta Backslash Pq escapes the escape character itself .It Ql \e" Ta Double quote Pq does not terminate a string .It Ql \e' Ta Single quote Pq does not terminate a character literal .It Ql \e{ Ta Open curly brace Pq does not start interpolation .It Ql \e} Ta Close curly brace Pq does not end interpolation .It Ql \en Ta Newline Pq ASCII $0A .It Ql \er Ta Carriage return Pq ASCII $0D .It Ql \et Ta Tab Pq ASCII $09 .It Ql \e0 Ta Null Pq ASCII $00 .El .Pp Multi-line string literals are contained in triple quotes .Pq Ql \&"\&"\&"for instance""" . Escape sequences work the same way in multi-line strings; however, literal newline characters will be included as-is, without needing to escape them with .Ql \er or .Ql \en . .Pp Raw string literals are prefixed by a hash .Sq # . Inside them, backslashes and braces are treated like regular characters, so they will not be expanded as macro arguments, interpolated symbols, or escape sequences. For example, the raw string .Ql #"\et\e1{s}\e" is equivalent to the regular string .Ql \&"\e\et\e\e1\e{s}\e\e" . (Note that this prevents raw strings from including the double quote character.) Raw strings also may be contained in triple quotes for them to be multi-line, so they can include literal newline or quote characters (although still not three quotes in a row). .Pp You can use the .Sq ++ operator to concatenate two strings. .Ql \&"str" ++ \&"ing" is equivalent to .Ql \&"string" , or to .Ql STRCAT("str", \&"ing") . .Pp You can use the .Sq === and .Sq !== operators to compare two strings. .Ql \&"str" === \&"ing" is equivalent to .Ql STRCMP("str", \&"ing") == 0 , and .Ql \&"str" !== \&"ing" is equivalent to .Ql STRCMP("str", \&"ing") != 0 . .Pp The following functions operate on string expressions, and return strings themselves: .Bl -column "STRSLICE(str, start, stop)" .It Sy Name Ta Sy Operation .It Fn STRCAT strs... Ta Concatenates Ar strs . .It Fn STRUPR str Ta Returns Ar str No with all ASCII letters .Pq Ql a-z in uppercase. .It Fn STRLWR str Ta Returns Ar str No with all ASCII letters .Pq Ql A-Z in lowercase. .It Fn STRSLICE str start stop Ta Returns a substring of Ar str No starting at Ar start No and ending at Ar stop No (exclusive). If Ar stop No is not specified, the substring continues to the end of Ar str . .It Fn STRRPL str old new Ta Returns Ar str No with each occurrence of the substring Ar old No replaced with Ar new . .It Fn STRFMT fmt args... Ta Returns the string Ar fmt No with each .Ql %spec pattern replaced by interpolating the format .Ar spec .Pq using the same syntax as Sx Symbol interpolation with its corresponding argument in .Ar args .Pq So %% Sc is replaced by the So % Sc character . .It Fn STRCHAR str idx Ta Returns the substring of Ar str No for the charmap entry at Ar idx No with the current charmap . Pq Ar idx No counts charmap entries, not characters. .El .Pp The following functions take varying operands, and return strings: .Bl -column "READFILE(name, max)" .It Fn REVCHAR vals... Ta Returns the string that is mapped to Ar vals No with the current charmap. If there is no unique charmap entry for Ar vals Ns , an error occurs. .It Fn READFILE name max Ta Returns the contents of the file Ar name No as a string. Reads up to Ar max No bytes, or the entire contents if Ar max No is not specified. If the file isn't found in the current directory, the include-path list passed to Xr rgbasm 1 Ap s Fl I No option on the command line will be searched. .El .Pp The following functions operate on string expressions, but return integers: .Bl -column "STRRFIND(str, sub)" .It Sy Name Ta Sy Operation .It Fn STRLEN str Ta Returns the number of characters in Ar str . .It Fn STRCMP str1 str2 Ta Compares Ar str1 No and Ar str2 No according to ASCII ordering of their characters. Returns -1 if Ar str1 No is lower than Ar str2 Ns , 1 if Ar str1 No is greater than Ar str2 Ns , or 0 if they match. .It Fn STRFIND str sub Ta Returns the first index of Ar sub No in Ar str Ns , or -1 if it's not present. .It Fn STRRFIND str sub Ta Returns the last index of Ar sub No in Ar str Ns , or -1 if it's not present. .It Fn BYTELEN str Ta Returns the number of bytes in Ar str . Pq Non-ASCII characters can be multiple bytes. .It Fn STRBYTE str idx Ta Returns the byte value at Ar idx No in Ar str . .It Fn INCHARMAP str Ta Returns 1 if Ar str No has an entry in the current charmap, or 0 otherwise. .It Fn CHARLEN str Ta Returns the number of charmap entries in Ar str No with the current charmap. .It Fn CHARCMP str1 str2 Ta Compares Ar str1 No and Ar str2 No according to their charmap entry values with the current charmap. Returns -1 if Ar str1 No is lower than Ar str2 Ns , 1 if Ar str1 No is greater than Ar str2 Ns , or 0 if they match. .It Fn CHARSIZE char Ta Returns how many values are in the charmap entry for Ar char No with the current charmap. .It Fn CHARVAL char idx Ta Returns the value at Ar idx No of the charmap entry for Ar char . If Ar idx No is not specified, Ar char No must have a single value, which is returned. .El .Pp Note that indexes count starting from 0 at the beginning, or from -1 at the end. The characters of a string are counted by .Ql STRLEN ; the charmap entries of a string are counted by .Ql CHARLEN ; and the values of a charmap entry are counted by .Ql CHARSIZE . .Ss Character maps When writing text strings that are meant to be displayed on the Game Boy, the character encoding in the ROM may need to be different than the source file encoding. For example, the tiles used for uppercase letters may be placed starting at tile index 128, which differs from ASCII starting at 65. .Pp Character maps allow mapping strings or character literals to arbitrary sequences of numbers: .Bd -literal -offset indent CHARMAP "A", 42 CHARMAP ':)', 39 CHARMAP "
", 13, 10 CHARMAP '€', $20ac .Ed .Pp This would result in .Ql db \(dqAmen :)
\(dq being equivalent to .Ql db 42, 109, 101, 110, 32, 39, 13, 10 , and .Ql dw \(dq25€\(dq being equivalent to .Ql dw 50, 53, $20ac . .Pp Character mappings are matched greedily, so the longest applicable one will be mapped in a string. Any characters in the string without defined mappings will be copied directly, using the source file's encoding of characters to bytes. .Pp It is possible to create multiple character maps and then switch between them as desired. This can be used to encode debug information in ASCII and use a different encoding for other purposes, for example. Initially, there is one character map called .Sq main and it is automatically selected as the current character map from the beginning. There is also a character map stack that can be used to save and restore which character map is currently active. .Bl -column "NEWCHARMAP name, basename" .It Sy Command Ta Sy Meaning .It Ic NEWCHARMAP Ar name Ta Creates a new, empty character map called Ar name No and switches to it . .It Ic NEWCHARMAP Ar name , basename Ta Creates a new character map called Ar name , No copied from character map Ar basename , No and switches to it . .It Ic SETCHARMAP Ar name Ta Switch to character map Ar name . .It Ic PUSHC Ta Push the current character map onto the stack. .It Ic PUSHC Ar name Ta Push the current character map onto the stack and switch to character map Ar name . .It Ic POPC Ta Pop a character map off the stack and switch to it. .El .Pp .Sy Note : Modifications to a character map take effect immediately from that point onward. .Ss Other functions There are a few other functions that do things beyond numeric or string operations: .Bl -column "SECTION(symbol)" .It Sy Name Ta Sy Operation .It Fn DEF symbol Ta Returns 1 if .Ar symbol has been defined, 0 otherwise. String constants are not expanded within the parentheses. .It Fn ISCONST arg Ta Returns 1 if Ar arg Ap s value is known by RGBASM (e.g. if it can be an argument to .Ic IF ) , or 0 if only RGBLINK can compute its value. .It Fn BANK arg Ta Returns a bank number. If .Ar arg is the symbol .Ic @ , this function returns the bank of the current section. If .Ar arg is a string, it returns the bank of the section that has that name. If .Ar arg is a label, it returns the bank number the label is in. The result may be constant if .Nm is able to compute it. .It Fn SECTION symbol Ta Returns the name of the section that .Ar symbol is in. .Ar symbol must have been defined already. .It Fn SIZEOF arg Ta If .Ar arg is a string, this function returns the size of the section named .Ar arg . If .Ar arg is a section type keyword, it returns the size of that section type. The result is not constant, since only RGBLINK can compute its value. If .Ar arg is an 8-bit or 16-bit register, it returns the size of that register. .It Fn STARTOF arg Ta If .Ar arg is a string, this function returns the starting address of the section named .Ar arg . If .Ar arg is a section type keyword, it returns the starting address of that section type. The result is not constant, since only RGBLINK can compute its value. .El .Sh SECTIONS Before you can start writing code, you must define a section. This tells the assembler what kind of information follows and where to put it. .Pp .Dl SECTION Ar name , type .Dl SECTION Ar name , type , options .Dl SECTION Ar name , type Ns Bo Ar addr Bc .Dl SECTION Ar name , type Ns Bo Ar addr Bc , Ar options .Pp .Ar name is a string enclosed in double quotes, which is the name of the section. If the type doesn't match, an error occurs. Each section must have a unique name, even across different source files, or the linker will treat it as an error. .Pp Possible section .Ar type Ns s are as follows: .Bl -tag -width Ds .It Ic ROM0 A ROM section. .Ar addr can range from .Ad $0000 to .Ad $3FFF , or .Ad $0000 to .Ad $7FFF if tiny ROM mode is enabled in the linker. .It Ic ROMX A banked ROM section. .Ar addr can range from .Ad $4000 to .Ad $7FFF . Becomes an alias for .Ic ROM0 if tiny ROM mode is enabled in the linker. .It Ic VRAM A banked video RAM section. .Ar addr can range from .Ad $8000 to .Ad $9FFF . .Ar bank can be 0 or 1, but bank 1 is unavailable if DMG mode is enabled in the linker. .It Ic SRAM A banked external (save) RAM section. .Ar addr can range from .Ad $A000 to .Ad $BFFF . .It Ic WRAM0 A general-purpose RAM section. .Ar addr can range from .Ad $C000 to .Ad $CFFF , or .Ad $C000 to .Ad $DFFF if WRAM0 mode is enabled in the linker. .It Ic WRAMX A banked general-purpose RAM section. .Ar addr can range from .Ad $D000 to .Ad $DFFF . .Ar bank can range from 1 to 7. Becomes an alias for .Ic WRAM0 if WRAM0 mode is enabled in the linker. .It Ic OAM An object attribute RAM section. .Ar addr can range from .Ad $FE00 to .Ad $FE9F . .It Ic HRAM A high RAM section. .Ar addr can range from .Ad $FF80 to .Ad $FFFE . .El .Pp RGBDS produces ROMs, which means that code and data can only be placed in .Ic ROM0 and .Ic ROMX sections. The other RAM section types are for statically allocated labels. If you need code or data in RAM, you will need to copy it from ROM to RAM yourself. See .Sx RAM code for an example of how to conveniently do that with a .Ic LOAD block. .Pp .Ar option Ns s are comma-separated and may include: .Bl -tag -width Ds .It Ic BANK Ns Bq Ar bank Specify which .Ar bank for the linker to place the section in. See above for possible values for .Ar bank , depending on .Ar type . .It Ic ALIGN Ns Bq Ar align , offset Place the section at an address whose .Ar align least-significant bits are equal to .Ar offset . Note that .Ic ALIGN Ns Bq Ar align is a shorthand for .Ic ALIGN Ns Bq Ar align , No 0 . This option can be used with .Bq Ar addr , as long as they don't contradict each other. It's also possible to request alignment in the middle of a section; see .Sx Requesting alignment below. .El .Pp If .Bq Ar addr is not specified, the section is considered .Dq floating ; the linker will automatically calculate an appropriate address for the section. Similarly, if .Ic BANK Ns Bq Ar bank is not specified, the linker will automatically find a bank with enough space. .Pp Sections can also be placed by using a linker script file. The format is described in .Xr rgblink 5 . They allow the user to place floating sections in the desired bank in the order specified in the script. This is useful if the sections can't be placed at an address manually because the size may change, but they have to be together. .Pp Section examples: .Bl -item .It .Bd -literal -offset indent SECTION "Cool Stuff", ROMX .Ed .Pp This switches to the section called .Dq CoolStuff , creating it if it doesn't already exist. It can end up in any ROM bank. Code and data may follow. .It If it is needed, the base address of the section can be specified: .Bd -literal -offset indent SECTION "Cool Stuff", ROMX[$4567] .Ed .It An example with a fixed bank: .Bd -literal -offset indent SECTION "Cool Stuff", ROMX[$4567], BANK[3] .Ed .It And if you want to force only the section's bank, and not its position within the bank, that's also possible: .Bd -literal -offset indent SECTION "Cool Stuff", ROMX, BANK[7] .Ed .It Alignment examples: The first one could be useful for defining an OAM buffer to be DMA'd, since it must be aligned to 256 bytes. The second could also be appropriate for GBC HDMA, or for an optimized copy code that requires alignment. .Bd -literal -offset indent SECTION "OAM Data", WRAM0, ALIGN[8] ;\ align to 256 bytes SECTION "VRAM Data", ROMX, BANK[2], ALIGN[4] ;\ align to 16 bytes .Ed .El .Pp The current section can be ended without starting a new section by using .Ic ENDSECTION . This directive will clear the section context, so you can no longer write code until you start another section. It can be useful to avoid accidentally defining code or data in the wrong section. .Ss Section stack .Ic POPS and .Ic PUSHS provide the interface to the section stack. The number of entries in the stack is limited only by the amount of memory in your machine. .Pp .Ic PUSHS will push the current section context on the section stack. .Ic POPS can then later be used to restore it. Useful for defining sections in included files when you don't want to override the section context at the point the file was included. .Pp .Ic PUSHS can also take the same arguments as .Ic SECTION , in order to push the current section context and define a new section at the same time: .Bd -literal -offset indent SECTION "Code", ROM0 Function: ld a, 42 PUSHS "Variables", WRAM0 wAnswer: db POPS ld [wAnswer], a .Ed .Ss RAM code Sometimes you want to have some code (or data) in RAM, e.g. for self-modifying code. But you can't just put it directly in a RAM section; you have to store it in ROM and copy it to RAM at some point. This means that the code will be executed at a different address range than where it's defined, which can be inconvenient for references to labels within that code. This situation is what .Ic LOAD blocks are designed for. Here's an example of how to use them: .Bd -literal -offset indent SECTION "LOAD example", ROMX CopyCode: ld de, RAMCode ld hl, RAMLocation ld c, RAMCode.end - RAMCode \&.loop ld a, [de] inc de ld [hli], a dec c jr nz, .loop ret RAMCode: LOAD "RAM code", WRAM0 RAMLocation: ld hl, .string ld de, $9864 \&.copy ld a, [hli] ld [de], a inc de and a jr nz, .copy ret \&.string db "Hello World!\e0" ENDL \&.end .Ed .Pp A .Ic LOAD block feels similar to a .Ic SECTION declaration because it creates a new one. All data and code generated within such a block is placed in the current section like usual, but all labels are created as if they were placed in this newly-created section. .Pp In the example above, all of the code and data will end up in the .Dq LOAD example section. You will notice the .Sq RAMCode and .Sq RAMLocation labels. The former is situated in ROM, where the code is stored, the latter in RAM, where the code will be loaded. .Pp You cannot nest .Ic LOAD blocks, nor can you change or stop the current section within them. .Pp The current .Ic LOAD block can be ended by using .Ic ENDL . This directive is only necessary if you want to resume writing code in its containing ROM section. Any of .Ic LOAD , SECTION , ENDSECTION , or .Ic POPS will end the current .Ic LOAD block before performing its own function. .Pp .Ic LOAD blocks can use the .Ic UNION or .Ic FRAGMENT modifiers as described in .Sx Unionized sections below. .Ss Unionized sections When you're tight on RAM, you may want to define overlapping static memory allocations, as explained in the .Sx Allocating overlapping spaces in RAM section. However, a .Ic UNION only works within a single file, so it can't be used e.g. to define temporary variables across several files, all of which use the same statically allocated memory. Unionized sections solve this problem. To declare a unionized section, add a .Ic UNION keyword after the .Ic SECTION one; the declaration is otherwise not different. Unionized sections follow some different rules from normal sections: .Bl -bullet -offset indent .It The same unionized section (i.e. having the same name) can be declared several times per .Nm invocation, and across several invocations. Different declarations are treated and merged identically whether within the same invocation, or different ones. .It If one section has been declared as unionized, all sections with the same name must be declared unionized as well. .It All declarations must have the same type. For example, even if .Xr rgblink 1 Ap s .Fl w flag is used, .Ic WRAM0 and .Ic WRAMX types are still considered different. .It Different constraints (alignment, bank, etc.) can be specified for each unionized section declaration, but they must all be compatible. For example, alignment must be compatible with any fixed address, all specified banks must be the same, etc. .It Unionized sections cannot have type .Ic ROM0 or .Ic ROMX . .El .Pp Different declarations of the same unionized section are not appended, but instead overlaid on top of each other, just like .Sx Allocating overlapping spaces in RAM . Similarly, the size of an unionized section is the largest of all its declarations. .Ss Section fragments Section fragments are sections with a small twist: when several fragments with the same name are encountered, they are concatenated into one section instead of producing an error, even across multiple object files. This works within the same file (paralleling the behavior "plain" sections has in previous versions), but also across object files. To declare a section fragment, add a .Ic FRAGMENT keyword after the .Ic SECTION one; the declaration is otherwise not different. However, similarly to .Sx Unionized sections , some rules must be followed: .Bl -bullet -offset indent .It If one section has been declared as fragment, all sections with the same name must be declared fragments as well. .It All declarations must have the same type. For example, even if .Xr rgblink 1 Ap s .Fl w flag is used, .Ic WRAM0 and .Ic WRAMX types are still considered different. .It Different constraints (alignment, bank, etc.) can be specified for each section fragment declaration, but they must all be compatible. For example, alignment must be compatible with any fixed address, all specified banks must be the same, etc. .It A section fragment may not be unionized; after all, that wouldn't make much sense. .El .Pp When RGBASM merges two fragments, the one encountered later is appended to the one encountered earlier. .Pp When RGBLINK merges two fragments, the one whose file was specified last is appended to the one whose file was specified first. For example, assuming .Ql bar.o , .Ql baz.o , and .Ql foo.o all contain a fragment with the same name, the command .Dl rgblink -o rom.gb baz.o foo.o bar.o would produce the fragment from .Ql baz.o first, followed by the one from .Ql foo.o , and the one from .Ql bar.o last. .Ss Fragment literals Fragment literals are useful for short blocks of code or data that are only referenced once. They are section fragments created by surrounding instructions or directives with .Ql [[ double brackets .Ql ]] , without a separate .Ic SECTION FRAGMENT declaration. .Pp The content of a fragment literal becomes a .Ic SECTION FRAGMENT , sharing the same name and bank as its parent ROM section, but without any other constraints. The parent section also becomes a .Ic FRAGMENT if it was not one already, so that it can be merged with its fragment literals. RGBLINK merges the fragments in no particular order. .Pp A fragment literal can take the place of any 16-bit integer constant .Ql n16 from the .Xr gbz80 7 documentation, as well as a .Ic DW item. The fragment literal then evaluates to its starting address. For example, you can .Ic CALL or .Ic JP to a fragment literal. .Pp This code using named labels: .Bd -literal -offset indent DataTable: dw First dw Second dw Third First: db 1 Second: db 4 Third: db 9 Routine: push hl ld hl, Left jr z, .got_it ld hl, Right \&.got_it call .print pop hl ret \&.print: ld de, $1003 ld bc, STARTOF(VRAM) jp Print Left: db "left\e0" Right: db "right\e0" .Ed .Pp is equivalent to this code using fragment literals: .Bd -literal -offset indent DataTable: dw [[ db 1 ]] dw [[ db 4 ]] dw [[ db 9 ]] Routine: push hl ld hl, [[ db "left\e0" ]] jr z, .got_it ld hl, [[ db "right\e0" ]] \&.got_it call [[ ld de, $1003 ld bc, STARTOF(VRAM) jp Print ]] pop hl ret .Ed .Pp The difference is that the example using fragment literals does not declare a particular order for its pieces. .Pp Fragment literals can be arbitrarily nested, so extreme use cases are .Em technically possible. This code using named labels: .Bd -literal -offset indent dw FortyTwo FortyTwo: call Sub1 jr Sub2 Sub1: ld a, [Twenty] ret Twenty: db 20 Sub2: jp Sub3 Sub3: call Sub1 inc a add a ret .Ed .Pp is equivalent to this code using fragment literals: .Bd -literal -offset indent dw [[ call [[ Sub1: ld a, [ [[db 20]] ] :: ret ]] jr [[ jp [[ call Sub1 :: inc a :: add a :: ret ]] ]] ]] .Ed .Sh SYMBOLS RGBDS supports several types of symbols: .Bl -hang .It Sy Label Numeric symbol designating a memory location. May or may not have a value known at assembly time. .It Sy Constant Numeric symbol whose value has to be known at assembly time. .It Sy Macro A block of .Nm code that can be invoked later. .It Sy String A text string that can be expanded later, similarly to a macro. .El .Pp Symbol names can contain ASCII letters, numbers, underscores .Sq _ , hashes .Sq # , dollar signs .Sq $ , and at signs .Sq @ . However, they must begin with either a letter or an underscore. Additionally, label names can contain up to a single dot .Ql \&. , which may not be the first character. .Pp A symbol cannot have the same name as a reserved keyword, unless its name is a .Dq raw identifier prefixed by a hash .Sq # . For example, .Ql #load denotes a symbol named .Ql load , and .Ql #LOAD denotes a different symbol named .Ql LOAD ; in both cases the .Sq # prevents them from being treated as the keyword .Ic LOAD . .Ss Labels One of the assembler's main tasks is to keep track of addresses for you, so you can work with meaningful names instead of .Dq magic numbers. Labels enable just that: a label ties a name to a specific location within a section. A label resolves to a bank and address, determined at the same time as its parent section's (see further in this section). .Pp A label is defined by writing its name at the beginning of a line, followed by one or two colons, without any whitespace between the label name and the colon(s). Declaring a label (global or local) with two colons .Ql :: will define and .Ic EXPORT it at the same time. (See .Sx Exporting and importing symbols below). When defining a local label, the colon can be omitted, and .Nm will act as if there was only one. .Pp A label is said to be .Em local if its name contains a dot .Ql \&. ; otherwise, it is said to be .Em global (not to be mistaken with .Dq exported , explained in .Sx Exporting and importing symbols below). More than one dot in label names is not allowed. .Pp For convenience, local labels can use a shorthand syntax: when a symbol name starting with a dot is found (for example, inside an expression, or when declaring a label), then the current .Dq label scope is implicitly prepended. .Pp Defining a global label sets it as the current .Dq label scope , until the next global label definition, or the end of the current section. .Pp Here are some examples of label definitions: .Bd -literal -offset indent GlobalLabel: AnotherGlobal: \&.locallabel ;\ This defines "AnotherGlobal.locallabel" \&.another_local: AnotherGlobal.with_another_local: ThisWillBeExported:: ;\ Note the two colons ThisWillBeExported.too:: .Ed .Pp In a numeric expression, a label evaluates to its address in memory. .Po To obtain its bank, use the .Ql BANK() function described in .Sx Other functions .Pc . For example, given the following, .Ql ld de, vPlayerTiles would be equivalent to .Ql ld de, $80C0 assuming the section ends up at .Ad $80C0 : .Bd -literal -offset indent SECTION "Player tiles", VRAM vPlayerTiles: ds 6 * 16 \&.end .Ed .Pp A label's location (and thus value) is usually not determined until the linking stage, so labels usually cannot be used as constants. However, if the section in which the label is defined has a fixed base address, its value is known at assembly time. .Pp Also, while .Nm obviously can compute the difference between two labels if both are constant, it is also able to compute the difference between two non-constant labels if they both belong to the same section, such as .Ql PlayerTiles and .Ql PlayerTiles.end above. .Ss Anonymous labels Anonymous labels are useful for short blocks of code. They are defined like normal labels, but without a name before the colon. Anonymous labels are independent of label scoping, so defining one does not change the scoped label, and referencing one is not affected by the current scoped label. .Pp Anonymous labels are referenced using a colon .Ql \&: followed by pluses .Ql + or minuses .Ql - . Thus .Ic :+ references the next one after the expression, .Ic :++ the one after that; .Ic :- references the one before the expression; and so on. .Bd -literal -offset indent ld hl, :++ : ld a, [hli] ; referenced by "jr nz" ldh [c], a dec c jr nz, :- ret : ; referenced by "ld hl" dw $7FFF, $1061, $03E0, $58A5 .Ed .Ss Variables An equal sign .Sq = is used to define mutable numeric symbols. Unlike the other symbols described below, variables can be redefined. This is useful for internal symbols in macros, for counters, etc. .Bd -literal -offset indent DEF ARRAY_SIZE EQU 4 DEF COUNT = 2 DEF COUNT = 3 DEF COUNT = ARRAY_SIZE + COUNT DEF COUNT *= 2 ;\ COUNT now has the value 14 .Ed .Pp Note that colons .Ql \&: following the name are not allowed. .Pp Variables can be conveniently redefined by compound assignment operators like in C: .Bl -column -offset indent "*= /= %=" .It Sy Operator Ta Sy Meaning .It Li += -= Ta Compound plus/minus .It Li *= /= %= Ta Compound multiply/divide/modulo .It Li <<= >>= Ta Compound shift left/right .It Li &= \&|= ^= Ta Compound and/or/xor .El .Pp Examples: .Bd -literal -offset indent DEF x = 10 DEF x += 1 ; x == 11 DEF y = x - 1 ; y == 10 DEF y *= 2 ; y == 20 DEF y >>= 1 ; y == 10 DEF x ^= y ; x == 1 .Ed .Pp Declaring a variable with .Ic EXPORT DEF or .Ic EXPORT REDEF will define and .Ic EXPORT it at the same time. (See .Sx Exporting and importing symbols below). .Ss Numeric constants .Ic EQU is used to define numeric constant symbols. Unlike .Sq = above, constants defined this way cannot be redefined. These constants can be used for unchanging values such as properties of the hardware. .Bd -literal -offset indent def SCREEN_WIDTH equ 160 ;\ In pixels def SCREEN_HEIGHT equ 144 .Ed .Pp Note that colons .Ql \&: following the name are not allowed. .Pp If you .Em really need to, the .Ic REDEF keyword will define or redefine a numeric constant symbol. (It can also be used for variables, although it's not necessary since they are mutable.) This can be used, for example, to update a constant using a macro, without making it mutable in general. .Bd -literal -offset indent def NUM_ITEMS equ 0 MACRO add_item redef NUM_ITEMS equ NUM_ITEMS + 1 def ITEM_{02x:NUM_ITEMS} equ \e1 ENDM add_item 1 add_item 4 add_item 9 add_item 16 assert NUM_ITEMS == 4 assert ITEM_04 == 16 .Ed .Pp Declaring a numeric constant with .Ic EXPORT DEF or .Ic EXPORT REDEF will define and .Ic EXPORT it at the same time. (See .Sx Exporting and importing symbols below). .Ss Offset constants The RS group of commands is a handy way of defining structure offsets: .Bd -literal -offset indent RSRESET DEF str_pStuff RW 1 DEF str_tData RB 256 DEF str_bCount RB 1 DEF str_SIZEOF RB 0 .Ed .Pp The example defines four constants as if by: .Bd -literal -offset indent DEF str_pStuff EQU 0 DEF str_tData EQU 2 DEF str_bCount EQU 258 DEF str_SIZEOF EQU 259 .Ed .Pp There are five commands in the RS group of commands: .Bl -column "DEF name RB constexpr" .It Sy Command Ta Sy Meaning .It Ic RSRESET Ta Equivalent to Ql RSSET 0 . .It Ic RSSET Ar constexpr Ta Sets the Ic _RS No counter to Ar constexpr . .It Ic DEF Ar name Ic RB Ar constexpr Ta Sets Ar name No to Ic _RS No and then adds Ar constexpr No to Ic _RS . .It Ic DEF Ar name Ic RW Ar constexpr Ta Sets Ar name No to Ic _RS No and then adds Ar constexpr No * 2 to Ic _RS . .It Ic DEF Ar name Ic RL Ar constexpr Ta Sets Ar name No to Ic _RS No and then adds Ar constexpr No * 4 to Ic _RS . .El .Pp If the .Ar constexpr argument to .Ic RB , RW , or .Ic RL is omitted, it's assumed to be 1. .Pp Note that colons .Ql \&: following the name are not allowed. .Pp Declaring an offset constant with .Ic EXPORT DEF will define and .Ic EXPORT it at the same time. (See .Sx Exporting and importing symbols below). .Ss String constants .Ic EQUS is used to define string constant symbols. Wherever the assembler reads a string constant, it gets .Em expanded : the symbol's name is replaced with its contents, similarly to .Ic #define in the C programming language. This expansion is disabled in a few contexts: .Ql DEF(name) , .Ql DEF name EQU/=/EQUS/etc ... , .Ql REDEF name EQU/=/EQUS/etc ... , .Ql FOR name, ... , .Ql PURGE name , and .Ql MACRO name will not expand string constants in their names. Expansion is also disabled if the string constant's name is a raw identifier prefixed by a hash .Sq # . .Bd -literal -offset indent DEF COUNTREG EQUS "[hl+]" ld a, COUNTREG DEF PLAYER_NAME EQUS "\e"John\e"" db PLAYER_NAME .Ed .Pp This will be interpreted as: .Bd -literal -offset indent ld a, [hl+] db "John" .Ed .Pp String constants can also be used to define small one-line macros: .Bd -literal -offset indent DEF pusha EQUS "push af\enpush bc\enpush de\enpush hl\en" .Ed .Pp Note that colons .Ql \&: following the name are not allowed. .Pp String constants, like numeric constants, cannot be redefined. However, the .Ic REDEF keyword will define or redefine a string constant symbol. For example: .Bd -literal -offset indent DEF s EQUS "Hello, " REDEF s EQUS "{s}world!" ; prints "Hello, world!" PRINTLN "{s}\en" .Ed .Pp String constants can't be exported or imported. .Pp .Sy Important note : When a string constant is expanded, its expansion may contain another string constant, which will be expanded as well, and may be recursive. If this creates an infinite loop, .Nm will error out once a certain depth is reached (see the .Fl r command-line option in .Xr rgbasm 1 ) . The same problem can occur if the expansion of a string constant invokes a macro, which itself expands. .Ss Macros One of the best features of an assembler is the ability to write macros for it. Macros can be called with arguments, and can react depending on input using .Ic IF constructs. .Bd -literal -offset indent MACRO my_macro ld a, 80 call MyFunc ENDM .Ed .Pp The example above defines .Ql my_macro as a new macro. String constants are not expanded within the name of the macro. .Pp Macros can't be exported or imported. .Pp Nesting macro definitions is not possible, so this won't work: .Bd -literal -offset indent MACRO outer MACRO inner PRINTLN "Hello!" ENDM ; this actually ends the 'outer' macro... ENDM ; ...and then this is a syntax error! .Ed .Pp But you can work around this limitation using .Ic EQUS , so this will work: .Bd -literal -offset indent MACRO outer DEF definition EQUS "MACRO inner\enPRINTLN \e"Hello!\e"\enENDM" definition PURGE definition ENDM .Ed .Pp More about how to define and invoke macros is described in .Sx THE MACRO LANGUAGE below. .Ss Exporting and importing symbols Importing and exporting of symbols is a feature that is very useful when your project spans many source files and, for example, you need to jump to a routine defined in another file. .Pp Exporting of symbols has to be done manually, importing is done automatically if .Nm finds a symbol it does not know about. .Pp The following will cause .Ar symbol1 , symbol2 and so on to be accessible to other files during the link process: .Dl Ic EXPORT Ar symbol1 Bq , Ar symbol2 , No ... .Pp For example, if you have the following three files: .Pp .Ql a.asm : .Bd -literal -offset indent -compact SECTION "a", WRAM0 LabelA: .Ed .Pp .Ql b.asm : .Bd -literal -offset indent -compact SECTION "b", WRAM0 ExportedLabelB1:: ExportedLabelB2: EXPORT ExportedLabelB2 .Ed .Pp .Ql c.asm : .Bd -literal -offset indent -compact SECTION "C", ROM0[0] dw LabelA dw ExportedLabelB1 dw ExportedLabelB2 .Ed .Pp Then .Ql c.asm can use .Ql ExportedLabelB1 and .Ql ExportedLabelB2 , but not .Ql LabelA , so linking them together will fail: .Bd -literal -offset indent $ rgbasm -o a.o a.asm $ rgbasm -o b.o b.asm $ rgbasm -o c.o c.asm $ rgblink a.o b.o c.o error: Undefined symbol "LabelA" at c.asm(2) Linking failed with 1 error .Ed .Pp Note also that only exported symbols will appear in symbol and map files produced by .Xr rgblink 1 . .Ss Purging symbols .Ic PURGE allows you to completely remove a symbol from the symbol table, as if it had never been defined. .Bd -literal -offset indent DEF value EQU 42 PURGE value DEF value EQUS "I'm a string now" ASSERT DEF(value) PURGE value ASSERT !DEF(value) .Ed .Pp Be .Em very careful when purging symbols that have been referenced in section data, or that have been exported, because it could result in unpredictable errors if something depends on the missing symbol (for example, expressions the linker needs to calculate). Purging labels at all is .Em not recommended. .Pp String constants are not expanded within the symbol names. .Ss Predeclared symbols The following symbols are defined by the assembler: .Bl -column -offset indent "__ISO_8601_LOCAL__" "EQUS" .It Sy Name Ta Sy Type Ta Sy Contents .It Dv @ Ta Ic EQU Ta PC value (essentially, the current memory address) .It Dv . Ta Ic EQUS Ta The current global label scope .It Dv .. Ta Ic EQUS Ta The current local label scope .It Dv __SCOPE__ Ta Ic EQUS Ta The innermost current label scope level (empty, ".", or "..") .It Dv _RS Ta Ic = Ta _RS Counter .It Dv _NARG Ta Ic EQU Ta Number of arguments passed to macro, updated by Ic SHIFT .It Dv __ISO_8601_LOCAL__ Ta Ic EQUS Ta ISO 8601 timestamp (local) .It Dv __ISO_8601_UTC__ Ta Ic EQUS Ta ISO 8601 timestamp (UTC) .It Dv __UTC_YEAR__ Ta Ic EQU Ta Today's year .It Dv __UTC_MONTH__ Ta Ic EQU Ta Today's month number, 1\[en]12 .It Dv __UTC_DAY__ Ta Ic EQU Ta Today's day of the month, 1\[en]31 .It Dv __UTC_HOUR__ Ta Ic EQU Ta Current hour, 0\[en]23 .It Dv __UTC_MINUTE__ Ta Ic EQU Ta Current minute, 0\[en]59 .It Dv __UTC_SECOND__ Ta Ic EQU Ta Current second, 0\[en]59 .It Dv __RGBDS_MAJOR__ Ta Ic EQU Ta Major version number of RGBDS .It Dv __RGBDS_MINOR__ Ta Ic EQU Ta Minor version number of RGBDS .It Dv __RGBDS_PATCH__ Ta Ic EQU Ta Patch version number of RGBDS .It Dv __RGBDS_RC__ Ta Ic EQU Ta Release candidate ID of RGBDS, not defined for final releases .It Dv __RGBDS_VERSION__ Ta Ic EQUS Ta Version of RGBDS, as printed by Ql rgbasm --version .El .Pp The current time values will be taken from the .Dv SOURCE_DATE_EPOCH environment variable if that is defined as a UNIX timestamp. Refer to the spec at .Lk https://reproducible-builds.org/docs/source-date-epoch/ reproducible-builds.org . .Sh DEFINING DATA .Ss Defining constant data in ROM .Ic DB defines a list of bytes that will be stored in the final image. Ideal for tables and text. .Bd -literal -offset indent DB 1,2,3,4,"This is a string" .Ed .Pp Alternatively, you can use .Ic DW to store a list of words (16-bit) or .Ic DL to store a list of double-words/longs (32-bit). Both of these write their data in little-endian byte order; for example, .Ql dw $CAFE is equivalent to .Ql db $FE, $CA and not .Ql db $CA, $FE . .Pp Strings are handled a little specially: they first undergo charmap conversion (see .Sx Character maps ) , then each resulting character is output individually. For example, under the default charmap, the following two lines are identical: .Bd -literal -offset indent DW "Hello!" DW "H", "e", "l", "l", "o", "!" .Ed .Pp If you do not want this special handling, enclose the string in parentheses. .Pp .Ic DS can also be used to fill a region of memory with some repeated values. For example: .Bd -literal -offset indent ; outputs 3 bytes: $AA, $AA, $AA DS 3, $AA ; outputs 7 bytes: $BB, $CC, $BB, $CC, $BB, $CC, $BB DS 7, $BB, $CC .Ed .Pp You can also use .Ic DB , DW and .Ic DL without arguments. This works exactly like .Ic DS 1 , DS 2 and .Ic DS 4 respectively. Consequently, no-argument .Ic DB , DW and .Ic DL can be used in a .Ic WRAM0 / .Ic WRAMX / .Ic HRAM / .Ic VRAM / .Ic SRAM section. .Ss Including binary data files You probably have some graphics, level data, etc. you'd like to include. Use .Ic INCBIN to include a raw binary file as it is. If the file isn't found in the current directory, the include-path list passed to .Xr rgbasm 1 Ap s .Fl I option on the command line will be searched. .Bd -literal -offset indent INCBIN "titlepic.bin" INCBIN "sprites/hero.bin" .Ed .Pp You can also include only part of a file with .Ic INCBIN . The example below includes 256 bytes from data.bin, starting from byte 78. .Bd -literal -offset indent INCBIN "data.bin", 78, 256 .Ed .Pp The length argument is optional. If only the start position is specified, the bytes from the start position until the end of the file will be included. .Ss Statically allocating space in RAM .Ic DS statically allocates a number of empty bytes. This is the preferred method of allocating space in a RAM section. You can also use .Ic DB , DW and .Ic DL without any arguments instead (see .Sx Defining constant data in ROM below). .Bd -literal -offset indent DS 42 ;\ Allocates 42 bytes .Ed .Pp Empty space in RAM sections will not be initialized. In ROM sections, it will be filled with the value passed to the .Fl p command-line option, except when using overlays with .Fl O . .Pp Instead of an exact number of bytes, you can specify .Ic ALIGN Ns Bq Ar align , offset to allocate however many bytes are required to align the subsequent data. Thus, .Sq Ic DS ALIGN Ns Bo Ar align , offset Bc , No ... is equivalent to .Sq Ic DS Ar n , No ... followed by .Sq Ic ALIGN Ns Bq Ar align , offset , where .Ar n is the minimum value needed to satisfy the .Ic ALIGN constraint (see .Sx Requesting alignment below). Note that .Ic ALIGN Ns Bq Ar align is a shorthand for .Ic ALIGN Ns Bq Ar align , No 0 . .Ss Allocating overlapping spaces in RAM Unions allow multiple static memory allocations to overlap, like unions in C. This does not increase the amount of memory available, but allows re-using the same memory region for different purposes. .Pp A union starts with a .Ic UNION keyword, and ends at the corresponding .Ic ENDU keyword. .Ic NEXTU separates each block of allocations, and you may use it as many times within a union as necessary. .Bd -literal -offset indent ; Let's say PC == $C0DE here UNION ; Here, PC == $C0DE wName:: ds 10 ; Now, PC == $C0E8 wNickname:: ds 10 ; PC == $C0F2 NEXTU ; PC is back to $C0DE wHealth:: dw ; PC == $C0E0 wLives:: db ; PC == $C0E1 ds 7 ; PC == $C0E8 wBonus:: db ; PC == $C0E9 NEXTU ; PC is back to $C0DE again wVideoBuffer: ds 16 ; PC == $C0EE ENDU ; Afterward, PC == $C0F2 .Ed .Pp In the example above, .Sq wName , wHealth , and .Sq wVideoBuffer all have the same value; so do .Sq wNickname and .Sq wBonus . Thus, keep in mind that .Ql ld [wHealth], a assembles to the exact same instruction as .Ql ld [wName], a . .Pp This whole union's total size is 20 bytes, the size of the largest block (the first one, containing .Sq wName and .Sq wNickname ) . .Pp Unions may be nested, with each inner union's size being determined as above, and affecting its outer union like any other allocation. .Pp Unions may be used in any section, but they may only contain space-allocating directives like .Ic DS (see .Sx Statically allocating space in RAM ) . .Ss Requesting alignment While .Ic ALIGN as presented in .Sx SECTIONS is often useful as-is, sometimes you instead want a particular piece of data (or code) in the middle of the section to be aligned. This is made easier through the use of mid-section .Ic ALIGN Ar align , offset . It will retroactively alter the section's attributes to ensure that the location the .Ic ALIGN directive is at, has its .Ar align lower bits equal to .Ar offset . .Pp If the constraint cannot be met (for example because the section is fixed at an incompatible address), an error is produced. Note that .Ic ALIGN Ar align is a shorthand for .Ic ALIGN Ar align , No 0 . .Pp There may be times when you don't just want to specify an alignment constraint at the current location, but also skip ahead until the constraint can be satisfied. In that case, you can use .Ic DS ALIGN Ns Bq Ar align , offset to allocate however many bytes are required to align the subsequent data. .Pp If the constraint cannot be met by skipping any amount of space, an error is produced. Note that .Ic ALIGN Ns Bq Ar align is a shorthand for .Ic ALIGN Ns Bq Ar align , No 0 . .Sh THE MACRO LANGUAGE .Ss Invoking macros A macro is invoked by using its name at the beginning of a line, like a directive, followed by any comma-separated arguments. .Bd -literal -offset indent add a, b ld sp, hl my_macro ;\ This will be expanded sub a, 87 my_macro 42 ;\ So will this ret c my_macro 1, 2 ;\ And this .Ed .Pp After .Nm has read the macro invocation line, it will expand the body of the macro (the lines between .Ic MACRO and .Ic ENDM ) in its place. .Pp .Sy Important note : When a macro body is expanded, its expansion may contain another macro invocation, which will be expanded as well, and may be recursive. If this creates an infinite loop, .Nm will error out once a certain depth is reached (see the .Fl r command-line option in .Xr rgbasm 1 ) . The same problem can occur if the expansion of a macro then expands a string constant, which itself expands. .Pp It's possible to pass arguments to macros as well! .Bd -literal -offset indent MACRO lb ld \e1, (\e2) << 8 | (\e3) ENDM lb hl, 20, 18 ; Expands to "ld hl, ((20) << 8) | (18)" lb de, 3 + 1, NUM**2 ; Expands to "ld de, ((3 + 1) << 8) | (NUM**2)" .Ed .Pp You expand the arguments inside the macro body by using the escape sequences .Ic \e1 through .Ic \e9 , \e1 being the first argument, .Ic \e2 being the second, and so on. Since there are only nine digits, you can only use the first nine macro arguments that way. To use the rest, you put the argument number in angle brackets, like .Ic \e<10> . .Pp This bracketed syntax supports decimal numbers and numeric symbols, where negative values count from the last argument. For example, .Ql \e<_NARG> or .Ql \e<-1> will get the last argument. .Pp Other macro arguments and symbol interpolations will also be expanded inside the angle brackets. For example, if .Ql \e1 is .Ql 13 , then .Ql \e<\e1> inside the macro body will expand to .Ql \e<13> . Or if .Ql DEF v10 = 42 and .Ql DEF x = 10 , then .Ql \e will expand to .Ql \e<42> . .Pp Macro arguments are passed as string constants, although there's no need to enclose them in quotes. Thus, arguments are not evaluated as expressions, but instead are expanded directly inside the macro body. This means that they support all the escape sequences of strings (see .Sx String expressions above), as well as some of their own: .Bl -column -offset indent "Sequence" .It Sy Sequence Ta Sy Meaning .It Ql \e, Ta Comma Pq does not terminate the argument .It Ql \e( Ta Open parenthesis Pq does not start enclosing argument contents .It Ql \e) Ta Close parenthesis Pq does not end enclosing argument contents .El .Pp Line continuations work as usual inside macros or lists of macro arguments. However, some characters need to be escaped, as in the following example: .Bd -literal -offset indent MACRO PrintMacro1 PRINTLN STRCAT(\e1) ENDM PrintMacro1 "Hello "\e, \e "world" MACRO PrintMacro2 PRINT \e1 ENDM PrintMacro2 STRCAT("Hello ", \e "world\en") .Ed .Pp The comma in .Ql PrintMacro1 needs to be escaped to prevent it from starting another macro argument. The comma in .Ql PrintMacro2 does not need escaping because it is inside parentheses, similar to macro arguments in the C programming language. The backslash in .Ql \en also does not need escaping because quoted string literals work as usual inside macro arguments. .Pp Since macro arguments are expanded directly, it's often a good idea to put parentheses around them if they're meant as part of a numeric expression. For instance, consider the following: .Bd -literal -offset indent MACRO print_double PRINTLN \e1 * 3 ENDM print_double 1 + 2 .Ed .Pp The body will expand to .Ql PRINTLN 1 + 2 * 3 , which will print 7 and not 9 as you might have expected. .Pp The .Ic SHIFT directive is only available inside macro bodies. It shifts the argument numbers by one to the left, so what was .Ic \e2 is now .Ic \e1 , what was .Ic \e3 is now .Ic \e2 , and so forth. (What was .Ic \e1 is no longer accessible, so .Dv _NARG is decreased by 1.) .Pp .Ic SHIFT can also take an integer parameter to shift that many times instead of once. A negative parameter will shift the arguments to the right, which can regain access to previously shifted ones. .Pp .Ic SHIFT is especially useful in .Ic REPT loops to iterate over different arguments, evaluating the same loop body each time. .Pp There are some escape sequences which are only valid inside the body of a macro: .Bl -column -offset indent "Sequence" .It Sy Sequence Ta Sy Meaning .It So \e1 Sc \[en] So \e9 Sc Ta The 1st\[en]9th macro argument .It Ql \e<...> Ta Further macro arguments .It Ql \e# Ta All Dv _NARG No macro arguments, separated by commas .It Ql \e@ Ta Unique symbol name affix Pq see below .El .Pp The .Ic \e@ escape sequence is often useful in macros which define symbols. Suppose your macro expands to a loop of assembly code: .Bd -literal -offset indent MACRO loop_c_times xor a, a \&.loop ld [hl+], a dec c jr nz, .loop ENDM .Ed .Pp If you use this macro more than once in the same label scope, it will define .Ql \&.loop twice, which is an error. To work around this problem, you can use .Ic \e@ as a label suffix: .Bd -literal -offset indent MACRO loop_c_times_fixed xor a, a \&.loop\e@ ld [hl+], a dec c jr nz, .loop\e@ ENDM .Ed .Pp This will expand to a different value in each invocation, similar to .Ic gensym in the Lisp programming language. .Pp .Ic \e@ also works in .Ic REPT blocks, expanding to a different value in each iteration. .Ss Automatically repeating blocks of code Suppose you want to unroll a time-consuming loop without copy-pasting it. .Ic REPT is here for that purpose. Everything between .Ic REPT and the matching .Ic ENDR will be repeated a number of times just as if you had done a copy/paste operation yourself. The following example will assemble .Ql add a, c four times: .Bd -literal -offset indent REPT 4 add a, c ENDR .Ed .Pp You can also use .Ic REPT to generate tables on the fly: .Bd -literal -offset indent ; Generate a table of square values from 0**2 = 0 to 100**2 = 10000 DEF x = 0 REPT 101 dw x * x DEF x += 1 ENDR .Ed .Pp As in macros, you can also use the escape sequence .Ic \e@ . .Ic REPT blocks can be nested. .Pp A common pattern is to repeat a block for each value in some range. .Ic FOR is simpler than .Ic REPT for that purpose. Everything between .Ic FOR and the matching .Ic ENDR will be repeated for each value of a given symbol. String constants are not expanded within the symbol name. For example, this code will produce a table of squared values from 0 to 255: .Bd -literal -offset indent FOR N, 256 dw N * N ENDR .Ed .Pp It acts just as if you had done: .Bd -literal -offset indent DEF N = 0 dw N * N DEF N = 1 dw N * N DEF N = 2 dw N * N ; ... DEF N = 255 dw N * N DEF N = 256 .Ed .Pp You can customize the range of .Ic FOR values, similarly to the .Ql range function in the Python programming language: .Bl -column "FOR V, start, stop, step" .It Sy Code Ta Sy Range .It Ic FOR Ar V , stop Ta Ar V No increments from 0 to Ar stop .It Ic FOR Ar V , start , stop Ta Ar V No increments from Ar start No to Ar stop .It Ic FOR Ar V , start , stop , step Ta Ar V No goes from Ar start No to Ar stop No by Ar step .El .Pp The .Ic FOR value will be updated by .Ar step until it reaches or exceeds .Ar stop , i.e. it covers the half-open range from .Ar start (inclusive) to .Ar stop (exclusive). The variable .Ar V will be assigned this value at the beginning of each new iteration; any changes made to it within the .Ic FOR loop's body will be overwritten. So the symbol .Ar V need not be already defined before any iterations of the .Ic FOR loop, but it must be a variable .Pq Sx Variables if so. For example: .Bd -literal -offset indent FOR V, 4, 25, 5 PRINT "{d:V} " DEF V *= 2 ENDR PRINTLN "done {d:V}" .Ed .Pp This will print: .Bd -literal -offset indent 4 9 14 19 24 done 29 .Ed .Pp Just like with .Ic REPT blocks, you can use the escape sequence .Ic \e@ inside of .Ic FOR blocks, and they can be nested. .Pp You can stop a repeating block with the .Ic BREAK command. A .Ic BREAK inside of a .Ic REPT or .Ic FOR block will interrupt the current iteration and not repeat any more. It will continue running code after the block's .Ic ENDR . For example: .Bd -literal -offset indent FOR V, 1, 100 PRINT "{d:V}" IF V == 5 PRINT " stop! " BREAK ENDC PRINT ", " ENDR PRINTLN "done {d:V}" .Ed .Pp This will print: .Bd -literal -offset indent 1, 2, 3, 4, 5 stop! done 5 .Ed .Ss Conditionally assembling blocks of code The four commands .Ic IF , ELIF , ELSE , and .Ic ENDC let you have .Nm skip over parts of your code depending on a condition. This is a powerful feature commonly used in macros. .Bd -literal -offset indent IF NUM < 0 PRINTLN "NUM < 0" ELIF NUM == 0 PRINTLN "NUM == 0" ELSE PRINTLN "NUM > 0" ENDC .Ed .Pp The .Ic ELIF (standing for "else if") and .Ic ELSE blocks are optional. .Ic IF / .Ic ELIF / .Ic ELSE / .Ic ENDC blocks can be nested. .Pp Note that if an .Ic ELSE block is found before an .Ic ELIF block, the .Ic ELIF block will be ignored. All .Ic ELIF blocks must go before the .Ic ELSE block. Also, if there is more than one .Ic ELSE block, all of them but the first one are ignored. .Ss Including other source files Use .Ic INCLUDE to process another assembler file and then return to the current file when done. If the file isn't found in the current directory, the include-path list passed to .Xr rgbasm 1 Ap s .Fl I option on the command line will be searched. You may nest .Ic INCLUDE calls infinitely (or until you run out of memory, whichever comes first). .Bd -literal -offset indent INCLUDE "irq.inc" .Ed .Pp You may also implicitly .Ic INCLUDE a file before the source file with the .Fl P option of .Xr rgbasm 1 . .Ss Printing things during assembly The .Ic PRINT and .Ic PRINTLN commands print text and values to the standard output. Useful for debugging macros, or wherever you may feel the need to tell yourself some important information. .Bd -literal -offset indent PRINT "Hello world!\en" PRINTLN "Hello world!" PRINT _NARG, " arguments\en" PRINTLN "sum: ", 2+3, " product: ", 2*3 PRINTLN STRFMT("E = %f", 2.718) .Ed .Bl -inset .It Ic PRINT prints out each of its comma-separated arguments. Numbers are printed as unsigned uppercase hexadecimal with a leading .Sq $ . For different formats, use .Ic STRFMT . .It Ic PRINTLN prints out each of its comma-separated arguments, if any, followed by a newline .Pq Ql \en . .El .Ss Aborting the assembly process .Ic FAIL and .Ic WARN can be used to print errors and warnings respectively during the assembly process. This is especially useful for macros that get an invalid argument. .Ic FAIL and .Ic WARN take a string as the only argument and they will print this string out as a normal error with a line number. .Pp .Ic FAIL stops assembling immediately while .Ic WARN shows the message but continues afterwards. .Pp If you need to ensure some assumption is correct when compiling, you can use .Ic ASSERT and .Ic STATIC_ASSERT . Syntax examples are given below: .Bd -literal -offset indent Function: xor a ASSERT LOW(MyByte) == 0 ld h, HIGH(MyByte) ld l, a ld a, [hli] ; You can also indent this! ASSERT BANK(OtherFunction) == BANK(Function) call OtherFunction ; Lowercase also works ld hl, FirstByte ld a, [hli] assert FirstByte + 1 == SecondByte ld b, [hl] ret \&.end ; If you specify one, a message will be printed STATIC_ASSERT .end - Function < 256, "Function is too large!" .Ed .Pp First, the difference between .Ic ASSERT and .Ic STATIC_ASSERT is that the former is evaluated by RGBASM if it can, otherwise by RGBLINK; but the latter is only ever evaluated by RGBASM. If RGBASM cannot compute the value of the argument to .Ic STATIC_ASSERT , it will produce an error. .Pp Second, as shown above, a string can be optionally added at the end, to give insight into what the assertion is checking. .Pp Finally, you can add one of .Ic WARN , FAIL or .Ic FATAL as the first optional argument to either .Ic ASSERT or .Ic STATIC_ASSERT . If the assertion fails, .Ic WARN will cause a simple warning (controlled by .Xr rgbasm 1 flag .Fl Wassert ) to be emitted; .Ic FAIL (the default) will cause a non-fatal error; and .Ic FATAL immediately aborts. .Sh MISCELLANEOUS .Ss Changing options while assembling .Ic OPT can be used to change some of the options during assembling from within the source, instead of defining them on the command-line. .Pq See Xr rgbasm 1 . .Pp .Ic OPT takes a comma-separated list of options as its argument: .Bd -literal -offset indent PUSHO OPT g.oOX, Wdiv ; acts like command-line `-g.oOX -Wdiv` OPT -Wdiv ; dashes before the options are optional DW `..ooOOXX ; uses the graphics constant characters from OPT g PRINTLN $80000000/-1 ; prints a warning about division POPO DW `00112233 ; uses the default graphics constant characters PRINTLN $80000000/-1 ; no warning by default .Ed .Pp .Ic OPT can modify the options .Cm b , g , p , Q , r , and .Cm W . .Pp .Ic POPO and .Ic PUSHO provide the interface to the option stack. .Ic PUSHO will push the current set of options on the option stack. .Ic POPO can then later be used to restore them. Useful if you want to change some options in an include file and you don't want to destroy the options set by the program that included your file. The stack's number of entries is limited only by the amount of memory in your machine. .Pp .Ic PUSHO can also take a comma-separated list of options, to push the current set and apply the argument set at the same time: .Bd -literal -offset indent PUSHO b.X, g.oOX DB %..XXXX.. DW `..ooOOXX POPO .Ed .Ss Excluding locations from backtraces Errors and warnings print .Em backtraces showing the location in the source file where the problem occurred, tracing the origin of the problem even through a chain of .Ic REPT , .Ic FOR , .Ic MACRO , and .Ic INCLUDE locations. Sometimes there are locations you would like to ignore; for example, a common utility macro when you only care about the line where the macro is used, or an .Ic INCLUDE file that only serves to include other files and is just filler in the backtrace. .Pp In those cases, you can .Em silence a location with a question mark .Sq \&? after the token: all of the locations created by a .Sq REPT? , .Sq FOR? , or .Sq MACRO? will not be printed, and any location created by a .Sq INCLUDE? , or a macro invocation whose name is immediately followed by a .Sq \&? , will not be printed. For example, if this were assembled as .Ql example.asm : .Bd -literal -offset indent MACRO lb assert -128 <= (\e2) && (\e2) < 256, "\e2 is not a byte" assert -128 <= (\e3) && (\e3) < 256, "\e3 is not a byte" ld \e1, (LOW(\e2) << 8) | LOW(\e3) ENDM SECTION "Code", ROM0 lb hl, $123, $45 .Ed .Pp This would print an error backtrace: .Bd -literal -offset indent error: Assertion failed: $123 is not a byte at example.asm::lb(2) <- example.asm(7) .Ed .Pp But if .Ql MACRO were changed to .Ql MACRO? , or .Ql lb hl were changed to .Ql lb? hl , then the error backtrace would not mention the location within the .Ql lb macro: .Bd -literal -offset indent error: Assertion failed: $123 is not a byte at example.asm(7) .Ed .Sh SEE ALSO .Xr rgbasm 1 , .Xr rgblink 1 , .Xr rgblink 5 , .Xr rgbfix 1 , .Xr rgbgfx 1 , .Xr gbz80 7 , .Xr rgbasm-old 5 , .Xr rgbds 5 , .Xr rgbds 7 .Sh HISTORY .Xr rgbasm 1 was originally written by .An Carsten S\(/orensen as part of the ASMotor package, and was later repackaged in RGBDS by .An Justin Lloyd . It is now maintained by a number of contributors at .Lk https://github.com/gbdev/rgbds . gbdev-rgbds-92bfe5d/man/rgbds.5000066400000000000000000000304231512540461700163530ustar00rootroot00000000000000.\" SPDX-License-Identifier: MIT .\" .Dd January 1, 2026 .Dt RGBDS 5 .Os .Sh NAME .Nm rgbds .Nd object file format documentation .Sh DESCRIPTION This is the description of the RGB object file format that is output by .Xr rgbasm 1 and read by .Xr rgblink 1 . .Sh FILE STRUCTURE The following types are used: .Pp .Cm LONG is a 32-bit integer stored in little-endian format. .Cm BYTE is an 8-bit integer. .Cm STRING is a 0-terminated string of .Cm BYTE . Brackets after a type .Pq e.g. Cm LONG Ns Bq Ar n indicate .Ar n consecutive elements .Pq here, Cm LONG Ns s . All items are contiguous, with no padding anywhere\(emthis also means that they may not be aligned in the file! .Pp .Cm REPT Ar n indicates that the fields between the .Cm REPT and corresponding .Cm ENDR are repeated .Ar n times. .Pp All IDs refer to objects within the file; for example, symbol ID $0001 refers to the second symbol defined in .Em this object file's .Sx Symbols array. The only exception is the .Sx Source file info nodes, whose IDs are backwards, i.e. source node ID $0000 refers to the .Em last node in the array, not the first one. References to other object files are made by imports (symbols), by name (sections), etc.\(embut never by ID. .Ss Header .Bl -tag -width Ds -compact .It Cm BYTE Ar Magic[4] "RGB9" .It Cm LONG Ar RevisionNumber The format's revision number this file uses. .Pq This is always in the same place in all revisions. .It Cm LONG Ar NumberOfSymbols How many symbols are defined in this object file. .It Cm LONG Ar NumberOfSections How many sections are defined in this object file. .El .Ss Source file info .Bl -tag -width Ds -compact .It Cm LONG Ar NumberOfNodes The number of source context nodes contained in this file. .It Cm REPT Ar NumberOfNodes .Bl -tag -width Ds -compact .It Cm LONG Ar ParentID ID of the parent node, -1 meaning that this is the root node. .Pp .Sy Important : the nodes are actually written in .Sy reverse order, meaning the node with ID 0 is the last one in the list! .It Cm LONG Ar ParentLineNo Line at which the parent node's context was exited; meaningless for the root node. .It Cm BYTE Ar Type Bits 0\(en6 indicate the node's type: .Bl -column "Value" -compact .It Sy Value Ta Sy Meaning .It 0 Ta REPT node .It 1 Ta File node .It 2 Ta Macro node .El .Pp Bit\ 7 being set means that the node is "quieted" .Pq see Do Excluding locations from backtraces Dc in Xr rgbasm 5 . .It Cm IF Ar Type No \(!= 0 If the node is not a REPT node... .Pp .Bl -tag -width Ds -compact .It Cm STRING Ar Name The node's name: either a file name, or the macro's name prefixed by its definition's file name .Pq e.g. Ql src/includes/defines.asm::error . .El .It Cm ELSE If the node is a REPT, it also contains the iteration counter of all parent REPTs. .Pp .Bl -tag -width Ds -compact .It Cm LONG Ar Depth .It Cm LONG Ar Iter Ns Bq Ar Depth The number of REPT iterations, by increasing depth. .El .It Cm ENDC .El .It Cm ENDR .El .Ss Symbols .Bl -tag -width Ds -compact .It Cm REPT Ar NumberOfSymbols .Bl -tag -width Ds -compact .It Cm STRING Ar Name This symbol's name. Local symbols are stored as their full name .Pq Ql Scope.symbol . .It Cm BYTE Ar Type .Bl -column "Value" -compact .It Sy Value Ta Sy Meaning .It 0 Ta Sy Local No symbol only used in this file . .It 1 Ta Sy Import No of an exported symbol (by name) from another object file . .It 2 Ta Sy Exported No symbol visible from other object files . .El .It Cm IF Ar Type No \(!= 1 If the symbol is defined in this object file... .Pp .Bl -tag -width Ds -compact .It Cm LONG Ar NodeID Context in which the symbol was defined. .It Cm LONG Ar LineNo Line number in the context at which the symbol was defined. .It Cm LONG Ar SectionID The ID of the section in which the symbol is defined. If the symbol doesn't belong to any specific section (i.e. it's a constant), this field contains -1. .It Cm LONG Ar Value The symbol's value. If the symbol belongs to a section, this is the offset within that symbol's section. .El .It Cm ENDC .El .It Cm ENDR .El .Ss Sections .Bl -tag -width Ds -compact .It Cm REPT Ar NumberOfSections .Bl -tag -width Ds -compact .It Cm STRING Ar Name The section's name. .It Cm LONG Ar NodeID Context in which the section was defined. .It Cm LONG Ar LineNo Line number in the context at which the section was defined. .It Cm LONG Ar Size The section's size, in bytes. .It Cm BYTE Ar Type Bits 0\(en2 indicate the section's type: .Bl -column "Value" -compact .It Sy Value Ta Sy Meaning .It 0 Ta WRAM0 .It 1 Ta VRAM .It 2 Ta ROMX .It 3 Ta ROM0 .It 4 Ta HRAM .It 5 Ta WRAMX .It 6 Ta SRAM .It 7 Ta OAM .El .Pp Bit\ 7 being set means that the section is a "union" .Pq see Do Unionized sections Dc in Xr rgbasm 5 . Bit\ 6 being set means that the section is a "fragment" .Pq see Do Section fragments Dc in Xr rgbasm 5 . These two bits are mutually exclusive. .It Cm LONG Ar Address Address this section must be placed at. This must either be valid for the section's .Ar Type (as affected by flags like .Fl t or .Fl d in .Xr rgblink 1 ) , or -1 to indicate that the linker should automatically decide .Pq the section is Dq floating . .It Cm LONG Ar Bank ID of the bank this section must be placed in. This must either be valid for the section's .Ar Type (with the same caveats as for the .Ar Address ) , or -1 to indicate that the linker should automatically decide. .It Cm BYTE Ar Alignment How many bits of the section's address should be equal to .Ar AlignOfs , starting from the least-significant bit. .It Cm LONG Ar AlignOfs Alignment offset. Must be strictly less than .Ql 1 << Ar Alignment . .It Cm IF Ar Type No \(eq 2 || Ar Type No \(eq 3 If the section has ROM type, it contains data. .Pp .Bl -tag -width Ds -compact .It Cm BYTE Ar Data Ns Bq Size The section's raw data. Bytes that will be patched over must be present, even though their contents will be overwritten. .It Cm LONG Ar NumberOfPatches How many patches must be applied to this section's .Ar Data . .It Cm REPT Ar NumberOfPatches .Bl -tag -width Ds -compact .It Cm LONG Ar NodeID Context in which the patch was defined. .It Cm LONG Ar LineNo Line number in the context at which the patch was defined. .It Cm LONG Ar Offset Offset within the section's .Ar Data at which the patch should be applied. Must not be greater than the section's .Ar Size minus the patch's size .Pq see Ar Type No below . .It Cm LONG Ar PCSectionID ID of the section in which PC is located. (This is usually the same section within which the patch is applied, except for e.g.\& .Ql LOAD blocks, see .Do RAM code Dc in Xr rgbasm 5 . ) .It Cm LONG Ar PCOffset Offset of the PC symbol within the section designated by .Ar PCSectionID . It is expected that PC points to the instruction's first byte for instruction operands (i.e.\& .Ql jp @ must be an infinite loop), and to the patch's first byte otherwise .Ql ( db , .Ql dw , .Ql dl ) . .It Cm BYTE Ar Type .Bl -column "Value" -compact .It Sy Value Ta Sy Meaning .It 0 Ta Single-byte patch .It 1 Ta Little-endian two-byte patch .It 2 Ta Little-endian four-byte patch .It 3 Ta Single-byte Ql jr patch; the patch's value will be subtracted to PC + 2 (i.e.\& .Ql jr @ must be the infinite loop .Ql 18 FE ) . .El .It Cm LONG Ar RPNSize Size of the .Ar RPNExpr below. .It Cm BYTE Ar RPNExpr Ns Bq RPNSize The patch's value, encoded as a RPN expression .Pq see Sx RPN expressions . .El .It Cm ENDR .El .It Cm ENDC .El .El .Ss Assertions .Bl -tag -width Ds -compact .It Cm LONG Ar NumberOfAssertions How many assertions this object file contains. .It Cm REPT Ar NumberOfAssertions Assertions are essentially patches with a message. .Pp .Bl -tag -width Ds -compact .It Cm LONG Ar NodeID Context in which the assertions was defined. .It Cm LONG Ar LineNo Line number in the context at which the assertion was defined. .It Cm LONG Ar Offset Unused leftover from the patch structure. .It Cm LONG Ar PCSectionID ID of the section in which PC is located. .It Cm LONG Ar PCOffset Offset of the PC symbol within the section designated by .Ar PCSectionID . .It Cm BYTE Ar Type Describes what should happen if the expression evaluates to a non-zero value. .Bl -column "Value" -compact .It Sy Value Ta Sy Meaning .It 0 Ta Print a warning message, and continue linking normally. .It 1 Ta Print an error message, so linking will fail, but allow other assertions to be evaluated. .It 2 Ta Print a fatal error message, and abort immediately. .El .It Cm LONG Ar RPNSize Size of the .Ar RPNExpr below. .It Cm BYTE Ar RPNExpr Ns Bq RPNSize The patch's value, encoded as a RPN expression .Pq see Sx RPN expressions . .It Cm STRING Ar Message The message displayed if the expression evaluates to a non-zero value. If empty, a generic message is displayed instead. .El .It Cm ENDR .El .Ss RPN expressions Expressions in the object file are stored as RPN, or .Dq Reverse Polish Notation , which is a notation that allows computing arbitrary expressions with just a simple stack. For example, the expression .Ql 2 5 - will first push the value .Dq 2 to the stack, then .Dq 5 . The .Ql - operator pops two arguments from the stack, subtracts them, and then pushes back the result .Pq Dq 3 on the stack. A well-formed RPN expression never tries to pop from an empty stack, and leaves exactly one value in it at the end. .Pp RGBDS encodes RPN expressions as an array of .Cm BYTE Ns s . The first byte encodes either an operator, or a literal, which consumes more .Cm BYTE Ns s after it: .Bl -column "Value" .It Sy Value Ta Sy Meaning .It Li $00 Ta Addition operator Pq Ql + .It Li $01 Ta Subtraction operator Pq Ql - .It Li $02 Ta Multiplication operator Pq Ql * .It Li $03 Ta Division operator Pq Ql / .It Li $04 Ta Modulo operator Pq Ql % .It Li $05 Ta Negation Pq unary Ql - .It Li $06 Ta Exponent operator Pq Ql ** .It Li $10 Ta Bitwise OR operator Pq Ql \&| .It Li $11 Ta Bitwise AND operator Pq Ql & .It Li $12 Ta Bitwise XOR operator Pq Ql ^ .It Li $13 Ta Bitwise complement operator Pq unary Ql ~ .It Li $21 Ta Logical AND operator Pq Ql && .It Li $22 Ta Logical OR operator Pq Ql || .It Li $23 Ta Logical complement operator Pq unary Ql \&! .It Li $30 Ta Equality operator Pq Ql == .It Li $31 Ta Non-equality operator Pq Ql != .It Li $32 Ta Greater-than operator Pq Ql > .It Li $33 Ta Less-than operator Pq Ql < .It Li $34 Ta Greater-than-or-equal operator Pq Ql >= .It Li $35 Ta Less-than-or-equal operator Pq Ql <= .It Li $40 Ta Left shift operator Pq Ql << .It Li $41 Ta Arithmetic/signed right shift operator Pq Ql >> .It Li $42 Ta Logical/unsigned right shift operator Pq Ql >>> .It Li $50 Ta Fn BANK symbol ; followed by the .Ar symbol Ap s Cm LONG ID. .It Li $51 Ta Fn BANK section ; followed by the .Ar section Ap s Cm STRING name. .It Li $52 Ta PC's Fn BANK Pq i.e. Ql BANK(@) . .It Li $53 Ta Fn SIZEOF section ; followed by the .Ar section Ap s Cm STRING name. .It Li $54 Ta Fn STARTOF section ; followed by the .Ar section Ap s Cm STRING name. .It Li $55 Ta Fn SIZEOF sectiontype ; followed by the .Ar sectiontype Ap s Cm BYTE value .Pq see the Ar Type No values in Sx Sections . .It Li $56 Ta Fn STARTOF sectiontype ; followed by the .Ar sectiontype Ap s Cm BYTE value .Pq see the Ar Type No values in Sx Sections . .It Li $60 Ta Ql ldh check. Checks if the value is a valid .Ql ldh operand .Pq see Do Load Instructions Dc in Xr gbz80 7 , i.e. that it is between either $00 and $FF, or $FF00 and $FFFF, both inclusive. The value is then ANDed with $00FF .Pq Ql & $FF . .It Li $61 Ta Ql rst check. Checks if the value is a valid .Ql rst vector .Pq see Do RST vec Dc in Xr gbz80 7 , that is, one of $00, $08, $10, $18, $20, $28, $30, or $38. The value is then ORed with $C7 .Pq Ql \&| $C7 . .It Li $62 Ta Ql bit/res/set check; followed by the instruction's .Cm BYTE mask. Checks if the value is a valid bit index .Pq see e.g. Do BIT u3, r8 Dc in Xr gbz80 7 , that is, from 0 to 7. The value is then ORed with the instruction's mask. .It Li $70 Ta Cm HIGH byte. .It Li $71 Ta Cm LOW byte. .It Li $72 Ta Cm BITWIDTH value. .It Li $73 Ta Cm TZCOUNT value. .It Li $80 Ta Integer literal; followed by the .Cm LONG integer. .It Li $81 Ta A symbol's value; followed by the symbol's .Cm LONG ID. .El .Sh SEE ALSO .Xr rgbasm 1 , .Xr rgbasm 5 , .Xr rgblink 1 , .Xr rgblink 5 , .Xr rgbfix 1 , .Xr rgbgfx 1 , .Xr gbz80 7 , .Xr rgbds 7 .Sh HISTORY .Xr rgbasm 1 and .Xr rgblink 1 were originally written by .An Carsten S\(/orensen as part of the ASMotor package, and was later repackaged in RGBDS by .An Justin Lloyd . It is now maintained by a number of contributors at .Lk https://github.com/gbdev/rgbds . gbdev-rgbds-92bfe5d/man/rgbds.7000066400000000000000000000042311512540461700163530ustar00rootroot00000000000000.\" SPDX-License-Identifier: MIT .\" .Dd January 1, 2026 .Dt RGBDS 7 .Os .Sh NAME .Nm rgbds .Nd Rednex Game Boy Development System .Sh EXAMPLES To get a working ROM image from a single assembly source file: .Bd -literal -offset indent $ rgbasm \-o game.o game.asm $ rgblink \-o game.gb game.o $ rgbfix \-v \-p 0 game.gb .Ed .Pp Or in a single command line, without creating an intermediate object file: .Bd -literal -offset indent $ (rgbasm -o - - | rgblink -o - - | rgbfix -v -p 0) < game.asm > game.gb .Ed .Sh SEE ALSO .Xr rgbasm 1 , .Xr rgbasm 5 , .Xr rgblink 1 , .Xr rgblink 5 , .Xr rgbfix 1 , .Xr rgbgfx 1 , .Xr gbz80 7 , .Xr rgbds 5 .Sh HISTORY .Bl -item .It 1996-10-01: .An Carsten S\(/orensen .Pq a.k.a. SurfSmurf releases xAsm, xLink, and RGBFix, a Game Boy SM83 (GBZ80) assembler/linker system for DOS/Win32. .It 1997-07-03: S\(/orensen releases ASMotor, packaging the three programs together and moving towards making them a general-purpose target-independent system. .It 1999-08-01: .An Justin Lloyd .Pq a.k.a. Otaku no Zoku adapts ASMotor to re-focus on SM83 assembly/machine code, and releases this version as RGBDS. .It 2009-06-11: .An Vegard Nossum adapts the code to be more UNIX-like and releases this version as rgbds-linux. .It 2010-01-12: .An Anthony J. Bentley forks Nossum's repository. The fork becomes the reference implementation of RGBDS. .It 2010-09-25: S\(/orensen continues development of .Lk https://github.com/asmotor/asmotor ASMotor to this day. .It 2015-01-18: .An stag019 begins implementing RGBGFX, a PNG-to-Game Boy graphics converter, for eventual integration into RGBDS. .It 2016-09-05: RGBGFX is integrated into Bentley's repository. .It 2017-02-23: Bentley's repository is moved to the .Lk https://github.com/rednex/rgbds rednex organization. .It 2018-01-26: The codebase is relicensed under the MIT license. .It 2020-09-15: The repository is moved to the .Lk https://github.com/gbdev/rgbds gbdev organization. .It 2022-05-17: The .Lk https://rgbds.gbdev.io rgbds.gbdev.io website for RGBDS documentation and downloads is published. .It 2025-10-31: RGBDS reaches version 1.0.0 and starts adhering to .Lk https://semver.org/ semantic versioning ("semver"). .El gbdev-rgbds-92bfe5d/man/rgbfix.1000066400000000000000000000277321512540461700165400ustar00rootroot00000000000000.\" SPDX-License-Identifier: MIT .\" .Dd January 1, 2026 .Dt RGBFIX 1 .Os .Sh NAME .Nm rgbfix .Nd Game Boy header utility and checksum fixer .Sh SYNOPSIS .Nm .Op Fl hjsVvw .Op Fl C | c .Op Fl \-color Ar when .Op Fl f Ar fix_spec .Op Fl i Ar game_id .Op Fl k Ar licensee_str .Op Fl L Ar logo_file .Op Fl l Ar licensee_id .Op Fl m Ar mbc_type .Op Fl n Ar rom_version .Op Fl o Ar out_file .Op Fl p Ar pad_value .Op Fl r Ar ram_size .Op Fl t Ar title_str .Op Fl W Ar warning .Ar .Sh DESCRIPTION The .Nm program changes headers of Game Boy ROM images, typically generated by .Xr rgblink 1 , though it will work with .Em any Game Boy ROM. It also performs other correctness operations, such as padding. .Nm only changes the fields for which it has values specified. Developers are advised to fill those fields with 0x00 bytes in their source code before running .Nm , and to have already populated whichever fields they don't specify using .Nm . .Sh ARGUMENTS .Nm accepts the usual short and long options, such as .Fl V and .Fl -version . Options later in the command line override those set earlier, except for when duplicate options are considered an error. Options can be abbreviated as long as the abbreviation is unambiguous: .Fl \-ver is .Fl \-version , but .Fl \-v is invalid because it could also be .Fl \-validate . .Pp Unless otherwise noted, passing .Ql - (a single dash) as a file name makes .Nm use standard input (for input files) or standard output (for output files). To suppress this behavior, and open a file in the current directory actually called .Ql - , pass .Ql ./- instead. Using standard input or output for more than one file in a single command may produce unexpected results. .Pp .Nm accepts decimal, hexadecimal, octal, and binary for numeric option arguments. Decimal numbers are written as usual; hexadecimal numbers must be prefixed with either .Ql $ or .Ql 0x ; octal numbers must be prefixed with either .Ql & or .Ql 0o ; and binary numbers must be prefixed with either .Ql % or .Ql 0b . (The prefixes .Ql $ and .Ql & will likely need escaping or quoting to avoid being interpreted by the shell.) Leading zeros (after the base prefix, if any) are accepted, and letters are not case-sensitive. For example, all of these are equivalent: .Ql 42 , .Ql 042 , .Ql 0x2A , .Ql 0X2A , .Ql 0x2a , .Ql &52 , .Ql 0o52 , .Ql 0O052 , .Ql 0b00101010 , .Ql 0B101010 . .Pp The following options are accepted: .Bl -tag -width Ds .It Fl C , Fl \-color-only Set the Game Boy Color\(enonly flag .Pq Ad 0x143 to 0xC0. This overrides .Fl c if it was set prior. .It Fl c , Fl \-color-compatible Set the Game Boy Color\(encompatible flag: .Pq Ad 0x143 to 0x80. This overrides .Fl c if it was set prior. .It Fl \-color Ar when Specify when to highlight warning and error messages with color: .Ql always , .Ql never , or .Ql auto . .Ql auto determines whether to use colors based on the .Ql Lk https://no-color.org/ NO_COLOR or .Ql Lk https://force-color.org/ FORCE_COLOR environment variables, or whether the output is to a TTY. .It Fl f Ar fix_spec , Fl \-fix-spec Ar fix_spec Fix certain header values that the Game Boy checks for correctness. Alternatively, intentionally trash these values by writing their binary inverse instead. .Ar fix_spec is a string containing any combination of the following characters: .Pp .Bl -tag -compact -width xx .It Cm l Fix the Nintendo logo .Pq Ad 0x104 Ns \(en Ns Ad 0x133 . .It Cm L Trash the Nintendo logo. .It Cm h Fix the header checksum .Pq Ad 0x14D . .It Cm H Trash the header checksum. .It Cm g Fix the global checksum .Pq Ad 0x14E Ns \(en Ns Ad 0x14F . .It Cm G Trash the global checksum. .El .It Fl h , Fl \-help Print help text for the program and exit. .It Fl i Ar game_id , Fl \-game-id Ar game_id Set the game ID string .Pq Ad 0x13F Ns \(en Ns Ad 0x142 to a given string. If it's longer than 4 characters, it will be truncated. .It Fl j , Fl \-non-japanese Set the non-Japanese region flag .Pq Ad 0x14A to 0x01. .It Fl k Ar licensee_str , Fl \-new-licensee Ar licensee_str Set the new licensee string .Pq Ad 0x144 Ns \(en Ns Ad 0x145 to a given string. If it's longer than 2 characters, it will be truncated. .It Fl L Ar logo_file , Fl \-logo Ar logo_file Specify a logo file to use instead of the official Nintendo logo. The file must be 48 bytes of 1bpp tile data; the source image should be 48 pixels wide and 8 pixels tall. .It Fl l Ar licensee_id , Fl \-old-licensee Ar licensee_id Set the old licensee code .Pq Ad 0x14B to a given value from 0 to 0xFF. This value is deprecated and should be set to 0x33 in all new software. .It Fl m Ar mbc_type , Fl \-mbc-type Ar mbc_type Set the MBC type .Pq Ad 0x147 to a given value from 0 to 0xFF. .Pp This value may also be an MBC name. The list of accepted names can be obtained by passing .Ql Cm help or .Ql Cm list as the argument. Any amount of whitespace (space and tabs) is allowed around plus signs, and the order of "components" is free, as long as the MBC name is first. There are special considerations to take for the TPP1 mapper; see the .Sx TPP1 section below. .It Fl n Ar rom_version , Fl \-rom-version Ar rom_version Set the ROM version .Pq Ad 0x14C to a given value from 0 to 0xFF. .It Fl o Ar out_file , Fl \-output Ar out_file Write the modified ROM image to the given file, or '-' to write to standard output. If not specified, the input files are modified in-place, or written to standard output if read from standard input. .It Fl p Ar pad_value , Fl \-pad-value Ar pad_value Pad the ROM image to a valid size with a given pad value from 0 to 255 (0xFF). .Nm will automatically pick a size from 32 KiB, 64 KiB, 128 KiB, ..., 8192 KiB. The cartridge size byte .Pq Ad 0x148 will be changed to reflect this new size. The recommended padding value is 0xFF, to speed up writing the ROM to flash chips, and to avoid "nop slides" into VRAM. .It Fl r Ar ram_size , Fl \-ram-size Ar ram_size Set the RAM size .Pq Ad 0x149 to a given value from 0 to 0xFF. .It Fl s , Fl \-sgb-compatible Set the SGB flag .Pq Ad 0x146 to 0x03. This flag will be ignored by the SGB unless the old licensee code .Pq Fl l is 0x33! .It Fl t Ar title , Fl \-title Ar title Set the title string .Pq Ad 0x134 Ns \(en Ns Ad 0x143 to a given string. If the title is longer than the maximum length, it will be truncated. The max length is 11 characters if the game ID .Pq Fl i is specified, 15 characters if the CGB flag .Fl ( c or .Fl C ) is specified but the game ID is not, and 16 characters otherwise. .It Fl V , Fl \-version Print the version of the program and exit. .It Fl v , Fl \-validate Equivalent to .Fl f Cm lhg . .It Fl W Ar warning , Fl \-warning Ar warning Set warning flag .Ar warning . A warning message will be printed if .Ar warning is an unknown warning flag. See the .Sx DIAGNOSTICS section for a list of warnings. .It Fl w Disable all warning output, even when turned into errors. .It @ Ns Ar at_file Read more options and arguments from a file, as if its contents were given on the command line. Arguments are separated by whitespace or newlines. Lines starting with a hash sign .Pq Ql # are considered comments and ignored. .Pp No shell processing is performed, such as wildcard or variable expansion. There is no support for escaping or quoting whitespace to be included in arguments. The standard .Ql -- to stop option processing also disables at-file processing. Note that while .Ql -- can be used .Em inside an at-file, it only disables option processing within that at-file, and processing continues in the parent scope. .El .Sh DIAGNOSTICS Warnings are diagnostic messages that indicate possibly erroneous behavior that does not necessarily compromise the header-fixing process. The following options alter the way warnings are processed. .Bl -tag -width Ds .It Fl Werror Make all warnings into errors. This can be negated as .Fl Wno-error to prevent turning all warnings into errors. .It Fl Werror= Make the specified warning or meta warning into an error. A warning's name is appended .Pq example: Fl Werror=obsolete , and this warning is implicitly enabled and turned into an error. This can be negated as .Fl Wno-error= to prevent turning a specified warning into an error, even if .Fl Werror is in effect. .El .Pp The following warnings are .Dq meta warnings, that enable a collection of other warnings. If a specific warning is toggled via a meta flag and a specific one, the more specific one takes priority. The position on the command-line acts as a tie breaker, the last one taking effect. .Bl -tag -width Ds .It Fl Wall This enables warnings that are likely to indicate an error or undesired behavior, and that can easily be fixed. .It Fl Weverything Enables literally every warning. .El .Pp The following warnings are actual warning flags; with each description, the corresponding warning flag is included. Note that each of these flags also has a negation (for example, .Fl Wobsolete enables the warning that .Fl Wno-obsolete disables; and .Fl Wall enables every warning that .Fl Wno-all disables). Only the non-default flag is listed here. Ignoring the .Dq no- prefix, entries are listed alphabetically. .Bl -tag -width Ds .It Fl Wno-mbc Warn when there are inconsistencies with or caveats about the specified MBC type. .It Fl Wno-obsolete Warn when obsolete features are encountered, which have been deprecated and may later be removed. .It Fl Wno-overwrite Warn when overwriting different non-zero bytes in the header. .It Fl Wno-sgb Warn when the SGB flag .Pq Fl s conflicts with the old licensee code .Pq Fl l . .It Fl Wno-truncation Warn when truncating values to fit the available space. .El .Sh EXAMPLES Most values in the ROM header do not matter to the actual console, and most are seldom useful anyway. The bare minimum requirements for a workable program are the header checksum, the Nintendo logo, and (if needed) the CGB/SGB flags. It is a good idea to pad the image to a valid size as well .Pq Do valid Dc meaning a power of 2, times 32 KiB . .Pp The following will make a plain, non-color Game Boy game without checking for a valid size: .Pp .D1 $ rgbfix -v foo.gb .Pp The following will make a SGB-enabled, color-enabled game with a title of .Dq foobar , and pad it to a valid size. .Pq The Game Boy itself does not use the title, but some emulators or ROM managers do. .Pp .D1 $ rgbfix -vcs -l 0x33 -p 255 -t foobar baz.gb .Pp The following will duplicate the header of the game .Dq Survival Kids , sans global checksum: .Pp .D1 $ rgbfix -cjsv -k A4 -l 0x33 -m 0x1B -p 0xFF -r 3 -t SURVIVALKIDAVKE \ SurvivalKids.gbc .Sh TPP1 TPP1 is a homebrew mapper designed as a functional superset of the common traditional MBCs, allowing larger ROM and RAM sizes combined with other hardware features. Its specification, as well as more resources, can be found online at .Lk https://github.com/aaaaaa123456789/tpp1 . .Ss MBC name The MBC name for TPP1 is more complex than standard mappers. It must be followed with the revision number, of the form .Ql major.minor , where both .Ql major and .Ql minor are decimal, 8-bit integers. There may be any amount of spaces or underscores between .Ql TPP1 and the revision number. .Nm only supports 1.x revisions, and will reject everything else. .Pp Like other mappers, the name may be followed with a list of optional, .Ql + Ns -separated features; however, .Ql RAM should not be specified, as the TPP1 mapper implicitly requests RAM if a non-zero RAM size is specified. Therefore, .Nm will ignore the .Ql RAM feature on a TPP1 mapper. .Ss Special considerations TPP1 overwrites the byte at .Ad 0x14A , usually indicating the region destination .Pq see Fl j , with one of its three identification bytes. Therefore, .Nm will warn about and ignore .Fl j if used in combination with TPP1. .Sh BUGS Please report bugs or mistakes in this documentation on .Lk https://github.com/gbdev/rgbds/issues GitHub . .Sh SEE ALSO .Xr rgbasm 1 , .Xr rgblink 1 , .Xr rgbgfx 1 , .Xr gbz80 7 , .Xr rgbds 7 .Sh HISTORY .Nm was originally written by .An Carsten S\(/orensen as a standalone program called GBFix, which was then packaged in ASMotor, and was later repackaged in RGBDS by .An Justin Lloyd . It is now maintained by a number of contributors at .Lk https://github.com/gbdev/rgbds . gbdev-rgbds-92bfe5d/man/rgbgfx.1000066400000000000000000000732131512540461700165310ustar00rootroot00000000000000'\" e .\" .\" SPDX-License-Identifier: MIT .\" .Dd January 1, 2026 .Dt RGBGFX 1 .Os .Sh NAME .Nm rgbgfx .Nd Game Boy graphics converter .Sh SYNOPSIS .Nm .Op Fl CmhOuVwXYZ .Op Fl v Op Fl v No ... .Op Fl a Ar attrmap | Fl A .Op Fl b Ar base_ids .Op Fl c Ar pal_spec .Op Fl \-color Ar when .Op Fl d Ar depth .Op Fl i Ar input_tiles .Op Fl L Ar slice .Op Fl l Ar base_pal .Op Fl N Ar nb_tiles .Op Fl n Ar nb_pals .Op Fl o Ar out_file .Op Fl p Ar pal_file | Fl P .Op Fl q Ar pal_map | Fl Q .Op Fl r Ar width .Op Fl s Ar nb_colors .Op Fl t Ar tilemap | Fl T .Op Fl W Ar warning .Op Fl x Ar quantity .Ar file .Sh DESCRIPTION The .Nm program converts PNG images into data suitable for display on the Game Boy and Game Boy Color, or vice-versa. .Pp The main function of .Nm is to divide the input PNG into 8\[tmu]8 pixel .Em squares , convert each of those squares into 1bpp or 2bpp tile data, and save all of the tile data in a file. It also has options to generate a tile map, attribute map, and/or palette set as well; more on that and how the conversion process can be tweaked below. .Sh ARGUMENTS .Nm accepts the usual short and long options, such as .Fl V and .Fl -version . Options later in the command line override those set earlier, except for when duplicate options are considered an error. Options can be abbreviated as long as the abbreviation is unambiguous: .Fl \-verb is .Fl \-verbose , but .Fl \-ver is invalid because it could also be .Fl \-version . .Pp Unless otherwise noted, passing .Ql - (a single dash) as a file name makes .Nm use standard input (for input files) or standard output (for output files). To suppress this behavior, and open a file in the current directory actually called .Ql - , pass .Ql ./- instead. Using standard input or output for more than one file in a single command may produce unexpected results. .Pp .Nm accepts decimal, hexadecimal, octal, and binary for numeric option arguments. Decimal numbers are written as usual; hexadecimal numbers must be prefixed with either .Ql $ or .Ql 0x ; octal numbers must be prefixed with either .Ql & or .Ql 0o ; and binary numbers must be prefixed with either .Ql % or .Ql 0b . (The prefixes .Ql $ and .Ql & will likely need escaping or quoting to avoid being interpreted by the shell.) Leading zeros (after the base prefix, if any) are accepted, and letters are not case-sensitive. For example, all of these are equivalent: .Ql 42 , .Ql 042 , .Ql 0x2A , .Ql 0X2A , .Ql 0x2a , .Ql &52 , .Ql 0o52 , .Ql 0O052 , .Ql 0b00101010 , .Ql 0B101010 . .Pp The following options are accepted: .Bl -tag -width Ds .It Fl a Ar attrmap , Fl \-attr-map Ar attrmap Generate an attribute map, which is a file containing tile .Dq attributes . For each square of the input image, its corresponding attribute map byte contains the mirroring bits (if .Fl m was specified), the bank bit .Pq see Fl N , and the palette index. See .Lk https://gbdev.io/pandocs/Tile_Maps#bg-map-attributes-cgb-mode-only Pan Docs for the individual bytes' format. The output is written just like the tile map (see .Fl t ) , follows the same order .Pq Fl Z , and has the same size. .It Fl A , Fl \-auto-attr-map Same as .Fl a Ar base_path Ns .attrmap .Pq see Sx Automatic output paths . .It Fl B Ar color , Fl \-background-color Ar color Set a background color to be omitted from output. Colors are accepted in .Ql #rgb or .Ql #rrggbb format, or as .Ql transparent . Input tiles which are entirely the specified background color are ignored and will not be output in tile data file. The tilemap, atrribute map, or palette map files .Em will use placeholder values where background tiles were. If a background color is specified, it cannot be used within tiles which are not ignored. .It Fl b Ar base_ids , Fl \-base-tiles Ar base_ids Set the base IDs for tile map output. .Ar base_ids should be one or two numbers between 0 and 255, separated by a comma; they are for bank 0 and bank 1 respectively. Both default to 0. .It Fl C , Fl \-color-curve Modifies the color palettes .Pq whether they are generated from the input image or taken from an input palette specification with a color curve mimicking the Game Boy Color's screen. This adjusts the .Em absolute RGB color values so that the .Em perceived colors, when displayed on Game Boy Color hardware .Pq or an emulator with an accurate display filter , will look like the original colors as displayed on a backlit computer screen. Note that GBC displays can look very different depending on the ambient light and their exact hardware model, so this color curve is only a "best effort". .It Fl c Ar pal_spec , Fl \-colors Ar pal_spec Use the specified color palettes instead of having .Nm automatically determine some. .Ar pal_spec can be one of the following: .Bl -tag -width Ds .It Sy inline palette spec If .Ar pal_spec begins with a hash character .Ql # , it is treated as an inline palette specification. It should contain a comma-separated list of hexadecimal colors, each beginning with a hash. Colors are accepted in .Ql #rgb or .Ql #rrggbb format. To leave one or more gaps in the palette, .Ql #none can be used instead of any color. Palettes must be separated by a colon or semicolon (the latter may require quoting to avoid special handling by the shell), and spaces are allowed around colons, semicolons and commas; trailing commas and semicolons are allowed. See .Sx EXAMPLES for an example of an inline palette specification. .It Sy embedded palette spec If .Ar pal_spec is the case-insensitive word .Cm embedded , then the first four colors of the input PNG's embedded palette are used. It is an error if the PNG is not indexed, or if colors other than these 4 are used. .Pq This is different from the default behavior of indexed PNGs, as then unused entries in the embedded palette are ignored, whereas they are not with Fl c Cm embedded . .It Sy DMG palette spec If .Ar pal_spec starts with case-insensitive .Cm dmg= , then the following two-digit hexadecimal number specifies four grayscale DMG color indexes. The number functions like the DMG's $FF47 .Sy BGP register (see .Lk https://gbdev.io/pandocs/Palettes.html Pan Docs for more information): the low two bits 0-1 specify which gray shade goes in color index 0, the next two bits 2-3 specify which gray shade goes in color index 1, and so on. Gray shade 0 is the lightest (white), 3 is the darkest (black). If .Ar pal_spec is the case-insensitive word .Cm dmg , then it acts like .Cm dmg=E4 , i.e. the darkest gray will end up in color index 0, and so on. The same gray shade cannot go in two color indexes. To specify a DMG palette, the input PNG must have all its colors in shades of gray, without any transparent colors. .It Sy automatic palette generation If .Ar pal_spec is the case-insensitive word .Cm auto , then a palette is automatically generated using the procedure described in .Sx PALETTE GENERATION . This is the default behavior if .Fl c was not specified. .It Sy external palette spec Otherwise, .Ar pal_spec is assumed to be an external palette specification. The expected format is .Ql format:path , where .Ar path is a path to a file .Ql ( - is not treated specially), which will be processed according to the .Ar format . See .Sx PALETTE SPECIFICATION FORMATS for a list of formats and their descriptions. .El .It Fl \-color Ar when Specify when to highlight warning and error messages with color: .Ql always , .Ql never , or .Ql auto . .Ql auto determines whether to use colors based on the .Ql Lk https://no-color.org/ NO_COLOR or .Ql Lk https://force-color.org/ FORCE_COLOR environment variables, or whether the output is to a TTY. .It Fl d Ar depth , Fl \-depth Ar depth Set the bit depth of the output tile data, in bits per pixel (bpp), either 1 or 2 (the default). This changes how tile data is output, and the maximum number of colors per palette (2 and 4 respectively). .It Fl h , Fl \-help Print help text for the program and exit. .It Fl i Ar input_tiles , Fl \-input-tileset Ar input_tiles Use the specified input tiles in addition to having .Nm automatically determine some. The input tiles will always be first in the .Fl o image output, and will always get the first IDs in the .Fl t tilemap output. .Ar input_tiles must contain 1bpp or 2bpp tile data .Pq whichever matches the Fl d No option used here , as could be previously generated with the .Fl o option. .Pp If the .Fl o option is also specified, then the input tiles will be assigned the first tile IDs, and any tiles from the input image that are not in the input tileset will be assigned subsequent IDs. But if the .Fl o option is .Em not specified, then the tile map can .Em only use tiles from the input tileset. Using .Fl o with .Fl i is useful if you want to precisely control the tile IDs of its tile map. Using .Fl i alone is more useful if you want several images to use a subset of shared tiles. .Pp If the image will use more than one color palette, it is .Em strongly advised to generate the palette set along with the input tile data, and pass .Fl c Cm gbc: Ns Ar input_palette along with .Fl i Ar input_tiles . This is because .Nm might not generate the same palette set for this image as it did for its input tileset. .Pp See .Sx EXAMPLES for examples of how to use this option. .Pp This option is ignored in .Sx REVERSE MODE . .It Fl L Ar slice , Fl \-slice Ar slice Only process a given rectangle of the image. This is useful for example if the input image is a sheet of some sort, and you want to convert each cel individually. The default is to process the whole image as-is. .Pp .Ar slice must be formatted as .Ql Ar X , Ns Ar Y : Ns Ar W , Ns Ar H : two comma-separated number pairs, separated by a colon. Whitespace is allowed around all punctuation. The first number pair specifies the X and Y coordinates of the top-left pixel that will be processed (anything above it or to its left will be ignored). The second number pair specifies how many tiles to process horizontally and vertically, respectively. .Pp .Fl L Sy is ignored in reverse mode , No no padding is inserted . .It Fl l Ar base_pal , Fl \-base-palette Ar base_pal Set the base ID for attribute map and palette map output. .Ar base_pal should be a number between 0 and 255. It defaults to 0. .It Fl m , Fl \-mirror-tiles Deduplicate tiles that are horizontally and/or vertically symmetrical mirror images of each other. Only one of each unique tile will be saved in the tile data file, with mirror images counting as duplicates. Useful with a tile map and attribute map together (see .Fl a and .Fl t ) to keep track of the duplicated tiles and the dimension(s) mirrored. Implies .Fl u . Equivalent to .Fl XY . .It Fl N Ar nb_tiles , Fl \-nb-tiles Ar nb_tiles Set a maximum number of tiles that can be placed in each VRAM bank. .Ar nb_tiles should be one or two numbers between 0 and 256, separated by a comma; if the latter is omitted, it defaults to 0. Setting either number to 0 prevents any tiles from being output in that bank. .Pp If more tiles are generated than can fit in the two banks combined, .Nm will abort. If .Fl N is not specified, no limit will be set on the amount of tiles placed in bank 0, and tiles will not be placed in bank 1. .It Fl n Ar nb_pals , Fl \-nb-palettes Ar nb_pals Abort if more than .Ar nb_pals palettes are generated. This may not be more than 256. .Pp Note that attribute map output only has 3 bits for the palette ID, so a limit higher than 8 may yield incomplete data unless relying on a palette map .Pq see Fl q . .It Fl O , Fl \-group-outputs Sets the .Sq base path to be the output tile data path from .Fl o instead of the input image path .Pq see Sx Automatic output paths . .It Fl o Ar out_file , Fl \-output Ar out_file Output the tile data in native 2bpp format or in 1bpp .Pq depending on Fl d to this file. .It Fl p Ar pal_file , Fl \-palette Ar pal_file Output the image's palette set to this file. .It Fl P , Fl \-auto-palette Same as .Fl p Ar base_path Ns .pal .Pq see Sx Automatic output paths . .It Fl q Ar pal_map , Fl \-palette-map Ar pal_map Output the image's palette map to this file. This is useful if the input image contains more than 8 palettes, as the attribute map only contains the lower 3 bits of the palette indices. .It Fl Q , Fl \-auto-palette-map Same as .Fl q Ar base_path Ns .palmap .Pq see Sx Automatic output paths . .It Fl r Ar width , Fl \-reverse Ar width Switches .Nm into .Dq Sy reverse mode. In this mode, instead of converting a PNG image into Game Boy data, .Nm will attempt to reverse the process, and render Game Boy data into an image. See .Sx REVERSE MODE below for details. .Pp .Ar width is the width of the image to generate, in tiles. .Fl r 0 chooses a width to make the image as square as possible. This is useful if you do not know the original width. .It Fl s Ar nb_colors , Fl \-palette-size Ar nb_colors Specify how many colors each palette contains, including the transparent one if any. .Ar nb_colors cannot be more than .Ql 1 << Ar depth .Pq see Fl d . .It Fl t Ar tilemap , Fl \-tilemap Ar tilemap Generate a file of tile indices. For each square of the input image, its corresponding tile map byte contains the index of the associated tile in the tile data file. The IDs wrap around from 255 back to 0, and do not include the bank bit; use .Fl a for that. Useful in combination with .Fl u and/or .Fl m to keep track of duplicate tiles. .It Fl T , Fl \-auto-tilemap Same as .Fl t Ar base_path Ns .tilemap .Pq see Sx Automatic output paths . .It Fl u , Fl \-unique-tiles Deduplicate identical tiles. Only one of each unique tile will be saved in the tile data file. Useful with a tile map .Pq see Fl t to keep track of the duplicated tiles. .Pp Note that if this option is enabled, no guarantee is made on the order in which tiles are output; while it .Em should be consistent across identical runs of a given .Nm release, the same is not true for different releases. .It Fl V , Fl \-version Print the version of the program and exit. .It Fl v , Fl \-verbose Be verbose. The verbosity level is increased by one each time the flag is specified, with each level including the previous: .Bl -enum -compact .It Print the .Nm configuration before taking actions. .It Print a notice before significant actions. .It Print some of the actions' intermediate results. .It Print some internal debug information. .It Print detailed internal information. .El The verbosity level does not go past 6. .Pp Note that verbose output is only intended to be consumed by humans, and may change without notice between RGBDS releases; relying on those for scripts is not advised. .It Fl W Ar warning , Fl \-warning Ar warning Set warning flag .Ar warning . A warning message will be printed if .Ar warning is an unknown warning flag. See the .Sx DIAGNOSTICS section for a list of warnings. .It Fl w Disable all warning output, even when turned into errors. .It Fl X , Fl \-mirror-x Deduplicate tiles that are horizontally symmetrical mirror images of each other across the X axis. Implies .Fl u . .It Fl x Ar quantity , Fl \-trim-end Ar quantity Do not output the last .Ar quantity tiles to the tile data file; no other output is affected. This is useful for trimming .Dq filler / blank squares at the end of an image. If fewer than .Ar quantity tiles would have been emitted, the file will be empty. .Pp Note that this is done .Em after deduplication if .Fl u was enabled, so you probably don't want to use this option in combination with .Fl u . Note also that the tiles that don't get output will not count towards .Fl N Ap s limit. .It Fl Y , Fl \-mirror-y Deduplicate tiles that are vertically symmetrical mirror images of each other across the Y axis. Implies .Fl u . .It Fl Z , Fl \-columns Read squares from the PNG in column-major order (column by column), instead of the default row-major order (line by line). This primarily affects tile map and attribute map output, although it may also change generated tile data and palettes. .It @ Ns Ar at_file Read more options and arguments from a file, as if its contents were given on the command line. Arguments are separated by whitespace or newlines. Lines starting with a hash sign .Pq Ql # are considered comments and ignored. .Pp No shell processing is performed, such as wildcard or variable expansion. There is no support for escaping or quoting whitespace to be included in arguments. The standard .Ql -- to stop option processing also disables at-file processing. Note that while .Ql -- can be used .Em inside an at-file, it only disables option processing within that at-file, and processing continues in the parent scope. .Pp See .Sx At-files below for an explanation of how this can be useful. .El .Ss At-files In a given project, many images are to be converted with different flags. The traditional way of solving this problem has been to specify the different flags for each image in the Makefile or build script; this can be inconvenient, as it centralizes all those flags away from the images they concern. .Pp To avoid these drawbacks, you can use .Dq at-files : any command-line argument that begins with an at sign .Pq Ql @ is interpreted as one, as documented above. At-files can be stored right next to the corresponding image, for example: .Pp .Dl $ rgbgfx -o image.2bpp -t image.tilemap @image.flags image.png .Pp This will read additional flags from the file .Ql image.flags , which could contain, for example, .Ql -b 128 to specify a base offset for the image's tiles. The above command could be generated from the following .Xr make 1 rule: .Bd -literal -offset indent %.2bpp %.tilemap: %.flags %.png rgbgfx -o $*.2bpp -t $*.tilemap @$*.flags $*.png .Ed .Sh PALETTE SPECIFICATION FORMATS The following formats are supported: .Bl -tag -width Ds .It Cm act .Lk https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1070626 Adobe Photoshop color table . .It Cm aco .Lk https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1055819 Adobe Photoshop color swatch . .It Cm gbc A GBC palette memory dump, as emitted by .Nm Fl p . Useful to force several images to share the same palette. .It Cm gpl .Lk https://docs.gimp.org/2.10/en/gimp-concepts-palettes.html GIMP palette . .It Cm hex Plaintext lines of hexadecimal colors in .Ql rrggbb format. .It Cm png An image of square color swatches, with each row defining the colors for one palette. Color swatches can be any square size. .It Cm psp .Lk https://www.selapa.net/swatches/colors/fileformats.php#psp_pal Paint Shop Pro palette . .El .Pp If you wish for another format to be supported, please open an issue (see .Sx BUGS below) or contact us, and supply a few sample files. .Sh PALETTE GENERATION .Nm must generate palettes from the colors in the input image, unless .Fl c was used; in that case, the provided palettes will be used. .Sy If the order of colors in the palettes is important to you , for example because you want to use palette swaps, please use .Fl c to specify the palette explicitly. .Pp First, if the image contains .Em any transparent pixel, color #0 of .Em all palettes will be allocated to it. This is done .Sy even if palettes were explicitly specified using Fl c ; then the specification only covers color #1 onwards. .Pq If you do not want this, ask your image editor to remove the alpha channel. .Pp After generating palettes, .Nm sorts colors within those palettes using the following rules: .EQ delim $$ .EN .Bl -bullet -offset indent .It If the PNG file internally contains a palette (often dubbed an .Dq indexed PNG), then colors in each output palette will be sorted according to their order in the PNG's palette. Any unused entries will be ignored, and only the first entry is considered if there are any duplicates. .Po If you want a given color to appear more than once, or an unused color to appear at all, you should specify the palettes explicitly instead using Fl c ; .Fl c Cm embedded may be appropriate. .Pc .It Otherwise, if the PNG only contains shades of gray, they will be categorized into as many .Dq bins as there are colors per palette, and the palette is set to these bins. The darkest gray will end up in bin #0, and so on; note that this is the opposite of the RGB method below. This is equivalent to having specified a DMG palette of .Fl c Cm dmg=E4 . If two distinct grays end up in the same bin, the RGB method is used instead. .Pp Be careful that .Nm is picky about what it considers .Dq grays : the red, green, and blue components of each color must .Em all be .Em exactly the same. .It If none of the above apply, colors are sorted from lightest (first) to darkest (last). The definition of luminance that .Nm uses is .Do $2126 times red + 7152 times green + 722 times blue$ .Dc . .El .EQ delim off .EN .Pp Note that the .Dq indexed behavior depends on an internal detail of how the PNG is saved, specifically its .Ql PLTE chunk. Since few image editors (such as GIMP) expose that detail, this behavior is only kept for compatibility and should be considered deprecated. .Pp It turns out that palette generation is an NP-complete problem known as "pagination", so .Nm does not attempt to find the optimal solution, but instead uses an "overload-and-remove" heuristic to find a good one in a reasonable amount of time. (There are no guarantees about how this algorithm will generate palettes, apart from the constraints documented above.) It is possible to compute the optimal solution externally (using a solver, for example), and then provide it to .Nm via .Fl c . .Sh OUTPUT FILES All files output by .Nm are binary files, and designed to follow the Game Boy and Game Boy Color's native formats. What follows is succinct descriptions of those formats, including .Nm Ns -specific details. For more complete, beginner-friendly descriptions of the native formats with illustrations, please check out .Lk https://gbdev.io/pandocs/Graphics Pan Docs . .Ss Tile data Tile data is output like a binary dump of VRAM, with no padding between tiles. Each tile is 16 bytes, 2 per row of 8 pixels; the bits of color IDs are split into each byte .Pq or Dq bitplane . The leftmost pixel's color ID is stored in the two bytes' most significant bits, and the rightmost pixel's color ID in their least significant bits. .Pp When the bit depth .Pq Fl d is set to 1, the most significant bitplane (second byte) of each row, being all zeros, is simply not output. .Ss Palette data Palette data is output like a dump of palette memory. Each color is written as GBC-native little-endian RGB555, with the unused bit 15 set to 0. There is no padding between colors, nor between palettes; however, empty colors in the palettes are output as 0xFFFF. .EQ delim $$ .EN For example, if 5 palettes are generated with .Fl s Cm 4 , the palette data file will be $2 times 4 times 5 = 40$ bytes long, even if some palettes contain less than 3 colors. .EQ delim off .EN Note that .Fl n only caps how many palettes are generated (and thus this file's size), but fewer may be generated still. .Ss Tile map data A tile map is an array of tile IDs, with one byte per tile ID. The first byte always corresponds to the ID of the tile in top-left corner of the input image; the second byte is either the ID of the tile to its right (by default), or below it .Pq with Fl Z ; and so on, continuing in the same direction. Rows / columns (respectively) are stored consecutively, with no padding. .Ss Attribute map data Attribute maps mirror the format of tile maps, like on the GBC, especially the order in which bytes are output. The contents of individual bytes follows the GBC's native format: .Bl -column "Bit 2\(en0" "Background Palette number" .It Bit 7 Ta BG-to-OAM Priority Ta Set to 0 .It Bit 6 Ta Vertical Flip Ta 0=Normal, 1=Mirror vertically .It Bit 5 Ta Horizontal Flip Ta 0=Normal, 1=Mirror horizontally .It Bit 4 Ta Not used Ta Set to 0 .It Bit 3 Ta Tile VRAM Bank number Ta 0=Bank 0, 1=Bank 1 .It Bit 2\(en0 Ta Background Palette number Ta BGP0-7 .El .Pp Note that if more than 8 palettes are used, only the lowest 3 bits of the palette ID are output. .Ss Automatic output paths For convenience, .Nm provides shortcuts to generate all files in the same directory. This is done by using the uppercase version of a flag .Pq for example, Fl A No instead of Fl a . The .Ar base_path is the input image path .Pq or the output tile data path from Fl o , No if Fl O No was given with its extension, if any, removed. .Pp For example, these two commands are equivalent: .Bd -literal -offset indent $ rgbgfx img/player.png -o build/player.2bpp -P $ rgbgfx img/player.png -o build/player.2bpp -p img/player.pal .Ed .Pp And so are these two: .Bd -literal -offset indent $ rgbgfx img/player.png -o build/player.2bpp -O -P $ rgbgfx img/player.png -o build/player.2bpp -p build/player.pal .Ed .Sh REVERSE MODE .Nm can produce a PNG image from valid data. This may be useful for ripping graphics, recovering lost source images, etc. An important caveat on that last one, though: the conversion process is .Sy lossy both ways, so the .Do reversed Dc image won't be perfectly identical to the original\(embut it should be close to a Game Boy's output . .Pq Keep in mind that many of consoles output different colors, so there is no true reference rendering. .Pp When using reverse mode, make sure to pass the same flags that were given when generating the data, especially .Fl C , d , N , s , x , and .Fl Z . .Do Sx At-files Dc may help with this . .Nm will warn about any inconsistencies it detects. .Pp Files that are normally outputs .Pq Fl a , p , t become inputs, and .Ar file will be written to instead of read from, and thus needs not exist beforehand. Any of these inputs not passed is assumed to be some default: .Bl -column "attribute map" .It palettes Ta Unspecified palette data makes .Nm assume DMG (monochrome Game Boy) mode: a single palette of 4 grays. It is possible to pass palettes using .Fl c instead of .Fl p . .It tile data Ta Tile data must be provided, as there is no reasonable assumption to fall back on. .It tile map Ta A missing tile map makes .Nm assume that tiles were not deduplicated, and should be laid out in the order they are stored. .It attribute map Ta Without an attribute map, .Nm assumes that no tiles were mirrored. .El .Sh DIAGNOSTICS Warnings are diagnostic messages that indicate possibly erroneous behavior that does not necessarily compromise the conversion process. The following options alter the way warnings are processed. .Bl -tag -width Ds .It Fl Werror Make all warnings into errors. This can be negated as .Fl Wno-error to prevent turning all warnings into errors. .It Fl Werror= Make the specified warning or meta warning into an error. A warning's name is appended .Pq example: Fl Werror=obsolete , and this warning is implicitly enabled and turned into an error. This can be negated as .Fl Wno-error= to prevent turning a specified warning into an error, even if .Fl Werror is in effect. .El .Pp The following warnings are .Dq meta warnings, that enable a collection of other warnings. If a specific warning is toggled via a meta flag and a specific one, the more specific one takes priority. The position on the command-line acts as a tie breaker, the last one taking effect. .Bl -tag -width Ds .It Fl Wall This enables warnings that are likely to indicate an error or undesired behavior, and that can easily be fixed. .It Fl Weverything Enables literally every warning. .El .Pp The following warnings are actual warning flags; with each description, the corresponding warning flag is included. Note that each of these flags also has a negation (for example, .Fl Wobsolete enables the warning that .Fl Wno-obsolete disables; and .Fl Wall enables every warning that .Fl Wno-all disables). Only the non-default flag is listed here. Ignoring the .Dq no- prefix, entries are listed alphabetically. .Bl -tag -width Ds .It Fl Wembedded Warn when a generated palette is sorted according to the input PNG's embedded palette but .Fl c Cm embedded was not provided. This warning is enabled by .Fl Weverything . .It Fl Wno-obsolete Warn when obsolete features are encountered, which have been deprecated and may later be removed. .It Fl Wtrim-nonempty Warn when .Fl x trims a nonempty tile. An "empty" tile uses entirely color 0 of its palette. This warning is enabled by .Fl Wall . .El .Sh EXAMPLES The following will only validate the .Ql tileset.png image (check its size, that all tiles have a suitable amount of colors, etc.), but output nothing: .Pp .Dl $ rgbgfx src/res/maps/overworld/tileset.png .Pp The following will convert the .Ql tileset.png image using the two given palettes (and only those), and store the generated 2bpp tile data in .Ql tileset.2bpp , and the attribute map in .Ql tileset.attrmap . .Pp .Dl $ rgbgfx -c '#ffffff,#8d05de, #dc7905,#000000 ; #fff,#8d05de, #7e0000 \&, #000' -A -o tileset.2bpp tileset.png .Pp The following will deduplicate the tiles in the .Ql title_screen.png image, keeping only one of each unique tile, and store the generated 2bpp tile data in .Ql title_screen.2bpp , and the tile map in .Ql title_screen.tilemap . .Pp .Dl $ rgbgfx -u title_screen.png -o title_screen.2bpp -t title_screen.tilemap .Pp The following will convert the given inline palette specification to a palette set, and store the palette set in .Ql colors.pal , without needing an input image. .Pp .Dl $ rgbgfx -c '#fff,#ff0,#f80,#000' -p colors.pal .Pp The following will convert two level images using the same tileset, and error out if any of them contain tiles not in the tileset. .Pp .Bd -literal -offset Ds $ rgbgfx tileset.png -o tileset.2bpp -O -P $ rgbgfx level1.png -i tileset.2bpp -c gbc:tileset.pal -t level1.tilemap -a level1.attrmap $ rgbgfx level2.png -i tileset.2bpp -c gbc:tileset.pal -t level2.tilemap -a level2.attrmap .Ed .Sh BUGS Please report bugs or mistakes in this documentation on .Lk https://github.com/gbdev/rgbds/issues GitHub . .Sh SEE ALSO .Xr rgbasm 1 , .Xr rgblink 1 , .Xr rgbfix 1 , .Xr rgbds 7 .Pp The Game Boy hardware reference .Lk https://gbdev.io/pandocs/Graphics Pan Docs , particularly the section about graphics. .Sh HISTORY .Nm was originally written by stag019 as a program to be packaged in RGBDS. It was later rewritten by .An ISSOtm , and is now maintained by a number of contributors at .Lk https://github.com/gbdev/rgbds . gbdev-rgbds-92bfe5d/man/rgblink.1000066400000000000000000000324571512540461700167070ustar00rootroot00000000000000.\" SPDX-License-Identifier: MIT .\" .Dd January 1, 2026 .Dt RGBLINK 1 .Os .Sh NAME .Nm rgblink .Nd Game Boy linker .Sh SYNOPSIS .Nm .Op Fl dhMtVvwx .Op Fl B Ar param .Op Fl \-color Ar when .Op Fl l Ar linker_script .Op Fl m Ar map_file .Op Fl n Ar sym_file .Op Fl O Ar overlay_file .Op Fl o Ar out_file .Op Fl p Ar pad_value .Op Fl S Ar spec .Op Fl W Ar warning .Ar .Sh DESCRIPTION The .Nm program links RGB object files, typically created by .Xr rgbasm 1 , into a single Game Boy ROM file. The object file format is documented in .Xr rgbds 5 . .Pp ROM0 sections are placed in the first 16 KiB of the output ROM, and ROMX sections are placed in any 16 KiB .Dq bank except the first. If your ROM will only be 32 KiB, you can use the .Fl t option to change this. .Pp Similarly, WRAM0 sections are placed in the first 4 KiB of WRAM .Pq Dq bank 0 , and WRAMX sections are placed in any bank of the last 4 KiB. If your ROM doesn't use banked WRAM, you can use the .Fl w option to change this. .Pp Also, if your ROM is designed for a monochrome Game Boy, you can make sure that you don't use any incompatible section by using the .Fl d option, which implies .Fl w but also prohibits the use of banked VRAM. .Sh ARGUMENTS .Nm accepts the usual short and long options, such as .Fl V and .Fl -version . Options later in the command line override those set earlier, except for when duplicate options are considered an error. Options can be abbreviated as long as the abbreviation is unambiguous: .Fl \-verb is .Fl \-verbose , but .Fl \-ver is invalid because it could also be .Fl \-version . .Pp Unless otherwise noted, passing .Ql - (a single dash) as a file name makes .Nm use standard input (for input files) or standard output (for output files). To suppress this behavior, and open a file in the current directory actually called .Ql - , pass .Ql ./- instead. Using standard input or output for more than one file in a single command may produce unexpected results. .Pp .Nm accepts decimal, hexadecimal, octal, and binary for numeric option arguments. Decimal numbers are written as usual; hexadecimal numbers must be prefixed with either .Ql $ or .Ql 0x ; octal numbers must be prefixed with either .Ql & or .Ql 0o ; and binary numbers must be prefixed with either .Ql % or .Ql 0b . (The prefixes .Ql $ and .Ql & will likely need escaping or quoting to avoid being interpreted by the shell.) Leading zeros (after the base prefix, if any) are accepted, and letters are not case-sensitive. For example, all of these are equivalent: .Ql 42 , .Ql 042 , .Ql 0x2A , .Ql 0X2A , .Ql 0x2a , .Ql &52 , .Ql 0o52 , .Ql 0O052 , .Ql 0b00101010 , .Ql 0B101010 . .Pp The following options are accepted: .Bl -tag -width Ds .It Fl B Ar param , Fl \-backtrace Ar param Configures how location backtraces are printed if warnings or errors occur. This flag may be specified multiple times with different parameters that combine meaningfully. If .Ar param is a positive number, it specifies the maximum backtrace depth, abbreviating deeper ones. Other valid parameter values are the following: .Bl -tag -width Ds .It Cm 0 Do not limit the maximum backtrace depth; this is the default. .It Cm all Force all locations to be printed, even "quiet" ones (see .Dq Excluding locations from backtraces in .Xr rgbasm 5 for details). .It Cm no-all Do not print "quieted" locations in backtraces; this is the default. .It Cm collapse Print all locations on one line. .It Cm no-collapse Print one location per line; this is the default. .El .It Fl \-color Ar when Specify when to highlight warning and error messages with color: .Ql always , .Ql never , or .Ql auto . .Ql auto determines whether to use colors based on the .Ql Lk https://no-color.org/ NO_COLOR or .Ql Lk https://force-color.org/ FORCE_COLOR environment variables, or whether the output is to a TTY. .It Fl d , Fl \-dmg Enable DMG mode. Prohibit the use of sections that doesn't exist on a DMG, such as VRAM bank 1. This option automatically enables .Fl w . .It Fl h , Fl \-help Print help text for the program and exit. .It Fl l Ar linker_script , Fl \-linkerscript Ar linker_script Specify a linker script file that tells the linker how sections must be placed in the ROM. The attributes assigned in the linker script must be consistent with any assigned in the code. See .Xr rgblink 5 for more information about the linker script format. .It Fl M , Fl \-no-sym-in-map If specified, the map file will not list symbols, only sections. .It Fl m Ar map_file , Fl \-map Ar map_file Write a map file to the given filename, listing how sections and symbols were assigned. .It Fl n Ar sym_file , Fl \-sym Ar sym_file Write a symbol file to the given filename, listing all visible labels and exported numeric constants. Labels output their bank and address, numeric constants output their value, following .Lk https://rgbds.gbdev.io/sym/ this specification . Several external programs can use this information, for example to help debugging ROMs. .It Fl O Ar overlay_file , Fl \-overlay Ar overlay_file If specified, sections will be overlaid "on top" of the ROM image .Ar overlay_file : empty space between sections will be filled by the corresponding bytes from .Ar overlay_file . This is useful to patch an existing ROM. Note that all sections must be fixed (forced bank .Sy and address)! .It Fl o Ar out_file , Fl \-output Ar out_file Write the ROM image to the given file. .It Fl p Ar pad_value , Fl \-pad Ar pad_value When inserting padding between sections, pad with this value. The default is 0. .It Fl S Ar spec , Fl \-scramble Ar spec Enables a different .Dq scrambling algorithm for placing sections. See .Sx Scrambling algorithm below for an explanation and a description of .Ar spec . .It Fl t , Fl \-tiny Expand the ROM0 section size from 16 KiB to the full 32 KiB assigned to ROM. ROMX sections that are fixed to a bank other than 1 become errors, other ROMX sections are treated as ROM0. Useful for ROMs that fit in 32 KiB. .It Fl V , Fl \-version Print the version of the program and exit. .It Fl v , Fl \-verbose Be verbose. The verbosity level is increased by one each time the flag is specified, with each level including the previous: .Bl -enum -compact .It Print the .Nm configuration before taking actions. .It Print a notice before significant actions. .It Print some of the actions' intermediate results. .It Print some internal debug information. .It Print detailed internal information. .El The verbosity level does not go past 6. .Pp Note that verbose output is only intended to be consumed by humans, and may change without notice between RGBDS releases; relying on those for scripts is not advised. .It Fl W Ar warning , Fl \-warning Ar warning Set warning flag .Ar warning . A warning message will be printed if .Ar warning is an unknown warning flag. See the .Sx DIAGNOSTICS section for a list of warnings. .It Fl w , Fl \-wramx Expand the WRAM0 section size from 4 KiB to the full 8 KiB assigned to WRAM. WRAMX sections that are fixed to a bank other than 1 become errors, other WRAMX sections are treated as WRAM0. .It Fl x , Fl \-nopad Disables padding the end of the final file. This option automatically enables .Fl t . You can use this to make binary files that are not a ROM. When making a ROM, note that not using this is not a replacement for .Xr rgbfix 1 Ap s Fl p option! .It @ Ns Ar at_file Read more options and arguments from a file, as if its contents were given on the command line. Arguments are separated by whitespace or newlines. Lines starting with a hash sign .Pq Ql # are considered comments and ignored. .Pp No shell processing is performed, such as wildcard or variable expansion. There is no support for escaping or quoting whitespace to be included in arguments. The standard .Ql -- to stop option processing also disables at-file processing. Note that while .Ql -- can be used .Em inside an at-file, it only disables option processing within that at-file, and processing continues in the parent scope. .El .Ss Scrambling algorithm The default section placement algorithm tries to place sections into as few banks as possible. (It turns out that section placement is an NP-complete problem known as "bin packing", so .Nm does not attempt to find the optimal solution, but instead uses a "first-fit" heuristic to find a good one in a reasonable amount of time. There are no guarantees about where this algorithm will place sections, apart from the bank, address, and alignment constraints manually specified for the sections.) .Pp .Dq Scrambling instead places sections into a given pool of banks, trying to minimize the number of sections sharing a given bank. This is useful to catch broken bank assumptions, such as expecting two different sections to land in the same bank (that is not guaranteed unless both are manually assigned the same bank number). .Pp A scrambling spec is a comma-separated list of region specs. A trailing comma is allowed, as well as whitespace between all specs and their components. Each region spec has the following form: .D1 Ar region Ns Op = Ns Ar size .Ar region must be one of the following (case-insensitive), while .Ar size must be a positive decimal integer between 1 and the corresponding maximum. Certain regions allow omitting the size, in which case it defaults to its max value. .Bl -column "Region name" "Max value" "Size optional" Region name Ta Max size Ta Size optional .Cm romx Ta 65535 Ta \&No .Cm sram Ta 255 Ta \&No .Cm wramx Ta 7 Ta Yes .El .Pp A .Ar size of 0 disables scrambling for that region. .Pp For example, .Ql romx=64,wramx=4 will scramble .Ic ROMX sections among ROM banks 1 to 64, .Ic WRAMX sections among RAM banks 1 to 4, and will not scramble .Ic SRAM sections. .Pp Later region specs override earlier ones; for example, .Ql romx=42, Romx=0 disables scrambling for .Cm romx . .Pp .Cm wramx scrambling is silently ignored if .Fl w is passed (including if implied by .Fl d ) , as .Ic WRAMX sections will be treated as .Ic WRAM0 . .Sh DIAGNOSTICS Warnings are diagnostic messages that indicate possibly erroneous behavior that does not necessarily compromise the linking process. The following options alter the way warnings are processed. .Bl -tag -width Ds .It Fl Werror Make all warnings into errors. This can be negated as .Fl Wno-error to prevent turning all warnings into errors. .It Fl Werror= Make the specified warning or meta warning into an error. A warning's name is appended .Pq example: Fl Werror=obsolete , and this warning is implicitly enabled and turned into an error. This can be negated as .Fl Wno-error= to prevent turning a specified warning into an error, even if .Fl Werror is in effect. .El .Pp The following warnings are .Dq meta warnings, that enable a collection of other warnings. If a specific warning is toggled via a meta flag and a specific one, the more specific one takes priority. The position on the command-line acts as a tie breaker, the last one taking effect. .Bl -tag -width Ds .It Fl Wall This enables warnings that are likely to indicate an error or undesired behavior, and that can easily be fixed. .It Fl Weverything Enables literally every warning. .El .Pp The following warnings are actual warning flags; with each description, the corresponding warning flag is included. Note that each of these flags also has a negation (for example, .Fl Wobsolete enables the warning that .Fl Wno-obsolete disables; and .Fl Wall enables every warning that .Fl Wno-all disables). Only the non-default flag is listed here. Ignoring the .Dq no- prefix, entries are listed alphabetically. .Bl -tag -width Ds .It Fl Wno-assert Warn when .Ic WARN Ns No -type assertions fail. (See .Dq Aborting the assembly process in .Xr rgbasm 5 for .Ic ASSERT ) . .It Fl Wdiv Warn when dividing the smallest negative integer (-2**31) by -1, which yields itself due to integer overflow. This warning is enabled by .Fl Wall . .It Fl Wno-obsolete Warn when obsolete features are encountered, which have been deprecated and may later be removed. .It Fl Wshift Warn when shifting right a negative value. Use a division by 2**N instead. This warning is enabled by .Fl Wall . .It Fl Wshift-amount Warn when a shift's operand is negative or greater than 32. This warning is enabled by .Fl Wall . .It Fl Wtruncation= Warn when an implicit truncation (for example, .Ic db to an 8-bit value) loses some bits. .Fl Wtruncation=0 or .Fl Wno-truncation disables this warning. .Fl Wtruncation=1 or just .Fl Wtruncation warns when an N-bit value is 2**N or greater, or less than -2**N. .Fl Wtruncation=2 also warns when an N-bit value is less than -2**(N-1), which will not fit in two's complement encoding. .El .Sh EXAMPLES All you need for a basic ROM is an object file, which can be made into a ROM image like so: .Pp .Dl $ rgblink -o bar.gb foo.o .Pp The resulting .Ar bar.gb will not have correct checksums (unless you put them in the assembly source). You should use .Xr rgbfix 1 to fix these so that the program will actually run in a Game Boy: .Pp .Dl $ rgbfix -v bar.gb .Pp Here is a more complete example: .Pp .Dl $ rgblink -o bin/game.gb -n bin/game.sym -p 0xFF obj/title.o obj/engine.o .Sh BUGS Please report bugs or mistakes in this documentation on .Lk https://github.com/gbdev/rgbds/issues GitHub . .Sh SEE ALSO .Xr rgbasm 1 , .Xr rgblink 5 , .Xr rgbfix 1 , .Xr rgbgfx 1 , .Xr gbz80 7 , .Xr rgbds 5 , .Xr rgbds 7 .Sh HISTORY .Nm was originally written by .An Carsten S\(/orensen as part of the ASMotor package, and was later repackaged in RGBDS by .An Justin Lloyd . It is now maintained by a number of contributors at .Lk https://github.com/gbdev/rgbds . gbdev-rgbds-92bfe5d/man/rgblink.5000066400000000000000000000133561512540461700167100ustar00rootroot00000000000000.\" SPDX-License-Identifier: MIT .\" .Dd January 1, 2026 .Dt RGBLINK 5 .Os .Sh NAME .Nm rgblink .Nd linker script file format .Sh DESCRIPTION The linker script is a file that allows specifying attributes for sections at link time, and in a centralized manner. There can only be one linker script per invocation of .Nm , but it can be split into several files .Pq using the Ic INCLUDE No directive . .Ss Basic syntax The linker script syntax is line-based. Each line may have a directive or section name, a comment, both, or neither. Whitespace (space and tab characters) is used to separate syntax elements, but is otherwise ignored. .Pp Comments begin with a semicolon .Ql \&; character, until the end of the line. They are simply ignored. .Pp Keywords are composed of letters and digits (but they can't start with a digit); they are all case-insensitive. .Pp Numbers can be written in a number of formats. .Bl -column -offset indent "Hexadecimal" "Possible prefixes" .It Sy Format type Ta Sy Possible prefixes Ta Sy Accepted characters .It Decimal Ta none Ta 0123456789 .It Hexadecimal Ta Li $ , 0x , 0X Ta 0123456789ABCDEF .It Octal Ta Li & , 0o , 0O Ta 01234567 .It Binary Ta Li % , 0b , 0B Ta 01 .El .Pp Underscores are also accepted in numbers, except at the beginning of one. This can be useful for grouping digits, like .Ql 1_234 or .Ql $ff_80 . .Pp Strings begin with a double quote, and end at the next (non-escaped) double quote. Strings must not contain literal newline characters. Most of the same character escapes as .Xr rgbasm 5 are supported, specifically .Ql \e\e , .Ql \e" , .Ql \en , .Ql \er , .Ql \et , and .Ql \e0 . Other backslash escape sequences in .Xr rgbasm 5 are only relevant to assembly code and do not apply in linker scripts. .Ss Directives .Bl -tag -width Ds .It Including other files .Ql Ic INCLUDE Ar path acts as if the contents of the file at .Ar path were copy-pasted in place of the .Ic INCLUDE directive. .Ar path must be a string. .It Specifying the active bank The active bank can be set by specifying its type (memory region) and number. The possible types are: .Ic ROM0 , ROMX , VRAM , SRAM , WRAM0 , WRAMX , OAM , and .Ic HRAM . The bank number can be omitted from the types that only contain a single bank, which are: .Ic ROM0 , .Ic ROMX No if Fl t No is passed to Xr rgblink 1 , .Ic VRAM No if Fl d No is passed to Xr rgblink 1 , .Ic WRAM0 , .Ic WRAMX No if Fl w No is passed to Xr rgblink 1 , .Ic OAM , and .Ic HRAM . .Pq Ic SRAM No is the only type that can never have its bank number omitted. .Pp After a bank specification, the .Dq current address is set to the last value it had for that bank. If the bank has never been active thus far, the .Dq current address defaults to the beginning of the bank .Pq e.g. Ad $4000 No for Ic ROMX No sections . .Pp Instead of giving a bank number, the keyword .Ic FLOATING can be used instead; this sets the type of the subsequent sections without binding them to a particular bank. (If the type only allows a single bank, e.g. .Ic ROM0 , then .Ic FLOATING is valid but redundant and has no effect.) Since no particular section is active, the .Dq current address is made floating (as if by a .Ql Ic FLOATING directive), and .Ic ORG is not allowed. .It Changing the current address A bank must be active for any of these directives to be used. .Pp .Ql Ic ORG Ar addr sets the .Dq current address to .Ar addr . This directive cannot be used to move the address backwards: .Ar addr must be greater than or equal to the .Dq current address . .Pp .Ql Ic FLOATING causes all sections between it and the next .Ic ORG or bank specification to be placed at addresses automatically determined by .Nm . .Pq \&It is, however, compatible with Ic ALIGN No below. .Pp .Ql Ic ALIGN Ar addr , Ar offset increases the .Dq current address until it is aligned to the specified boundary (i.e. the .Ar align lowest bits of the address are equal to .Ar offset ) . If .Ar offset is omitted, it is implied to be 0. For example, if the .Dq current address is $0007, .Ql ALIGN 8 would set it to $0100, and .Ql ALIGN 8 , 10 would set it to $000A. .Pp .Ql Ic DS Ar size increases the .Dq current address by .Ar size . The gap is not allocated, so smaller floating sections can later be placed there. .El .Ss Section placement A section can be placed simply by naming it (with a string). Its bank is set to the active bank, and its address to the .Dq current address . Any constraints the section already possesses (whether from earlier in the linker script, or from the object files being linked) must be consistent with what the linker script specifies: the section's type must match, the section's bank number (if set) must match the active bank, etc. In particular, if the section has an alignment constraint, the address at which it is placed by the linker script must obey that constraint; otherwise, an error will occur. .Pp After a section is placed, the .Dq current address is increased by the section's size. This must not increase it past the end of the active memory region. .Pp The section must have been defined in the object files being linked, unless the section name is followed by the keyword .Ic OPTIONAL . .Sh EXAMPLES .Bd -literal -offset indent ; This line contains only a comment ROMX $F ; start a bank "Some functions" ; a section name ALIGN 8 ; a directive "Some \e"array\e"" WRAMX 2 ; start another bank org $d123 ; another directive "Some variables" .Ed .Sh SEE ALSO .Xr rgbasm 1 , .Xr rgbasm 5 , .Xr rgblink 1 , .Xr rgbfix 1 , .Xr rgbgfx 1 , .Xr gbz80 7 , .Xr rgbds 5 , .Xr rgbds 7 .Sh HISTORY .Xr rgblink 1 was originally written by .An Carsten S\(/orensen as part of the ASMotor package, and was later repackaged in RGBDS by .An Justin Lloyd . It is now maintained by a number of contributors at .Lk https://github.com/gbdev/rgbds . gbdev-rgbds-92bfe5d/src/000077500000000000000000000000001512540461700151765ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/src/.gitignore000066400000000000000000000000431512540461700171630ustar00rootroot00000000000000# Generated by CMake /.version.cpp gbdev-rgbds-92bfe5d/src/CMakeLists.txt000066400000000000000000000065741512540461700177520ustar00rootroot00000000000000# SPDX-License-Identifier: MIT configure_file(version.cpp _version.cpp ESCAPE_QUOTES) set(common_src "extern/getopt.cpp" "cli.cpp" "diagnostics.cpp" "style.cpp" "usage.cpp" "util.cpp" "_version.cpp" ) find_package(BISON 3.0.0 REQUIRED) set(BISON_FLAGS "-Wall -Dlr.type=ielr") # Set some optimization flags on versions that support them if(BISON_VERSION VERSION_GREATER_EQUAL "3.5") set(BISON_FLAGS "${BISON_FLAGS} -Dparse.lac=full -Dapi.token.raw=true") endif() if(BISON_VERSION VERSION_GREATER_EQUAL "3.6") set(BISON_FLAGS "${BISON_FLAGS} -Dparse.error=detailed") else() set(BISON_FLAGS "${BISON_FLAGS} -Dparse.error=verbose") endif() BISON_TARGET(ASM_PARSER "asm/parser.y" "${PROJECT_SOURCE_DIR}/src/asm/parser.cpp" COMPILE_FLAGS "${BISON_FLAGS}" DEFINES_FILE "${PROJECT_SOURCE_DIR}/src/asm/parser.hpp" ) BISON_TARGET(LINKER_SCRIPT_PARSER "link/script.y" "${PROJECT_SOURCE_DIR}/src/link/script.cpp" COMPILE_FLAGS "${BISON_FLAGS}" DEFINES_FILE "${PROJECT_SOURCE_DIR}/src/link/script.hpp" ) set(rgbasm_src "${BISON_ASM_PARSER_OUTPUT_SOURCE}" "asm/actions.cpp" "asm/charmap.cpp" "asm/fixpoint.cpp" "asm/format.cpp" "asm/fstack.cpp" "asm/lexer.cpp" "asm/macro.cpp" "asm/main.cpp" "asm/opt.cpp" "asm/output.cpp" "asm/rpn.cpp" "asm/section.cpp" "asm/symbol.cpp" "asm/warning.cpp" "extern/utf8decoder.cpp" "backtrace.cpp" "linkdefs.cpp" "opmath.cpp" "verbosity.cpp" ) set(rgblink_src "${BISON_LINKER_SCRIPT_PARSER_OUTPUT_SOURCE}" "link/assign.cpp" "link/fstack.cpp" "link/lexer.cpp" "link/layout.cpp" "link/main.cpp" "link/object.cpp" "link/output.cpp" "link/patch.cpp" "link/sdas_obj.cpp" "link/section.cpp" "link/symbol.cpp" "link/warning.cpp" "extern/utf8decoder.cpp" "backtrace.cpp" "linkdefs.cpp" "opmath.cpp" "verbosity.cpp" ) set(rgbfix_src "fix/fix.cpp" "fix/main.cpp" "fix/mbc.cpp" "fix/warning.cpp" ) set(rgbgfx_src "gfx/color_set.cpp" "gfx/main.cpp" "gfx/pal_packing.cpp" "gfx/pal_sorting.cpp" "gfx/pal_spec.cpp" "gfx/palette.cpp" "gfx/png.cpp" "gfx/process.cpp" "gfx/reverse.cpp" "gfx/rgba.cpp" "gfx/warning.cpp" "verbosity.cpp" ) foreach(PROG "asm" "fix" "gfx" "link") add_executable(rgb${PROG} ${rgb${PROG}_src} ${common_src} ) install(TARGETS rgb${PROG} RUNTIME DESTINATION bin) # Required to run tests set_target_properties(rgb${PROG} PROPERTIES # hack for MSVC: no-op generator expression to stop generation of "per-configuration subdirectory" RUNTIME_OUTPUT_DIRECTORY $<1:${CMAKE_SOURCE_DIR}>) endforeach() if(LIBPNG_FOUND) # pkg-config target_include_directories(rgbgfx PRIVATE ${LIBPNG_INCLUDE_DIRS}) target_link_directories(rgbgfx PRIVATE ${LIBPNG_LIBRARY_DIRS}) target_link_libraries(rgbgfx PRIVATE ${LIBPNG_LIBRARIES}) else() target_compile_definitions(rgbgfx PRIVATE ${PNG_DEFINITIONS}) target_include_directories(rgbgfx PRIVATE ${PNG_INCLUDE_DIRS}) target_link_libraries(rgbgfx PRIVATE ${PNG_LIBRARIES}) endif() include(CheckLibraryExists) check_library_exists("m" "sin" "" HAS_LIBM) if(HAS_LIBM) target_link_libraries(rgbasm PRIVATE "m") endif() gbdev-rgbds-92bfe5d/src/asm/000077500000000000000000000000001512540461700157565ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/src/asm/.gitignore000066400000000000000000000000421512540461700177420ustar00rootroot00000000000000/parser.cpp /parser.hpp /stack.hh gbdev-rgbds-92bfe5d/src/asm/actions.cpp000066400000000000000000000404051512540461700201250ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "asm/actions.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "extern/utf8decoder.hpp" #include "helpers.hpp" #include "linkdefs.hpp" #include "asm/charmap.hpp" #include "asm/format.hpp" #include "asm/fstack.hpp" #include "asm/lexer.hpp" #include "asm/output.hpp" #include "asm/rpn.hpp" // Expression #include "asm/section.hpp" #include "asm/symbol.hpp" #include "asm/warning.hpp" void act_If(int32_t condition) { lexer_IncIFDepth(); if (condition) { lexer_RunIFBlock(); } else { lexer_SetMode(LEXER_SKIP_TO_ELIF); } } void act_Elif(int32_t condition) { if (lexer_GetIFDepth() == 0) { fatal("Found `ELIF` outside of a conditional (not after an `IF`/`ELIF` block)"); } if (lexer_RanIFBlock()) { if (lexer_ReachedELSEBlock()) { fatal("Found `ELIF` after an `ELSE` block"); } // This should be redundant, as the lexer will have skipped to `ENDC` since // an `ELIF` after a taken `IF` needs to not evaluate its condition. lexer_SetMode(LEXER_SKIP_TO_ENDC); // LCOV_EXCL_LINE } else if (condition) { lexer_RunIFBlock(); } else { lexer_SetMode(LEXER_SKIP_TO_ELIF); } } void act_Else() { if (lexer_GetIFDepth() == 0) { fatal("Found `ELSE` outside of a conditional (not after an `IF`/`ELIF` block)"); } if (lexer_RanIFBlock()) { if (lexer_ReachedELSEBlock()) { // This should be redundant, as the lexer handles this error first. fatal("Found `ELSE` after an `ELSE` block"); // LCOV_EXCL_LINE } lexer_SetMode(LEXER_SKIP_TO_ENDC); } else { lexer_RunIFBlock(); lexer_ReachELSEBlock(); } } void act_Endc() { lexer_DecIFDepth(); } AlignmentSpec act_Alignment(int32_t alignment, int32_t alignOfs) { AlignmentSpec spec = {0, 0}; if (alignment > 16) { error("Alignment must be between 0 and 16, not %u", alignment); } else if (alignOfs <= -(1 << alignment) || alignOfs >= 1 << alignment) { error( "The absolute alignment offset (%" PRIu32 ") must be less than alignment size (%d)", static_cast(alignOfs < 0 ? -alignOfs : alignOfs), 1 << alignment ); } else { spec.alignment = alignment; spec.alignOfs = alignOfs < 0 ? (1 << alignment) + alignOfs : alignOfs; } return spec; } static void failAssert(AssertionType type, std::string const &message) { switch (type) { case ASSERT_FATAL: if (message.empty()) { fatal("Assertion failed"); } else { fatal("Assertion failed: %s", message.c_str()); } case ASSERT_ERROR: if (message.empty()) { error("Assertion failed"); } else { error("Assertion failed: %s", message.c_str()); } break; case ASSERT_WARN: if (message.empty()) { warning(WARNING_ASSERT, "Assertion failed"); } else { warning(WARNING_ASSERT, "Assertion failed: %s", message.c_str()); } break; } } void act_Assert(AssertionType type, Expression const &expr, std::string const &message) { if (!expr.isKnown()) { out_CreateAssert(type, expr, message, sect_GetOutputOffset()); } else if (expr.value() == 0) { failAssert(type, message); } } void act_StaticAssert(AssertionType type, int32_t condition, std::string const &message) { if (!condition) { failAssert(type, message); } } std::optional act_ReadFile(std::string const &name, uint32_t maxLen) { FILE *file = nullptr; if (std::optional fullPath = fstk_FindFile(name); fullPath) { file = fopen(fullPath->c_str(), "rb"); } if (!file) { if (fstk_FileError(name, "`READFILE`")) { // If `fstk_FileError` returned true due to `-MG`, we should abort due to a // missing file, so return `std::nullopt`, which tells the caller to `YYACCEPT` return std::nullopt; } return ""; } Defer closeFile{[&] { fclose(file); }}; size_t readSize = maxLen; if (fseek(file, 0, SEEK_END) == 0) { // If the file is seekable and shorter than the max length, // just read as many bytes as there are if (long fileSize = ftell(file); static_cast(fileSize) < readSize) { readSize = fileSize; } fseek(file, 0, SEEK_SET); // LCOV_EXCL_START } else if (errno != ESPIPE) { error( "Error determining size of `READFILE` file \"%s\": %s", name.c_str(), strerror(errno) ); // LCOV_EXCL_STOP } std::string contents; contents.resize(readSize); if (fread(&contents[0], 1, readSize, file) < readSize || ferror(file)) { // LCOV_EXCL_START error("Error reading `READFILE` file \"%s\": %s", name.c_str(), strerror(errno)); return ""; // LCOV_EXCL_STOP } return contents; } uint32_t act_CharToNum(std::string const &str) { if (std::vector units = charmap_Convert(str); units.size() == 1) { // The string is a single character with a single unit value, // which can be used directly as a numeric character. return static_cast(units[0]); } else { error("Character literals must be a single charmap unit"); return 0; } } uint32_t act_StringToNum(std::string const &str) { warning( WARNING_OBSOLETE, "Treating strings as numbers is deprecated; use character literals or `CHARVAL` instead" ); if (std::vector units = charmap_Convert(str); units.size() == 1) { // The string is a single character with a single unit value, // which can be used directly as a number. return static_cast(units[0]); } else { error("Strings as numbers must be a single charmap unit"); return 0; } } static uint32_t adjustNegativeIndex(int32_t idx, size_t len, char const *functionName) { // String functions adjust negative index arguments the same way, // such that position -1 is the last character of a string. if (idx < 0) { idx += len; } if (idx < 0) { warning(WARNING_BUILTIN_ARG, "%s: Index starts at 0", functionName); idx = 0; } return static_cast(idx); } static uint32_t adjustNegativePos(int32_t pos, size_t len, char const *functionName) { // STRSUB and CHARSUB adjust negative position arguments the same way, // such that position -1 is the last character of a string. if (pos < 0) { pos += len + 1; } if (pos < 1) { warning(WARNING_BUILTIN_ARG, "%s: Position starts at 1", functionName); pos = 1; } return static_cast(pos); } int32_t act_CharVal(std::string const &str) { if (size_t len = charmap_CharSize(str); len == 0) { error("CHARVAL: No character mapping for \"%s\"", str.c_str()); return 0; } else if (len != 1) { error("CHARVAL: Character mapping for \"%s\" must have a single value", str.c_str()); return 0; } else { return *charmap_CharValue(str, 0); } } int32_t act_CharVal(std::string const &str, int32_t negIdx) { if (size_t len = charmap_CharSize(str); len != 0) { uint32_t idx = adjustNegativeIndex(negIdx, len, "CHARVAL"); if (std::optional val = charmap_CharValue(str, idx); val.has_value()) { return *val; } else { warning( WARNING_BUILTIN_ARG, "CHARVAL: Index %" PRIu32 " is past the end of the character mapping", idx ); return 0; } } else { error("CHARVAL: No character mapping for \"%s\"", str.c_str()); return 0; } } uint8_t act_StringByte(std::string const &str, int32_t negIdx) { size_t len = str.length(); if (uint32_t idx = adjustNegativeIndex(negIdx, len, "STRBYTE"); idx < len) { return static_cast(str[idx]); } else { warning( WARNING_BUILTIN_ARG, "STRBYTE: Index %" PRIu32 " is past the end of the string", idx ); return 0; } } static void errorInvalidUTF8Byte(uint8_t byte, char const *functionName) { error("%s: Invalid UTF-8 byte 0x%02hhX", functionName, byte); } size_t act_StringLen(std::string const &str, bool printErrors) { size_t len = 0; uint32_t state = UTF8_ACCEPT; uint32_t codepoint = 0; for (char c : str) { uint8_t byte = static_cast(c); switch (decode(&state, &codepoint, byte)) { case UTF8_REJECT: if (printErrors) { errorInvalidUTF8Byte(byte, "STRLEN"); } state = UTF8_ACCEPT; // fallthrough case UTF8_ACCEPT: ++len; break; } } // Check for partial code point. if (state != UTF8_ACCEPT) { if (printErrors) { error("STRLEN: Incomplete UTF-8 character"); } ++len; } return len; } std::string act_StringSlice(std::string const &str, int32_t negStart, std::optional negStop) { size_t adjustLen = act_StringLen(str, false); uint32_t start = adjustNegativeIndex(negStart, adjustLen, "STRSLICE"); uint32_t stop = negStop ? adjustNegativeIndex(*negStop, adjustLen, "STRSLICE") : adjustLen; size_t strLen = str.length(); size_t index = 0; uint32_t state = UTF8_ACCEPT; uint32_t codepoint = 0; uint32_t curIdx = 0; // Advance to starting index in source string. while (index < strLen && curIdx < start) { switch (decode(&state, &codepoint, str[index])) { case UTF8_REJECT: errorInvalidUTF8Byte(str[index], "STRSLICE"); state = UTF8_ACCEPT; // fallthrough case UTF8_ACCEPT: ++curIdx; break; } ++index; } // An index 1 past the end of the string is allowed, but will trigger the // "Length too big" warning below if the length is nonzero. if (index >= strLen && start > curIdx) { warning( WARNING_BUILTIN_ARG, "STRSLICE: Start index %" PRIu32 " is past the end of the string", start ); } size_t startIndex = index; // Advance to ending index in source string. while (index < strLen && curIdx < stop) { switch (decode(&state, &codepoint, str[index])) { case UTF8_REJECT: errorInvalidUTF8Byte(str[index], "STRSLICE"); state = UTF8_ACCEPT; // fallthrough case UTF8_ACCEPT: ++curIdx; break; } ++index; } // Check for partial code point. if (state != UTF8_ACCEPT) { error("STRSLICE: Incomplete UTF-8 character"); ++curIdx; } if (curIdx < stop) { warning( WARNING_BUILTIN_ARG, "STRSLICE: Stop index %" PRIu32 " is past the end of the string", stop ); } return str.substr(startIndex, index - startIndex); } std::string act_StringSub(std::string const &str, int32_t negPos, std::optional optLen) { warning(WARNING_OBSOLETE, "`STRSUB` is deprecated; use 0-indexed `STRSLICE` instead"); size_t adjustLen = act_StringLen(str, false); uint32_t pos = adjustNegativePos(negPos, adjustLen, "STRSUB"); uint32_t len = optLen ? *optLen : pos > adjustLen ? 0 : adjustLen + 1 - pos; size_t strLen = str.length(); size_t index = 0; uint32_t state = UTF8_ACCEPT; uint32_t codepoint = 0; uint32_t curPos = 1; // Advance to starting position in source string. while (index < strLen && curPos < pos) { switch (decode(&state, &codepoint, str[index])) { case UTF8_REJECT: errorInvalidUTF8Byte(str[index], "STRSUB"); state = UTF8_ACCEPT; // fallthrough case UTF8_ACCEPT: ++curPos; break; } ++index; } // A position 1 past the end of the string is allowed, but will trigger the // "Length too big" warning below if the length is nonzero. if (index >= strLen && pos > curPos) { warning( WARNING_BUILTIN_ARG, "STRSUB: Position %" PRIu32 " is past the end of the string", pos ); } size_t startIndex = index; uint32_t curLen = 0; // Compute the result length in bytes. while (index < strLen && curLen < len) { switch (decode(&state, &codepoint, str[index])) { case UTF8_REJECT: errorInvalidUTF8Byte(str[index], "STRSUB"); state = UTF8_ACCEPT; // fallthrough case UTF8_ACCEPT: ++curLen; break; } ++index; } // Check for partial code point. if (state != UTF8_ACCEPT) { error("STRSUB: Incomplete UTF-8 character"); ++curLen; } if (curLen < len) { warning(WARNING_BUILTIN_ARG, "STRSUB: Length too big: %" PRIu32, len); } return str.substr(startIndex, index - startIndex); } size_t act_CharLen(std::string const &str) { std::string_view view = str; size_t len; for (len = 0; charmap_ConvertNext(view, nullptr); ++len) {} return len; } std::string act_StringChar(std::string const &str, int32_t negIdx) { size_t adjustLen = act_CharLen(str); uint32_t idx = adjustNegativeIndex(negIdx, adjustLen, "STRCHAR"); std::string_view view = str; size_t charLen = 1; // Advance to starting index in source string. for (uint32_t curIdx = 0; charLen && curIdx < idx; ++curIdx) { charLen = charmap_ConvertNext(view, nullptr); } std::string_view start = view; if (!charmap_ConvertNext(view, nullptr)) { warning( WARNING_BUILTIN_ARG, "STRCHAR: Index %" PRIu32 " is past the end of the string", idx ); } start = start.substr(0, start.length() - view.length()); return std::string(start); } std::string act_CharSub(std::string const &str, int32_t negPos) { warning(WARNING_OBSOLETE, "`CHARSUB` is deprecated; use 0-indexed `STRCHAR` instead"); size_t adjustLen = act_CharLen(str); uint32_t pos = adjustNegativePos(negPos, adjustLen, "CHARSUB"); std::string_view view = str; size_t charLen = 1; // Advance to starting position in source string. for (uint32_t curPos = 1; charLen && curPos < pos; ++curPos) { charLen = charmap_ConvertNext(view, nullptr); } std::string_view start = view; if (!charmap_ConvertNext(view, nullptr)) { warning( WARNING_BUILTIN_ARG, "CHARSUB: Position %" PRIu32 " is past the end of the string", pos ); } start = start.substr(0, start.length() - view.length()); return std::string(start); } int32_t act_CharCmp(std::string_view str1, std::string_view str2) { std::vector seq1, seq2; size_t idx1 = 0, idx2 = 0; for (;;) { if (idx1 >= seq1.size()) { idx1 = 0; seq1.clear(); charmap_ConvertNext(str1, &seq1); } if (idx2 >= seq2.size()) { idx2 = 0; seq2.clear(); charmap_ConvertNext(str2, &seq2); } if (seq1.empty() != seq2.empty()) { return seq1.empty() ? -1 : 1; } else if (seq1.empty()) { return 0; } else { int32_t value1 = seq1[idx1++], value2 = seq2[idx2++]; if (value1 != value2) { return (value1 > value2) - (value1 < value2); } } } } std::string act_StringReplace(std::string_view str, std::string const &old, std::string const &rep) { if (old.empty()) { warning(WARNING_EMPTY_STRRPL, "STRRPL: Cannot replace an empty string"); return std::string(str); } std::string rpl; while (!str.empty()) { auto pos = str.find(old); if (pos == str.npos) { rpl.append(str); break; } rpl.append(str, 0, pos); rpl.append(rep); str.remove_prefix(pos + old.size()); } return rpl; } std::string act_StringFormat( std::string const &spec, std::vector> const &args ) { std::string str; size_t argIndex = 0; for (size_t i = 0; spec[i] != '\0';) { if (int c = spec[i]; c != '%') { str += c; ++i; continue; } if (int c = spec[++i]; c == '%') { str += c; ++i; continue; } else if (c == '\0') { error("STRFMT: Illegal '%%' at end of format string"); str += '%'; break; } FormatSpec fmt{}; size_t n = fmt.parseSpec(spec.c_str() + i); i += n; if (!fmt.isValid()) { error("STRFMT: Invalid format spec for argument %zu", argIndex + 1); str += spec.substr(i - n - 1, n + 1); // include the '%' } else if (argIndex >= args.size()) { // Will warn after formatting is done. str += '%'; } else if (std::holds_alternative(args[argIndex])) { fmt.appendNumber(str, std::get(args[argIndex])); } else { fmt.appendString(str, std::get(args[argIndex])); } ++argIndex; } if (argIndex < args.size()) { size_t extra = args.size() - argIndex; error("STRFMT: %zu unformatted argument%s", extra, extra == 1 ? "" : "s"); } else if (argIndex > args.size()) { error( "STRFMT: Not enough arguments for format spec (expected %zu, got %zu)", argIndex, args.size() ); } return str; } std::string act_SectionName(std::string const &symName) { Symbol *sym = sym_FindScopedValidSymbol(symName); if (!sym) { if (sym_IsPurgedScoped(symName)) { fatal("Undefined symbol `%s`; it was purged", symName.c_str()); } else { fatal("Undefined symbol `%s`", symName.c_str()); } } Section const *section = sym->getSection(); if (!section) { fatal("`%s` does not belong to any section", sym->name.c_str()); } return section->name; } void act_CompoundAssignment(std::string const &symName, RPNCommand op, int32_t constValue) { Expression oldExpr, constExpr, newExpr; oldExpr.makeSymbol(symName); constExpr.makeNumber(constValue); newExpr.makeBinaryOp(op, std::move(oldExpr), constExpr); int32_t newValue = newExpr.getConstVal(); sym_AddVar(symName, newValue); } gbdev-rgbds-92bfe5d/src/asm/charmap.cpp000066400000000000000000000210471512540461700201010ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "asm/charmap.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "extern/utf8decoder.hpp" #include "helpers.hpp" #include "itertools.hpp" // InsertionOrderedMap #include "util.hpp" #include "asm/warning.hpp" // Charmaps are stored using a structure known as "trie". // Essentially a tree, where each nodes stores a single character's worth of info: // whether there exists a mapping that ends at the current character, struct CharmapNode { std::vector value; // The mapped value, if there exists a mapping that ends here // These MUST be indexes and not pointers, because pointers get invalidated by reallocation! size_t next[256]; // Indexes of where to go next, 0 = nowhere bool isTerminal() const { return !value.empty(); } }; struct Charmap { std::string name; std::vector nodes; // first node is reserved for the root node }; // Traverse the trie depth-first to derive the character mappings in definition order template bool forEachChar(Charmap const &charmap, CallbackFnT callback) { // clang-format off: nested initializers for (std::stack> prefixes({{0, ""}}); !prefixes.empty();) { // clang-format on auto [nodeIdx, mapping] = std::move(prefixes.top()); prefixes.pop(); CharmapNode const &node = charmap.nodes[nodeIdx]; if (node.isTerminal() && !callback(nodeIdx, mapping)) { return false; } for (unsigned c = 0; c < std::size(node.next); ++c) { if (size_t nextIdx = node.next[c]; nextIdx) { prefixes.push({nextIdx, mapping + static_cast(c)}); } } } return true; } static InsertionOrderedMap charmaps; static Charmap *currentCharmap; static std::stack charmapStack; bool charmap_ForEach( void (*mapFunc)(std::string const &), void (*charFunc)(std::string const &, std::vector) ) { for (Charmap const &charmap : charmaps) { std::map mappings; forEachChar(charmap, [&mappings](size_t nodeIdx, std::string const &mapping) { mappings[nodeIdx] = mapping; return true; }); mapFunc(charmap.name); for (auto const &[nodeIdx, mapping] : mappings) { charFunc(mapping, charmap.nodes[nodeIdx].value); } } return !charmaps.empty(); } void charmap_New(std::string const &name, std::string const *baseName) { std::optional baseIdx = std::nullopt; if (baseName != nullptr) { baseIdx = charmaps.findIndex(*baseName); if (!baseIdx) { error("Undefined base charmap `%s`", baseName->c_str()); } } if (charmaps.contains(name)) { error("Charmap `%s` is already defined", name.c_str()); return; } // Init the new charmap's fields Charmap &charmap = charmaps.add(name); charmap.name = name; if (baseIdx) { charmap.nodes = charmaps[*baseIdx].nodes; // Copies `charmaps[*baseIdx].nodes` } else { charmap.nodes.emplace_back(); // Zero-init the root node } currentCharmap = &charmap; } void charmap_Set(std::string const &name) { if (auto index = charmaps.findIndex(name); index) { currentCharmap = &charmaps[*index]; } else { error("Undefined charmap `%s`", name.c_str()); } } void charmap_Push() { charmapStack.push(currentCharmap); } void charmap_Pop() { if (charmapStack.empty()) { error("No entries in the charmap stack"); return; } currentCharmap = charmapStack.top(); charmapStack.pop(); } void charmap_CheckStack() { if (!charmapStack.empty()) { warning(WARNING_UNMATCHED_DIRECTIVE, "`PUSHC` without corresponding `POPC`"); } } void charmap_Add(std::string const &mapping, std::vector &&value) { if (mapping.empty()) { error("Cannot map an empty string"); return; } Charmap &charmap = *currentCharmap; size_t nodeIdx = 0; for (char c : mapping) { size_t &nextIdxRef = charmap.nodes[nodeIdx].next[static_cast(c)]; size_t nextIdx = nextIdxRef; if (!nextIdx) { // Switch to and zero-init the new node nextIdxRef = charmap.nodes.size(); nextIdx = nextIdxRef; // This may reallocate `charmap.nodes` and invalidate `nextIdxRef`, // which is why we keep the actual value in `nextIdx` charmap.nodes.emplace_back(); } nodeIdx = nextIdx; } CharmapNode &node = charmap.nodes[nodeIdx]; if (node.isTerminal()) { warning(WARNING_CHARMAP_REDEF, "Overriding charmap mapping"); } std::swap(node.value, value); } bool charmap_HasChar(std::string const &mapping) { Charmap const &charmap = *currentCharmap; size_t nodeIdx = 0; for (char c : mapping) { nodeIdx = charmap.nodes[nodeIdx].next[static_cast(c)]; if (!nodeIdx) { return false; } } return charmap.nodes[nodeIdx].isTerminal(); } static CharmapNode const *charmapEntry(std::string const &mapping) { Charmap const &charmap = *currentCharmap; size_t nodeIdx = 0; for (char c : mapping) { nodeIdx = charmap.nodes[nodeIdx].next[static_cast(c)]; if (!nodeIdx) { return nullptr; } } return &charmap.nodes[nodeIdx]; } size_t charmap_CharSize(std::string const &mapping) { CharmapNode const *node = charmapEntry(mapping); return node && node->isTerminal() ? node->value.size() : 0; } std::optional charmap_CharValue(std::string const &mapping, size_t idx) { if (CharmapNode const *node = charmapEntry(mapping); node && node->isTerminal() && idx < node->value.size()) { return node->value[idx]; } return std::nullopt; } std::vector charmap_Convert(std::string const &input) { std::vector output; for (std::string_view inputView = input; charmap_ConvertNext(inputView, &output);) {} return output; } size_t charmap_ConvertNext(std::string_view &input, std::vector *output) { // The goal is to match the longest mapping possible. // For that, advance through the trie with each character read. // If that would lead to a dead end, rewind characters until the last match, and output. // If no match, read a UTF-8 codepoint and output that. Charmap const &charmap = *currentCharmap; size_t matchIdx = 0; size_t rewindDistance = 0; size_t inputIdx = 0; for (size_t nodeIdx = 0; inputIdx < input.length();) { nodeIdx = charmap.nodes[nodeIdx].next[static_cast(input[inputIdx])]; if (!nodeIdx) { break; } ++inputIdx; // Consume that char if (charmap.nodes[nodeIdx].isTerminal()) { matchIdx = nodeIdx; // This node matches, register it rewindDistance = 0; // If no longer match is found, rewind here } else { ++rewindDistance; } } // We are at a dead end (either because we reached the end of input, or of the trie), // so rewind up to the last match, and output. inputIdx -= rewindDistance; // This will rewind all the way if no match found size_t matchLen = 0; if (matchIdx) { // A match was found, use it std::vector const &value = charmap.nodes[matchIdx].value; if (output) { output->insert(output->end(), RANGE(value)); } matchLen = value.size(); } else if (inputIdx < input.length()) { // No match found, but there is some input left size_t codepointLen = 0; // This will write the codepoint's value to `output`, little-endian for (uint32_t state = UTF8_ACCEPT, codepoint = 0; inputIdx + codepointLen < input.length();) { if (decode(&state, &codepoint, input[inputIdx + codepointLen]) == UTF8_REJECT) { error("Input string is not valid UTF-8"); codepointLen = 1; break; } ++codepointLen; if (state == UTF8_ACCEPT) { break; } } if (output) { output->insert( output->end(), input.data() + inputIdx, input.data() + inputIdx + codepointLen ); } // Warn if this character is not mapped but any others are if (int firstChar = input[inputIdx]; charmap.nodes.size() > 1) { warning(WARNING_UNMAPPED_CHAR_1, "Unmapped character %s", printChar(firstChar)); } else if (charmap.name != DEFAULT_CHARMAP_NAME) { warning( WARNING_UNMAPPED_CHAR_2, "Unmapped character %s not in `" DEFAULT_CHARMAP_NAME "` charmap", printChar(firstChar) ); } inputIdx += codepointLen; matchLen = codepointLen; } input = input.substr(inputIdx); return matchLen; } std::string charmap_Reverse(std::vector const &value, bool &unique) { Charmap const &charmap = *currentCharmap; std::string revMapping; unique = forEachChar(charmap, [&](size_t nodeIdx, std::string const &mapping) { if (charmap.nodes[nodeIdx].value == value) { if (revMapping.empty()) { revMapping = mapping; } else { revMapping.clear(); return false; } } return true; }); return revMapping; } gbdev-rgbds-92bfe5d/src/asm/fixpoint.cpp000066400000000000000000000046061512540461700203300ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Fixed-point math routines #include "asm/fixpoint.hpp" #include #include #include static constexpr double tau = std::numbers::pi * 2; static double fix2double(int32_t i, int32_t q) { return i / pow(2.0, q); } static int32_t double2fix(double d, int32_t q) { if (isnan(d)) { return 0; } if (isinf(d)) { return d < 0 ? INT32_MIN : INT32_MAX; } return static_cast(round(d * pow(2.0, q))); } static double turn2rad(double t) { return t * tau; } static double rad2turn(double r) { return r / tau; } int32_t fix_Sin(int32_t i, int32_t q) { return double2fix(sin(turn2rad(fix2double(i, q))), q); } int32_t fix_Cos(int32_t i, int32_t q) { return double2fix(cos(turn2rad(fix2double(i, q))), q); } int32_t fix_Tan(int32_t i, int32_t q) { return double2fix(tan(turn2rad(fix2double(i, q))), q); } int32_t fix_ASin(int32_t i, int32_t q) { return double2fix(rad2turn(asin(fix2double(i, q))), q); } int32_t fix_ACos(int32_t i, int32_t q) { return double2fix(rad2turn(acos(fix2double(i, q))), q); } int32_t fix_ATan(int32_t i, int32_t q) { return double2fix(rad2turn(atan(fix2double(i, q))), q); } int32_t fix_ATan2(int32_t i, int32_t j, int32_t q) { return double2fix(rad2turn(atan2(fix2double(i, q), fix2double(j, q))), q); } int32_t fix_Mul(int32_t i, int32_t j, int32_t q) { return double2fix(fix2double(i, q) * fix2double(j, q), q); } int32_t fix_Div(int32_t i, int32_t j, int32_t q) { double dividend = fix2double(i, q); double divisor = fix2double(j, q); if (fpclassify(divisor) == FP_ZERO) { return dividend < 0 ? INT32_MIN : dividend > 0 ? INT32_MAX : 0; } return double2fix(dividend / divisor, q); } int32_t fix_Mod(int32_t i, int32_t j, int32_t q) { return double2fix(fmod(fix2double(i, q), fix2double(j, q)), q); } int32_t fix_Pow(int32_t i, int32_t j, int32_t q) { return double2fix(pow(fix2double(i, q), fix2double(j, q)), q); } int32_t fix_Log(int32_t i, int32_t j, int32_t q) { double divisor = log(fix2double(j, q)); if (fpclassify(divisor) == FP_ZERO) { return INT32_MAX; } return double2fix(log(fix2double(i, q)) / divisor, q); } int32_t fix_Round(int32_t i, int32_t q) { return double2fix(round(fix2double(i, q)), q); } int32_t fix_Ceil(int32_t i, int32_t q) { return double2fix(ceil(fix2double(i, q)), q); } int32_t fix_Floor(int32_t i, int32_t q) { return double2fix(floor(fix2double(i, q)), q); } gbdev-rgbds-92bfe5d/src/asm/format.cpp000066400000000000000000000154111512540461700177540ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "asm/format.hpp" #include #include #include #include #include #include #include #include #include "util.hpp" // parseNumber #include "asm/main.hpp" // options #include "asm/warning.hpp" size_t FormatSpec::parseSpec(char const *spec) { size_t i = 0; auto parseSpecNumber = [&spec, &i]() { char const *end = &spec[i]; size_t number = parseNumber(end, BASE_10).value_or(0); i += end - &spec[i]; return number; }; // if (char c = spec[i]; c == ' ' || c == '+') { ++i; sign = c; } // if (spec[i] == '#') { ++i; exact = true; } // if (spec[i] == '-') { ++i; alignLeft = true; } // if (spec[i] == '0') { ++i; padZero = true; } // if (isDigit(spec[i])) { width = parseSpecNumber(); } // if (spec[i] == '.') { ++i; hasFrac = true; fracWidth = parseSpecNumber(); } // if (spec[i] == 'q') { ++i; hasPrec = true; precision = parseSpecNumber(); } // switch (char c = spec[i]; c) { case 'd': case 'u': case 'X': case 'x': case 'b': case 'o': case 'f': case 's': ++i; type = c; break; } // Done parsing parsed = true; return i; } static std::string escapeString(std::string const &str) { std::string escaped; for (char c : str) { // Escape characters that need escaping switch (c) { case '\n': escaped += "\\n"; break; case '\r': escaped += "\\r"; break; case '\t': escaped += "\\t"; break; case '\0': escaped += "\\0"; break; case '\\': case '"': case '{': escaped += '\\'; [[fallthrough]]; default: escaped += c; break; } } return escaped; } void FormatSpec::appendString(std::string &str, std::string const &value) const { int useType = type; if (!useType) { // No format was specified useType = 's'; } if (sign) { error("Formatting string with sign flag '%c'", sign); } if (padZero) { error("Formatting string with padding flag '0'"); } if (hasFrac) { error("Formatting string with fractional width"); } if (hasPrec) { error("Formatting string with fractional precision"); } if (useType != 's') { error("Formatting string as type '%c'", useType); } std::string useValue = exact ? escapeString(value) : value; size_t valueLen = useValue.length(); size_t totalLen = width > valueLen ? width : valueLen; size_t padLen = totalLen - valueLen; str.reserve(str.length() + totalLen); if (alignLeft) { str.append(useValue); str.append(padLen, ' '); } else { str.append(padLen, ' '); str.append(useValue); } } void FormatSpec::appendNumber(std::string &str, uint32_t value) const { int useType = type; bool useExact = exact; if (!useType) { // No format was specified; default to uppercase $hex useType = 'X'; useExact = true; } if (useType != 'X' && useType != 'x' && useType != 'b' && useType != 'o' && useType != 'f' && useExact) { error("Formatting type '%c' with exact flag '#'", useType); } if (useType != 'f' && hasFrac) { error("Formatting type '%c' with fractional width", useType); } if (useType != 'f' && hasPrec) { error("Formatting type '%c' with fractional precision", useType); } if (useType == 's') { error("Formatting number as type 's'"); } char signChar = sign; // 0 or ' ' or '+' if (useType == 'd' || useType == 'f') { if (int32_t v = value; v < 0) { signChar = '-'; if (v != INT32_MIN) { // -INT32_MIN is UB value = -v; } } } // The longest possible formatted number is fixed-point with 10 digits, 255 fractional digits, // and a precision suffix, for 270 total bytes (counting the NUL terminator). // (Actually 269 since a 2-digit precision cannot reach 10 integer digits.) // Make the buffer somewhat larger just in case. char valueBuf[300]; if (useType == 'b') { // Special case for binary (since `snprintf` doesn't support it) // Buffer the digits from least to greatest char *ptr = valueBuf; do { *ptr++ = (value & 1) + '0'; value >>= 1; } while (value); // Reverse the digits and terminate the string std::reverse(valueBuf, ptr); *ptr = '\0'; } else if (useType == 'f') { // Special case for fixed-point (since it needs fractional part and precision) // Default fractional width (C++'s is 6 for "%f"; here 5 is enough for Q16.16) size_t useFracWidth = hasFrac ? fracWidth : 5; if (useFracWidth > 255) { error("Fractional width %zu too long, limiting to 255", useFracWidth); useFracWidth = 255; } // Default precision taken from default `-Q` option size_t defaultPrec = options.fixPrecision; size_t usePrec = hasPrec ? precision : defaultPrec; if (usePrec < 1 || usePrec > 31) { error( "Fixed-point constant precision %zu invalid, defaulting to %zu", usePrec, defaultPrec ); usePrec = defaultPrec; } // Floating-point formatting works for all fixed-point values double fval = fabs(value / pow(2.0, usePrec)); if (int fracWidthArg = static_cast(useFracWidth); useExact) { snprintf(valueBuf, sizeof(valueBuf), "%.*fq%zu", fracWidthArg, fval, usePrec); } else { snprintf(valueBuf, sizeof(valueBuf), "%.*f", fracWidthArg, fval); } } else { // `value` has already been made non-negative, so type 'd' is OK here even for `INT32_MIN`. // The sign will be printed later from `signChar`. char const *spec = useType == 'd' || useType == 'u' ? "%" PRIu32 : useType == 'X' ? "%" PRIX32 : useType == 'x' ? "%" PRIx32 : useType == 'o' ? "%" PRIo32 : "%" PRIu32; snprintf(valueBuf, sizeof(valueBuf), spec, value); } char prefixChar = !useExact ? 0 : useType == 'X' || useType == 'x' ? '$' : useType == 'b' ? '%' : useType == 'o' ? '&' : 0; size_t valueLen = strlen(valueBuf); size_t numLen = (signChar != 0) + (prefixChar != 0) + valueLen; size_t totalLen = width > numLen ? width : numLen; size_t padLen = totalLen - numLen; str.reserve(str.length() + totalLen); if (alignLeft) { if (signChar) { str += signChar; } if (prefixChar) { str += prefixChar; } str.append(valueBuf); str.append(padLen, ' '); } else { if (padZero) { // sign, then prefix, then zero padding if (signChar) { str += signChar; } if (prefixChar) { str += prefixChar; } str.append(padLen, '0'); } else { // space padding, then sign, then prefix str.append(padLen, ' '); if (signChar) { str += signChar; } if (prefixChar) { str += prefixChar; } } str.append(valueBuf); } } gbdev-rgbds-92bfe5d/src/asm/fstack.cpp000066400000000000000000000356551512540461700177530ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "asm/fstack.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "backtrace.hpp" #include "helpers.hpp" #include "itertools.hpp" // reversed #include "linkdefs.hpp" #include "platform.hpp" // strncasecmp #include "verbosity.hpp" #include "asm/lexer.hpp" #include "asm/macro.hpp" #include "asm/main.hpp" #include "asm/symbol.hpp" #include "asm/warning.hpp" using namespace std::literals; struct Context { std::shared_ptr fileInfo; LexerState lexerState{}; // If the shared_ptr is empty, `\@` is not permitted for this context. // Otherwise, if the pointee string is empty, it means that a unique ID has not been requested // for this context yet, and it should be generated. // Note that several contexts can share the same unique ID (since `INCLUDE` preserves its // parent's, and likewise "back-propagates" a unique ID if requested), hence using `shared_ptr`. std::shared_ptr uniqueIDStr = nullptr; std::shared_ptr macroArgs = nullptr; // Macro args are *saved* here uint32_t nbReptIters = 0; bool isForLoop = false; int32_t forValue = 0; int32_t forStep = 0; std::string forName{}; }; static std::stack contextStack; // The first include path for `fstk_FindFile` to try is none at all static std::vector includePaths = {""}; // -I static std::deque preIncludeNames; // -P static bool failedOnMissingInclude = false; void FileStackNode::printBacktrace(uint32_t curLineNo) const { using TraceItem = std::pair; std::vector items; for (TraceItem item{this, curLineNo};;) { auto &[node, itemLineNo] = item; bool loud = !node->isQuiet || tracing.loud; if (loud) { items.emplace_back(node, itemLineNo); } if (!node->parent) { assume(node->type != NODE_REPT && std::holds_alternative(node->data)); break; } if (loud || node->type != NODE_REPT) { // Quiet REPT nodes will pass their interior line number up to their parent, // which is more precise than the parent's own line number (since that will be // the line number of the "REPT?" or "FOR?" itself). itemLineNo = node->lineNo; } node = &*node->parent; } using TraceNode = std::pair; std::vector traceNodes; traceNodes.reserve(items.size()); for (auto &[node, itemLineNo] : reversed(items)) { if (std::holds_alternative>(node->data)) { assume(!traceNodes.empty()); // REPT nodes use their parent's name std::string reptName = traceNodes.back().first; if (std::vector const &nodeIters = node->iters(); !nodeIters.empty()) { reptName.append(NODE_SEPARATOR REPT_NODE_PREFIX); reptName.append(std::to_string(nodeIters.front())); } traceNodes.emplace_back(reptName, itemLineNo); } else { traceNodes.emplace_back(node->name(), itemLineNo); } } trace_PrintBacktrace( traceNodes, [](TraceNode const &node) { return node.first.c_str(); }, [](TraceNode const &node) { return node.second; } ); } void fstk_TraceCurrent() { if (!lexer_AtTopLevel()) { assume(!contextStack.empty()); contextStack.top().fileInfo->printBacktrace(lexer_GetLineNo()); } lexer_TraceStringExpansions(); } // LCOV_EXCL_START void fstk_VerboseOutputConfig() { assume(checkVerbosity(VERB_CONFIG)); // -I/--include if (includePaths.size() > 1) { fputs("\tInclude file paths:\n", stderr); for (std::string const &path : includePaths) { if (!path.empty()) { fprintf(stderr, "\t - %s\n", path.c_str()); } } } // -P/--preinclude if (!preIncludeNames.empty()) { fputs("\tPreincluded files:\n", stderr); for (std::string const &name : preIncludeNames) { fprintf(stderr, "\t - %s\n", name.c_str()); } } } // LCOV_EXCL_STOP std::shared_ptr fstk_GetFileStack() { return contextStack.empty() ? nullptr : contextStack.top().fileInfo; } std::shared_ptr fstk_GetUniqueIDStr() { static uint64_t nextUniqueID = 1; std::shared_ptr &str = contextStack.top().uniqueIDStr; // If a unique ID is allowed but has not been generated yet, generate one now. if (str && str->empty()) { *str = "_u"s + std::to_string(nextUniqueID++); } return str; } MacroArgs *fstk_GetCurrentMacroArgs() { // This returns a raw pointer, *not* a shared pointer, so its returned value // does *not* keep the current macro args alive! return contextStack.top().macroArgs.get(); } void fstk_AddIncludePath(std::string const &path) { if (path.empty()) { return; } std::string &includePath = includePaths.emplace_back(path); if (includePath.back() != '/') { includePath += '/'; } } void fstk_AddPreIncludeFile(std::string const &path) { preIncludeNames.emplace_front(path); } static bool isValidFilePath(std::string const &path) { struct stat statBuf; return stat(path.c_str(), &statBuf) == 0 && !S_ISDIR(statBuf.st_mode); // Reject directories } static void printDep(std::string const &path) { options.printDep(path); if (options.dependFile && options.generatePhonyDeps && isValidFilePath(path)) { fprintf(options.dependFile, "%s:\n", path.c_str()); } } std::optional fstk_FindFile(std::string const &path) { for (std::string &incPath : includePaths) { if (std::string fullPath = incPath + path; isValidFilePath(fullPath)) { printDep(fullPath); return fullPath; } } if (options.missingIncludeState != INC_ERROR) { printDep(path); } // Set `errno` as if `fopen` had failed on a nonexistent file. // This allows a subsequent `fstk_FileError` to report correctly with `strerror`. errno = ENOENT; return std::nullopt; } bool yywrap() { uint32_t ifDepth = lexer_GetIFDepth(); if (ifDepth != 0) { fatal( "Ended block with %" PRIu32 " unterminated conditional%s (`IF`/`ELIF`/`ELSE` block%s)", ifDepth, ifDepth == 1 ? "" : "s", ifDepth == 1 ? "" : "s" ); } if (Context &context = contextStack.top(); context.fileInfo->type == NODE_REPT) { // The context is a REPT or FOR block, which may loop // If the node is referenced outside this context, we can't edit it, so duplicate it if (context.fileInfo.use_count() > 1) { context.fileInfo = std::make_shared(*context.fileInfo); context.fileInfo->ID = UINT32_MAX; // The copy is not yet registered } std::vector &fileInfoIters = context.fileInfo->iters(); // If this is a FOR, update the symbol value if (context.isForLoop && fileInfoIters.front() <= context.nbReptIters) { // Avoid arithmetic overflow runtime error uint32_t forValue = static_cast(context.forValue) + static_cast(context.forStep); context.forValue = forValue <= INT32_MAX ? forValue : -static_cast(~forValue) - 1; Symbol *sym = sym_AddVar(context.forName, context.forValue); // This error message will refer to the current iteration if (sym->type != SYM_VAR) { fatal("Failed to update `FOR` symbol value"); } } // Advance to the next iteration ++fileInfoIters.front(); // If this wasn't the last iteration, wrap instead of popping if (fileInfoIters.front() <= context.nbReptIters) { lexer_RestartRept(context.fileInfo->lineNo); context.uniqueIDStr->clear(); // Invalidate the current unique ID (if any). return false; } } else if (contextStack.size() == 1) { return true; } contextStack.pop(); contextStack.top().lexerState.setAsCurrentState(); return false; } static void checkRecursionDepth() { if (contextStack.size() > options.maxRecursionDepth) { fatal("Recursion limit (%zu) exceeded", options.maxRecursionDepth); } } static void newFileContext(std::string const &filePath, bool isQuiet, bool updateStateNow) { checkRecursionDepth(); std::shared_ptr uniqueIDStr = nullptr; std::shared_ptr macroArgs = nullptr; auto fileInfo = std::make_shared(NODE_FILE, filePath == "-" ? "" : filePath, isQuiet); if (!contextStack.empty()) { Context &oldContext = contextStack.top(); fileInfo->parent = oldContext.fileInfo; fileInfo->lineNo = lexer_GetLineNo(); // Called before setting the lexer state uniqueIDStr = oldContext.uniqueIDStr; // Make a copy of the ID macroArgs = oldContext.macroArgs; } Context &context = contextStack.emplace(Context{ .fileInfo = fileInfo, .uniqueIDStr = uniqueIDStr, .macroArgs = macroArgs, }); context.lexerState.setFileAsNextState(filePath, updateStateNow); } static void newMacroContext(Symbol const ¯o, std::shared_ptr macroArgs, bool isQuiet) { checkRecursionDepth(); Context &oldContext = contextStack.top(); std::string fileInfoName; for (FileStackNode const *node = macro.src.get(); node; node = node->parent.get()) { if (node->type != NODE_REPT) { fileInfoName.append(node->name()); break; } } if (macro.src->type == NODE_REPT) { std::vector const &srcIters = macro.src->iters(); for (uint32_t iter : reversed(srcIters)) { fileInfoName.append(NODE_SEPARATOR REPT_NODE_PREFIX); fileInfoName.append(std::to_string(iter)); } } fileInfoName.append(NODE_SEPARATOR); fileInfoName.append(macro.name); auto fileInfo = std::make_shared(NODE_MACRO, fileInfoName, isQuiet); assume(!contextStack.empty()); // The top level context cannot be a MACRO fileInfo->parent = oldContext.fileInfo; fileInfo->lineNo = lexer_GetLineNo(); Context &context = contextStack.emplace(Context{ .fileInfo = fileInfo, .uniqueIDStr = std::make_shared(), // Create a new, not-yet-generated ID .macroArgs = macroArgs, }); context.lexerState.setViewAsNextState("MACRO", macro.getMacro(), macro.fileLine); } static Context & newReptContext(int32_t reptLineNo, ContentSpan const &span, uint32_t count, bool isQuiet) { checkRecursionDepth(); Context &oldContext = contextStack.top(); std::vector fileInfoIters{1}; if (oldContext.fileInfo->type == NODE_REPT && !oldContext.fileInfo->iters().empty()) { // Append all parent iter counts fileInfoIters.insert(fileInfoIters.end(), RANGE(oldContext.fileInfo->iters())); } auto fileInfo = std::make_shared(NODE_REPT, fileInfoIters, isQuiet); assume(!contextStack.empty()); // The top level context cannot be a REPT fileInfo->parent = oldContext.fileInfo; fileInfo->lineNo = reptLineNo; Context &context = contextStack.emplace(Context{ .fileInfo = fileInfo, .uniqueIDStr = std::make_shared(), // Create a new, not-yet-generated ID .macroArgs = oldContext.macroArgs, }); context.lexerState.setViewAsNextState("REPT", span, reptLineNo); context.nbReptIters = count; return context; } bool fstk_FileError(std::string const &path, char const *description) { if (options.missingIncludeState == INC_ERROR) { error("Error opening %s file \"%s\": %s", description, path.c_str(), strerror(errno)); } else { failedOnMissingInclude = true; // LCOV_EXCL_START if (options.missingIncludeState == GEN_EXIT) { verbosePrint( VERB_NOTICE, "Aborting due to '-MG' on %s file \"%s\": %s\n", description, path.c_str(), strerror(errno) ); return true; } assume(options.missingIncludeState == GEN_CONTINUE); // LCOV_EXCL_STOP } return false; } bool fstk_FailedOnMissingInclude() { return failedOnMissingInclude; } bool fstk_RunInclude(std::string const &path, bool isQuiet) { if (std::optional fullPath = fstk_FindFile(path); fullPath) { newFileContext(*fullPath, isQuiet, false); return false; } return fstk_FileError(path, "`INCLUDE`"); } void fstk_RunMacro( std::string const ¯oName, std::shared_ptr macroArgs, bool isQuiet ) { auto makeSuggestion = [¯oName, ¯oArgs]() -> std::optional { std::shared_ptr arg = macroArgs->getArg(1); if (!arg) { return std::nullopt; } char const *str = arg->c_str(); static char const *types[] = {"EQUS", "EQU", "RB", "RW", "RL", "="}; for (char const *type : types) { if (strncasecmp(str, type, strlen(type)) == 0) { return "\"DEF "s + macroName + " " + type + " ...\""; } } if (strncasecmp(str, "SET", literal_strlen("SET")) == 0) { return "\"DEF "s + macroName + " = ...\""; } if (str[0] == ':') { return "a label \""s + macroName + (str[1] == ':' ? "::" : ":") + "\""; } return std::nullopt; }; if (Symbol *macro = sym_FindExactSymbol(macroName); !macro) { if (sym_IsPurgedExact(macroName)) { error("Undefined macro `%s`; it was purged", macroName.c_str()); } else if (std::optional suggestion = makeSuggestion(); suggestion) { error( "Undefined macro `%s` (did you mean %s?)", macroName.c_str(), suggestion->c_str() ); } else { error("Undefined macro `%s`", macroName.c_str()); } } else if (macro->type != SYM_MACRO) { error("`%s` is not a macro", macroName.c_str()); } else { newMacroContext(*macro, macroArgs, isQuiet || macro->isQuiet); } } void fstk_RunRept(uint32_t count, int32_t reptLineNo, ContentSpan const &span, bool isQuiet) { if (count) { newReptContext(reptLineNo, span, count, isQuiet); } } void fstk_RunFor( std::string const &symName, int32_t start, int32_t stop, int32_t step, int32_t reptLineNo, ContentSpan const &span, bool isQuiet ) { if (Symbol *sym = sym_AddVar(symName, start); sym->type != SYM_VAR) { return; } uint32_t count = 0; if (step > 0 && start < stop) { count = (static_cast(stop) - start - 1) / step + 1; } else if (step < 0 && stop < start) { count = (static_cast(start) - stop - 1) / -static_cast(step) + 1; } else if (step == 0) { error("`FOR` cannot have a step value of 0"); } if ((step > 0 && start > stop) || (step < 0 && start < stop)) { warning( WARNING_BACKWARDS_FOR, "`FOR` goes backwards from %d to %d by %d", start, stop, step ); } if (count == 0) { return; } Context &context = newReptContext(reptLineNo, span, count, isQuiet); context.isForLoop = true; context.forValue = start; context.forStep = step; context.forName = symName; } bool fstk_Break() { if (contextStack.top().fileInfo->type != NODE_REPT) { error("`BREAK` can only be used inside a loop (`REPT`/`FOR` block)"); return false; } contextStack.top().nbReptIters = 0; // Prevent more iterations return true; } void fstk_NewRecursionDepth(size_t newDepth) { if (contextStack.size() > newDepth + 1) { fatal("Recursion limit (%zu) exceeded", newDepth); } options.maxRecursionDepth = newDepth; } bool fstk_Init(std::string const &mainPath) { newFileContext(mainPath, false, true); for (std::string const &name : preIncludeNames) { if (std::optional fullPath = fstk_FindFile(name); fullPath) { newFileContext(*fullPath, false, false); } else if (fstk_FileError(name, "pre-included")) { return false; } } return true; } gbdev-rgbds-92bfe5d/src/asm/lexer.cpp000066400000000000000000001742761512540461700176220ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "asm/lexer.hpp" #include #include #include #include #include #include #include #include #include #include #include // nothrow #include #include #include #include #include #include #include #include #include #include #include #include "helpers.hpp" #include "platform.hpp" #include "style.hpp" #include "util.hpp" #include "verbosity.hpp" #include "asm/format.hpp" #include "asm/fstack.hpp" #include "asm/macro.hpp" #include "asm/main.hpp" #include "asm/rpn.hpp" #include "asm/symbol.hpp" #include "asm/warning.hpp" // Include this last so it gets all type & constant definitions #include "parser.hpp" // For token definitions, generated from parser.y // Bison 3.6 changed token "types" to "kinds"; cast to int for simple compatibility #define T_(name) static_cast(yy::parser::token::name) struct Token { int type; std::variant value; Token() : type(T_(NUMBER)), value(std::monostate{}) {} Token(int type_) : type(type_), value(std::monostate{}) {} Token(int type_, uint32_t value_) : type(type_), value(value_) {} Token(int type_, std::string const &value_) : type(type_), value(value_) {} Token(int type_, std::string &&value_) : type(type_), value(value_) {} }; // This map lists all RGBASM keywords which `yylex_NORMAL` lexes as identifiers. // All non-identifier tokens are lexed separately. static UpperMap const keywords{ {"ADC", T_(SM83_ADC) }, {"ADD", T_(SM83_ADD) }, {"AND", T_(SM83_AND) }, {"BIT", T_(SM83_BIT) }, {"CALL", T_(SM83_CALL) }, {"CCF", T_(SM83_CCF) }, {"CPL", T_(SM83_CPL) }, {"CP", T_(SM83_CP) }, {"DAA", T_(SM83_DAA) }, {"DEC", T_(SM83_DEC) }, {"DI", T_(SM83_DI) }, {"EI", T_(SM83_EI) }, {"HALT", T_(SM83_HALT) }, {"INC", T_(SM83_INC) }, {"JP", T_(SM83_JP) }, {"JR", T_(SM83_JR) }, {"LD", T_(SM83_LD) }, {"LDI", T_(SM83_LDI) }, {"LDD", T_(SM83_LDD) }, {"LDH", T_(SM83_LDH) }, {"NOP", T_(SM83_NOP) }, {"OR", T_(SM83_OR) }, {"POP", T_(SM83_POP) }, {"PUSH", T_(SM83_PUSH) }, {"RES", T_(SM83_RES) }, {"RETI", T_(SM83_RETI) }, {"RET", T_(SM83_RET) }, {"RLCA", T_(SM83_RLCA) }, {"RLC", T_(SM83_RLC) }, {"RLA", T_(SM83_RLA) }, {"RL", T_(SM83_RL) }, {"RRC", T_(SM83_RRC) }, {"RRCA", T_(SM83_RRCA) }, {"RRA", T_(SM83_RRA) }, {"RR", T_(SM83_RR) }, {"RST", T_(SM83_RST) }, {"SBC", T_(SM83_SBC) }, {"SCF", T_(SM83_SCF) }, {"SET", T_(SM83_SET) }, {"SLA", T_(SM83_SLA) }, {"SRA", T_(SM83_SRA) }, {"SRL", T_(SM83_SRL) }, {"STOP", T_(SM83_STOP) }, {"SUB", T_(SM83_SUB) }, {"SWAP", T_(SM83_SWAP) }, {"XOR", T_(SM83_XOR) }, {"NZ", T_(CC_NZ) }, {"Z", T_(CC_Z) }, {"NC", T_(CC_NC) }, // There is no `T_(CC_C)`; it's handled before as `T_(TOKEN_C)` {"AF", T_(MODE_AF) }, {"BC", T_(MODE_BC) }, {"DE", T_(MODE_DE) }, {"HL", T_(MODE_HL) }, {"SP", T_(MODE_SP) }, {"HLD", T_(MODE_HL_DEC) }, {"HLI", T_(MODE_HL_INC) }, {"A", T_(TOKEN_A) }, {"B", T_(TOKEN_B) }, {"C", T_(TOKEN_C) }, {"D", T_(TOKEN_D) }, {"E", T_(TOKEN_E) }, {"H", T_(TOKEN_H) }, {"L", T_(TOKEN_L) }, {"DEF", T_(OP_DEF) }, {"FRAGMENT", T_(POP_FRAGMENT) }, {"BANK", T_(OP_BANK) }, {"ALIGN", T_(POP_ALIGN) }, {"SIZEOF", T_(OP_SIZEOF) }, {"STARTOF", T_(OP_STARTOF) }, {"ROUND", T_(OP_ROUND) }, {"CEIL", T_(OP_CEIL) }, {"FLOOR", T_(OP_FLOOR) }, {"DIV", T_(OP_FDIV) }, {"MUL", T_(OP_FMUL) }, {"FMOD", T_(OP_FMOD) }, {"POW", T_(OP_POW) }, {"LOG", T_(OP_LOG) }, {"SIN", T_(OP_SIN) }, {"COS", T_(OP_COS) }, {"TAN", T_(OP_TAN) }, {"ASIN", T_(OP_ASIN) }, {"ACOS", T_(OP_ACOS) }, {"ATAN", T_(OP_ATAN) }, {"ATAN2", T_(OP_ATAN2) }, {"HIGH", T_(OP_HIGH) }, {"LOW", T_(OP_LOW) }, {"ISCONST", T_(OP_ISCONST) }, {"BITWIDTH", T_(OP_BITWIDTH) }, {"TZCOUNT", T_(OP_TZCOUNT) }, {"BYTELEN", T_(OP_BYTELEN) }, {"READFILE", T_(OP_READFILE) }, {"STRBYTE", T_(OP_STRBYTE) }, {"STRCAT", T_(OP_STRCAT) }, {"STRCHAR", T_(OP_STRCHAR) }, {"STRCMP", T_(OP_STRCMP) }, {"STRFIND", T_(OP_STRFIND) }, {"STRFMT", T_(OP_STRFMT) }, {"STRIN", T_(OP_STRIN) }, {"STRLEN", T_(OP_STRLEN) }, {"STRLWR", T_(OP_STRLWR) }, {"STRRFIND", T_(OP_STRRFIND) }, {"STRRIN", T_(OP_STRRIN) }, {"STRRPL", T_(OP_STRRPL) }, {"STRSLICE", T_(OP_STRSLICE) }, {"STRSUB", T_(OP_STRSUB) }, {"STRUPR", T_(OP_STRUPR) }, {"CHARCMP", T_(OP_CHARCMP) }, {"CHARLEN", T_(OP_CHARLEN) }, {"CHARSIZE", T_(OP_CHARSIZE) }, {"CHARSUB", T_(OP_CHARSUB) }, {"CHARVAL", T_(OP_CHARVAL) }, {"INCHARMAP", T_(OP_INCHARMAP) }, {"REVCHAR", T_(OP_REVCHAR) }, {"INCLUDE", T_(POP_INCLUDE) }, {"PRINT", T_(POP_PRINT) }, {"PRINTLN", T_(POP_PRINTLN) }, {"EXPORT", T_(POP_EXPORT) }, {"DS", T_(POP_DS) }, {"DB", T_(POP_DB) }, {"DW", T_(POP_DW) }, {"DL", T_(POP_DL) }, {"SECTION", T_(POP_SECTION) }, {"ENDSECTION", T_(POP_ENDSECTION) }, {"PURGE", T_(POP_PURGE) }, {"RSRESET", T_(POP_RSRESET) }, {"RSSET", T_(POP_RSSET) }, {"INCBIN", T_(POP_INCBIN) }, {"CHARMAP", T_(POP_CHARMAP) }, {"NEWCHARMAP", T_(POP_NEWCHARMAP) }, {"SETCHARMAP", T_(POP_SETCHARMAP) }, {"PUSHC", T_(POP_PUSHC) }, {"POPC", T_(POP_POPC) }, {"FAIL", T_(POP_FAIL) }, {"WARN", T_(POP_WARN) }, {"FATAL", T_(POP_FATAL) }, {"ASSERT", T_(POP_ASSERT) }, {"STATIC_ASSERT", T_(POP_STATIC_ASSERT)}, {"MACRO", T_(POP_MACRO) }, {"ENDM", T_(POP_ENDM) }, {"SHIFT", T_(POP_SHIFT) }, {"REPT", T_(POP_REPT) }, {"FOR", T_(POP_FOR) }, {"ENDR", T_(POP_ENDR) }, {"BREAK", T_(POP_BREAK) }, {"LOAD", T_(POP_LOAD) }, {"ENDL", T_(POP_ENDL) }, {"IF", T_(POP_IF) }, {"ELSE", T_(POP_ELSE) }, {"ELIF", T_(POP_ELIF) }, {"ENDC", T_(POP_ENDC) }, {"UNION", T_(POP_UNION) }, {"NEXTU", T_(POP_NEXTU) }, {"ENDU", T_(POP_ENDU) }, {"WRAM0", T_(SECT_WRAM0) }, {"VRAM", T_(SECT_VRAM) }, {"ROMX", T_(SECT_ROMX) }, {"ROM0", T_(SECT_ROM0) }, {"HRAM", T_(SECT_HRAM) }, {"WRAMX", T_(SECT_WRAMX) }, {"SRAM", T_(SECT_SRAM) }, {"OAM", T_(SECT_OAM) }, {"RB", T_(POP_RB) }, {"RW", T_(POP_RW) }, // There is no `T_(POP_RL)`; it's handled before as `T_(SM83_RL)` {"EQU", T_(POP_EQU) }, {"EQUS", T_(POP_EQUS) }, {"REDEF", T_(POP_REDEF) }, {"PUSHS", T_(POP_PUSHS) }, {"POPS", T_(POP_POPS) }, {"PUSHO", T_(POP_PUSHO) }, {"POPO", T_(POP_POPO) }, {"OPT", T_(POP_OPT) }, }; static LexerState *lexerState = nullptr; static LexerState *lexerStateEOL = nullptr; bool lexer_AtTopLevel() { return lexerState == nullptr; } void LexerState::clear(uint32_t lineNo_) { mode = LEXER_NORMAL; atLineStart = true; lastToken = T_(YYEOF); nextToken = 0; ifStack.clear(); capturing = false; captureBuf = nullptr; disableExpansions = false; expansionScanDistance = 0; expandStrings = true; expansions.clear(); lineNo = lineNo_; // Will be incremented at next line start } static void nextLine() { // Newlines read within an expansion should not increase the line count if (lexerState->expansions.empty()) { ++lexerState->lineNo; } } uint32_t lexer_GetIFDepth() { return lexerState->ifStack.size(); } void lexer_IncIFDepth() { lexerState->ifStack.push_front({.ranIfBlock = false, .reachedElseBlock = false}); } void lexer_DecIFDepth() { if (lexerState->ifStack.empty()) { fatal("Found `ENDC` outside of a conditional (not after an `IF`/`ELIF`/`ELSE` block)"); } lexerState->ifStack.pop_front(); } bool lexer_RanIFBlock() { return lexerState->ifStack.front().ranIfBlock; } bool lexer_ReachedELSEBlock() { return lexerState->ifStack.front().reachedElseBlock; } void lexer_RunIFBlock() { lexerState->ifStack.front().ranIfBlock = true; } void lexer_ReachELSEBlock() { lexerState->ifStack.front().reachedElseBlock = true; } void LexerState::setAsCurrentState() { lexerState = this; } void LexerState::setFileAsNextState(std::string const &filePath, bool updateStateNow) { if (filePath == "-") { path = ""; content.emplace(STDIN_FILENO); verbosePrint(VERB_INFO, "Opening stdin\n"); // LCOV_EXCL_LINE } else { struct stat statBuf; if (stat(filePath.c_str(), &statBuf) != 0) { // LCOV_EXCL_START fatal("Failed to stat file \"%s\": %s", filePath.c_str(), strerror(errno)); // LCOV_EXCL_STOP } path = filePath; if (std::streamsize size = statBuf.st_size; statBuf.st_size > 0) { // Read the entire file for better performance // Ideally we'd use C++20 `auto ptr = std::make_shared(size)`, // but it has insufficient compiler support auto ptr = std::shared_ptr(new (std::nothrow) char[size]); if (std::ifstream fs(path, std::ios::binary); !fs) { // LCOV_EXCL_START fatal("Failed to open file \"%s\": %s", path.c_str(), strerror(errno)); // LCOV_EXCL_STOP } else if (!fs.read(ptr.get(), size) || fs.gcount() != size) { // LCOV_EXCL_START fatal("Failed to read file \"%s\": %s", path.c_str(), strerror(errno)); // LCOV_EXCL_STOP } content.emplace(ptr, size); // LCOV_EXCL_START verbosePrint(VERB_INFO, "File \"%s\" is fully read\n", path.c_str()); // LCOV_EXCL_STOP } else { // LCOV_EXCL_START if (statBuf.st_size == 0) { verbosePrint(VERB_INFO, "File \"%s\" is empty\n", path.c_str()); } else { verbosePrint( VERB_INFO, "Failed to stat file \"%s\": %s\n", path.c_str(), strerror(errno) ); } // LCOV_EXCL_STOP // Have a fallback if reading the file failed int fd = open(path.c_str(), O_RDONLY); if (fd < 0) { // LCOV_EXCL_START fatal("Failed to open file \"%s\": %s", path.c_str(), strerror(errno)); // LCOV_EXCL_STOP } content.emplace(fd); verbosePrint(VERB_INFO, "File \"%s\" is opened\n", path.c_str()); // LCOV_EXCL_LINE } } clear(0); if (updateStateNow) { lexerState = this; } else { lexerStateEOL = this; } } void LexerState::setViewAsNextState(char const *name, ContentSpan const &span, uint32_t lineNo_) { path = name; // Used to report read errors in `.peek()` content.emplace(span); clear(lineNo_); lexerStateEOL = this; } void lexer_RestartRept(uint32_t lineNo) { if (std::holds_alternative(lexerState->content)) { std::get(lexerState->content).offset = 0; } lexerState->clear(lineNo); } LexerState::~LexerState() { // A big chunk of the lexer state soundness is the file stack ("fstack"). // Each context in the fstack has its own *unique* lexer state; thus, we always guarantee // that lexer states lifetimes are always properly managed, since they're handled solely // by the fstack... with *one* exception. // Assume a context is pushed on top of the fstack, and the corresponding lexer state gets // scheduled at EOF; `lexerStateEOL` thus becomes a (weak) ref to that lexer state... // It has been possible, due to a bug, that the corresponding fstack context gets popped // before EOL, deleting the associated state... but it would still be switched to at EOL. // This assumption checks that this doesn't happen again. // It could be argued that deleting a state that's scheduled for EOF could simply clear // `lexerStateEOL`, but there's currently no situation in which this should happen. assume(this != lexerStateEOL); } bool Expansion::advance() { assume(offset <= size()); return ++offset > size(); } BufferedContent::~BufferedContent() { close(fd); } void BufferedContent::advance() { assume(offset < std::size(buf)); if (++offset == std::size(buf)) { offset = 0; // Wrap around if necessary } if (size > 0) { --size; } } void BufferedContent::refill() { size_t target = std::size(buf) - size; // Aim: making the buf full // Compute the index we'll start writing to size_t startIndex = (offset + size) % std::size(buf); // If the range to fill passes over the buffer wrapping point, we need two reads if (startIndex + target > std::size(buf)) { size_t nbExpectedChars = std::size(buf) - startIndex; size_t nbReadChars = readMore(startIndex, nbExpectedChars); startIndex += nbReadChars; if (startIndex == std::size(buf)) { startIndex = 0; } // If the read was incomplete, don't perform a second read target -= nbReadChars; if (nbReadChars < nbExpectedChars) { target = 0; } } if (target != 0) { readMore(startIndex, target); } } size_t BufferedContent::readMore(size_t startIndex, size_t nbChars) { // This buffer overflow made me lose WEEKS of my life. Never again. assume(startIndex + nbChars <= std::size(buf)); ssize_t nbReadChars = read(fd, &buf[startIndex], nbChars); if (nbReadChars == -1) { // LCOV_EXCL_START fatal("Error reading file \"%s\": %s", lexerState->path.c_str(), strerror(errno)); // LCOV_EXCL_STOP } size += nbReadChars; // `nbReadChars` cannot be negative, so it's fine to cast to `size_t` return static_cast(nbReadChars); } void lexer_SetMode(LexerMode mode) { lexerState->mode = mode; } void lexer_ToggleStringExpansion(bool enable) { lexerState->expandStrings = enable; } // Functions for the actual lexer to obtain characters static void beginExpansion(std::shared_ptr str, std::optional name) { if (name) { lexer_CheckRecursionDepth(); } // Do not expand empty strings if (str->empty()) { return; } lexerState->expansions.push_front({.name = name, .contents = str, .offset = 0}); } void lexer_CheckRecursionDepth() { if (lexerState->expansions.size() > options.maxRecursionDepth + 1) { fatal("Recursion limit (%zu) exceeded", options.maxRecursionDepth); } } static bool isMacroChar(char c) { return c == '@' || c == '#' || c == '<' || (c >= '1' && c <= '9'); } // Forward declarations for `readBracketedMacroArgNum` static int peek(); static void shiftChar(); static int bumpChar(); static int nextChar(); static uint32_t readDecimalNumber(int initial); static uint32_t readBracketedMacroArgNum() { bool disableExpansions = lexerState->disableExpansions; lexerState->disableExpansions = false; Defer restoreExpansions{[&] { lexerState->disableExpansions = disableExpansions; }}; int32_t num = 0; int c = peek(); bool empty = false; bool symbolError = false; bool negative = c == '-'; if (negative) { c = nextChar(); } if (isDigit(c)) { uint32_t n = readDecimalNumber(bumpChar()); if (n > INT32_MAX) { error("Number in bracketed macro argument is too large"); return 0; } num = negative ? -n : static_cast(n); } else if (startsIdentifier(c) || c == '#') { if (c == '#') { c = nextChar(); if (!startsIdentifier(c)) { error("Empty raw symbol in bracketed macro argument"); return 0; } } std::string symName; for (; continuesIdentifier(c); c = nextChar()) { symName += c; } if (Symbol const *sym = sym_FindScopedValidSymbol(symName); !sym) { if (sym_IsPurgedScoped(symName)) { error("Bracketed symbol `%s` does not exist; it was purged", symName.c_str()); } else { error("Bracketed symbol `%s` does not exist", symName.c_str()); } num = 0; symbolError = true; } else if (!sym->isNumeric()) { error("Bracketed symbol `%s` is not numeric", symName.c_str()); num = 0; symbolError = true; } else { num = static_cast(sym->getConstantValue()); } } else { empty = true; } c = bumpChar(); if (c != '>') { error("Invalid character %s in bracketed macro argument", printChar(c)); return 0; } else if (empty) { error("Empty bracketed macro argument"); return 0; } else if (num == 0 && !symbolError) { error("Invalid bracketed macro argument \"\\<0>\""); return 0; } else { return num; } } static std::shared_ptr readMacroArg() { if (int c = bumpChar(); c == '@') { std::shared_ptr str = fstk_GetUniqueIDStr(); if (!str) { error("`\\@` cannot be used outside of a macro or loop (`REPT`/`FOR` block)"); } return str; } else if (c == '#') { MacroArgs *macroArgs = fstk_GetCurrentMacroArgs(); if (!macroArgs) { error("`\\#` cannot be used outside of a macro"); return nullptr; } std::shared_ptr str = macroArgs->getAllArgs(); assume(str); // '\#' should always be defined (at least as an empty string) return str; } else if (c == '<') { int32_t num = readBracketedMacroArgNum(); if (num == 0) { // The error was already reported by `readBracketedMacroArgNum`. return nullptr; } MacroArgs *macroArgs = fstk_GetCurrentMacroArgs(); if (!macroArgs) { error("`\\<%" PRIu32 ">` cannot be used outside of a macro", num); return nullptr; } std::shared_ptr str = macroArgs->getArg(num); if (!str) { error("Macro argument `\\<%" PRId32 ">` not defined", num); } return str; } else { assume(c >= '1' && c <= '9'); MacroArgs *macroArgs = fstk_GetCurrentMacroArgs(); if (!macroArgs) { error("`\\%c` cannot be used outside of a macro", c); return nullptr; } std::shared_ptr str = macroArgs->getArg(c - '0'); if (!str) { error("Macro argument `\\%c` not defined", c); } return str; } } int LexerState::peekChar() { // This is `.peekCharAhead()` modified for zero lookahead distance for (Expansion &exp : expansions) { if (exp.offset < exp.size()) { return static_cast((*exp.contents)[exp.offset]); } } if (std::holds_alternative(content)) { auto &view = std::get(content); if (view.offset < view.span.size) { return static_cast(view.span.ptr[view.offset]); } } else { auto &cbuf = std::get(content); if (cbuf.size == 0) { cbuf.refill(); } assume(cbuf.offset < std::size(cbuf.buf)); if (cbuf.size > 0) { return static_cast(cbuf.buf[cbuf.offset]); } } // If there aren't enough chars, give up return EOF; } int LexerState::peekCharAhead() { // We only need one character of lookahead, for macro arguments uint8_t distance = 1; for (Expansion &exp : expansions) { // An expansion that has reached its end will have `exp.offset` == `exp.size()`, // and `.peekCharAhead()` will continue with its parent assume(exp.offset <= exp.size()); if (size_t idx = exp.offset + distance; idx < exp.size()) { // Macro args can't be recursive, since `peek()` marks them as scanned, so // this is a failsafe that (as far as I can tell) won't ever actually run. return static_cast((*exp.contents)[idx]); // LCOV_EXCL_LINE } distance -= exp.size() - exp.offset; } if (std::holds_alternative(content)) { auto &view = std::get(content); if (view.offset + distance < view.span.size) { return static_cast(view.span.ptr[view.offset + distance]); } } else { auto &cbuf = std::get(content); assume(distance < std::size(cbuf.buf)); if (cbuf.size <= distance) { cbuf.refill(); } if (cbuf.size > distance) { return static_cast(cbuf.buf[(cbuf.offset + distance) % std::size(cbuf.buf)]); } } // If there aren't enough chars, give up return EOF; } // Forward declarations for `peek` static std::pair> readInterpolation(size_t depth); static int peek() { for (;;) { int c = lexerState->peekChar(); if (lexerState->expansionScanDistance > 0) { return c; } ++lexerState->expansionScanDistance; // Do not consider again if (lexerState->disableExpansions) { return c; } else if (c == '\\') { // If character is a backslash, check for a macro arg ++lexerState->expansionScanDistance; if (!isMacroChar(lexerState->peekCharAhead())) { return c; } // If character is a macro arg char, do macro arg expansion shiftChar(); if (std::shared_ptr str = readMacroArg(); str) { beginExpansion(str, std::nullopt); // Mark the entire macro arg expansion as "painted blue" // so that macro args can't be recursive // https://en.wikipedia.org/wiki/Painted_blue lexerState->expansionScanDistance += str->length(); } // Continue in the next iteration } else if (c == '{') { // If character is an open brace, do symbol interpolation shiftChar(); if (auto interp = readInterpolation(0); interp.first && interp.second) { beginExpansion(interp.second, interp.first->name); } // Continue in the next iteration } else { return c; } } } static void shiftChar() { if (lexerState->capturing) { if (lexerState->captureBuf) { int c = peek(); assume(c != EOF); // Avoid calling `shiftChar()` when it could be EOF while capturing lexerState->captureBuf->push_back(c); } ++lexerState->captureSize; } --lexerState->expansionScanDistance; for (;;) { if (!lexerState->expansions.empty()) { // Advance within the current expansion if (Expansion &exp = lexerState->expansions.front(); exp.advance()) { // When advancing would go past an expansion's end, // move up to its parent and try again to advance lexerState->expansions.pop_front(); continue; } } else { // Advance within the file contents if (std::holds_alternative(lexerState->content)) { ++std::get(lexerState->content).offset; } else { std::get(lexerState->content).advance(); } } return; } } static bool consumeChar(int c) { // This is meant to be called when the "extra" behavior of `peek()` is not wanted, // e.g. painting the peeked-at character "blue". if (lexerState->peekChar() != c) { return false; } // Increment `lexerState->expansionScanDistance` to prevent `shiftChar()` from calling // `peek()` and to balance its decrement. ++lexerState->expansionScanDistance; shiftChar(); return true; } static int bumpChar() { int c = peek(); shiftChar(); return c; } static int nextChar() { shiftChar(); return peek(); } template static int skipChars(PredicateFnT predicate) { int c = peek(); while (predicate(c)) { c = nextChar(); } return c; } static void handleCRLF(int c) { if (c == '\r' && peek() == '\n') { shiftChar(); } } static auto scopedDisableExpansions() { lexerState->disableExpansions = true; return Defer{[&] { lexerState->disableExpansions = false; }}; } // "Services" provided by the lexer to the rest of the program uint32_t lexer_GetLineNo() { return lexerState->lineNo; } void lexer_TraceStringExpansions() { if (!lexerState) { return; } for (Expansion &exp : lexerState->expansions) { // Only print EQUS expansions, not string args if (exp.name) { style_Set(stderr, STYLE_CYAN, false); fputs(" while expanding symbol `", stderr); style_Set(stderr, STYLE_CYAN, true); fputs(exp.name->c_str(), stderr); style_Set(stderr, STYLE_CYAN, false); fputs("`\n", stderr); } } style_Reset(stderr); } // Functions to discard non-tokenized characters static void discardBlockComment() { Defer reenableExpansions = scopedDisableExpansions(); for (;;) { int c = bumpChar(); switch (c) { case EOF: error("Unterminated block comment"); return; case '\r': handleCRLF(c); [[fallthrough]]; case '\n': nextLine(); continue; case '/': if (peek() == '*') { warning( WARNING_NESTED_COMMENT, "\"/" // Prevent simple syntax highlighters from seeing this as a comment "*\" in block comment" ); } continue; case '*': if (peek() == '/') { shiftChar(); return; } [[fallthrough]]; default: continue; } } } static void discardComment() { Defer reenableExpansions = scopedDisableExpansions(); skipChars([](int c) { return c != EOF && !isNewline(c); }); } static void discardLineContinuation() { for (;;) { if (int c = peek(); isBlankSpace(c)) { shiftChar(); } else if (isNewline(c)) { shiftChar(); handleCRLF(c); nextLine(); break; } else if (c == ';') { discardComment(); } else if (c == EOF) { error("Invalid line continuation at end of file"); break; } else { error("Invalid character %s after line continuation", printChar(c)); break; } } } // Functions to read tokenizable values static std::string readAnonLabelRef(char c) { // We come here having already peeked at one char, so no need to do it again uint32_t n = 1; while (nextChar() == c) { ++n; } return sym_MakeAnonLabelName(n, c == '-'); } static uint32_t readFractionalPart(uint32_t integer) { uint32_t value = 0, divisor = 1; uint8_t precision = 0; enum { READFRACTIONALPART_DIGITS, READFRACTIONALPART_PRECISION, READFRACTIONALPART_PRECISION_DIGITS, } state = READFRACTIONALPART_DIGITS; bool nonDigit = true; for (int c = peek();; c = nextChar()) { if (state == READFRACTIONALPART_DIGITS) { if (c == '_') { if (nonDigit) { error("Invalid integer constant, '_' after another '_'"); } nonDigit = true; continue; } if (c == 'q' || c == 'Q') { state = READFRACTIONALPART_PRECISION; nonDigit = false; // '_' is allowed before 'q'/'Q' continue; } else if (!isDigit(c)) { break; } nonDigit = false; if (divisor > (UINT32_MAX - (c - '0')) / 10) { warning(WARNING_LARGE_CONSTANT, "Precision of fixed-point constant is too large"); // Discard any additional digits skipChars([](int d) { return isDigit(d) || d == '_'; }); break; } value = value * 10 + (c - '0'); divisor *= 10; } else { if (c == '.' && state == READFRACTIONALPART_PRECISION) { state = READFRACTIONALPART_PRECISION_DIGITS; continue; } else if (!isDigit(c)) { break; } precision = precision * 10 + (c - '0'); } } if (precision == 0) { if (state >= READFRACTIONALPART_PRECISION) { error("Invalid fixed-point constant, no significant digits after 'q'"); } precision = options.fixPrecision; } else if (precision > 31) { error("Fixed-point constant precision must be between 1 and 31"); precision = options.fixPrecision; } if (nonDigit) { error("Invalid fixed-point constant, trailing '_'"); } if (integer >= (1ULL << (32 - precision))) { warning(WARNING_LARGE_CONSTANT, "Magnitude of fixed-point constant is too large"); return 0; } // Cast to unsigned avoids undefined overflow behavior uint32_t fractional = static_cast(round(static_cast(value) / divisor * pow(2.0, precision))); return (integer << precision) | fractional; } static bool isValidDigit(char c) { return isAlphanumeric(c) || c == '.' || c == '#' || c == '@'; } static bool isCustomBinDigit(int c) { return isBinDigit(c) || c == options.binDigits[0] || c == options.binDigits[1]; } static bool checkDigitErrors(char const *digits, size_t n, char const *type) { for (size_t i = 0; i < n; ++i) { char c = digits[i]; if (!isValidDigit(c)) { error("Invalid digit for %s constant %s", type, printChar(c)); return false; } if (c >= '0' && c < static_cast(n + '0') && c != static_cast(i + '0')) { error("Changed digit for %s constant %s", type, printChar(c)); return false; } for (size_t j = i + 1; j < n; ++j) { if (c == digits[j]) { error("Repeated digit for %s constant %s", type, printChar(c)); return false; } } } return true; } void lexer_SetBinDigits(char const digits[2]) { if (size_t n = std::size(options.binDigits); checkDigitErrors(digits, n, "binary")) { memcpy(options.binDigits, digits, n); } } void lexer_SetGfxDigits(char const digits[4]) { if (size_t n = std::size(options.gfxDigits); checkDigitErrors(digits, n, "graphics")) { memcpy(options.gfxDigits, digits, n); } } static uint32_t readBinaryNumber(char const *prefix) { uint32_t value = 0; bool empty = true; bool nonDigit = false; for (int c = peek();; c = nextChar()) { if (c == '_') { if (nonDigit) { error("Invalid integer constant, '_' after another '_'"); } nonDigit = true; continue; } int bit; if (c == '0' || c == options.binDigits[0]) { bit = 0; } else if (c == '1' || c == options.binDigits[1]) { bit = 1; } else { break; } empty = false; nonDigit = false; if (value > (UINT32_MAX - bit) / 2) { warning(WARNING_LARGE_CONSTANT, "Integer constant is too large"); // Discard any additional digits skipChars([](int d) { return isCustomBinDigit(d) || d == '_'; }); return 0; } value = value * 2 + bit; } if (empty) { error("Invalid integer constant, no digits after %s", prefix); } if (nonDigit) { error("Invalid integer constant, trailing '_'"); } return value; } static uint32_t readOctalNumber(char const *prefix) { uint32_t value = 0; bool empty = true; bool nonDigit = false; for (int c = peek();; c = nextChar()) { if (c == '_') { if (nonDigit) { error("Invalid integer constant, '_' after another '_'"); } nonDigit = true; continue; } if (!isOctDigit(c)) { break; } c = c - '0'; empty = false; nonDigit = false; if (value > (UINT32_MAX - c) / 8) { warning(WARNING_LARGE_CONSTANT, "Integer constant is too large"); // Discard any additional digits skipChars([](int d) { return isOctDigit(d) || d == '_'; }); return 0; } value = value * 8 + c; } if (empty) { error("Invalid integer constant, no digits after %s", prefix); } if (nonDigit) { error("Invalid integer constant, trailing '_'"); } return value; } static uint32_t readDecimalNumber(int initial) { assume(isDigit(initial)); uint32_t value = initial - '0'; bool nonDigit = false; for (int c = peek();; c = nextChar()) { if (c == '_') { if (nonDigit) { error("Invalid integer constant, '_' after another '_'"); } nonDigit = true; continue; } if (!isDigit(c)) { break; } c = c - '0'; nonDigit = false; if (value > (UINT32_MAX - c) / 10) { warning(WARNING_LARGE_CONSTANT, "Integer constant is too large"); // Discard any additional digits skipChars([](int d) { return isDigit(d) || d == '_'; }); return 0; } value = value * 10 + c; } if (nonDigit) { error("Invalid integer constant, trailing '_'"); } return value; } static uint32_t readHexNumber(char const *prefix) { uint32_t value = 0; bool empty = true; bool nonDigit = false; for (int c = peek();; c = nextChar()) { if (c == '_') { if (nonDigit) { error("Invalid integer constant, '_' after another '_'"); } nonDigit = true; continue; } if (!isHexDigit(c)) { break; } c = parseHexDigit(c); empty = false; nonDigit = false; if (value > (UINT32_MAX - c) / 16) { warning(WARNING_LARGE_CONSTANT, "Integer constant is too large"); // Discard any additional digits skipChars([](int d) { return isHexDigit(d) || d == '_'; }); return 0; } value = value * 16 + c; } if (empty) { error("Invalid integer constant, no digits after %s", prefix); } if (nonDigit) { error("Invalid integer constant, trailing '_'"); } return value; } static uint32_t readGfxConstant() { uint32_t bitPlaneLower = 0, bitPlaneUpper = 0; uint8_t width = 0; bool nonDigit = false; for (int c = peek();; c = nextChar()) { if (c == '_') { if (nonDigit) { error("Invalid integer constant, '_' after another '_'"); } nonDigit = true; continue; } uint32_t pixel; if (c == '0' || c == options.gfxDigits[0]) { pixel = 0; } else if (c == '1' || c == options.gfxDigits[1]) { pixel = 1; } else if (c == '2' || c == options.gfxDigits[2]) { pixel = 2; } else if (c == '3' || c == options.gfxDigits[3]) { pixel = 3; } else { break; } nonDigit = false; if (width < 8) { bitPlaneLower = bitPlaneLower << 1 | (pixel & 1); bitPlaneUpper = bitPlaneUpper << 1 | (pixel >> 1); } if (width < 9) { ++width; } } if (width == 0) { error("Invalid graphics constant, no digits after '`'"); } else if (width == 9) { warning( WARNING_LARGE_CONSTANT, "Graphics constant is too large; only first 8 pixels considered" ); } if (nonDigit) { error("Invalid graphics constant, trailing '_'"); } return bitPlaneUpper << 8 | bitPlaneLower; } // Functions to read identifiers and keywords static Token readIdentifier(char firstChar, bool raw) { std::string identifier(1, firstChar); bool keywordBeforeLocal = false; int tokenType = firstChar == '.' ? T_(LOCAL) : T_(SYMBOL); // Continue reading while the char is in the identifier charset for (int c = peek(); continuesIdentifier(c); c = nextChar()) { // If the char was a dot, the identifier is a local label if (c == '.') { // Check for a keyword before a non-raw local label if (!raw && tokenType != T_(LOCAL) && keywords.find(identifier) != keywords.end()) { keywordBeforeLocal = true; } tokenType = T_(LOCAL); } identifier += c; } // Check for a keyword if the identifier is not raw and not a local label if (!raw && tokenType != T_(LOCAL)) { if (auto search = keywords.find(identifier); search != keywords.end()) { return Token(search->second); } } // Label scopes `.` and `..` are the only nonlocal identifiers that start with a dot if (sym_IsDotScope(identifier)) { tokenType = T_(SYMBOL); } // A keyword before a non-raw local label is an error if (keywordBeforeLocal) { error( "Identifier \"%s\" begins with a keyword; did you mean to put a space between them?", identifier.c_str() ); } return Token(tokenType, identifier); } // Functions to read strings static std::pair> readInterpolation(size_t depth) { if (depth > options.maxRecursionDepth) { fatal("Recursion limit (%zu) exceeded", options.maxRecursionDepth); } std::string identifier; FormatSpec fmt{}; for (;;) { // Use `consumeChar()` since `peek()` might expand nested interpolations and recursively // call `readInterpolation()`, which can cause stack overflow. if (consumeChar('{')) { if (auto interp = readInterpolation(depth + 1); interp.first && interp.second) { beginExpansion(interp.second, interp.first->name); } continue; // Restart, reading from the new buffer } else if (int c = peek(); c == EOF || isNewline(c) || c == '"') { error("Missing '}'"); break; } else if (c == '}') { shiftChar(); break; } else if (c == ':' && !fmt.isParsed()) { // Format spec, only once shiftChar(); size_t n = fmt.parseSpec(identifier.c_str()); if (!fmt.isValid() || n != identifier.length()) { error("Invalid format spec \"%s\"", identifier.c_str()); } identifier.clear(); // Now that format has been set, restart at beginning of string } else { shiftChar(); identifier += c; } } if (identifier.starts_with('#')) { // Skip a '#' raw symbol prefix, but after expanding any nested interpolations. identifier.erase(0, 1); } else if (keywords.find(identifier) != keywords.end()) { // Don't allow symbols that alias keywords without a '#' prefix. error( "Interpolated symbol `%s` is a reserved keyword; add a '#' prefix to use it as a raw " "symbol", identifier.c_str() ); return {nullptr, nullptr}; } if (Symbol const *sym = sym_FindScopedValidSymbol(identifier); !sym || !sym->isDefined()) { if (sym_IsPurgedScoped(identifier)) { error("Interpolated symbol `%s` does not exist; it was purged", identifier.c_str()); } else { error("Interpolated symbol `%s` does not exist", identifier.c_str()); } return {sym, nullptr}; } else if (sym->type == SYM_EQUS) { auto buf = std::make_shared(); fmt.appendString(*buf, *sym->getEqus()); return {sym, buf}; } else if (sym->isNumeric()) { auto buf = std::make_shared(); fmt.appendNumber(*buf, sym->getConstantValue()); return {sym, buf}; } else { error("Interpolated symbol `%s` is not a numeric or string symbol", identifier.c_str()); return {sym, nullptr}; } } static void appendExpandedString(std::string &str, std::string const &expanded) { if (lexerState->mode != LEXER_RAW) { str.append(expanded); return; } str.reserve(str.length() + expanded.length()); for (char c : expanded) { // Escape characters that need escaping switch (c) { case '\n': str += "\\n"; break; // LCOV_EXCL_START case '\r': // A literal CR in a string may get treated as a LF, so '\r' is not tested. str += "\\r"; break; // LCOV_EXCL_STOP case '\t': str += "\\t"; break; case '\0': str += "\\0"; break; case '\\': case '"': case '\'': case '{': str += '\\'; [[fallthrough]]; default: str += c; break; } } } static void appendCharInLiteral(std::string &str, int c) { bool rawMode = lexerState->mode == LEXER_RAW; // Symbol interpolation if (c == '{') { // We'll be exiting the string/character scope, so re-enable expansions lexerState->disableExpansions = false; if (auto interp = readInterpolation(0); interp.second) { appendExpandedString(str, *interp.second); } lexerState->disableExpansions = true; return; } // Regular characters will just get copied if (c != '\\') { str += c; return; } c = peek(); switch (c) { // Character escape case '\\': case '"': case '\'': case '{': case '}': if (rawMode) { str += '\\'; } str += c; shiftChar(); break; case 'n': str += rawMode ? "\\n" : "\n"; shiftChar(); break; case 'r': str += rawMode ? "\\r" : "\r"; shiftChar(); break; case 't': str += rawMode ? "\\t" : "\t"; shiftChar(); break; case '0': if (rawMode) { str += "\\0"; } else { str += '\0'; } shiftChar(); break; // Line continuation case ' ': case '\t': case '\r': case '\n': discardLineContinuation(); break; // Macro arg case '@': case '#': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '<': if (std::shared_ptr arg = readMacroArg(); arg) { appendExpandedString(str, *arg); } break; case EOF: // Can't really print that one error("Illegal character escape '\\' at end of input"); str += '\\'; break; default: error("Illegal character escape %s", printChar(c)); str += c; shiftChar(); break; } } static void readString(std::string &str, bool rawString) { Defer reenableExpansions = scopedDisableExpansions(); bool rawMode = lexerState->mode == LEXER_RAW; // We reach this function after reading a single quote, but we also support triple quotes bool multiline = false; if (rawMode) { str += '"'; } if (peek() == '"') { if (rawMode) { str += '"'; } shiftChar(); // Use `consumeChar()` since `peek()` would mark the third character here as "painted blue" // whether or not it is a third quote, which would incorrectly prevent expansions right // after an empty string "". if (!consumeChar('"')) { // "" is an empty string, skip the loop return; } // """ begins a multi-line string if (rawMode) { str += '"'; } multiline = true; } for (;;) { int c = peek(); // '\r', '\n' or EOF ends a single-line string early if (c == EOF || (!multiline && isNewline(c))) { error("Unterminated string"); return; } // We'll be staying in the string, so we can safely consume the char shiftChar(); // Handle '\r' or '\n' (in multiline strings only, already handled above otherwise) if (isNewline(c)) { handleCRLF(c); nextLine(); str += '\n'; continue; } if (c != '"') { // Append the character or handle special ones if (rawString) { str += c; } else { appendCharInLiteral(str, c); } continue; } // Close the string and return if it's terminated if (!multiline) { if (rawMode) { str += c; } return; } // Only """ ends a multi-line string if (peek() != '"') { str += c; continue; } if (nextChar() != '"') { str += "\"\""; continue; } shiftChar(); if (rawMode) { str += "\"\"\""; } return; } } static void readCharacter(std::string &str) { // This is essentially a simplified `readString` Defer reenableExpansions = scopedDisableExpansions(); bool rawMode = lexerState->mode == LEXER_RAW; // We reach this function after reading a single quote if (rawMode) { str += '\''; } for (;;) { switch (int c = peek(); c) { case '\r': case '\n': case EOF: // '\r', '\n' or EOF ends a character early error("Unterminated character"); return; case '\'': // Close the character and return if it's terminated shiftChar(); if (rawMode) { str += c; } return; default: // Append the character or handle special ones shiftChar(); appendCharInLiteral(str, c); } } } // Lexer core static Token yylex_SKIP_TO_ENDC(); // Forward declaration for `yylex_NORMAL` // Must stay in sync with the `switch` in `yylex_NORMAL`! static bool isGarbageCharacter(int c) { // EOF is not garbage (it can't be reported anyway) if (c == EOF) { return false; } // Whitespace characters are not garbage, even the non-"printable" ones if (isWhitespace(c)) { return false; } // Printable characters which are nevertheless garbage: braces should have been interpolated if (c == '{' || c == '}') { return true; } // All other printable characters are not garbage (i.e. `yylex_NORMAL` handles them), and // all other nonprintable characters are garbage (including '\0') return !isPrintable(c); } static void reportGarbageCharacters(int c) { // '#' can be garbage if it doesn't start a raw string or identifier assume(isGarbageCharacter(c) || c == '#'); bool isAscii = isPrintable(c); if (isGarbageCharacter(peek())) { // At least two characters are garbage; group them into one error report std::string garbage = printChar(c); while (isGarbageCharacter(peek())) { c = bumpChar(); isAscii &= isPrintable(c); garbage += ", "; garbage += printChar(c); } error("Invalid characters %s%s", garbage.c_str(), isAscii ? "" : " (is the file UTF-8?)"); } else { error("Invalid character %s%s", printChar(c), isAscii ? "" : " (is the file UTF-8?)"); } } static Token oneOrTwo(int c, int longer, int shorter) { if (peek() == c) { shiftChar(); return Token(longer); } return Token(shorter); } static Token oneOrTwo(int c1, int longer1, int c2, int longer2, int shorter) { if (int c = peek(); c == c1) { shiftChar(); return Token(longer1); } else if (c == c2) { shiftChar(); return Token(longer2); } else { return Token(shorter); } } static Token yylex_NORMAL() { if (int nextToken = lexerState->nextToken; nextToken) { lexerState->nextToken = 0; return Token(nextToken); } for (;;) { int c = bumpChar(); switch (c) { // Ignore blank space and comments case ';': discardComment(); [[fallthrough]]; case ' ': case '\t': break; // Handle unambiguous single-char tokens case '~': return Token(T_(OP_NOT)); case '?': return Token(T_(QUESTIONMARK)); case '@': { std::string symName("@"); return Token(T_(SYMBOL), symName); } case '(': return Token(T_(LPAREN)); case ')': return Token(T_(RPAREN)); case ',': return Token(T_(COMMA)); // Handle ambiguous 1- or 2-char tokens case '[': // Either [ or [[ return oneOrTwo('[', T_(LBRACKS), T_(LBRACK)); case ']': // Either ] or ]] if (peek() == ']') { shiftChar(); // `[[ Fragment literals ]]` inject an EOL token to end their contents // even without a newline. Retroactively lex the `]]` after it. lexerState->nextToken = T_(RBRACKS); return Token(T_(EOL)); } return Token(T_(RBRACK)); case '+': // Either +=, ADD, or CAT return oneOrTwo('=', T_(POP_ADDEQ), '+', T_(OP_CAT), T_(OP_ADD)); case '-': // Either -= or SUB return oneOrTwo('=', T_(POP_SUBEQ), T_(OP_SUB)); case '*': // Either *=, MUL, or EXP return oneOrTwo('=', T_(POP_MULEQ), '*', T_(OP_EXP), T_(OP_MUL)); case '/': // Either /=, DIV, or a block comment if (peek() == '*') { shiftChar(); discardBlockComment(); break; } return oneOrTwo('=', T_(POP_DIVEQ), T_(OP_DIV)); case '|': // Either |=, binary OR, or logical OR return oneOrTwo('=', T_(POP_OREQ), '|', T_(OP_LOGICOR), T_(OP_OR)); case '^': // Either ^= or XOR return oneOrTwo('=', T_(POP_XOREQ), T_(OP_XOR)); // Handle ambiguous 1-, 2-, or 3-char tokens case '=': // Either assignment, EQ or string EQ if (peek() == '=') { shiftChar(); return oneOrTwo('=', T_(OP_STREQU), T_(OP_LOGICEQU)); } return Token(T_(POP_EQUAL)); case '!': // Either negation, NEQ, or string NEQ if (peek() == '=') { shiftChar(); return oneOrTwo('=', T_(OP_STRNE), T_(OP_LOGICNE)); } return Token(T_(OP_LOGICNOT)); case '<': // Either <<=, LT, LTE, or left shift if (peek() == '<') { shiftChar(); return oneOrTwo('=', T_(POP_SHLEQ), T_(OP_SHL)); } return oneOrTwo('=', T_(OP_LOGICLE), T_(OP_LOGICLT)); case '>': // Either >>=, GT, GTE, or either kind of right shift if (peek() == '>') { shiftChar(); return oneOrTwo('=', T_(POP_SHREQ), '>', T_(OP_USHR), T_(OP_SHR)); } return oneOrTwo('=', T_(OP_LOGICGE), T_(OP_LOGICGT)); case ':': // Either :, ::, or an anonymous label ref c = peek(); if (c == '+' || c == '-') { std::string symName = readAnonLabelRef(c); return Token(T_(ANON), symName); } return oneOrTwo(':', T_(DOUBLE_COLON), T_(COLON)); // Handle numbers case '0': // Decimal, fixed-point, or base-prefix number switch (peek()) { case 'x': case 'X': shiftChar(); return Token(T_(NUMBER), readHexNumber("\"0x\"")); case 'o': case 'O': shiftChar(); return Token(T_(NUMBER), readOctalNumber("\"0o\"")); case 'b': case 'B': shiftChar(); return Token(T_(NUMBER), readBinaryNumber("\"0b\"")); } [[fallthrough]]; // Decimal or fixed-point number case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { uint32_t n = readDecimalNumber(c); if (peek() == '.') { shiftChar(); n = readFractionalPart(n); } return Token(T_(NUMBER), n); } case '&': // Either &=, binary AND, logical AND, or an octal constant c = peek(); if (isOctDigit(c) || c == '_') { return Token(T_(NUMBER), readOctalNumber("'&'")); } return oneOrTwo('=', T_(POP_ANDEQ), '&', T_(OP_LOGICAND), T_(OP_AND)); case '%': // Either %=, MOD, or a binary constant c = peek(); if (isCustomBinDigit(c) || c == '_') { return Token(T_(NUMBER), readBinaryNumber("'%'")); } return oneOrTwo('=', T_(POP_MODEQ), T_(OP_MOD)); case '$': // Hex constant return Token(T_(NUMBER), readHexNumber("'$'")); case '`': // Gfx constant return Token(T_(NUMBER), readGfxConstant()); // Handle string and character literals case '"': { std::string str; readString(str, false); return Token(T_(STRING), str); } case '\'': { std::string chr; readCharacter(chr); return Token(T_(CHARACTER), chr); } // Handle newlines and EOF case '\r': handleCRLF(c); [[fallthrough]]; case '\n': return Token(T_(NEWLINE)); case EOF: return Token(T_(YYEOF)); // Handle line continuations case '\\': // Macro args were handled by `peek`, and character escapes do not exist // outside of string literals, so this must be a line continuation. discardLineContinuation(); break; // Handle raw strings... or fall through if '#' is not followed by '"' case '#': if (peek() == '"') { shiftChar(); std::string str; readString(str, true); return Token(T_(STRING), str); } [[fallthrough]]; // Handle identifiers... or report garbage characters default: bool raw = c == '#'; if (raw && startsIdentifier(peek())) { c = bumpChar(); } else if (!startsIdentifier(c)) { reportGarbageCharacters(c); break; } Token token = readIdentifier(c, raw); // An ELIF after a taken IF needs to not evaluate its condition if (token.type == T_(POP_ELIF) && lexerState->lastToken == T_(NEWLINE) && lexer_GetIFDepth() > 0 && lexer_RanIFBlock() && !lexer_ReachedELSEBlock()) { return yylex_SKIP_TO_ENDC(); } // If a keyword, don't try to expand if (token.type != T_(SYMBOL) && token.type != T_(LOCAL)) { return token; } // `token` is either a `SYMBOL` or a `LOCAL`, and both have a `std::string` value. assume(std::holds_alternative(token.value)); std::string const &identifier = std::get(token.value); // Raw symbols and local symbols cannot be string expansions if (!raw && token.type == T_(SYMBOL) && lexerState->expandStrings) { // Attempt string expansion if (Symbol const *sym = sym_FindExactSymbol(identifier); sym && sym->type == SYM_EQUS) { beginExpansion(sym->getEqus(), sym->name); continue; // Restart, reading from the new buffer } } // We need to distinguish between: // - label definitions (which are followed by a ':' and use the token `LABEL`) // - quiet macro invocations (which are followed by a '?' and use the token `QMACRO`) // - regular macro invocations (which use the token `SYMBOL`) // - label scopes "." and ".." (which use the token `SYMBOL` no matter what) // // If we had one `IDENTIFIER` token, the parser would need to perform "lookahead" to // determine which rule applies. But since macros need to enter "raw" mode to parse // their arguments, which may not even be valid tokens in "normal" mode, we cannot use // lookahead to check for the presence of a `COLON` or `QUESTIONMARK`. // // Instead, we have separate `SYMBOL`, `LABEL`, and `QMACRO` tokens, and decide which // one to lex depending on the character *immediately* following the identifier. // Thus "name:" is a label definition, and "name?" is a quiet macro invocation, but // "name :" and "name ?" and just "name" are all regular macro invocations. if (token.type == T_(SYMBOL) && !sym_IsDotScope(identifier)) { c = peek(); token.type = c == ':' ? T_(LABEL) : c == '?' ? T_(QMACRO) : T_(SYMBOL); } return token; } // If we exited the switch, i.e. read some characters without yet returning a token, // we can't be at the start of the line lexerState->atLineStart = false; } } static Token yylex_RAW() { // This is essentially a highly modified `readString` std::string str; int c; for (size_t parenDepth = 0;;) { c = peek(); switch (c) { case '"': // String literals inside macro args shiftChar(); readString(str, false); break; case '\'': // Character literals inside macro args shiftChar(); readCharacter(str); break; case '#': // Raw string literals inside macro args str += c; if (nextChar() == '"') { shiftChar(); readString(str, true); } break; case ';': // Comments inside macro args discardComment(); c = peek(); [[fallthrough]]; case '\r': // End of line case '\n': case EOF: goto finish; case '/': // Block comments inside macro args if (nextChar() == '*') { shiftChar(); discardBlockComment(); continue; } str += c; // Append the slash break; case ',': // End of macro arg if (parenDepth == 0) { goto finish; } goto append; case '(': // Open parentheses inside macro args if (parenDepth < UINT_MAX) { ++parenDepth; } goto append; case ')': // Close parentheses inside macro args if (parenDepth > 0) { --parenDepth; } goto append; case '\\': // Character escape c = nextChar(); switch (c) { case ',': // Escapes only valid inside a macro arg case '(': case ')': case '\\': // Escapes shared with string literals case '"': case '\'': case '{': case '}': break; case 'n': c = '\n'; break; case 'r': c = '\r'; break; case 't': c = '\t'; break; case '0': c = '\0'; break; case ' ': case '\t': case '\r': case '\n': discardLineContinuation(); continue; case EOF: // Can't really print that one error("Illegal character escape '\\' at end of input"); c = '\\'; break; // Macro args were already handled by peek, so '\@', // '\#', and '\0'-'\9' should not occur here. default: error("Illegal character escape %s", printChar(c)); break; } [[fallthrough]]; default: // Regular characters will just get copied append: str += c; shiftChar(); break; } } finish: // Can't `break` out of a nested `for`-`switch` // Trim left and right blank space str.erase(str.begin(), std::find_if_not(RANGE(str), isBlankSpace)); str.erase(std::find_if_not(RRANGE(str), isBlankSpace).base(), str.end()); // Returning COMMAs to the parser would mean that two consecutive commas // (i.e. an empty argument) need to return two different tokens (STRING // then COMMA) without advancing the read. To avoid this, commas in raw // mode end the current macro argument but are not tokenized themselves. if (c == ',') { shiftChar(); return Token(T_(STRING), str); } // The last argument may end in a trailing comma, newline, or EOF. // To allow trailing commas, raw mode will continue after the last // argument, immediately lexing the newline or EOF again (i.e. with // an empty raw string before it). This will not be treated as a // macro argument. To pass an empty last argument, use a second // trailing comma. if (!str.empty()) { return Token(T_(STRING), str); } lexer_SetMode(LEXER_NORMAL); if (isNewline(c)) { shiftChar(); handleCRLF(c); return Token(T_(NEWLINE)); } return Token(T_(YYEOF)); } static int skipPastEOL() { if (lexerState->atLineStart) { lexerState->atLineStart = false; return skipChars(isBlankSpace); } for (;;) { if (int c = bumpChar(); c == EOF) { return EOF; } else if (isNewline(c)) { handleCRLF(c); nextLine(); return skipChars(isBlankSpace); } else if (c == '\\') { // Unconditionally skip the next char, including line continuations c = bumpChar(); if (isNewline(c)) { handleCRLF(c); nextLine(); } } } } // This function uses the fact that `IF` and `REPT` constructs are only valid // when there's nothing before them on their lines. This enables filtering // "meaningful" tokens (at line start) vs. "meaningless" (everything else) ones. // It's especially important due to macro args not being handled in this // state, and lexing them in "normal" mode potentially producing such tokens. static Token skipToLeadingIdentifier() { for (;;) { if (int c = skipPastEOL(); c == EOF) { return Token(T_(YYEOF)); } else if (startsIdentifier(c)) { shiftChar(); return readIdentifier(c, false); } } } static Token skipIfBlock(bool toEndc) { lexer_SetMode(LEXER_NORMAL); Defer reenableExpansions = scopedDisableExpansions(); for (uint32_t startingDepth = lexer_GetIFDepth();;) { switch (Token token = skipToLeadingIdentifier(); token.type) { case T_(YYEOF): return token; case T_(POP_IF): lexer_IncIFDepth(); break; case T_(POP_ELIF): if (lexer_ReachedELSEBlock()) { // This should be redundant, as the parser handles this error first. fatal("Found `ELIF` after an `ELSE` block"); // LCOV_EXCL_LINE } if (!toEndc && lexer_GetIFDepth() == startingDepth) { return token; } break; case T_(POP_ELSE): if (lexer_ReachedELSEBlock()) { fatal("Found `ELSE` after an `ELSE` block"); } lexer_ReachELSEBlock(); if (!toEndc && lexer_GetIFDepth() == startingDepth) { return token; } break; case T_(POP_ENDC): if (lexer_GetIFDepth() == startingDepth) { return token; } lexer_DecIFDepth(); break; } } } static Token yylex_SKIP_TO_ELIF() { return skipIfBlock(false); } static Token yylex_SKIP_TO_ENDC() { return skipIfBlock(true); } static Token yylex_SKIP_TO_ENDR() { lexer_SetMode(LEXER_NORMAL); // This does not have to look for an `ENDR` token because the entire `REPT` or `FOR` body has // been captured into the current fstack context, so it can just skip to the end of that // context, which yields an EOF. Defer reenableExpansions = scopedDisableExpansions(); for (;;) { switch (Token token = skipToLeadingIdentifier(); token.type) { case T_(YYEOF): return token; case T_(POP_IF): lexer_IncIFDepth(); break; case T_(POP_ENDC): lexer_DecIFDepth(); break; } } } yy::parser::symbol_type yylex() { if (lexerState->atLineStart && lexerStateEOL) { lexerState = lexerStateEOL; lexerStateEOL = nullptr; } if (lexerState->lastToken == T_(EOB) && yywrap()) { return yy::parser::make_YYEOF(); } if (lexerState->atLineStart) { nextLine(); } static Token (* const lexerModeFuncs[NB_LEXER_MODES])() = { yylex_NORMAL, yylex_RAW, yylex_SKIP_TO_ELIF, yylex_SKIP_TO_ENDC, yylex_SKIP_TO_ENDR, }; Token token = lexerModeFuncs[lexerState->mode](); // Captures end at their buffer's boundary no matter what if (token.type == T_(YYEOF) && !lexerState->capturing) { token.type = T_(EOB); } lexerState->lastToken = token.type; lexerState->atLineStart = token.type == T_(NEWLINE) || token.type == T_(EOB); // LCOV_EXCL_START verbosePrint(VERB_TRACE, "Lexed `%s` token\n", yy::parser::symbol_type(token.type).name()); // LCOV_EXCL_STOP if (std::holds_alternative(token.value)) { return yy::parser::symbol_type(token.type, std::get(token.value)); } else if (std::holds_alternative(token.value)) { return yy::parser::symbol_type(token.type, std::get(token.value)); } else { assume(std::holds_alternative(token.value)); return yy::parser::symbol_type(token.type); } } template static Capture makeCapture(char const *name, CallbackFnT callback) { // Due to parser internals, it reads the EOL after the expression before calling this. // Thus, we don't need to keep one in the buffer afterwards. // The following assumption checks that. assume(lexerState->atLineStart); assume(!lexerState->capturing && lexerState->captureBuf == nullptr); lexerState->capturing = true; lexerState->captureSize = 0; Capture capture = { .lineNo = lexer_GetLineNo(), .span = {.ptr = nullptr, .size = 0} }; if (std::holds_alternative(lexerState->content) && lexerState->expansions.empty()) { auto &view = std::get(lexerState->content); capture.span.ptr = view.makeSharedContentPtr(); } else { assume(lexerState->captureBuf == nullptr); lexerState->captureBuf = std::make_shared>(); // We'll retrieve the capture buffer when done capturing assume(capture.span.ptr == nullptr); } Defer reenableExpansions = scopedDisableExpansions(); for (;;) { nextLine(); if (int c = skipChars(isBlankSpace); startsIdentifier(c)) { shiftChar(); int tokenType = readIdentifier(c, false).type; if (size_t endTokenLength = callback(tokenType); endTokenLength > 0) { if (!capture.span.ptr) { // Retrieve the capture buffer now that we're done capturing capture.span.ptr = lexerState->makeSharedCaptureBufPtr(); } // Subtract the length of the ending token; we know we have read it exactly, not // e.g. an interpolation or EQUS expansion, since those are disabled. capture.span.size = lexerState->captureSize - endTokenLength; break; } } // Just consume characters until EOL or EOF if (int c = skipChars([](int d) { return d != EOF && !isNewline(d); }); c == EOF) { error("Unterminated %s", name); capture.span = {.ptr = nullptr, .size = lexerState->captureSize}; break; } else { assume(isNewline(c)); shiftChar(); handleCRLF(c); } } lexerState->atLineStart = false; // The ending token or EOF puts us past the start of the line lexerState->capturing = false; lexerState->captureBuf = nullptr; return capture; } Capture lexer_CaptureRept() { size_t depth = 0; return makeCapture("loop (`REPT`/`FOR` block)", [&depth](int tokenType) { if (tokenType == T_(POP_REPT) || tokenType == T_(POP_FOR)) { ++depth; } else if (tokenType == T_(POP_ENDR)) { if (depth == 0) { return literal_strlen("ENDR"); } --depth; } return 0; }); } Capture lexer_CaptureMacro() { return makeCapture("macro definition", [](int tokenType) { return tokenType == T_(POP_ENDM) ? literal_strlen("ENDM") : 0; }); } gbdev-rgbds-92bfe5d/src/asm/macro.cpp000066400000000000000000000035231512540461700175660ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "asm/macro.hpp" #include #include #include #include #include #include "asm/warning.hpp" std::shared_ptr MacroArgs::getArg(int32_t i) const { // Bracketed macro arguments adjust negative indexes such that -1 is the last argument. if (i < 0) { i += args.size() + 1; } int32_t realIndex = i + shift - 1; return realIndex < 0 || static_cast(realIndex) >= args.size() ? nullptr : args[realIndex]; } std::shared_ptr MacroArgs::getAllArgs() const { size_t nbArgs = args.size(); if (shift >= nbArgs) { return std::make_shared(""); } size_t len = 0; for (uint32_t i = shift; i < nbArgs; ++i) { len += args[i]->length() + 1; // 1 for comma } auto str = std::make_shared(); str->reserve(len + 1); // 1 for comma for (uint32_t i = shift; i < nbArgs; ++i) { std::shared_ptr const &arg = args[i]; str->append(*arg); // Commas go between args and after a last empty arg if (i < nbArgs - 1 || arg->empty()) { str->push_back(','); // no space after comma } } return str; } void MacroArgs::appendArg(std::shared_ptr arg) { if (arg->empty()) { warning(WARNING_EMPTY_MACRO_ARG, "Empty macro argument"); } args.push_back(arg); } void MacroArgs::shiftArgs(int32_t count) { if (size_t nbArgs = args.size(); count > 0 && (static_cast(count) > nbArgs || shift > nbArgs - count)) { warning(WARNING_MACRO_SHIFT, "Cannot shift macro arguments past their end"); shift = nbArgs; } else if (count < 0 && shift < static_cast(-count)) { warning(WARNING_MACRO_SHIFT, "Cannot shift macro arguments past their beginning"); shift = 0; } else { shift += count; } } gbdev-rgbds-92bfe5d/src/asm/main.cpp000066400000000000000000000403621512540461700174130ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "asm/main.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "backtrace.hpp" #include "cli.hpp" #include "diagnostics.hpp" #include "helpers.hpp" #include "parser.hpp" // Generated from parser.y #include "platform.hpp" #include "style.hpp" #include "usage.hpp" #include "util.hpp" // UpperMap #include "verbosity.hpp" #include "asm/charmap.hpp" #include "asm/fstack.hpp" #include "asm/opt.hpp" #include "asm/output.hpp" #include "asm/section.hpp" #include "asm/symbol.hpp" #include "asm/warning.hpp" Options options; // Flags which must be processed after the option parsing finishes static struct LocalOptions { std::optional dependFileName; // -M std::unordered_map> stateFileSpecs; // -s std::optional inputFileName; // } localOptions; // Short options static char const *optstring = "B:b:D:Eg:hI:M:o:P:p:Q:r:s:VvW:wX:"; // Long-only option variable static int longOpt; // `--color` and variants of `-M` // Equivalent long options // Please keep in the same order as short opts. // Also, make sure long opts don't create ambiguity: // A long opt's name should start with the same letter as its short opt, // except if it doesn't create any ambiguity (`verbose` versus `version`). // This is because long opt matching, even to a single char, is prioritized // over short opt matching. static option const longopts[] = { {"backtrace", required_argument, nullptr, 'B'}, {"binary-digits", required_argument, nullptr, 'b'}, {"define", required_argument, nullptr, 'D'}, {"export-all", no_argument, nullptr, 'E'}, {"gfx-chars", required_argument, nullptr, 'g'}, {"help", no_argument, nullptr, 'h'}, {"include", required_argument, nullptr, 'I'}, {"dependfile", required_argument, nullptr, 'M'}, {"output", required_argument, nullptr, 'o'}, {"preinclude", required_argument, nullptr, 'P'}, {"pad-value", required_argument, nullptr, 'p'}, {"q-precision", required_argument, nullptr, 'Q'}, {"recursion-depth", required_argument, nullptr, 'r'}, {"state", required_argument, nullptr, 's'}, {"version", no_argument, nullptr, 'V'}, {"verbose", no_argument, nullptr, 'v'}, {"warning", required_argument, nullptr, 'W'}, {"max-errors", required_argument, nullptr, 'X'}, {"color", required_argument, &longOpt, 'c'}, {"MC", no_argument, &longOpt, 'C'}, {"MG", no_argument, &longOpt, 'G'}, {"MP", no_argument, &longOpt, 'P'}, {"MQ", required_argument, &longOpt, 'Q'}, {"MT", required_argument, &longOpt, 'T'}, {nullptr, no_argument, nullptr, 0 }, }; // clang-format off: nested initializers static Usage usage = { .name = "rgbasm", .flags = { "[-EhVvw]", "[-B depth]", "[-b chars]", "[-D name[=value]]", "[-g chars]", "[-I path]", "[-M depend_file]", "[-MC]", "[-MG]", "[-MP]", "[-MT target_file]", "[-MQ target_file]", "[-o out_file]", "[-P include_file]", "[-p pad_value]", "[-Q precision]", "[-r depth]", "[-s features:state_file]", "[-W warning]", "[-X max_errors]", "", }, .options = { {{"-E", "--export-all"}, {"export all labels"}}, {{"-M", "--dependfile "}, {"set the output dependency file"}}, {{"-o", "--output "}, {"set the output object file"}}, {{"-p", "--pad-value "}, {"set the value to use for `DS`"}}, {{"-s", "--state :"}, {"set an output state file"}}, {{"-V", "--version"}, {"print RGBASM version and exit"}}, {{"-W", "--warning "}, {"enable or disable warnings"}}, }, }; // clang-format on static std::string escapeMakeChars(std::string &str) { std::string escaped; size_t pos = 0; for (;;) { // All dollars needs to be doubled size_t nextPos = str.find('$', pos); if (nextPos == std::string::npos) { break; } escaped.append(str, pos, nextPos - pos); escaped.append("$$"); pos = nextPos + literal_strlen("$"); } escaped.append(str, pos, str.length() - pos); return escaped; } // Parse a comma-separated string of '-s/--state' features static std::vector parseStateFeatures(char *str) { std::vector features; for (char *feature = str; feature;) { // Split "," so `feature` is "" and `next` is "" char *next = strchr(feature, ','); if (next) { *next++ = '\0'; } // Trim blank spaces from the beginning of `feature`... feature += strspn(feature, " \t"); // ...and from the end if (char *end = strpbrk(feature, " \t"); end) { *end = '\0'; } // A feature must be specified if (*feature == '\0') { fatal("Empty feature for option '-s'"); } // Parse the `feature` and update the `features` list static UpperMap const featureNames{ {"EQU", STATE_EQU }, {"VAR", STATE_VAR }, {"EQUS", STATE_EQUS }, {"CHAR", STATE_CHAR }, {"MACRO", STATE_MACRO}, }; if (!strcasecmp(feature, "all")) { if (!features.empty()) { warnx("Redundant feature before \"%s\" for option '-s'", feature); } features.assign({STATE_EQU, STATE_VAR, STATE_EQUS, STATE_CHAR, STATE_MACRO}); } else if (auto search = featureNames.find(feature); search == featureNames.end()) { fatal("Invalid feature for option '-s': \"%s\"", feature); } else if (StateFeature value = search->second; std::find(RANGE(features), value) != features.end()) { warnx("Ignoring duplicate feature for option '-s': \"%s\"", feature); } else { features.push_back(value); } feature = next; } return features; } static void parseArg(int ch, char *arg) { switch (ch) { case 'B': if (!trace_ParseTraceDepth(arg)) { fatal("Invalid argument for option '-B'"); } break; case 'b': if (strlen(arg) == 2) { opt_B(arg); } else { fatal("Must specify exactly 2 characters for option '-b'"); } break; case 'D': { char *equals = strchr(arg, '='); if (equals) { *equals = '\0'; sym_AddString(arg, std::make_shared(equals + 1)); } else { sym_AddString(arg, std::make_shared("1")); } break; } case 'E': options.exportAll = true; break; case 'g': if (strlen(arg) == 4) { opt_G(arg); } else { fatal("Must specify exactly 4 characters for option '-g'"); } break; // LCOV_EXCL_START case 'h': usage.printAndExit(0); // LCOV_EXCL_STOP case 'I': fstk_AddIncludePath(arg); break; case 'M': if (localOptions.dependFileName) { warnx( "Overriding dependency file \"%s\"", *localOptions.dependFileName == "-" ? "" : localOptions.dependFileName->c_str() ); } localOptions.dependFileName = arg; break; case 'o': if (options.objectFileName) { warnx("Overriding output file \"%s\"", options.objectFileName->c_str()); } options.objectFileName = arg; break; case 'P': fstk_AddPreIncludeFile(arg); break; case 'p': if (std::optional padByte = parseWholeNumber(arg); !padByte) { fatal("Invalid argument for option '-p'"); } else if (*padByte > 0xFF) { fatal("Argument for option '-p' must be between 0 and 0xFF"); } else { opt_P(*padByte); } break; case 'Q': { char const *precisionArg = arg; if (precisionArg[0] == '.') { ++precisionArg; } if (std::optional precision = parseWholeNumber(precisionArg); !precision) { fatal("Invalid argument for option '-Q'"); } else if (*precision < 1 || *precision > 31) { fatal("Argument for option '-Q' must be between 1 and 31"); } else { opt_Q(*precision); } break; } case 'r': if (std::optional maxDepth = parseWholeNumber(arg); !maxDepth) { fatal("Invalid argument for option '-r'"); } else if (errno == ERANGE) { fatal("Argument for option '-r' is out of range"); } else { options.maxRecursionDepth = *maxDepth; } break; case 's': { // Split ":" so `arg` is "" and `name` is "" char *name = strchr(arg, ':'); if (!name) { fatal("Invalid argument for option '-s'"); } *name++ = '\0'; std::vector features = parseStateFeatures(arg); if (localOptions.stateFileSpecs.find(name) != localOptions.stateFileSpecs.end()) { warnx("Overriding state file \"%s\"", name); } localOptions.stateFileSpecs.emplace(name, std::move(features)); break; } // LCOV_EXCL_START case 'V': usage.printVersion(false); exit(0); case 'v': incrementVerbosity(); break; // LCOV_EXCL_STOP case 'W': opt_W(arg); break; case 'w': warnings.state.warningsEnabled = false; break; case 'X': if (std::optional maxErrors = parseWholeNumber(arg); !maxErrors) { fatal("Invalid argument for option '-X'"); } else if (*maxErrors > UINT64_MAX) { fatal("Argument for option '-X' must be between 0 and %" PRIu64, UINT64_MAX); } else { options.maxErrors = *maxErrors; } break; case 0: // Long-only options switch (longOpt) { case 'c': if (!style_Parse(arg)) { fatal("Invalid argument for option '--color'"); } break; case 'C': options.missingIncludeState = GEN_CONTINUE; break; case 'G': options.missingIncludeState = GEN_EXIT; break; case 'P': options.generatePhonyDeps = true; break; case 'Q': case 'T': { std::string newTarget = arg; if (longOpt == 'Q') { newTarget = escapeMakeChars(newTarget); } if (options.targetFileName) { *options.targetFileName += ' '; *options.targetFileName += newTarget; } else { options.targetFileName = newTarget; } break; } } break; case 1: // Positional argument if (localOptions.inputFileName) { usage.printAndExit("More than one input file specified"); } localOptions.inputFileName = arg; break; // LCOV_EXCL_START default: usage.printAndExit(1); // LCOV_EXCL_STOP } } // LCOV_EXCL_START static void verboseOutputConfig() { if (!checkVerbosity(VERB_CONFIG)) { return; } style_Set(stderr, STYLE_MAGENTA, false); usage.printVersion(true); printVVVVVVerbosity(); fputs("Options:\n", stderr); // -E/--export-all if (options.exportAll) { fputs("\tExport all labels by default\n", stderr); } // -b/--binary-digits if (options.binDigits[0] != '0' || options.binDigits[1] != '1') { fprintf( stderr, "\tBinary digits: '%c', '%c'\n", options.binDigits[0], options.binDigits[1] ); } // -g/--gfx-chars if (options.gfxDigits[0] != '0' || options.gfxDigits[1] != '1' || options.gfxDigits[2] != '2' || options.gfxDigits[3] != '3') { fprintf( stderr, "\tGraphics characters: '%c', '%c', '%c', '%c'\n", options.gfxDigits[0], options.gfxDigits[1], options.gfxDigits[2], options.gfxDigits[3] ); } // -Q/--q-precision fprintf( stderr, "\tFixed-point precision: Q%d.%" PRIu8 "\n", 32 - options.fixPrecision, options.fixPrecision ); // -p/--pad-value fprintf(stderr, "\tPad value: 0x%02" PRIx8 "\n", options.padByte); // -r/--recursion-depth fprintf(stderr, "\tMaximum recursion depth %zu\n", options.maxRecursionDepth); // -X/--max-errors if (options.maxErrors) { fprintf(stderr, "\tMaximum %" PRIu64 " errors\n", options.maxErrors); } // -D/--define static bool hasDefines = false; // `static` so `sym_ForEach` callback can see it sym_ForEach([](Symbol &sym) { if (!sym.isBuiltin && sym.type == SYM_EQUS) { if (!hasDefines) { fputs("\tDefinitions:\n", stderr); hasDefines = true; } fprintf(stderr, "\t - def %s equs \"%s\"\n", sym.name.c_str(), sym.getEqus()->c_str()); } }); // -s/--state if (!localOptions.stateFileSpecs.empty()) { fputs("\tOutput state files:\n", stderr); static char const *featureNames[NB_STATE_FEATURES] = { "equ", "var", "equs", "char", "macro", }; for (auto const &[name, features] : localOptions.stateFileSpecs) { fprintf(stderr, "\t - %s: ", name == "-" ? "" : name.c_str()); for (size_t i = 0; i < features.size(); ++i) { if (i > 0) { fputs(", ", stderr); } fputs(featureNames[features[i]], stderr); } putc('\n', stderr); } } // asmfile if (localOptions.inputFileName) { fprintf( stderr, "\tInput asm file: %s\n", *localOptions.inputFileName == "-" ? "" : localOptions.inputFileName->c_str() ); } // -o/--output if (options.objectFileName) { fprintf(stderr, "\tOutput object file: %s\n", options.objectFileName->c_str()); } fstk_VerboseOutputConfig(); if (localOptions.dependFileName) { fprintf(stderr, "\tOutput dependency file: %s\n", localOptions.dependFileName->c_str()); // -MT or -MQ if (options.targetFileName) { fprintf(stderr, "\tTarget file(s): %s\n", options.targetFileName->c_str()); } // -MG or -MC switch (options.missingIncludeState) { case INC_ERROR: fputs("\tExit with an error on a missing dependency\n", stderr); break; case GEN_EXIT: fputs("\tExit normally on a missing dependency\n", stderr); break; case GEN_CONTINUE: fputs("\tContinue processing after a missing dependency\n", stderr); break; } // -MP if (options.generatePhonyDeps) { fputs("\tGenerate phony dependencies\n", stderr); } } fputs("Ready for assembly\n", stderr); style_Reset(stderr); } // LCOV_EXCL_STOP int main(int argc, char *argv[]) { // Support SOURCE_DATE_EPOCH for reproducible builds // https://reproducible-builds.org/docs/source-date-epoch/ time_t now = time(nullptr); if (char const *sourceDateEpoch = getenv("SOURCE_DATE_EPOCH"); sourceDateEpoch) { // Use `strtoul`, not `parseWholeNumber`, because SOURCE_DATE_EPOCH does // not conventionally support our custom base prefixes now = static_cast(strtoul(sourceDateEpoch, nullptr, 0)); } sym_Init(now); // Maximum of 100 errors only applies if rgbasm is printing errors to a terminal if (isatty(STDERR_FILENO)) { options.maxErrors = 100; // LCOV_EXCL_LINE } cli_ParseArgs(argc, argv, optstring, longopts, parseArg, usage); if (!options.targetFileName && options.objectFileName) { options.targetFileName = options.objectFileName; } verboseOutputConfig(); if (!localOptions.inputFileName) { usage.printAndExit("No input file specified (pass \"-\" to read from standard input)"); } // LCOV_EXCL_START verbosePrint( VERB_NOTICE, "Assembling \"%s\"\n", *localOptions.inputFileName == "-" ? "" : localOptions.inputFileName->c_str() ); // LCOV_EXCL_STOP if (localOptions.dependFileName) { if (!options.targetFileName) { fatal("Dependency files can only be created if a target file is specified with either " "'-o', '-MQ' or '-MT'"); } if (*localOptions.dependFileName == "-") { options.dependFile = stdout; } else { options.dependFile = fopen(localOptions.dependFileName->c_str(), "w"); if (options.dependFile == nullptr) { // LCOV_EXCL_START fatal( "Failed to open dependency file \"%s\": %s", localOptions.dependFileName->c_str(), strerror(errno) ); // LCOV_EXCL_STOP } } } options.printDep(*localOptions.inputFileName); charmap_New(DEFAULT_CHARMAP_NAME, nullptr); // Init lexer and file stack, and parse (`yy::parser` is auto-generated from `parser.y`) if (yy::parser parser; fstk_Init(*localOptions.inputFileName) && parser.parse() != 0) { // Exited due to YYABORT or YYNOMEM fatal("Unrecoverable error while parsing"); // LCOV_EXCL_LINE } // If parse aborted without errors due to a missing INCLUDE, and `-MG` was given, exit normally if (fstk_FailedOnMissingInclude()) { requireZeroErrors(); return 0; } sect_CheckUnionClosed(); sect_CheckLoadClosed(); sect_CheckSizes(); charmap_CheckStack(); opt_CheckStack(); sect_CheckStack(); requireZeroErrors(); out_WriteObject(); for (auto const &[name, features] : localOptions.stateFileSpecs) { out_WriteState(name, features); } return 0; } gbdev-rgbds-92bfe5d/src/asm/opt.cpp000066400000000000000000000066751512540461700173020ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include #include // std::size #include #include #include #include #include #include #include "diagnostics.hpp" #include "util.hpp" #include "asm/fstack.hpp" #include "asm/lexer.hpp" #include "asm/main.hpp" // options #include "asm/warning.hpp" struct OptStackEntry { char binDigits[2]; char gfxDigits[4]; uint8_t fixPrecision; uint8_t padByte; size_t maxRecursionDepth; DiagnosticsState warningStates; }; static std::stack stack; void opt_B(char const binDigits[2]) { lexer_SetBinDigits(binDigits); } void opt_G(char const gfxDigits[4]) { lexer_SetGfxDigits(gfxDigits); } void opt_P(uint8_t padByte) { options.padByte = padByte; } void opt_Q(uint8_t fixPrecision) { options.fixPrecision = fixPrecision; } void opt_R(size_t maxRecursionDepth) { fstk_NewRecursionDepth(maxRecursionDepth); lexer_CheckRecursionDepth(); } void opt_W(char const *flag) { warnings.processWarningFlag(flag); } void opt_Parse(char const *s) { if (s[0] == '-') { ++s; // Skip a leading '-' } char c = *s++; while (isBlankSpace(*s)) { ++s; // Skip leading blank spaces } switch (c) { case 'b': if (strlen(s) == 2) { opt_B(s); } else { error("Must specify exactly 2 characters for option 'b'"); } break; case 'g': if (strlen(s) == 4) { opt_G(s); } else { error("Must specify exactly 4 characters for option 'g'"); } break; case 'p': if (std::optional padByte = parseWholeNumber(s); !padByte) { error("Invalid argument for option 'p'"); } else if (*padByte > 0xFF) { error("Argument for option 'p' must be between 0 and 0xFF"); } else { opt_P(*padByte); } break; case 'Q': if (s[0] == '.') { ++s; // Skip leading '.' } if (std::optional precision = parseWholeNumber(s); !precision) { error("Invalid argument for option 'Q'"); } else if (*precision < 1 || *precision > 31) { error("Argument for option 'Q' must be between 1 and 31"); } else { opt_Q(*precision); } break; case 'r': if (std::optional maxRecursionDepth = parseWholeNumber(s); !maxRecursionDepth) { error("Invalid argument for option 'r'"); } else if (errno == ERANGE) { error("Argument for option 'r' is out of range"); } else { opt_R(*maxRecursionDepth); } break; case 'W': if (strlen(s) > 0) { opt_W(s); } else { error("Must specify an argument for option 'W'"); } break; default: error("Unknown option '%c'", c); break; } } void opt_Push() { OptStackEntry entry; memcpy(entry.binDigits, options.binDigits, std::size(options.binDigits)); memcpy(entry.gfxDigits, options.gfxDigits, std::size(options.gfxDigits)); entry.padByte = options.padByte; entry.fixPrecision = options.fixPrecision; entry.maxRecursionDepth = options.maxRecursionDepth; entry.warningStates = warnings.state; stack.push(entry); } void opt_Pop() { if (stack.empty()) { error("No entries in the option stack"); return; } OptStackEntry entry = stack.top(); stack.pop(); opt_B(entry.binDigits); opt_G(entry.gfxDigits); opt_P(entry.padByte); opt_Q(entry.fixPrecision); opt_R(entry.maxRecursionDepth); // `opt_W` does not apply a whole warning state; it processes one flag string warnings.state = entry.warningStates; } void opt_CheckStack() { if (!stack.empty()) { warning(WARNING_UNMATCHED_DIRECTIVE, "`PUSHO` without corresponding `POPO`"); } } gbdev-rgbds-92bfe5d/src/asm/output.cpp000066400000000000000000000255131512540461700200300ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "asm/output.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "helpers.hpp" // assume, Defer #include "linkdefs.hpp" #include "platform.hpp" #include "asm/charmap.hpp" #include "asm/fstack.hpp" #include "asm/lexer.hpp" #include "asm/main.hpp" #include "asm/rpn.hpp" #include "asm/section.hpp" #include "asm/symbol.hpp" #include "asm/warning.hpp" struct Assertion { Patch patch; Section *section; std::string message; }; // List of symbols to put in the object file static std::vector objectSymbols; static std::deque assertions; static std::deque> fileStackNodes; static void putLong(uint32_t n, FILE *file) { uint8_t bytes[] = { static_cast(n), static_cast(n >> 8), static_cast(n >> 16), static_cast(n >> 24), }; fwrite(bytes, 1, sizeof(bytes), file); } static void putString(std::string const &s, FILE *file) { fputs(s.c_str(), file); putc('\0', file); } void out_RegisterNode(std::shared_ptr node) { // If node is not already registered, register it (and parents), and give it a unique ID for (; node && node->ID == UINT32_MAX; node = node->parent) { node->ID = fileStackNodes.size(); fileStackNodes.push_front(node); } } static void writePatch(Patch const &patch, FILE *file) { assume(patch.src->ID != UINT32_MAX); putLong(patch.src->ID, file); putLong(patch.lineNo, file); putLong(patch.offset, file); putLong(patch.pcSection ? patch.pcSection->getID() : UINT32_MAX, file); putLong(patch.pcOffset, file); putc(patch.type, file); putLong(patch.rpn.size(), file); fwrite(patch.rpn.data(), 1, patch.rpn.size(), file); } static void writeSection(Section const §, FILE *file) { assume(sect.src->ID != UINT32_MAX); putString(sect.name, file); putLong(sect.src->ID, file); putLong(sect.fileLine, file); putLong(sect.size, file); assume((sect.type & SECTTYPE_TYPE_MASK) == sect.type); bool isUnion = sect.modifier == SECTION_UNION; bool isFragment = sect.modifier == SECTION_FRAGMENT; putc(sect.type | isUnion << SECTTYPE_UNION_BIT | isFragment << SECTTYPE_FRAGMENT_BIT, file); putLong(sect.org, file); putLong(sect.bank, file); putc(sect.align, file); putLong(sect.alignOfs, file); if (sectTypeHasData(sect.type)) { fwrite(sect.data.data(), 1, sect.size, file); putLong(sect.patches.size(), file); for (Patch const &patch : sect.patches) { writePatch(patch, file); } } } static void writeSymbol(Symbol const &sym, FILE *file) { putString(sym.name, file); if (!sym.isDefined()) { putc(SYMTYPE_IMPORT, file); } else { assume(sym.src->ID != UINT32_MAX); Section *symSection = sym.getSection(); putc(sym.isExported ? SYMTYPE_EXPORT : SYMTYPE_LOCAL, file); putLong(sym.src->ID, file); putLong(sym.fileLine, file); putLong(symSection ? symSection->getID() : UINT32_MAX, file); putLong(sym.getOutputValue(), file); } } void out_RegisterSymbol(Symbol &sym) { // Check for `sym.src`, to skip any built-in symbol from rgbasm if (sym.src && sym.ID == UINT32_MAX && !sym_IsPC(&sym)) { sym.ID = objectSymbols.size(); // Set the symbol's ID within the object file objectSymbols.push_back(&sym); out_RegisterNode(sym.src); } } static void initPatch(Patch &patch, uint32_t type, Expression const &expr, uint32_t ofs) { patch.type = type; patch.src = fstk_GetFileStack(); // All patches are assumed to eventually be written, so the file stack node is registered out_RegisterNode(patch.src); patch.lineNo = lexer_GetLineNo(); patch.offset = ofs; patch.pcSection = sect_GetSymbolSection(); patch.pcOffset = sect_GetSymbolOffset(); expr.encode(patch.rpn); } void out_CreatePatch(uint32_t type, Expression const &expr, uint32_t ofs, uint32_t pcShift) { // Add the patch to the list assume(sect_GetOutputBank().has_value()); Patch &patch = *sect_AddOutputPatch(); initPatch(patch, type, expr, ofs); // If the patch had a quantity of bytes output before it, // PC is not at the patch's location, but at the location // before those bytes. patch.pcOffset -= pcShift; } void out_CreateAssert( AssertionType type, Expression const &expr, std::string const &message, uint32_t ofs ) { Assertion &assertion = assertions.emplace_front(); initPatch(assertion.patch, type, expr, ofs); assertion.message = message; } static void writeAssert(Assertion const &assert, FILE *file) { writePatch(assert.patch, file); putString(assert.message, file); } static void writeFileStackNode(FileStackNode const &node, FILE *file) { putLong(node.parent ? node.parent->ID : UINT32_MAX, file); putLong(node.lineNo, file); putc(node.type | node.isQuiet << FSTACKNODE_QUIET_BIT, file); if (node.type != NODE_REPT) { putString(node.name(), file); } else { std::vector const &nodeIters = node.iters(); putLong(nodeIters.size(), file); // Iters are stored by decreasing depth, so reverse the order for output for (uint32_t iter : reversed(nodeIters)) { putLong(iter, file); } } } void out_WriteObject() { if (!options.objectFileName) { return; } static FILE *file; // `static` so `sect_ForEach` callback can see it char const *objectFileName = options.objectFileName->c_str(); if (*options.objectFileName != "-") { file = fopen(objectFileName, "wb"); } else { objectFileName = ""; (void)setmode(STDOUT_FILENO, O_BINARY); file = stdout; } if (!file) { // LCOV_EXCL_START fatal("Failed to open object file \"%s\": %s", objectFileName, strerror(errno)); // LCOV_EXCL_STOP } Defer closeFile{[&] { fclose(file); }}; // Also write symbols that weren't written above sym_ForEach(out_RegisterSymbol); fputs(RGBDS_OBJECT_VERSION_STRING, file); putLong(RGBDS_OBJECT_REV, file); putLong(objectSymbols.size(), file); putLong(sect_CountSections(), file); putLong(fileStackNodes.size(), file); for (auto it = fileStackNodes.begin(); it != fileStackNodes.end(); ++it) { writeFileStackNode(**it, file); // The list is supposed to have decrementing IDs assume(it + 1 == fileStackNodes.end() || it[1]->ID == it[0]->ID - 1); } for (Symbol const *sym : objectSymbols) { writeSymbol(*sym, file); } sect_ForEach([](Section §) { writeSection(sect, file); }); putLong(assertions.size(), file); for (Assertion const &assert : assertions) { writeAssert(assert, file); } } static void dumpString(std::string const &escape, FILE *file) { for (char c : escape) { // Escape characters that need escaping switch (c) { case '\n': fputs("\\n", file); break; case '\r': fputs("\\r", file); break; case '\t': fputs("\\t", file); break; case '\0': fputs("\\0", file); break; case '\\': case '"': case '{': putc('\\', file); [[fallthrough]]; default: putc(c, file); break; } } } // Symbols are ordered by file, then by definition order static bool compareSymbols(Symbol const *sym1, Symbol const *sym2) { return sym1->defIndex < sym2->defIndex; } static bool dumpEquConstants(FILE *file) { static std::vector equConstants; // `static` so `sym_ForEach` callback can see it equConstants.clear(); sym_ForEach([](Symbol &sym) { if (!sym.isBuiltin && sym.type == SYM_EQU) { equConstants.push_back(&sym); } }); std::sort(RANGE(equConstants), compareSymbols); for (Symbol const *sym : equConstants) { uint32_t value = static_cast(sym->getOutputValue()); fprintf(file, "def %s equ $%" PRIx32 "\n", sym->name.c_str(), value); } return !equConstants.empty(); } static bool dumpVariables(FILE *file) { static std::vector variables; // `static` so `sym_ForEach` callback can see it variables.clear(); sym_ForEach([](Symbol &sym) { if (!sym.isBuiltin && sym.type == SYM_VAR) { variables.push_back(&sym); } }); std::sort(RANGE(variables), compareSymbols); for (Symbol const *sym : variables) { uint32_t value = static_cast(sym->getOutputValue()); fprintf(file, "def %s = $%" PRIx32 "\n", sym->name.c_str(), value); } return !variables.empty(); } static bool dumpEqusConstants(FILE *file) { static std::vector equsConstants; // `static` so `sym_ForEach` callback can see it equsConstants.clear(); sym_ForEach([](Symbol &sym) { if (!sym.isBuiltin && sym.type == SYM_EQUS) { equsConstants.push_back(&sym); } }); std::sort(RANGE(equsConstants), compareSymbols); for (Symbol const *sym : equsConstants) { fprintf(file, "def %s equs \"", sym->name.c_str()); dumpString(*sym->getEqus(), file); fputs("\"\n", file); } return !equsConstants.empty(); } static bool dumpCharmaps(FILE *file) { static FILE *charmapFile; // `static` so `charmap_ForEach` callbacks can see it charmapFile = file; // Characters are ordered by charmap, then by definition order return charmap_ForEach( [](std::string const &name) { fprintf(charmapFile, "newcharmap %s\n", name.c_str()); }, [](std::string const &mapping, std::vector value) { fputs("charmap \"", charmapFile); dumpString(mapping, charmapFile); putc('"', charmapFile); for (int32_t v : value) { fprintf(charmapFile, ", $%" PRIx32, v); } putc('\n', charmapFile); } ); } static bool dumpMacros(FILE *file) { static std::vector macros; // `static` so `sym_ForEach` callback can see it macros.clear(); sym_ForEach([](Symbol &sym) { if (!sym.isBuiltin && sym.type == SYM_MACRO) { macros.push_back(&sym); } }); std::sort(RANGE(macros), compareSymbols); for (Symbol const *sym : macros) { ContentSpan const &body = sym->getMacro(); fprintf(file, "macro %s\n", sym->name.c_str()); fwrite(body.ptr.get(), 1, body.size, file); fputs("endm\n", file); } return !macros.empty(); } void out_WriteState(std::string name, std::vector const &features) { // State files may include macro bodies, which may contain arbitrary characters, // so output as binary to preserve them. FILE *file; if (name != "-") { file = fopen(name.c_str(), "wb"); } else { name = ""; (void)setmode(STDOUT_FILENO, O_BINARY); file = stdout; } if (!file) { // LCOV_EXCL_START fatal("Failed to open state file \"%s\": %s", name.c_str(), strerror(errno)); // LCOV_EXCL_STOP } Defer closeFile{[&] { fclose(file); }}; static char const *dumpHeadings[NB_STATE_FEATURES] = { "Numeric constants", "Variables", "String constants", "Character maps", "Macros", }; static bool (* const dumpFuncs[NB_STATE_FEATURES])(FILE *) = { dumpEquConstants, dumpVariables, dumpEqusConstants, dumpCharmaps, dumpMacros, }; fputs("; File generated by rgbasm\n", file); for (StateFeature feature : features) { fprintf(file, "\n; %s\n", dumpHeadings[feature]); if (!dumpFuncs[feature](file)) { fputs("; No values\n", file); } } } gbdev-rgbds-92bfe5d/src/asm/parser.y000066400000000000000000001350751512540461700174570ustar00rootroot00000000000000// SPDX-License-Identifier: MIT %language "c++" %define api.value.type variant %define api.token.constructor %code requires { #include #include #include #include #include "linkdefs.hpp" #include "asm/actions.hpp" #include "asm/lexer.hpp" #include "asm/macro.hpp" #include "asm/rpn.hpp" #include "asm/section.hpp" struct ForArgs { int32_t start; int32_t stop; int32_t step; }; struct StrFmtArgList { std::string format; std::vector> args; }; } %code { #include #include #include #include #include #include #include #include "extern/utf8decoder.hpp" #include "helpers.hpp" #include "util.hpp" // toLower, toUpper #include "asm/charmap.hpp" #include "asm/fixpoint.hpp" #include "asm/fstack.hpp" #include "asm/main.hpp" #include "asm/opt.hpp" #include "asm/output.hpp" #include "asm/section.hpp" #include "asm/symbol.hpp" #include "asm/warning.hpp" using namespace std::literals; yy::parser::symbol_type yylex(); // Provided by lexer.cpp template static auto handleSymbolByType( std::string const &symName, NumCallbackFnT numCallback, StrCallbackFnT strCallback ) { if (Symbol *sym = sym_FindScopedSymbol(symName); sym && sym->type == SYM_EQUS) { return strCallback(*sym->getEqus()); } else { Expression expr; expr.makeSymbol(symName); return numCallback(expr); } } // The CPU encodes instructions in a logical way, so most instructions actually follow patterns. // These enums thus help with bit twiddling to compute opcodes. enum { REG_B, REG_C, REG_D, REG_E, REG_H, REG_L, REG_HL_IND, REG_A }; enum { REG_BC_IND, REG_DE_IND, REG_HL_INDINC, REG_HL_INDDEC }; // REG_AF == REG_SP since LD/INC/ADD/DEC allow SP, while PUSH/POP allow AF enum { REG_BC, REG_DE, REG_HL, REG_SP, REG_AF = REG_SP }; // Names are not needed for AF or SP static char const *reg_tt_names[] = { "BC", "DE", "HL" }; static char const *reg_tt_high_names[] = { "B", "D", "H" }; static char const *reg_tt_low_names[] = { "C", "E", "L" }; // CC_NZ == CC_Z ^ 1, and CC_NC == CC_C ^ 1, so `!` can toggle them enum { CC_NZ, CC_Z, CC_NC, CC_C }; } /******************** Tokens ********************/ %token YYEOF 0 "end of file" %token NEWLINE "end of line" %token EOB "end of buffer" %token EOL "end of fragment literal" // General punctuation %token COMMA "," %token COLON ":" DOUBLE_COLON "::" %token LBRACK "[" RBRACK "]" %token LBRACKS "[[" RBRACKS "]]" %token LPAREN "(" RPAREN ")" %token QUESTIONMARK "?" // Arithmetic operators %token OP_ADD "+" OP_SUB "-" %token OP_MUL "*" OP_DIV "/" OP_MOD "%" %token OP_EXP "**" // String operators %token OP_CAT "++" %token OP_STREQU "===" OP_STRNE "!==" // Comparison operators %token OP_LOGICEQU "==" OP_LOGICNE "!=" %token OP_LOGICLT "<" OP_LOGICGT ">" %token OP_LOGICLE "<=" OP_LOGICGE ">=" // Logical operators %token OP_LOGICAND "&&" OP_LOGICOR "||" %token OP_LOGICNOT "!" // Binary operators %token OP_AND "&" OP_OR "|" OP_XOR "^" %token OP_SHL "<<" OP_SHR ">>" OP_USHR ">>>" %token OP_NOT "~" // Operator precedence %left OP_LOGICOR %left OP_LOGICAND %left OP_LOGICEQU OP_LOGICNE OP_LOGICLT OP_LOGICGT OP_LOGICLE OP_LOGICGE %left OP_ADD OP_SUB %left OP_AND OP_OR OP_XOR %left OP_SHL OP_SHR OP_USHR %left OP_MUL OP_DIV OP_MOD %left OP_CAT %precedence NEG // applies to unary OP_LOGICNOT, OP_ADD, OP_SUB, OP_NOT %right OP_EXP // Assignment operators (only for variables) %token POP_EQUAL "=" %token POP_ADDEQ "+=" POP_SUBEQ "-=" %token POP_MULEQ "*=" POP_DIVEQ "/=" POP_MODEQ "%=" %token POP_ANDEQ "&=" POP_OREQ "|=" POP_XOREQ "^=" %token POP_SHLEQ "<<=" POP_SHREQ ">>=" // SM83 registers %token TOKEN_A "a" %token TOKEN_B "b" TOKEN_C "c" %token TOKEN_D "d" TOKEN_E "e" %token TOKEN_H "h" TOKEN_L "l" %token MODE_AF "af" MODE_BC "bc" MODE_DE "de" MODE_HL "hl" MODE_SP "sp" %token MODE_HL_INC "hli/hl+" MODE_HL_DEC "hld/hl-" // SM83 condition codes %token CC_Z "z" CC_NZ "nz" CC_NC "nc" // There is no CC_C, only TOKEN_C // SM83 instructions %token SM83_ADC "adc" %token SM83_ADD "add" %token SM83_AND "and" %token SM83_BIT "bit" %token SM83_CALL "call" %token SM83_CCF "ccf" %token SM83_CP "cp" %token SM83_CPL "cpl" %token SM83_DAA "daa" %token SM83_DEC "dec" %token SM83_DI "di" %token SM83_EI "ei" %token SM83_HALT "halt" %token SM83_INC "inc" %token SM83_JP "jp" %token SM83_JR "jr" %token SM83_LDD "ldd" %token SM83_LDH "ldh" %token SM83_LDI "ldi" %token SM83_LD "ld" %token SM83_NOP "nop" %token SM83_OR "or" %token SM83_POP "pop" %token SM83_PUSH "push" %token SM83_RES "res" %token SM83_RETI "reti" %token SM83_RET "ret" %token SM83_RLA "rla" %token SM83_RLCA "rlca" %token SM83_RLC "rlc" %token SM83_RL "rl" %token SM83_RRA "rra" %token SM83_RRCA "rrca" %token SM83_RRC "rrc" %token SM83_RR "rr" %token SM83_RST "rst" %token SM83_SBC "sbc" %token SM83_SCF "scf" %token SM83_SET "set" %token SM83_SLA "sla" %token SM83_SRA "sra" %token SM83_SRL "srl" %token SM83_STOP "stop" %token SM83_SUB "sub" %token SM83_SWAP "swap" %token SM83_XOR "xor" // Statement keywords %token POP_ALIGN "ALIGN" %token POP_ASSERT "ASSERT" %token POP_BREAK "BREAK" %token POP_CHARMAP "CHARMAP" %token POP_DB "DB" %token POP_DL "DL" %token POP_DS "DS" %token POP_DW "DW" %token POP_ELIF "ELIF" %token POP_ELSE "ELSE" %token POP_ENDC "ENDC" %token POP_ENDL "ENDL" %token POP_ENDM "ENDM" %token POP_ENDR "ENDR" %token POP_ENDSECTION "ENDSECTION" %token POP_ENDU "ENDU" %token POP_EQU "EQU" %token POP_EQUS "EQUS" %token POP_EXPORT "EXPORT" %token POP_FAIL "FAIL" %token POP_FATAL "FATAL" %token POP_FOR "FOR" %token POP_FRAGMENT "FRAGMENT" %token POP_IF "IF" %token POP_INCBIN "INCBIN" %token POP_INCLUDE "INCLUDE" %token POP_LOAD "LOAD" %token POP_MACRO "MACRO" %token POP_NEWCHARMAP "NEWCHARMAP" %token POP_NEXTU "NEXTU" %token POP_OPT "OPT" %token POP_POPC "POPC" %token POP_POPO "POPO" %token POP_POPS "POPS" %token POP_PRINTLN "PRINTLN" %token POP_PRINT "PRINT" %token POP_PURGE "PURGE" %token POP_PUSHC "PUSHC" %token POP_PUSHO "PUSHO" %token POP_PUSHS "PUSHS" %token POP_RB "RB" %token POP_REDEF "REDEF" %token POP_REPT "REPT" %token POP_RSRESET "RSRESET" %token POP_RSSET "RSSET" // There is no POP_RL, only SM83_RL %token POP_RW "RW" %token POP_SECTION "SECTION" %token POP_SETCHARMAP "SETCHARMAP" %token POP_SHIFT "SHIFT" %token POP_STATIC_ASSERT "STATIC_ASSERT" %token POP_UNION "UNION" %token POP_WARN "WARN" // Function keywords %token OP_ACOS "ACOS" %token OP_ASIN "ASIN" %token OP_ATAN "ATAN" %token OP_ATAN2 "ATAN2" %token OP_BANK "BANK" %token OP_BITWIDTH "BITWIDTH" %token OP_BYTELEN "BYTELEN" %token OP_CEIL "CEIL" %token OP_CHARCMP "CHARCMP" %token OP_CHARLEN "CHARLEN" %token OP_CHARSIZE "CHARSIZE" %token OP_CHARSUB "CHARSUB" %token OP_CHARVAL "CHARVAL" %token OP_COS "COS" %token OP_DEF "DEF" %token OP_FDIV "FDIV" %token OP_FLOOR "FLOOR" %token OP_FMOD "FMOD" %token OP_FMUL "FMUL" %token OP_HIGH "HIGH" %token OP_INCHARMAP "INCHARMAP" %token OP_ISCONST "ISCONST" %token OP_LOG "LOG" %token OP_LOW "LOW" %token OP_POW "POW" %token OP_READFILE "READFILE" %token OP_REVCHAR "REVCHAR" %token OP_ROUND "ROUND" %token OP_SIN "SIN" %token OP_SIZEOF "SIZEOF" %token OP_STARTOF "STARTOF" %token OP_STRBYTE "STRBYTE" %token OP_STRCAT "STRCAT" %token OP_STRCHAR "STRCHAR" %token OP_STRCMP "STRCMP" %token OP_STRFIND "STRFIND" %token OP_STRFMT "STRFMT" %token OP_STRIN "STRIN" %token OP_STRLEN "STRLEN" %token OP_STRLWR "STRLWR" %token OP_STRRFIND "STRRFIND" %token OP_STRRIN "STRRIN" %token OP_STRRPL "STRRPL" %token OP_STRSLICE "STRSLICE" %token OP_STRSUB "STRSUB" %token OP_STRUPR "STRUPR" %token OP_TAN "TAN" %token OP_TZCOUNT "TZCOUNT" // Section types %token SECT_HRAM "HRAM" %token SECT_OAM "OAM" %token SECT_ROM0 "ROM0" %token SECT_ROMX "ROMX" %token SECT_SRAM "SRAM" %token SECT_VRAM "VRAM" %token SECT_WRAM0 "WRAM0" %token SECT_WRAMX "WRAMX" // Literals %token NUMBER "number" %token STRING "string" %token CHARACTER "character" %token SYMBOL "symbol" %token LABEL "label" %token LOCAL "local label" %token ANON "anonymous label" %token QMACRO "quiet macro" /******************** Data types ********************/ // RPN expressions %type relocexpr // `relocexpr_no_str` exists because strings usually count as numeric expressions, but some // contexts treat numbers and strings differently, e.g. `db "string"` or `print "string"`. %type relocexpr_no_str %type reloc_3bit %type reloc_8bit %type reloc_16bit // Constant numbers %type iconst %type uconst // Constant numbers used only in specific contexts %type precision_arg %type rs_uconst %type sect_org %type shift_const // Strings %type string %type string_literal %type strcat_args // Strings used for identifiers %type def_id %type redef_id %type def_numeric %type def_equ %type redef_equ %type def_set %type def_rb %type def_rw %type def_rl %type def_equs %type redef_equs %type scoped_sym // `scoped_sym_no_anon` exists because anonymous labels usually count as "scoped symbols", but some // contexts treat anonymous labels and other labels/symbols differently, e.g. `purge` or `export`. %type scoped_sym_no_anon %type fragment_literal %type fragment_literal_name // SM83 instruction parameters %type reg_r %type reg_r_no_a %type reg_a %type reg_ss %type reg_rr %type reg_tt %type reg_tt_no_af %type reg_bc_or_de %type ccode_expr %type ccode %type op_a_n %type op_a_r %type op_mem_ind %type op_sp_offset // Data types used only in specific contexts %type align_spec %type assert_type %type capture_macro %type capture_rept %type compound_eq %type > charmap_args %type > ds_args %type for_args %type > macro_args %type > purge_args %type sect_attrs %type sect_mod %type sect_type %type strfmt_args %type strfmt_va_args %type maybe_quiet %% /******************** Parser rules ********************/ // Assembly files. asm_file: lines; lines: %empty | lines diff_mark line // Continue parsing the next line on a syntax error | error { lexer_SetMode(LEXER_NORMAL); lexer_ToggleStringExpansion(true); } endofline { yyerrok; } ; diff_mark: %empty // OK | OP_ADD { ::error( "syntax error, unexpected '+' at the beginning of the line (is it a leftover diff " "mark?)" ); } | OP_SUB { ::error( "syntax error, unexpected '-' at the beginning of the line (is it a leftover diff " "mark?)" ); } ; // Lines and line directives. line: plain_directive endofline | line_directive // Directives that manage newlines themselves ; endofline: NEWLINE | EOB | EOL; // For "logistical" reasons, these directives must manage newlines themselves. // This is because we need to switch the lexer's mode *after* the newline has been read, // and to avoid causing some grammar conflicts (token reducing is finicky). // This is DEFINITELY one of the more FRAGILE parts of the codebase, handle with care. line_directive: macro_def | rept | for | break | include | if | endc // It's important that all of these require being at line start for `skipIfBlock` | elif | else ; if: POP_IF iconst NEWLINE { act_If($2); } ; elif: POP_ELIF iconst NEWLINE { act_Elif($2); } ; else: POP_ELSE NEWLINE { act_Else(); } ; // Directives, labels, functions, and values. plain_directive: label | label data | label macro_invocation | label directive ; endc: POP_ENDC endofline { act_Endc(); } ; def_id: OP_DEF { lexer_ToggleStringExpansion(false); } SYMBOL { lexer_ToggleStringExpansion(true); $$ = std::move($3); } ; redef_id: POP_REDEF { lexer_ToggleStringExpansion(false); } SYMBOL { lexer_ToggleStringExpansion(true); $$ = std::move($3); } ; scoped_sym_no_anon: SYMBOL | LABEL | LOCAL; scoped_sym: scoped_sym_no_anon | ANON; label: %empty | LABEL COLON { sym_AddLabel($1); } | LABEL DOUBLE_COLON { sym_AddLabel($1); sym_Export($1); } | LOCAL { sym_AddLocalLabel($1); } | LOCAL COLON { sym_AddLocalLabel($1); } | LOCAL DOUBLE_COLON { sym_AddLocalLabel($1); sym_Export($1); } | COLON { sym_AddAnonLabel(); } ; macro_invocation: SYMBOL { // Parsing 'macro_args' will restore the lexer's normal mode lexer_SetMode(LEXER_RAW); } macro_args { fstk_RunMacro($1, $3, false); } | QMACRO { // Parsing 'macro_args' will restore the lexer's normal mode lexer_SetMode(LEXER_RAW); } macro_args { fstk_RunMacro($1, $3, true); } ; macro_args: %empty { $$ = std::make_shared(); } | macro_args STRING { $$ = std::move($1); $$->appendArg(std::make_shared($2)); } ; directive: print | println | export | export_def | section | rsreset | rsset | union | nextu | endu | incbin | charmap | newcharmap | setcharmap | pushc | popc | pushc_setcharmap | load | shift | fail | warn | assert | def_numeric | def_equs | redef_equs | purge | pops | pushs | pushs_section | endsection | popo | pusho | opt | align ; def_numeric: def_equ | redef_equ | def_set | def_rb | def_rw | def_rl ; trailing_comma: %empty | COMMA; compound_eq: POP_ADDEQ { $$ = RPN_ADD; } | POP_SUBEQ { $$ = RPN_SUB; } | POP_MULEQ { $$ = RPN_MUL; } | POP_DIVEQ { $$ = RPN_DIV; } | POP_MODEQ { $$ = RPN_MOD; } | POP_XOREQ { $$ = RPN_XOR; } | POP_OREQ { $$ = RPN_OR; } | POP_ANDEQ { $$ = RPN_AND; } | POP_SHLEQ { $$ = RPN_SHL; } | POP_SHREQ { $$ = RPN_SHR; } ; align: POP_ALIGN align_spec { sect_AlignPC($2.alignment, $2.alignOfs); } ; align_spec: uconst { $$ = act_Alignment($1, 0); } | uconst COMMA iconst { $$ = act_Alignment($1, $3); } ; opt: POP_OPT { // Parsing 'opt_list' will restore the lexer's normal mode lexer_SetMode(LEXER_RAW); } opt_list ; opt_list: opt_list_entry | opt_list opt_list_entry ; opt_list_entry: STRING { opt_Parse($1.c_str()); } ; popo: POP_POPO { opt_Pop(); } ; pusho: POP_PUSHO { opt_Push(); // Parsing 'pusho_opt_list' will restore the lexer's normal mode lexer_SetMode(LEXER_RAW); } pusho_opt_list ; pusho_opt_list: %empty { lexer_SetMode(LEXER_NORMAL); } | opt_list ; pops: POP_POPS { sect_PopSection(); } ; pushs: POP_PUSHS { sect_PushSection(); } ; endsection: POP_ENDSECTION { sect_EndSection(); } ; fail: POP_FAIL string { fatal("%s", $2.c_str()); } ; warn: POP_WARN string { warning(WARNING_USER, "%s", $2.c_str()); } ; assert_type: %empty { $$ = ASSERT_ERROR; } | POP_WARN COMMA { $$ = ASSERT_WARN; } | POP_FAIL COMMA { $$ = ASSERT_ERROR; } | POP_FATAL COMMA { $$ = ASSERT_FATAL; } ; assert: POP_ASSERT assert_type relocexpr { act_Assert($2, $3, ""); } | POP_ASSERT assert_type relocexpr COMMA string { act_Assert($2, $3, $5); } | POP_STATIC_ASSERT assert_type iconst { act_StaticAssert($2, $3, ""); } | POP_STATIC_ASSERT assert_type iconst COMMA string { act_StaticAssert($2, $3, $5); } ; shift: POP_SHIFT shift_const { if (MacroArgs *macroArgs = fstk_GetCurrentMacroArgs(); macroArgs) { macroArgs->shiftArgs($2); } else { ::error("Cannot shift macro arguments outside of a macro"); } } ; shift_const: %empty { $$ = 1; } | iconst ; load: POP_LOAD sect_mod string COMMA sect_type sect_org sect_attrs { sect_SetLoadSection($3, $5, $6, $7, $2); } | POP_ENDL { sect_EndLoadSection(nullptr); } ; maybe_quiet: %empty { $$ = false; } | QUESTIONMARK { $$ = true; } ; rept: POP_REPT maybe_quiet uconst NEWLINE capture_rept endofline { if ($5.span.ptr) { fstk_RunRept($3, $5.lineNo, $5.span, $2); } } ; for: POP_FOR { lexer_ToggleStringExpansion(false); } maybe_quiet SYMBOL { lexer_ToggleStringExpansion(true); } COMMA for_args NEWLINE capture_rept endofline { if ($9.span.ptr) { fstk_RunFor($4, $7.start, $7.stop, $7.step, $9.lineNo, $9.span, $3); } } ; capture_rept: %empty { $$ = lexer_CaptureRept(); } ; for_args: iconst { $$.start = 0; $$.stop = $1; $$.step = 1; } | iconst COMMA iconst { $$.start = $1; $$.stop = $3; $$.step = 1; } | iconst COMMA iconst COMMA iconst { $$.start = $1; $$.stop = $3; $$.step = $5; } ; break: label POP_BREAK endofline { if (fstk_Break()) { lexer_SetMode(LEXER_SKIP_TO_ENDR); } } ; macro_def: POP_MACRO { lexer_ToggleStringExpansion(false); } maybe_quiet SYMBOL { lexer_ToggleStringExpansion(true); } NEWLINE capture_macro endofline { if ($7.span.ptr) { sym_AddMacro($4, $7.lineNo, $7.span, $3); } } ; capture_macro: %empty { $$ = lexer_CaptureMacro(); } ; rsset: POP_RSSET uconst { sym_SetRSValue($2); } ; rsreset: POP_RSRESET { sym_SetRSValue(0); } ; rs_uconst: %empty { $$ = 1; } | uconst ; union: POP_UNION { sect_StartUnion(); } ; nextu: POP_NEXTU { sect_NextUnionMember(); } ; endu: POP_ENDU { sect_EndUnion(); } ; def_equ: def_id POP_EQU iconst { $$ = std::move($1); sym_AddEqu($$, $3); } ; redef_equ: redef_id POP_EQU iconst { $$ = std::move($1); sym_RedefEqu($$, $3); } ; def_set: def_id POP_EQUAL iconst { $$ = std::move($1); sym_AddVar($$, $3); } | redef_id POP_EQUAL iconst { $$ = std::move($1); sym_AddVar($$, $3); } | def_id compound_eq iconst { $$ = std::move($1); act_CompoundAssignment($$, $2, $3); } | redef_id compound_eq iconst { $$ = std::move($1); act_CompoundAssignment($$, $2, $3); } ; def_rb: def_id POP_RB rs_uconst { $$ = std::move($1); uint32_t rs = sym_GetRSValue(); sym_AddEqu($$, rs); sym_SetRSValue(rs + $3); } ; def_rw: def_id POP_RW rs_uconst { $$ = std::move($1); uint32_t rs = sym_GetRSValue(); sym_AddEqu($$, rs); sym_SetRSValue(rs + 2 * $3); } ; def_rl: def_id SM83_RL rs_uconst { $$ = std::move($1); uint32_t rs = sym_GetRSValue(); sym_AddEqu($$, rs); sym_SetRSValue(rs + 4 * $3); } ; def_equs: def_id POP_EQUS string { $$ = std::move($1); sym_AddString($$, std::make_shared($3)); } ; redef_equs: redef_id POP_EQUS string { $$ = std::move($1); sym_RedefString($$, std::make_shared($3)); } ; purge: POP_PURGE { lexer_ToggleStringExpansion(false); } purge_args trailing_comma { for (std::string &arg : $3) { sym_Purge(arg); } lexer_ToggleStringExpansion(true); } ; purge_args: scoped_sym_no_anon { $$.push_back($1); } | purge_args COMMA scoped_sym_no_anon { $$ = std::move($1); $$.push_back($3); } ; export: POP_EXPORT export_list trailing_comma; export_list: export_list_entry | export_list COMMA export_list_entry ; export_list_entry: scoped_sym_no_anon { sym_Export($1); } ; export_def: POP_EXPORT def_numeric { sym_Export($2); } ; include: label POP_INCLUDE maybe_quiet string endofline { if (fstk_RunInclude($4, $3)) { YYACCEPT; } } ; incbin: POP_INCBIN string { if (sect_BinaryFile($2, 0)) { YYACCEPT; } } | POP_INCBIN string COMMA uconst { if (sect_BinaryFile($2, $4)) { YYACCEPT; } } | POP_INCBIN string COMMA uconst COMMA uconst { if (sect_BinaryFileSlice($2, $4, $6)) { YYACCEPT; } } ; charmap: POP_CHARMAP string COMMA charmap_args trailing_comma { charmap_Add($2, std::move($4)); } | POP_CHARMAP CHARACTER COMMA charmap_args trailing_comma { charmap_Add($2, std::move($4)); } ; charmap_args: iconst { $$.push_back(std::move($1)); } | charmap_args COMMA iconst { $$ = std::move($1); $$.push_back(std::move($3)); } ; newcharmap: POP_NEWCHARMAP SYMBOL { charmap_New($2, nullptr); } | POP_NEWCHARMAP SYMBOL COMMA SYMBOL { charmap_New($2, &$4); } ; setcharmap: POP_SETCHARMAP SYMBOL { charmap_Set($2); } ; pushc: POP_PUSHC { charmap_Push(); } ; pushc_setcharmap: POP_PUSHC SYMBOL { charmap_Push(); charmap_Set($2); } ; popc: POP_POPC { charmap_Pop(); } ; print: POP_PRINT print_exprs trailing_comma; println: POP_PRINTLN { putchar('\n'); fflush(stdout); } | POP_PRINTLN print_exprs trailing_comma { putchar('\n'); fflush(stdout); } ; print_exprs: print_expr | print_exprs COMMA print_expr ; print_expr: relocexpr_no_str { printf("$%" PRIX32, $1.getConstVal()); } | string_literal { // Allow printing NUL characters fwrite($1.data(), 1, $1.length(), stdout); } | scoped_sym { handleSymbolByType( $1, [](Expression const &expr) { printf("$%" PRIX32, expr.getConstVal()); }, [](std::string const &str) { fwrite(str.data(), 1, str.length(), stdout); } ); } ; reloc_3bit: relocexpr { $$ = std::move($1); $$.checkNBit(3); } ; constlist_8bit: constlist_8bit_entry | constlist_8bit COMMA constlist_8bit_entry ; constlist_8bit_entry: relocexpr_no_str { $1.checkNBit(8); sect_RelByte($1, 0); } | string_literal { std::vector output = charmap_Convert($1); sect_ByteString(output); } | scoped_sym { handleSymbolByType( $1, [](Expression const &expr) { expr.checkNBit(8); sect_RelByte(expr, 0); }, [](std::string const &str) { std::vector output = charmap_Convert(str); sect_ByteString(output); } ); } ; constlist_16bit: constlist_16bit_entry | constlist_16bit COMMA constlist_16bit_entry ; constlist_16bit_entry: relocexpr_no_str { $1.checkNBit(16); sect_RelWord($1, 0); } | string_literal { std::vector output = charmap_Convert($1); sect_WordString(output); } | scoped_sym { handleSymbolByType( $1, [](Expression const &expr) { expr.checkNBit(16); sect_RelWord(expr, 0); }, [](std::string const &str) { std::vector output = charmap_Convert(str); sect_WordString(output); } ); } | fragment_literal { Expression expr; expr.makeSymbol($1); expr.checkNBit(16); sect_RelWord(expr, 0); } ; constlist_32bit: constlist_32bit_entry | constlist_32bit COMMA constlist_32bit_entry ; constlist_32bit_entry: relocexpr_no_str { sect_RelLong($1, 0); } | string_literal { std::vector output = charmap_Convert($1); sect_LongString(output); } | scoped_sym { handleSymbolByType( $1, [](Expression const &expr) { sect_RelLong(expr, 0); }, [](std::string const &str) { std::vector output = charmap_Convert(str); sect_LongString(output); } ); } ; reloc_8bit: relocexpr { $$ = std::move($1); $$.checkNBit(8); } ; reloc_16bit: relocexpr { $$ = std::move($1); $$.checkNBit(16); } | fragment_literal { $$.makeSymbol($1); } ; fragment_literal: LBRACKS fragment_literal_name asm_file RBRACKS { sect_PopSection(); $$ = std::move($2); } ; fragment_literal_name: %empty { $$ = sect_PushSectionFragmentLiteral(); sym_AddLabel($$); } ; relocexpr: relocexpr_no_str { $$ = std::move($1); } | string_literal { $$.makeNumber(act_StringToNum($1)); } | scoped_sym { $$ = handleSymbolByType( $1, [](Expression const &expr) { return expr; }, [](std::string const &str) { Expression expr; expr.makeNumber(act_StringToNum(str)); return expr; } ); } ; relocexpr_no_str: NUMBER { $$.makeNumber($1); } | CHARACTER { $$.makeNumber(act_CharToNum($1)); } | string OP_STREQU string { $$.makeNumber($1.compare($3) == 0); } | string OP_STRNE string { $$.makeNumber($1.compare($3) != 0); } | OP_LOGICNOT relocexpr %prec NEG { $$.makeUnaryOp(RPN_LOGNOT, std::move($2)); } | relocexpr OP_LOGICOR relocexpr { $$.makeBinaryOp(RPN_LOGOR, std::move($1), $3); } | relocexpr OP_LOGICAND relocexpr { $$.makeBinaryOp(RPN_LOGAND, std::move($1), $3); } | relocexpr OP_LOGICEQU relocexpr { $$.makeBinaryOp(RPN_LOGEQ, std::move($1), $3); } | relocexpr OP_LOGICGT relocexpr { $$.makeBinaryOp(RPN_LOGGT, std::move($1), $3); } | relocexpr OP_LOGICLT relocexpr { $$.makeBinaryOp(RPN_LOGLT, std::move($1), $3); } | relocexpr OP_LOGICGE relocexpr { $$.makeBinaryOp(RPN_LOGGE, std::move($1), $3); } | relocexpr OP_LOGICLE relocexpr { $$.makeBinaryOp(RPN_LOGLE, std::move($1), $3); } | relocexpr OP_LOGICNE relocexpr { $$.makeBinaryOp(RPN_LOGNE, std::move($1), $3); } | relocexpr OP_ADD relocexpr { $$.makeBinaryOp(RPN_ADD, std::move($1), $3); } | relocexpr OP_SUB relocexpr { $$.makeBinaryOp(RPN_SUB, std::move($1), $3); } | relocexpr OP_XOR relocexpr { $$.makeBinaryOp(RPN_XOR, std::move($1), $3); } | relocexpr OP_OR relocexpr { $$.makeBinaryOp(RPN_OR, std::move($1), $3); } | relocexpr OP_AND relocexpr { $$.makeBinaryOp(RPN_AND, std::move($1), $3); } | relocexpr OP_SHL relocexpr { $$.makeBinaryOp(RPN_SHL, std::move($1), $3); } | relocexpr OP_SHR relocexpr { $$.makeBinaryOp(RPN_SHR, std::move($1), $3); } | relocexpr OP_USHR relocexpr { $$.makeBinaryOp(RPN_USHR, std::move($1), $3); } | relocexpr OP_MUL relocexpr { $$.makeBinaryOp(RPN_MUL, std::move($1), $3); } | relocexpr OP_DIV relocexpr { $$.makeBinaryOp(RPN_DIV, std::move($1), $3); } | relocexpr OP_MOD relocexpr { $$.makeBinaryOp(RPN_MOD, std::move($1), $3); } | relocexpr OP_EXP relocexpr { $$.makeBinaryOp(RPN_EXP, std::move($1), $3); } | OP_ADD relocexpr %prec NEG { $$ = std::move($2); } | OP_SUB relocexpr %prec NEG { $$.makeUnaryOp(RPN_NEG, std::move($2)); } | OP_NOT relocexpr %prec NEG { $$.makeUnaryOp(RPN_NOT, std::move($2)); } | OP_HIGH LPAREN relocexpr RPAREN { $$.makeUnaryOp(RPN_HIGH, std::move($3)); } | OP_LOW LPAREN relocexpr RPAREN { $$.makeUnaryOp(RPN_LOW, std::move($3)); } | OP_BITWIDTH LPAREN relocexpr RPAREN { $$.makeUnaryOp(RPN_BITWIDTH, std::move($3)); } | OP_TZCOUNT LPAREN relocexpr RPAREN { $$.makeUnaryOp(RPN_TZCOUNT, std::move($3)); } | OP_ISCONST LPAREN relocexpr RPAREN { $$.makeNumber($3.isKnown()); } | OP_BANK LPAREN scoped_sym RPAREN { // '@' is also a SYMBOL; it is handled here $$.makeBankSymbol($3); } | OP_BANK LPAREN string_literal RPAREN { $$.makeBankSection($3); } | OP_SIZEOF LPAREN string RPAREN { $$.makeSizeOfSection($3); } | OP_STARTOF LPAREN string RPAREN { $$.makeStartOfSection($3); } | OP_SIZEOF LPAREN sect_type RPAREN { $$.makeSizeOfSectionType($3); } | OP_STARTOF LPAREN sect_type RPAREN { $$.makeStartOfSectionType($3); } | OP_SIZEOF LPAREN MODE_R8 RPAREN { $$.makeNumber(1); } | OP_SIZEOF LPAREN MODE_R16 RPAREN { $$.makeNumber(2); } | OP_DEF { lexer_ToggleStringExpansion(false); } LPAREN scoped_sym RPAREN { $$.makeNumber(sym_FindScopedValidSymbol($4) != nullptr); lexer_ToggleStringExpansion(true); } | OP_ROUND LPAREN iconst precision_arg RPAREN { $$.makeNumber(fix_Round($3, $4)); } | OP_CEIL LPAREN iconst precision_arg RPAREN { $$.makeNumber(fix_Ceil($3, $4)); } | OP_FLOOR LPAREN iconst precision_arg RPAREN { $$.makeNumber(fix_Floor($3, $4)); } | OP_FDIV LPAREN iconst COMMA iconst precision_arg RPAREN { $$.makeNumber(fix_Div($3, $5, $6)); } | OP_FMUL LPAREN iconst COMMA iconst precision_arg RPAREN { $$.makeNumber(fix_Mul($3, $5, $6)); } | OP_FMOD LPAREN iconst COMMA iconst precision_arg RPAREN { $$.makeNumber(fix_Mod($3, $5, $6)); } | OP_POW LPAREN iconst COMMA iconst precision_arg RPAREN { $$.makeNumber(fix_Pow($3, $5, $6)); } | OP_LOG LPAREN iconst COMMA iconst precision_arg RPAREN { $$.makeNumber(fix_Log($3, $5, $6)); } | OP_SIN LPAREN iconst precision_arg RPAREN { $$.makeNumber(fix_Sin($3, $4)); } | OP_COS LPAREN iconst precision_arg RPAREN { $$.makeNumber(fix_Cos($3, $4)); } | OP_TAN LPAREN iconst precision_arg RPAREN { $$.makeNumber(fix_Tan($3, $4)); } | OP_ASIN LPAREN iconst precision_arg RPAREN { $$.makeNumber(fix_ASin($3, $4)); } | OP_ACOS LPAREN iconst precision_arg RPAREN { $$.makeNumber(fix_ACos($3, $4)); } | OP_ATAN LPAREN iconst precision_arg RPAREN { $$.makeNumber(fix_ATan($3, $4)); } | OP_ATAN2 LPAREN iconst COMMA iconst precision_arg RPAREN { $$.makeNumber(fix_ATan2($3, $5, $6)); } | OP_STRCMP LPAREN string COMMA string RPAREN { $$.makeNumber($3.compare($5)); } | OP_STRFIND LPAREN string COMMA string RPAREN { size_t pos = $3.find($5); $$.makeNumber(pos != std::string::npos ? pos : -1); } | OP_STRRFIND LPAREN string COMMA string RPAREN { size_t pos = $3.rfind($5); $$.makeNumber(pos != std::string::npos ? pos : -1); } | OP_STRIN LPAREN string COMMA string RPAREN { warning(WARNING_OBSOLETE, "`STRIN` is deprecated; use 0-indexed `STRFIND` instead"); size_t pos = $3.find($5); $$.makeNumber(pos != std::string::npos ? pos + 1 : 0); } | OP_STRRIN LPAREN string COMMA string RPAREN { warning(WARNING_OBSOLETE, "`STRRIN` is deprecated; use 0-indexed `STRRFIND` instead"); size_t pos = $3.rfind($5); $$.makeNumber(pos != std::string::npos ? pos + 1 : 0); } | OP_STRLEN LPAREN string RPAREN { $$.makeNumber(act_StringLen($3, true)); } | OP_BYTELEN LPAREN string RPAREN { $$.makeNumber($3.length()); } | OP_CHARLEN LPAREN string RPAREN { $$.makeNumber(act_CharLen($3)); } | OP_INCHARMAP LPAREN string RPAREN { $$.makeNumber(charmap_HasChar($3)); } | OP_CHARCMP LPAREN string COMMA string RPAREN { $$.makeNumber(act_CharCmp($3, $5)); } | OP_CHARSIZE LPAREN string RPAREN { size_t charSize = charmap_CharSize($3); if (charSize == 0) { ::error("CHARSIZE: No character mapping for \"%s\"", $3.c_str()); } $$.makeNumber(charSize); } | OP_CHARVAL LPAREN string COMMA iconst RPAREN { $$.makeNumber(act_CharVal($3, $5)); } | OP_CHARVAL LPAREN string RPAREN { $$.makeNumber(act_CharVal($3)); } | OP_STRBYTE LPAREN string COMMA iconst RPAREN { $$.makeNumber(act_StringByte($3, $5)); } | LPAREN relocexpr RPAREN { $$ = std::move($2); } ; uconst: iconst { $$ = $1; if ($$ < 0) { fatal("Constant must not be negative: %d", $$); } } ; iconst: relocexpr { $$ = $1.getConstVal(); } ; precision_arg: %empty { $$ = options.fixPrecision; } | COMMA iconst { $$ = $2; if ($$ < 1 || $$ > 31) { ::error("Fixed-point precision must be between 1 and 31, not %" PRId32, $$); $$ = options.fixPrecision; } } ; string_literal: STRING { $$ = std::move($1); } | string OP_CAT string { $$ = std::move($1); $$.append($3); } | OP_READFILE LPAREN string RPAREN { if (std::optional contents = act_ReadFile($3, UINT32_MAX); contents) { $$ = std::move(*contents); } else { YYACCEPT; } } | OP_READFILE LPAREN string COMMA uconst RPAREN { if (std::optional contents = act_ReadFile($3, $5); contents) { $$ = std::move(*contents); } else { YYACCEPT; } } | OP_STRSLICE LPAREN string COMMA iconst COMMA iconst RPAREN { $$ = act_StringSlice($3, $5, $7); } | OP_STRSLICE LPAREN string COMMA iconst RPAREN { $$ = act_StringSlice($3, $5, std::nullopt); } | OP_STRSUB LPAREN string COMMA iconst COMMA uconst RPAREN { $$ = act_StringSub($3, $5, $7); } | OP_STRSUB LPAREN string COMMA iconst RPAREN { $$ = act_StringSub($3, $5, std::nullopt); } | OP_STRCHAR LPAREN string COMMA iconst RPAREN { $$ = act_StringChar($3, $5); } | OP_CHARSUB LPAREN string COMMA iconst RPAREN { $$ = act_CharSub($3, $5); } | OP_REVCHAR LPAREN charmap_args RPAREN { bool unique; $$ = charmap_Reverse($3, unique); if (!unique) { ::error("REVCHAR: Multiple character mappings to values"); } else if ($$.empty()) { ::error("REVCHAR: No character mapping to values"); } } | OP_STRCAT LPAREN RPAREN { $$.clear(); } | OP_STRCAT LPAREN strcat_args RPAREN { $$ = std::move($3); } | OP_STRUPR LPAREN string RPAREN { $$ = std::move($3); std::transform(RANGE($$), $$.begin(), toUpper); } | OP_STRLWR LPAREN string RPAREN { $$ = std::move($3); std::transform(RANGE($$), $$.begin(), toLower); } | OP_STRRPL LPAREN string COMMA string COMMA string RPAREN { $$ = act_StringReplace($3, $5, $7); } | OP_STRFMT LPAREN strfmt_args RPAREN { $$ = act_StringFormat($3.format, $3.args); } | POP_SECTION LPAREN scoped_sym RPAREN { $$ = act_SectionName($3); } ; string: string_literal { $$ = std::move($1); } | scoped_sym { if (Symbol *sym = sym_FindScopedSymbol($1); sym && sym->type == SYM_EQUS) { $$ = *sym->getEqus(); } else { ::error("`%s` is not a string symbol", $1.c_str()); } } ; strcat_args: string { $$ = std::move($1); } | strcat_args COMMA string { $$ = std::move($1); $$.append($3); } ; strfmt_args: %empty {} | string strfmt_va_args { $$ = std::move($2); $$.format = std::move($1); } ; strfmt_va_args: %empty {} | strfmt_va_args COMMA relocexpr_no_str { $$ = std::move($1); $$.args.push_back(static_cast($3.getConstVal())); } | strfmt_va_args COMMA string_literal { $$ = std::move($1); $$.args.push_back(std::move($3)); } | strfmt_va_args COMMA scoped_sym { $$ = std::move($1); handleSymbolByType( $3, [&](Expression const &expr) { $$.args.push_back(static_cast(expr.getConstVal())); }, [&](std::string const &str) { $$.args.push_back(str); } ); } ; section: POP_SECTION sect_mod string COMMA sect_type sect_org sect_attrs { sect_NewSection($3, $5, $6, $7, $2); } ; pushs_section: POP_PUSHS sect_mod string COMMA sect_type sect_org sect_attrs { sect_PushSection(); sect_NewSection($3, $5, $6, $7, $2); } ; sect_mod: %empty { $$ = SECTION_NORMAL; } | POP_UNION { $$ = SECTION_UNION; } | POP_FRAGMENT { $$ = SECTION_FRAGMENT; } ; sect_type: SECT_WRAM0 { $$ = SECTTYPE_WRAM0; } | SECT_VRAM { $$ = SECTTYPE_VRAM; } | SECT_ROMX { $$ = SECTTYPE_ROMX; } | SECT_ROM0 { $$ = SECTTYPE_ROM0; } | SECT_HRAM { $$ = SECTTYPE_HRAM; } | SECT_WRAMX { $$ = SECTTYPE_WRAMX; } | SECT_SRAM { $$ = SECTTYPE_SRAM; } | SECT_OAM { $$ = SECTTYPE_OAM; } ; sect_org: %empty { $$ = -1; } | LBRACK uconst RBRACK { $$ = $2; if ($$ < 0 || $$ > 0xFFFF) { ::error("Address $%x is not 16-bit", $$); $$ = -1; } } ; sect_attrs: %empty { $$.alignment = 0; $$.alignOfs = 0; $$.bank = -1; } | sect_attrs COMMA POP_ALIGN LBRACK align_spec RBRACK { $$ = $1; $$.alignment = $5.alignment; $$.alignOfs = $5.alignOfs; } | sect_attrs COMMA OP_BANK LBRACK uconst RBRACK { $$ = $1; $$.bank = $5; // We cannot check the validity of this yet } ; // CPU instructions and data declarations data: datum | datum DOUBLE_COLON data ; datum: db | dw | dl | ds | sm83_adc | sm83_add | sm83_and | sm83_bit | sm83_call | sm83_ccf | sm83_cp | sm83_cpl | sm83_daa | sm83_dec | sm83_di | sm83_ei | sm83_halt | sm83_inc | sm83_jp | sm83_jr | sm83_ld | sm83_ldd | sm83_ldh | sm83_ldi | sm83_nop | sm83_or | sm83_pop | sm83_push | sm83_res | sm83_ret | sm83_reti | sm83_rl | sm83_rla | sm83_rlc | sm83_rlca | sm83_rr | sm83_rra | sm83_rrc | sm83_rrca | sm83_rst | sm83_sbc | sm83_scf | sm83_set | sm83_sla | sm83_sra | sm83_srl | sm83_stop | sm83_sub | sm83_swap | sm83_xor ; ds: POP_DS uconst { sect_Skip($2, true); } | POP_DS uconst COMMA ds_args trailing_comma { sect_RelBytes($2, $4); } | POP_DS POP_ALIGN LBRACK align_spec RBRACK trailing_comma { uint32_t n = sect_GetAlignBytes($4.alignment, $4.alignOfs); sect_Skip(n, true); sect_AlignPC($4.alignment, $4.alignOfs); } | POP_DS POP_ALIGN LBRACK align_spec RBRACK COMMA ds_args trailing_comma { uint32_t n = sect_GetAlignBytes($4.alignment, $4.alignOfs); sect_RelBytes(n, $7); sect_AlignPC($4.alignment, $4.alignOfs); } ; ds_args: reloc_8bit { $$.push_back(std::move($1)); } | ds_args COMMA reloc_8bit { $$ = std::move($1); $$.push_back(std::move($3)); } ; db: POP_DB { sect_Skip(1, false); } | POP_DB constlist_8bit trailing_comma ; dw: POP_DW { sect_Skip(2, false); } | POP_DW constlist_16bit trailing_comma ; dl: POP_DL { sect_Skip(4, false); } | POP_DL constlist_32bit trailing_comma ; sm83_adc: SM83_ADC op_a_n { sect_ConstByte(0xCE); sect_RelByte($2, 1); } | SM83_ADC op_a_r { sect_ConstByte(0x88 | $2); } ; sm83_add: SM83_ADD op_a_n { sect_ConstByte(0xC6); sect_RelByte($2, 1); } | SM83_ADD op_a_r { sect_ConstByte(0x80 | $2); } | SM83_ADD MODE_HL COMMA reg_ss { sect_ConstByte(0x09 | ($4 << 4)); } | SM83_ADD MODE_SP COMMA reloc_8bit { sect_ConstByte(0xE8); sect_RelByte($4, 1); } ; sm83_and: SM83_AND op_a_n { sect_ConstByte(0xE6); sect_RelByte($2, 1); } | SM83_AND op_a_r { sect_ConstByte(0xA0 | $2); } ; sm83_bit: SM83_BIT reloc_3bit COMMA reg_r { uint8_t mask = static_cast(0x40 | $4); $2.addCheckBitIndex(mask); sect_ConstByte(0xCB); if (!$2.isKnown()) { sect_RelByte($2, 0); } else { sect_ConstByte(mask | ($2.value() << 3)); } } ; sm83_call: SM83_CALL reloc_16bit { sect_ConstByte(0xCD); sect_RelWord($2, 1); } | SM83_CALL ccode_expr COMMA reloc_16bit { sect_ConstByte(0xC4 | ($2 << 3)); sect_RelWord($4, 1); } ; sm83_ccf: SM83_CCF { sect_ConstByte(0x3F); } ; sm83_cp: SM83_CP op_a_n { sect_ConstByte(0xFE); sect_RelByte($2, 1); } | SM83_CP op_a_r { sect_ConstByte(0xB8 | $2); } ; sm83_cpl: SM83_CPL { sect_ConstByte(0x2F); } | SM83_CPL MODE_A { sect_ConstByte(0x2F); } ; sm83_daa: SM83_DAA { sect_ConstByte(0x27); } ; sm83_dec: SM83_DEC reg_r { sect_ConstByte(0x05 | ($2 << 3)); } | SM83_DEC reg_ss { sect_ConstByte(0x0B | ($2 << 4)); } ; sm83_di: SM83_DI { sect_ConstByte(0xF3); } ; sm83_ei: SM83_EI { sect_ConstByte(0xFB); } ; sm83_halt: SM83_HALT { sect_ConstByte(0x76); } ; sm83_inc: SM83_INC reg_r { sect_ConstByte(0x04 | ($2 << 3)); } | SM83_INC reg_ss { sect_ConstByte(0x03 | ($2 << 4)); } ; sm83_jp: SM83_JP reloc_16bit { sect_ConstByte(0xC3); sect_RelWord($2, 1); } | SM83_JP ccode_expr COMMA reloc_16bit { sect_ConstByte(0xC2 | ($2 << 3)); sect_RelWord($4, 1); } | SM83_JP MODE_HL { sect_ConstByte(0xE9); } ; sm83_jr: SM83_JR reloc_16bit { sect_ConstByte(0x18); sect_PCRelByte($2, 1); } | SM83_JR ccode_expr COMMA reloc_16bit { sect_ConstByte(0x20 | ($2 << 3)); sect_PCRelByte($4, 1); } ; sm83_ldi: SM83_LDI LBRACK MODE_HL RBRACK COMMA MODE_A { sect_ConstByte(0x02 | (2 << 4)); } | SM83_LDI MODE_A COMMA LBRACK MODE_HL RBRACK { sect_ConstByte(0x0A | (2 << 4)); } ; sm83_ldd: SM83_LDD LBRACK MODE_HL RBRACK COMMA MODE_A { sect_ConstByte(0x02 | (3 << 4)); } | SM83_LDD MODE_A COMMA LBRACK MODE_HL RBRACK { sect_ConstByte(0x0A | (3 << 4)); } ; sm83_ldh: SM83_LDH MODE_A COMMA op_mem_ind { $4.addCheckHRAM(); sect_ConstByte(0xF0); if (!$4.isKnown()) { sect_RelByte($4, 1); } else { sect_ConstByte($4.value()); } } | SM83_LDH op_mem_ind COMMA MODE_A { $2.addCheckHRAM(); sect_ConstByte(0xE0); if (!$2.isKnown()) { sect_RelByte($2, 1); } else { sect_ConstByte($2.value()); } } | SM83_LDH MODE_A COMMA c_ind { sect_ConstByte(0xF2); } | SM83_LDH MODE_A COMMA ff00_c_ind { sect_ConstByte(0xF2); } | SM83_LDH c_ind COMMA MODE_A { sect_ConstByte(0xE2); } | SM83_LDH ff00_c_ind COMMA MODE_A { sect_ConstByte(0xE2); } ; c_ind: LBRACK MODE_C RBRACK; ff00_c_ind: LBRACK relocexpr OP_ADD MODE_C RBRACK { // This has to use `relocexpr`, not `iconst`, to avoid a shift/reduce conflict if ($2.getConstVal() != 0xFF00) { ::error("Base value must be equal to $FF00 for [$FF00+C]"); } } ; sm83_ld: sm83_ld_mem | sm83_ld_c_ind | sm83_ld_rr | sm83_ld_ss | sm83_ld_hl | sm83_ld_sp | sm83_ld_r_no_a | sm83_ld_a ; sm83_ld_hl: SM83_LD MODE_HL COMMA MODE_SP op_sp_offset { sect_ConstByte(0xF8); sect_RelByte($5, 1); } | SM83_LD MODE_HL COMMA reloc_16bit { sect_ConstByte(0x01 | (REG_HL << 4)); sect_RelWord($4, 1); } | SM83_LD MODE_HL COMMA reg_tt_no_af { ::error( "\"LD HL, %s\" is not a valid instruction; use \"LD H, %s\" and \"LD L, %s\"", reg_tt_names[$4], reg_tt_high_names[$4], reg_tt_low_names[$4] ); } ; sm83_ld_sp: SM83_LD MODE_SP COMMA MODE_HL { sect_ConstByte(0xF9); } | SM83_LD MODE_SP COMMA reg_bc_or_de { ::error("\"LD SP, %s\" is not a valid instruction", reg_tt_names[$4]); } | SM83_LD MODE_SP COMMA reloc_16bit { sect_ConstByte(0x01 | (REG_SP << 4)); sect_RelWord($4, 1); } ; sm83_ld_mem: SM83_LD op_mem_ind COMMA MODE_SP { sect_ConstByte(0x08); sect_RelWord($2, 1); } | SM83_LD op_mem_ind COMMA MODE_A { sect_ConstByte(0xEA); sect_RelWord($2, 1); } ; sm83_ld_c_ind: SM83_LD ff00_c_ind COMMA MODE_A { sect_ConstByte(0xE2); } ; sm83_ld_rr: SM83_LD reg_rr COMMA MODE_A { sect_ConstByte(0x02 | ($2 << 4)); } ; sm83_ld_r_no_a: SM83_LD reg_r_no_a COMMA reloc_8bit { sect_ConstByte(0x06 | ($2 << 3)); sect_RelByte($4, 1); } | SM83_LD reg_r_no_a COMMA reg_r { if ($2 == REG_HL_IND && $4 == REG_HL_IND) { ::error("\"LD [HL], [HL]\" is not a valid instruction"); } else { sect_ConstByte(0x40 | ($2 << 3) | $4); } } ; sm83_ld_a: SM83_LD reg_a COMMA reloc_8bit { sect_ConstByte(0x06 | ($2 << 3)); sect_RelByte($4, 1); } | SM83_LD reg_a COMMA reg_r { sect_ConstByte(0x40 | ($2 << 3) | $4); } | SM83_LD reg_a COMMA ff00_c_ind { sect_ConstByte(0xF2); } | SM83_LD reg_a COMMA reg_rr { sect_ConstByte(0x0A | ($4 << 4)); } | SM83_LD reg_a COMMA op_mem_ind { sect_ConstByte(0xFA); sect_RelWord($4, 1); } ; sm83_ld_ss: SM83_LD reg_bc_or_de COMMA reloc_16bit { sect_ConstByte(0x01 | ($2 << 4)); sect_RelWord($4, 1); } | SM83_LD reg_bc_or_de COMMA reg_tt_no_af { ::error( "\"LD %s, %s\" is not a valid instruction; use \"LD %s, %s\" and \"LD %s, %s\"", reg_tt_names[$2], reg_tt_names[$4], reg_tt_high_names[$2], reg_tt_high_names[$4], reg_tt_low_names[$2], reg_tt_low_names[$4] ); } // HL is taken care of in sm83_ld_hl // SP is taken care of in sm83_ld_sp ; sm83_nop: SM83_NOP { sect_ConstByte(0x00); } ; sm83_or: SM83_OR op_a_n { sect_ConstByte(0xF6); sect_RelByte($2, 1); } | SM83_OR op_a_r { sect_ConstByte(0xB0 | $2); } ; sm83_pop: SM83_POP reg_tt { sect_ConstByte(0xC1 | ($2 << 4)); } ; sm83_push: SM83_PUSH reg_tt { sect_ConstByte(0xC5 | ($2 << 4)); } ; sm83_res: SM83_RES reloc_3bit COMMA reg_r { uint8_t mask = static_cast(0x80 | $4); $2.addCheckBitIndex(mask); sect_ConstByte(0xCB); if (!$2.isKnown()) { sect_RelByte($2, 0); } else { sect_ConstByte(mask | ($2.value() << 3)); } } ; sm83_ret: SM83_RET { sect_ConstByte(0xC9); } | SM83_RET ccode_expr { sect_ConstByte(0xC0 | ($2 << 3)); } ; sm83_reti: SM83_RETI { sect_ConstByte(0xD9); } ; sm83_rl: SM83_RL reg_r { sect_ConstByte(0xCB); sect_ConstByte(0x10 | $2); } ; sm83_rla: SM83_RLA { sect_ConstByte(0x17); } ; sm83_rlc: SM83_RLC reg_r { sect_ConstByte(0xCB); sect_ConstByte(0x00 | $2); } ; sm83_rlca: SM83_RLCA { sect_ConstByte(0x07); } ; sm83_rr: SM83_RR reg_r { sect_ConstByte(0xCB); sect_ConstByte(0x18 | $2); } ; sm83_rra: SM83_RRA { sect_ConstByte(0x1F); } ; sm83_rrc: SM83_RRC reg_r { sect_ConstByte(0xCB); sect_ConstByte(0x08 | $2); } ; sm83_rrca: SM83_RRCA { sect_ConstByte(0x0F); } ; sm83_rst: SM83_RST reloc_8bit { $2.addCheckRST(); if (!$2.isKnown()) { sect_RelByte($2, 0); } else { sect_ConstByte(0xC7 | $2.value()); } } ; sm83_sbc: SM83_SBC op_a_n { sect_ConstByte(0xDE); sect_RelByte($2, 1); } | SM83_SBC op_a_r { sect_ConstByte(0x98 | $2); } ; sm83_scf: SM83_SCF { sect_ConstByte(0x37); } ; sm83_set: SM83_SET reloc_3bit COMMA reg_r { uint8_t mask = static_cast(0xC0 | $4); $2.addCheckBitIndex(mask); sect_ConstByte(0xCB); if (!$2.isKnown()) { sect_RelByte($2, 0); } else { sect_ConstByte(mask | ($2.value() << 3)); } } ; sm83_sla: SM83_SLA reg_r { sect_ConstByte(0xCB); sect_ConstByte(0x20 | $2); } ; sm83_sra: SM83_SRA reg_r { sect_ConstByte(0xCB); sect_ConstByte(0x28 | $2); } ; sm83_srl: SM83_SRL reg_r { sect_ConstByte(0xCB); sect_ConstByte(0x38 | $2); } ; sm83_stop: SM83_STOP { sect_ConstByte(0x10); sect_ConstByte(0x00); } | SM83_STOP reloc_8bit { sect_ConstByte(0x10); sect_RelByte($2, 1); } ; sm83_sub: SM83_SUB op_a_n { sect_ConstByte(0xD6); sect_RelByte($2, 1); } | SM83_SUB op_a_r { sect_ConstByte(0x90 | $2); } ; sm83_swap: SM83_SWAP reg_r { sect_ConstByte(0xCB); sect_ConstByte(0x30 | $2); } ; sm83_xor: SM83_XOR op_a_n { sect_ConstByte(0xEE); sect_RelByte($2, 1); } | SM83_XOR op_a_r { sect_ConstByte(0xA8 | $2); } ; // Registers or values. op_mem_ind: LBRACK reloc_16bit RBRACK { $$ = std::move($2); } ; op_a_r: reg_r | MODE_A COMMA reg_r { $$ = $3; } ; op_a_n: reloc_8bit { $$ = std::move($1); } | MODE_A COMMA reloc_8bit { $$ = std::move($3); } ; op_sp_offset: OP_ADD relocexpr { $$ = std::move($2); $$.checkNBit(8); } | OP_SUB relocexpr { $$.makeUnaryOp(RPN_NEG, std::move($2)); $$.checkNBit(8); } | %empty { ::error("\"LD HL, SP\" is not a valid instruction; use \"LD HL, SP + 0\""); } ; // Registers and condition codes. MODE_R8: MODE_A | MODE_B | MODE_C | MODE_D | MODE_E | MODE_H | MODE_L | LBRACK MODE_BC RBRACK | LBRACK MODE_DE RBRACK | LBRACK MODE_HL RBRACK | hl_ind_inc | hl_ind_dec ; MODE_R16: MODE_AF | MODE_BC | MODE_DE | MODE_HL | MODE_SP ; MODE_A: TOKEN_A | OP_HIGH LPAREN MODE_AF RPAREN ; MODE_B: TOKEN_B | OP_HIGH LPAREN MODE_BC RPAREN ; MODE_C: TOKEN_C | OP_LOW LPAREN MODE_BC RPAREN ; MODE_D: TOKEN_D | OP_HIGH LPAREN MODE_DE RPAREN ; MODE_E: TOKEN_E | OP_LOW LPAREN MODE_DE RPAREN ; MODE_H: TOKEN_H | OP_HIGH LPAREN MODE_HL RPAREN ; MODE_L: TOKEN_L | OP_LOW LPAREN MODE_HL RPAREN ; ccode_expr: ccode | OP_LOGICNOT ccode_expr { $$ = $2 ^ 1; } ; ccode: CC_NZ { $$ = CC_NZ; } | CC_Z { $$ = CC_Z; } | CC_NC { $$ = CC_NC; } | TOKEN_C { $$ = CC_C; } ; reg_r: reg_r_no_a | reg_a; reg_r_no_a: MODE_B { $$ = REG_B; } | MODE_C { $$ = REG_C; } | MODE_D { $$ = REG_D; } | MODE_E { $$ = REG_E; } | MODE_H { $$ = REG_H; } | MODE_L { $$ = REG_L; } | LBRACK MODE_HL RBRACK { $$ = REG_HL_IND; } ; reg_a: MODE_A { $$ = REG_A; } ; reg_tt: reg_tt_no_af | MODE_AF { $$ = REG_AF; } ; reg_ss: reg_tt_no_af | MODE_SP { $$ = REG_SP; } ; reg_tt_no_af: reg_bc_or_de | MODE_HL { $$ = REG_HL; } ; reg_bc_or_de: MODE_BC { $$ = REG_BC; } | MODE_DE { $$ = REG_DE; } ; reg_rr: LBRACK MODE_BC RBRACK { $$ = REG_BC_IND; } | LBRACK MODE_DE RBRACK { $$ = REG_DE_IND; } | hl_ind_inc { $$ = REG_HL_INDINC; } | hl_ind_dec { $$ = REG_HL_INDDEC; } ; hl_ind_inc: LBRACK MODE_HL_INC RBRACK | LBRACK MODE_HL OP_ADD RBRACK ; hl_ind_dec: LBRACK MODE_HL_DEC RBRACK | LBRACK MODE_HL OP_SUB RBRACK ; %% /******************** Error handler ********************/ void yy::parser::error(std::string const &str) { ::error("%s", str.c_str()); } gbdev-rgbds-92bfe5d/src/asm/rpn.cpp000066400000000000000000000434341512540461700172710ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "asm/rpn.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "helpers.hpp" // assume #include "linkdefs.hpp" #include "opmath.hpp" #include "asm/output.hpp" #include "asm/section.hpp" #include "asm/symbol.hpp" #include "asm/warning.hpp" using namespace std::literals; int32_t Expression::getConstVal() const { if (!isKnown()) { error("Expected constant expression: %s", std::get(data).c_str()); return 0; } return value(); } Symbol const *Expression::symbolOf() const { if (rpn.size() != 1 || rpn[0].command != RPN_SYM) { return nullptr; } return sym_FindScopedSymbol(std::get(rpn[0].data)); } bool Expression::isDiffConstant(Symbol const *sym) const { // Check if both expressions only refer to a single symbol Symbol const *sym1 = symbolOf(); if (!sym1 || !sym || sym1->type != SYM_LABEL || sym->type != SYM_LABEL) { return false; } Section const *sect1 = sym1->getSection(); Section const *sect2 = sym->getSection(); return sect1 && (sect1 == sect2); } void Expression::makeNumber(uint32_t value) { assume(rpn.empty()); data = static_cast(value); } void Expression::makeSymbol(std::string const &symName) { assume(rpn.empty()); if (Symbol *sym = sym_FindScopedSymbol(symName); sym_IsPC(sym) && !sect_GetSymbolSection()) { error("PC has no value outside of a section"); data = 0; } else if (sym && !sym->isNumeric() && !sym->isLabel()) { error("`%s` is not a numeric symbol", symName.c_str()); data = 0; } else if (!sym || !sym->isConstant()) { data = sym_IsPC(sym) ? "PC is not constant at assembly time" : (sym && sym->isDefined() ? "`"s + symName + "` is not constant at assembly time" : "undefined symbol `"s + symName + "`") + (sym_IsPurgedScoped(symName) ? "; it was purged" : ""); sym = sym_Ref(symName); rpn.emplace_back(RPN_SYM, sym->name); } else { data = static_cast(sym->getConstantValue()); } } void Expression::makeBankSymbol(std::string const &symName) { assume(rpn.empty()); if (Symbol const *sym = sym_FindScopedSymbol(symName); sym_IsPC(sym)) { // The @ symbol is treated differently. if (std::optional outputBank = sect_GetOutputBank(); !outputBank) { error("PC has no bank outside of a section"); data = 1; } else if (*outputBank == UINT32_MAX) { data = "Current section's bank is not known"; rpn.emplace_back(RPN_BANK_SELF); } else { data = static_cast(*outputBank); } } else if (sym && !sym->isLabel()) { error("`BANK` argument must be a label"); data = 1; } else { sym = sym_Ref(symName); assume(sym); // If the symbol didn't exist, it should have been created if (sym->getSection() && sym->getSection()->bank != UINT32_MAX) { // Symbol's section is known and bank is fixed data = static_cast(sym->getSection()->bank); } else { data = sym_IsPurgedScoped(symName) ? "`"s + symName + "`'s bank is not known; it was purged" : "`"s + symName + "`'s bank is not known"; rpn.emplace_back(RPN_BANK_SYM, sym->name); } } } void Expression::makeBankSection(std::string const §Name) { assume(rpn.empty()); if (Section *sect = sect_FindSectionByName(sectName); sect && sect->bank != UINT32_MAX) { data = static_cast(sect->bank); } else { data = "Section \""s + sectName + "\"'s bank is not known"; rpn.emplace_back(RPN_BANK_SECT, sectName); } } void Expression::makeSizeOfSection(std::string const §Name) { assume(rpn.empty()); if (Section *sect = sect_FindSectionByName(sectName); sect && sect->isSizeKnown()) { data = static_cast(sect->size); } else { data = "Section \""s + sectName + "\"'s size is not known"; rpn.emplace_back(RPN_SIZEOF_SECT, sectName); } } void Expression::makeStartOfSection(std::string const §Name) { assume(rpn.empty()); if (Section *sect = sect_FindSectionByName(sectName); sect && sect->org != UINT32_MAX) { data = static_cast(sect->org); } else { data = "Section \""s + sectName + "\"'s start is not known"; rpn.emplace_back(RPN_STARTOF_SECT, sectName); } } void Expression::makeSizeOfSectionType(SectionType type) { assume(rpn.empty()); data = "Section type's size is not known"; rpn.emplace_back(RPN_SIZEOF_SECTTYPE, static_cast(type)); } void Expression::makeStartOfSectionType(SectionType type) { assume(rpn.empty()); data = "Section type's start is not known"; rpn.emplace_back(RPN_STARTOF_SECTTYPE, static_cast(type)); } static bool tryConstZero(Expression const &lhs, Expression const &rhs) { Expression const &expr = lhs.isKnown() ? lhs : rhs; return expr.isKnown() && expr.value() == 0; } static bool tryConstNonzero(Expression const &lhs, Expression const &rhs) { Expression const &expr = lhs.isKnown() ? lhs : rhs; return expr.isKnown() && expr.value() != 0; } static bool tryConstLogNot(Expression const &expr) { Symbol const *sym = expr.symbolOf(); if (!sym || !sym->getSection() || !sym->isDefined()) { return false; } assume(sym->isNumeric()); Section const § = *sym->getSection(); int32_t unknownBits = (1 << 16) - (1 << sect.align); // `sym->getValue()` attempts to add the section's address, but that's `UINT32_MAX` // because the section is floating (otherwise we wouldn't be here) assume(sect.org == UINT32_MAX); int32_t symbolOfs = sym->getValue() + 1; int32_t knownBits = (symbolOfs + sect.alignOfs) & ~unknownBits; return knownBits != 0; } // Returns a constant LOW() from non-constant argument, or -1 if it cannot be computed. // This is possible if the argument is a symbol belonging to an `ALIGN[8]` section. static int32_t tryConstLow(Expression const &expr) { Symbol const *sym = expr.symbolOf(); if (!sym || !sym->getSection() || !sym->isDefined()) { return -1; } assume(sym->isNumeric()); // The low byte must not cover any unknown bits Section const § = *sym->getSection(); if (sect.align < 8) { return -1; } // `sym->getValue()` attempts to add the section's address, but that's `UINT32_MAX` // because the section is floating (otherwise we wouldn't be here) assume(sect.org == UINT32_MAX); int32_t symbolOfs = sym->getValue() + 1; return op_low(symbolOfs + sect.alignOfs); } // Returns a constant binary AND with one non-constant operand, or -1 if it cannot be computed. // This is possible if one operand is a symbol belonging to an `ALIGN[N]` section, and the other is // a constant that only keeps (some of) the lower N bits. static int32_t tryConstMask(Expression const &lhs, Expression const &rhs) { Symbol const *lhsSymbol = lhs.symbolOf(); Symbol const *rhsSymbol = lhsSymbol ? nullptr : rhs.symbolOf(); bool lhsIsSymbol = lhsSymbol && lhsSymbol->getSection(); bool rhsIsSymbol = rhsSymbol && rhsSymbol->getSection(); if (!lhsIsSymbol && !rhsIsSymbol) { return -1; } // If the lhs isn't a symbol, try again the other way around Symbol const &sym = lhsIsSymbol ? *lhsSymbol : *rhsSymbol; Expression const &expr = lhsIsSymbol ? rhs : lhs; // Opposite side of `sym` if (!sym.isDefined() || !expr.isKnown()) { return -1; } assume(sym.isNumeric()); // We can now safely use `expr.value()` int32_t mask = expr.value(); // The mask must not cover any unknown bits Section const § = *sym.getSection(); if (int32_t unknownBits = (1 << 16) - (1 << sect.align); (unknownBits & mask) != 0) { return -1; } // `sym.getValue()` attempts to add the section's address, but that's `UINT32_MAX` // because the section is floating (otherwise we wouldn't be here) assume(sect.org == UINT32_MAX); int32_t symbolOfs = sym.getValue() + 1; return (symbolOfs + sect.alignOfs) & mask; } void Expression::makeUnaryOp(RPNCommand op, Expression &&src) { assume(rpn.empty()); // First, check if the expression is known if (src.isKnown()) { // If the expressions is known, just compute the value switch (int32_t val = src.value(); op) { case RPN_NEG: data = op_neg(val); break; case RPN_NOT: data = ~val; break; case RPN_LOGNOT: data = !val; break; case RPN_HIGH: data = op_high(val); break; case RPN_LOW: data = op_low(val); break; case RPN_BITWIDTH: data = op_bitwidth(val); break; case RPN_TZCOUNT: data = op_tzcount(val); break; // LCOV_EXCL_START default: // `makeUnaryOp` should never be called with a non-unary operator! unreachable_(); } // LCOV_EXCL_STOP } else if (op == RPN_LOGNOT && tryConstLogNot(src)) { data = 0; } else if (int32_t constVal; op == RPN_LOW && (constVal = tryConstLow(src)) != -1) { data = constVal; } else { // If it's not known, just reuse its RPN vector and append the operator data = std::move(src.data); std::swap(rpn, src.rpn); rpn.emplace_back(op); } } void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const &src2) { assume(rpn.empty()); // First, check if the expressions are known if (src1.isKnown() && src2.isKnown()) { // If both expressions are known, just compute the value int32_t lval = src1.value(), rval = src2.value(); uint32_t ulval = static_cast(lval), urval = static_cast(rval); switch (op) { case RPN_LOGOR: data = lval || rval; break; case RPN_LOGAND: data = lval && rval; break; case RPN_LOGEQ: data = lval == rval; break; case RPN_LOGGT: data = lval > rval; break; case RPN_LOGLT: data = lval < rval; break; case RPN_LOGGE: data = lval >= rval; break; case RPN_LOGLE: data = lval <= rval; break; case RPN_LOGNE: data = lval != rval; break; case RPN_ADD: data = static_cast(ulval + urval); break; case RPN_SUB: data = static_cast(ulval - urval); break; case RPN_XOR: data = lval ^ rval; break; case RPN_OR: data = lval | rval; break; case RPN_AND: data = lval & rval; break; case RPN_SHL: if (rval < 0) { warning(WARNING_SHIFT_AMOUNT, "Shifting left by negative amount %" PRId32, rval); } if (rval >= 32) { warning(WARNING_SHIFT_AMOUNT, "Shifting left by large amount %" PRId32, rval); } data = op_shift_left(lval, rval); break; case RPN_SHR: if (lval < 0) { warning(WARNING_SHIFT, "Shifting right negative value %" PRId32, lval); } if (rval < 0) { warning(WARNING_SHIFT_AMOUNT, "Shifting right by negative amount %" PRId32, rval); } if (rval >= 32) { warning(WARNING_SHIFT_AMOUNT, "Shifting right by large amount %" PRId32, rval); } data = op_shift_right(lval, rval); break; case RPN_USHR: if (rval < 0) { warning(WARNING_SHIFT_AMOUNT, "Shifting right by negative amount %" PRId32, rval); } if (rval >= 32) { warning(WARNING_SHIFT_AMOUNT, "Shifting right by large amount %" PRId32, rval); } data = op_shift_right_unsigned(lval, rval); break; case RPN_MUL: data = static_cast(ulval * urval); break; case RPN_DIV: if (rval == 0) { fatal("Division by zero"); } if (lval == INT32_MIN && rval == -1) { warning( WARNING_DIV, "Division of %" PRId32 " by -1 yields %" PRId32, INT32_MIN, INT32_MIN ); data = INT32_MIN; } else { data = op_divide(lval, rval); } break; case RPN_MOD: if (rval == 0) { fatal("Modulo by zero"); } if (lval == INT32_MIN && rval == -1) { data = 0; } else { data = op_modulo(lval, rval); } break; case RPN_EXP: if (rval < 0) { fatal("Exponentiation by negative power"); } data = op_exponent(lval, rval); break; // LCOV_EXCL_START default: // `makeBinaryOp` should never be called with a non-binary operator! unreachable_(); } // LCOV_EXCL_STOP } else if (op == RPN_SUB && src1.isDiffConstant(src2.symbolOf())) { data = src1.symbolOf()->getValue() - src2.symbolOf()->getValue(); } else if ((op == RPN_LOGAND || op == RPN_AND) && tryConstZero(src1, src2)) { data = 0; } else if (op == RPN_LOGOR && tryConstNonzero(src1, src2)) { data = 1; } else if (int32_t constVal; op == RPN_AND && (constVal = tryConstMask(src1, src2)) != -1) { data = constVal; } else { // If it's not known, start computing the RPN expression // Convert the left-hand expression if it's constant if (src1.isKnown()) { uint32_t lval = src1.value(); // Use the other expression's un-const reason data = std::move(src2.data); rpn.emplace_back(RPN_CONST, lval); } else { // Otherwise just reuse its RPN vector data = std::move(src1.data); std::swap(rpn, src1.rpn); } // Now, merge the right expression into the left one if (src2.isKnown()) { // If the right expression is constant, append its value uint32_t rval = src2.value(); rpn.emplace_back(RPN_CONST, rval); } else { // Otherwise just extend with its RPN vector rpn.insert(rpn.end(), RANGE(src2.rpn)); } // Append the operator rpn.emplace_back(op); } } void Expression::addCheckHRAM() { if (!isKnown()) { rpn.emplace_back(RPN_HRAM); } else if (int32_t val = value(); val >= 0xFF00 && val <= 0xFFFF) { // That range is valid; only keep the lower byte data = val & 0xFF; } else { error("Source address $%" PRIx32 " not between $FF00 to $FFFF", val); } } void Expression::addCheckRST() { if (!isKnown()) { rpn.emplace_back(RPN_RST); } else if (int32_t val = value(); val & ~0x38) { // A valid RST address must be masked with 0x38 error("Invalid address $%" PRIx32 " for `RST`", val); } } void Expression::addCheckBitIndex(uint8_t mask) { assume((mask & 0xC0) != 0x00); // The high two bits must correspond to BIT, RES, or SET if (!isKnown()) { rpn.emplace_back(RPN_BIT_INDEX, mask); } else if (int32_t val = value(); val & ~0x07) { // A valid bit index must be masked with 0x07 static char const *instructions[4] = {"instruction", "`BIT`", "`RES`", "`SET`"}; error("Invalid bit index %" PRId32 " for %s", val, instructions[mask >> 6]); } } // Checks that an RPN expression's value fits within N bits (signed or unsigned) void Expression::checkNBit(uint8_t n) const { if (isKnown()) { ::checkNBit(value(), n, nullptr); } } bool checkNBit(int32_t v, uint8_t n, char const *name) { assume(n != 0); // That doesn't make sense assume(n < CHAR_BIT * sizeof(int)); // Otherwise `1 << n` is UB if (v < -(1 << n) || v >= 1 << n) { warning( WARNING_TRUNCATION_1, "%s must be %u-bit%s", name ? name : "Expression", n, n == 8 && !name ? "; use `LOW()` to force 8-bit" : "" ); return false; } if (v < -(1 << (n - 1))) { warning( WARNING_TRUNCATION_2, "%s must be %u-bit%s", name ? name : "Expression", n, n == 8 && !name ? "; use `LOW()` to force 8-bit" : "" ); return false; } return true; } void Expression::encode(std::vector &buffer) const { assume(buffer.empty()); if (isKnown()) { // If the RPN expression's value is known, output a constant directly uint32_t val = value(); buffer.resize(5); buffer[0] = RPN_CONST; buffer[1] = val & 0xFF; buffer[2] = val >> 8; buffer[3] = val >> 16; buffer[4] = val >> 24; } else { // If the RPN expression's value is not known, serialize its RPN values buffer.reserve(rpn.size() * 2); // Rough estimate of the serialized size for (RPNValue const &val : rpn) { val.appendEncoded(buffer); } } } RPNValue::RPNValue(RPNCommand cmd) : command(cmd), data(std::monostate{}) { assume( cmd != RPN_SIZEOF_SECTTYPE && cmd != RPN_STARTOF_SECTTYPE && cmd != RPN_BIT_INDEX && cmd != RPN_CONST && cmd != RPN_SYM && cmd != RPN_BANK_SYM && cmd != RPN_BANK_SECT && cmd != RPN_SIZEOF_SECT && cmd != RPN_STARTOF_SECT ); } RPNValue::RPNValue(RPNCommand cmd, uint8_t val) : command(cmd), data(val) { assume(cmd == RPN_SIZEOF_SECTTYPE || cmd == RPN_STARTOF_SECTTYPE || cmd == RPN_BIT_INDEX); } RPNValue::RPNValue(RPNCommand cmd, uint32_t val) : command(cmd), data(val) { assume(cmd == RPN_CONST); } RPNValue::RPNValue(RPNCommand cmd, std::string const &name) : command(cmd), data(name) { assume( cmd == RPN_SYM || cmd == RPN_BANK_SYM || cmd == RPN_BANK_SECT || cmd == RPN_SIZEOF_SECT || cmd == RPN_STARTOF_SECT ); } void RPNValue::appendEncoded(std::vector &buffer) const { // Every command starts with its own ID buffer.push_back(command); switch (command) { case RPN_CONST: { // The command ID is followed by a four-byte integer assume(std::holds_alternative(data)); uint32_t val = std::get(data); buffer.push_back(val & 0xFF); buffer.push_back(val >> 8); buffer.push_back(val >> 16); buffer.push_back(val >> 24); break; } case RPN_SYM: case RPN_BANK_SYM: { // The command ID is followed by a four-byte symbol ID assume(std::holds_alternative(data)); // The symbol name is always written expanded Symbol *sym = sym_FindExactSymbol(std::get(data)); out_RegisterSymbol(*sym); // Ensure that `sym->ID` is set buffer.push_back(sym->ID & 0xFF); buffer.push_back(sym->ID >> 8); buffer.push_back(sym->ID >> 16); buffer.push_back(sym->ID >> 24); break; } case RPN_BANK_SECT: case RPN_SIZEOF_SECT: case RPN_STARTOF_SECT: { // The command ID is followed by a NUL-terminated section name string assume(std::holds_alternative(data)); std::string const &name = std::get(data); buffer.reserve(buffer.size() + name.length() + 1); buffer.insert(buffer.end(), RANGE(name)); buffer.push_back('\0'); break; } case RPN_SIZEOF_SECTTYPE: case RPN_STARTOF_SECTTYPE: case RPN_BIT_INDEX: // The command ID is followed by a byte value assume(std::holds_alternative(data)); buffer.push_back(std::get(data)); break; default: // Other command IDs are not followed by anything assume(std::holds_alternative(data)); break; } } gbdev-rgbds-92bfe5d/src/asm/section.cpp000066400000000000000000000714531512540461700201400ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "asm/section.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "helpers.hpp" #include "itertools.hpp" // InsertionOrderedMap #include "linkdefs.hpp" #include "asm/fstack.hpp" #include "asm/lexer.hpp" #include "asm/main.hpp" #include "asm/output.hpp" #include "asm/rpn.hpp" #include "asm/symbol.hpp" #include "asm/warning.hpp" using namespace std::literals; struct UnionStackEntry { uint32_t start; uint32_t size; }; struct SectionStackEntry { Section *section; Section *loadSection; std::pair labelScopes; uint32_t offset; int32_t loadOffset; std::stack unionStack; }; static Section *currentSection = nullptr; static InsertionOrderedMap
sections; static uint32_t curOffset; // Offset into the current section (see `sect_GetSymbolOffset`) static std::deque sectionStack; static Section *currentLoadSection = nullptr; static std::pair currentLoadLabelScopes = {nullptr, nullptr}; static int32_t loadOffset; // Offset into the LOAD section's parent (see sect_GetOutputOffset) static std::stack currentUnionStack; [[nodiscard]] static bool requireSection() { if (currentSection) { return true; } error("Cannot output data outside of a `SECTION`"); return false; } [[nodiscard]] static bool requireCodeSection() { if (!requireSection()) { return false; } if (sectTypeHasData(currentSection->type)) { return true; } error( "Section \"%s\" cannot contain code or data (not `ROM0` or `ROMX`)", currentSection->name.c_str() ); return false; } size_t sect_CountSections() { return sections.size(); } void sect_ForEach(void (*callback)(Section &)) { for (Section § : sections) { callback(sect); } } void sect_CheckSizes() { for (Section const § : sections) { if (uint32_t maxSize = sectionTypeInfo[sect.type].size; sect.size > maxSize) { error( "Section \"%s\" grew too big (max size = 0x%" PRIX32 " bytes, reached 0x%" PRIX32 ")", sect.name.c_str(), maxSize, sect.size ); } } } Section *sect_FindSectionByName(std::string const &name) { auto index = sections.findIndex(name); return index ? §ions[*index] : nullptr; } #define sectError(...) \ do { \ error(__VA_ARGS__); \ ++nbSectErrors; \ } while (0) static unsigned int mergeSectUnion(Section §, uint32_t org, uint8_t alignment, uint16_t alignOffset) { unsigned int nbSectErrors = 0; assume(alignment < 16); // Should be ensured by the caller uint32_t alignSize = 1u << alignment; uint32_t alignMask = alignSize - 1; assume(sect.align <= 16); // Left-shifting by 32 or more would be UB uint32_t sectAlignSize = 1u << sect.align; uint32_t sectAlignMask = sectAlignSize - 1; if (org != UINT32_MAX) { // If both are fixed, they must be the same if (sect.org != UINT32_MAX && sect.org != org) { sectError( "Section already declared as fixed at different address $%04" PRIx32, sect.org ); } else if (sect.align != 0 && ((org - sect.alignOfs) & sectAlignMask)) { sectError( "Section already declared as aligned to %" PRIu32 " bytes (offset %" PRIu16 ")", sectAlignSize, sect.alignOfs ); } else { // Otherwise, just override sect.org = org; } } else if (alignment != 0) { // Make sure any fixed address given is compatible if (sect.org != UINT32_MAX) { if ((sect.org - alignOffset) & alignMask) { sectError( "Section already declared as fixed at incompatible address $%04" PRIx32, sect.org ); } // Check if alignment offsets are compatible } else if ((alignOffset & sectAlignMask) != (sect.alignOfs & alignMask)) { sectError( "Section already declared with incompatible %" PRIu32 "-byte alignment (offset %" PRIu16 ")", sectAlignSize, sect.alignOfs ); } else if (alignment > sect.align) { // If the section is not fixed, its alignment is the largest of both sect.align = alignment; sect.alignOfs = alignOffset; } } return nbSectErrors; } static unsigned int mergeFragments(Section §, uint32_t org, uint8_t alignment, uint16_t alignOffset) { unsigned int nbSectErrors = 0; assume(alignment < 16); // Should be ensured by the caller uint32_t alignSize = 1u << alignment; uint32_t alignMask = alignSize - 1; assume(sect.align <= 16); // Left-shifting by 32 or more would be UB uint32_t sectAlignSize = 1u << sect.align; uint32_t sectAlignMask = sectAlignSize - 1; // Fragments only need "compatible" constraints, and they end up with the strictest // combination of both. // The merging is however performed at the *end* of the original section! if (org != UINT32_MAX) { uint16_t curOrg = org - sect.size; // If both are fixed, they must be the same if (sect.org != UINT32_MAX && sect.org != curOrg) { sectError( "Section already declared as fixed at incompatible address $%04" PRIx32, sect.org ); } else if (sect.align != 0 && ((curOrg - sect.alignOfs) & sectAlignMask)) { sectError( "Section already declared as aligned to %" PRIu32 " bytes (offset %" PRIu16 ")", sectAlignSize, sect.alignOfs ); } else { // Otherwise, just override sect.org = curOrg; } } else if (alignment != 0) { // Make sure any fixed address given is compatible if (uint32_t curOfs = (alignOffset - sect.size) & alignMask; sect.org != UINT32_MAX) { if ((sect.org - curOfs) & alignMask) { sectError( "Section already declared as fixed at incompatible address $%04" PRIx32, sect.org ); } // Check if alignment offsets are compatible } else if ((curOfs & sectAlignMask) != (sect.alignOfs & alignMask)) { sectError( "Section already declared with incompatible %" PRIu32 "-byte alignment (offset %" PRIu16 ")", sectAlignSize, sect.alignOfs ); } else if (alignment > sect.align) { // If the section is not fixed, its alignment is the largest of both sect.align = alignment; sect.alignOfs = curOfs; } } return nbSectErrors; } static void mergeSections( Section §, SectionType type, uint32_t org, uint32_t bank, uint8_t alignment, uint16_t alignOffset, SectionModifier mod ) { unsigned int nbSectErrors = 0; if (type != sect.type) { sectError( "Section already exists but with type `%s`", sectionTypeInfo[sect.type].name.c_str() ); } if (sect.modifier != mod) { sectError("Section already declared as `SECTION %s`", sectionModNames[sect.modifier]); } else { switch (mod) { case SECTION_UNION: case SECTION_FRAGMENT: { unsigned int (*merge)(Section &, uint32_t, uint8_t, uint16_t) = mod == SECTION_UNION ? mergeSectUnion : mergeFragments; nbSectErrors += merge(sect, org, alignment, alignOffset); // If the section's bank is unspecified, override it if (sect.bank == UINT32_MAX) { sect.bank = bank; } // If both specify a bank, it must be the same one else if (bank != UINT32_MAX && sect.bank != bank) { sectError("Section already declared with different bank %" PRIu32, sect.bank); } break; } case SECTION_NORMAL: errorNoTrace([&]() { fputs("Section already defined\n", stderr); fstk_TraceCurrent(); fputs(" and also:\n", stderr); sect.src->printBacktrace(sect.fileLine); ++nbSectErrors; }); break; } } if (nbSectErrors) { fatal( "Cannot create section \"%s\" (%u error%s)", sect.name.c_str(), nbSectErrors, nbSectErrors == 1 ? "" : "s" ); } } #undef sectError static Section *createSection( std::string const &name, SectionType type, uint32_t org, uint32_t bank, uint8_t alignment, uint16_t alignOffset, SectionModifier mod ) { // Add the new section to the list Section § = sections.add(name); sect.name = name; sect.type = type; sect.modifier = mod; sect.src = fstk_GetFileStack(); sect.fileLine = lexer_GetLineNo(); sect.size = 0; sect.org = org; sect.bank = bank; sect.align = alignment; sect.alignOfs = alignOffset; out_RegisterNode(sect.src); // It is only needed to allocate memory for ROM sections. if (sectTypeHasData(type)) { sect.data.resize(sectionTypeInfo[type].size); } return § } static Section *createSectionFragmentLiteral(Section const &parent) { assume(sections.contains(parent.name)); Section § = sections.addAnonymous(); sect.name = parent.name; sect.type = parent.type; sect.modifier = SECTION_FRAGMENT; sect.src = fstk_GetFileStack(); sect.fileLine = lexer_GetLineNo(); sect.size = 0; sect.org = UINT32_MAX; sect.bank = parent.bank == 0 ? UINT32_MAX : parent.bank; sect.align = 0; sect.alignOfs = 0; out_RegisterNode(sect.src); // Section fragment literals must be ROM sections. assume(sectTypeHasData(sect.type)); sect.data.resize(sectionTypeInfo[sect.type].size); return § } static Section *getSection( std::string const &name, SectionType type, uint32_t org, SectionSpec const &attrs, SectionModifier mod ) { uint32_t bank = attrs.bank; uint8_t alignment = attrs.alignment; uint16_t alignOffset = attrs.alignOfs; assume(alignment <= 16); // Should be ensured by the caller uint32_t alignSize = 1u << alignment; uint32_t alignMask = alignSize - 1; // First, validate parameters, and normalize them if applicable if (bank != UINT32_MAX) { if (type != SECTTYPE_ROMX && type != SECTTYPE_VRAM && type != SECTTYPE_SRAM && type != SECTTYPE_WRAMX) { error("`BANK` only allowed for `ROMX`, `WRAMX`, `SRAM`, or `VRAM` sections"); } else if (bank < sectionTypeInfo[type].firstBank || bank > sectionTypeInfo[type].lastBank) { error( "%s bank value $%04" PRIx32 " out of range ($%04" PRIx32 " to $%04" PRIx32 ")", sectionTypeInfo[type].name.c_str(), bank, sectionTypeInfo[type].firstBank, sectionTypeInfo[type].lastBank ); } } else if (sectTypeBanks(type) == 1) { // If the section type only has a single bank, implicitly force it bank = sectionTypeInfo[type].firstBank; } // This should be redundant, as the parser guarantees that `AlignmentSpec` will be valid. if (alignOffset >= alignSize) { // LCOV_EXCL_START error( "Alignment offset (%" PRIu16 ") must be smaller than alignment size (%" PRIu32 ")", alignOffset, alignSize ); alignOffset = 0; // LCOV_EXCL_STOP } if (org != UINT32_MAX) { if (org < sectionTypeInfo[type].startAddr || org > sectTypeEndAddr(type)) { error( "Section \"%s\"'s fixed address $%04" PRIx32 " is outside of range [$%04" PRIx16 "; $%04" PRIx16 "]", name.c_str(), org, sectionTypeInfo[type].startAddr, sectTypeEndAddr(type) ); } } if (alignment != 0) { // It doesn't make sense to have both alignment and org set if (org != UINT32_MAX) { if ((org - alignOffset) & alignMask) { error("Section \"%s\"'s fixed address does not match its alignment", name.c_str()); } alignment = 0; // Ignore it if it's satisfied } else if (sectionTypeInfo[type].startAddr & alignMask) { error( "Section \"%s\"'s alignment cannot be attained in %s", name.c_str(), sectionTypeInfo[type].name.c_str() ); alignment = 0; // Ignore it if it's unattainable org = 0; } else if (alignment == 16) { // Treat an alignment of 16 as fixing the address. alignment = 0; org = alignOffset; // The address is known to be valid, since the alignment itself is. } } // Check if another section exists with the same name; merge if yes, otherwise create one Section *sect = sect_FindSectionByName(name); if (sect) { mergeSections(*sect, type, org, bank, alignment, alignOffset, mod); } else { sect = createSection(name, type, org, bank, alignment, alignOffset, mod); } return sect; } static void changeSection() { if (!currentUnionStack.empty()) { fatal("Cannot change the section within a `UNION`"); } sym_ResetCurrentLabelScopes(); } uint32_t Section::getID() const { // Section fragments share the same name but have different IDs, so search by identity if (auto search = std::find_if(RANGE(sections), [this](Section const &s) { return &s == this; }); search != sections.end()) { return static_cast(std::distance(sections.begin(), search)); } return UINT32_MAX; // LCOV_EXCL_LINE } bool Section::isSizeKnown() const { // SECTION UNION and SECTION FRAGMENT can still grow if (modifier != SECTION_NORMAL) { return false; } // The current section (or current load section if within one) is still growing if (this == currentSection || this == currentLoadSection) { return false; } // Any section on the stack is still growing for (SectionStackEntry &entry : sectionStack) { if (entry.section && entry.section->name == name) { return false; } } return true; } void sect_NewSection( std::string const &name, SectionType type, uint32_t org, SectionSpec const &attrs, SectionModifier mod ) { for (SectionStackEntry &entry : sectionStack) { if (entry.section && entry.section->name == name) { fatal("Section \"%s\" is already on the stack", name.c_str()); } } if (mod == SECTION_UNION && sectTypeHasData(type)) { error("Cannot declare ROM sections as `UNION`"); return; } if (currentLoadSection) { sect_EndLoadSection("SECTION"); } Section *sect = getSection(name, type, org, attrs, mod); changeSection(); curOffset = mod == SECTION_UNION ? 0 : sect->size; loadOffset = 0; // This is still used when checking for section size overflow! currentSection = sect; } void sect_SetLoadSection( std::string const &name, SectionType type, uint32_t org, SectionSpec const &attrs, SectionModifier mod ) { // Important info: currently, UNION and LOAD cannot interact, since UNION is prohibited in // "code" sections, whereas LOAD is restricted to them. // Therefore, any interactions are NOT TESTED, so lift either of those restrictions at // your own peril! ^^ if (!requireCodeSection()) { return; } if (sectTypeHasData(type)) { error("`LOAD` blocks cannot create a ROM section"); return; } if (currentLoadSection) { sect_EndLoadSection("LOAD"); } Section *sect = getSection(name, type, org, attrs, mod); currentLoadLabelScopes = sym_GetCurrentLabelScopes(); changeSection(); loadOffset = curOffset - (mod == SECTION_UNION ? 0 : sect->size); curOffset -= loadOffset; currentLoadSection = sect; } void sect_EndLoadSection(char const *cause) { if (cause) { warning(WARNING_UNTERMINATED_LOAD, "`LOAD` block without `ENDL` terminated by `%s`", cause); } if (!currentLoadSection) { error("Found `ENDL` outside of a `LOAD` block"); return; } changeSection(); curOffset += loadOffset; loadOffset = 0; currentLoadSection = nullptr; sym_SetCurrentLabelScopes(currentLoadLabelScopes); } void sect_CheckLoadClosed() { if (currentLoadSection) { warning(WARNING_UNTERMINATED_LOAD, "`LOAD` block without `ENDL` terminated by EOF"); } } Section *sect_GetSymbolSection() { return currentLoadSection ? currentLoadSection : currentSection; } uint32_t sect_GetSymbolOffset() { return curOffset; } uint32_t sect_GetOutputOffset() { return curOffset + loadOffset; } std::optional sect_GetOutputBank() { return currentSection ? std::optional(currentSection->bank) : std::nullopt; } Patch *sect_AddOutputPatch() { return currentSection ? ¤tSection->patches.emplace_front() : nullptr; } // Returns how many bytes need outputting for the specified alignment and offset to succeed uint32_t sect_GetAlignBytes(uint8_t alignment, uint16_t offset) { Section *sect = sect_GetSymbolSection(); if (!sect) { return 0; } bool isFixed = sect->org != UINT32_MAX; // If the section is not aligned, no bytes are needed // (fixed sections count as being maximally aligned for this purpose) uint8_t curAlignment = isFixed ? 16 : sect->align; if (curAlignment == 0) { return 0; } // We need `(pcValue + curOffset + return value) & minAlignMask == offset` uint16_t pcValue = isFixed ? sect->org : sect->alignOfs; uint32_t minAlignMask = (1u << std::min(alignment, curAlignment)) - 1; return static_cast(offset - curOffset - pcValue) & minAlignMask; } void sect_AlignPC(uint8_t alignment, uint16_t offset) { if (!requireSection()) { return; } assume(alignment <= 16); // Should be ensured by the caller uint32_t alignSize = 1u << alignment; uint32_t alignMask = alignSize - 1; Section *sect = sect_GetSymbolSection(); assume(sect->align <= 16); // Left-shifting by 32 or more would be UB uint32_t sectAlignSize = 1u << sect->align; uint32_t sectAlignMask = sectAlignSize - 1; if (sect->org != UINT32_MAX) { if (uint32_t actualOffset = (sect->org + curOffset) & alignMask; actualOffset != offset) { error( "Section is misaligned (at PC = $%04" PRIx32 ", expected ALIGN[%" PRIu32 ", %" PRIu32 "], got ALIGN[%" PRIu32 ", %" PRIu32 "])", sect->org + curOffset, alignment, offset, alignment, actualOffset ); } } else if (uint32_t actualOffset = (sect->alignOfs + curOffset) & alignMask; sect->align != 0 && (actualOffset & sectAlignMask) != (offset & sectAlignMask)) { error( "Section is misaligned ($%04" PRIx32 " bytes into the section, expected ALIGN[%" PRIu32 ", %" PRIu32 "], got ALIGN[%" PRIu32 ", %" PRIu32 "])", curOffset, alignment, offset, alignment, actualOffset ); } else if (alignment == 16) { // Treat an alignment large enough as fixing the address. // Note that this also ensures that a section's alignment never becomes 16 or greater. sect->align = 0; // Reset the alignment, since we're fixing the address. sect->org = offset - curOffset; } else if (alignment > sect->align) { sect->align = alignment; // We need `(sect->alignOfs + curOffset) & alignMask == offset` sect->alignOfs = (offset - curOffset) & alignMask; } } static void growSection(uint32_t growth) { if (growth > 0 && curOffset > UINT32_MAX - growth) { fatal("Section size would overflow internal counter"); } curOffset += growth; if (uint32_t outOffset = sect_GetOutputOffset(); outOffset > currentSection->size) { currentSection->size = outOffset; } if (currentLoadSection && curOffset > currentLoadSection->size) { currentLoadSection->size = curOffset; } } static void writeByte(uint8_t byte) { if (uint32_t index = sect_GetOutputOffset(); index < currentSection->data.size()) { currentSection->data[index] = byte; } growSection(1); } static void writeWord(uint16_t value) { writeByte(value & 0xFF); writeByte(value >> 8); } static void writeLong(uint32_t value) { writeByte(value & 0xFF); writeByte(value >> 8); writeByte(value >> 16); writeByte(value >> 24); } static void createPatch(PatchType type, Expression const &expr, uint32_t pcShift) { out_CreatePatch(type, expr, sect_GetOutputOffset(), pcShift); } void sect_StartUnion() { // Important info: currently, UNION and LOAD cannot interact, since UNION is prohibited in // "code" sections, whereas LOAD is restricted to them. // Therefore, any interactions are NOT TESTED, so lift either of those restrictions at // your own peril! ^^ if (!currentSection) { error("`UNION`s must be inside a `SECTION`"); return; } if (sectTypeHasData(currentSection->type)) { error("Cannot use `UNION` inside of `ROM0` or `ROMX` sections"); return; } currentUnionStack.push({.start = curOffset, .size = 0}); } static void endUnionMember() { UnionStackEntry &member = currentUnionStack.top(); uint32_t memberSize = curOffset - member.start; if (memberSize > member.size) { member.size = memberSize; } curOffset = member.start; } void sect_NextUnionMember() { if (currentUnionStack.empty()) { error("Found `NEXTU` outside of a `UNION` construct"); return; } endUnionMember(); } void sect_EndUnion() { if (currentUnionStack.empty()) { error("Found `ENDU` outside of a `UNION` construct"); return; } endUnionMember(); curOffset += currentUnionStack.top().size; currentUnionStack.pop(); } void sect_CheckUnionClosed() { if (!currentUnionStack.empty()) { error("Unterminated `UNION` construct"); } } void sect_ConstByte(uint8_t byte) { if (!requireCodeSection()) { return; } writeByte(byte); } void sect_ByteString(std::vector const &str) { if (!requireCodeSection()) { return; } for (int32_t unit : str) { if (!checkNBit(unit, 8, "All character units")) { break; } } for (int32_t unit : str) { writeByte(static_cast(unit)); } } void sect_WordString(std::vector const &str) { if (!requireCodeSection()) { return; } for (int32_t unit : str) { if (!checkNBit(unit, 16, "All character units")) { break; } } for (int32_t unit : str) { writeWord(static_cast(unit)); } } void sect_LongString(std::vector const &str) { if (!requireCodeSection()) { return; } for (int32_t unit : str) { writeLong(static_cast(unit)); } } void sect_Skip(uint32_t skip, bool ds) { if (!requireSection()) { return; } if (!sectTypeHasData(currentSection->type)) { growSection(skip); } else { if (!ds) { warning( WARNING_EMPTY_DATA_DIRECTIVE, "`%s` directive without data in ROM", (skip == 4) ? "DL" : (skip == 2) ? "DW" : "DB" ); } // We know we're in a code SECTION while (skip--) { writeByte(options.padByte); } } } void sect_RelByte(Expression const &expr, uint32_t pcShift) { if (!requireCodeSection()) { return; } if (!expr.isKnown()) { createPatch(PATCHTYPE_BYTE, expr, pcShift); writeByte(0); } else { writeByte(expr.value()); } } void sect_RelBytes(uint32_t n, std::vector const &exprs) { if (!requireCodeSection()) { return; } for (uint32_t i = 0; i < n; ++i) { if (Expression const &expr = exprs[i % exprs.size()]; !expr.isKnown()) { createPatch(PATCHTYPE_BYTE, expr, i); writeByte(0); } else { writeByte(expr.value()); } } } void sect_RelWord(Expression const &expr, uint32_t pcShift) { if (!requireCodeSection()) { return; } if (!expr.isKnown()) { createPatch(PATCHTYPE_WORD, expr, pcShift); writeWord(0); } else { writeWord(expr.value()); } } void sect_RelLong(Expression const &expr, uint32_t pcShift) { if (!requireCodeSection()) { return; } if (!expr.isKnown()) { createPatch(PATCHTYPE_LONG, expr, pcShift); writeLong(0); } else { writeLong(expr.value()); } } void sect_PCRelByte(Expression const &expr, uint32_t pcShift) { if (!requireCodeSection()) { return; } if (Symbol const *pc = sym_GetPC(); !expr.isDiffConstant(pc)) { createPatch(PATCHTYPE_JR, expr, pcShift); writeByte(0); } else { Symbol const *sym = expr.symbolOf(); // The offset wraps (jump from ROM to HRAM, for example) int16_t offset; // Offset is relative to the byte *after* the operand if (sym == pc) { offset = -2; // PC as operand to `jr` is lower than reference PC by 2 } else { offset = sym->getValue() - (pc->getValue() + 1); } if (offset < -128 || offset > 127) { error( "`JR` target must be between -128 and 127 bytes away, not %" PRId16 "; use `JP` instead", offset ); writeByte(0); } else { writeByte(offset); } } } bool sect_BinaryFile(std::string const &name, uint32_t startPos) { if (!requireCodeSection()) { return false; } FILE *file = nullptr; if (std::optional fullPath = fstk_FindFile(name); fullPath) { file = fopen(fullPath->c_str(), "rb"); } if (!file) { return fstk_FileError(name, "`INCBIN`"); } Defer closeFile{[&] { fclose(file); }}; if (fseek(file, 0, SEEK_END) == 0) { if (startPos > ftell(file)) { error("Specified start position is greater than length of file \"%s\"", name.c_str()); return false; } // The file is seekable; skip to the specified start position fseek(file, startPos, SEEK_SET); } else { // LCOV_EXCL_START if (errno != ESPIPE) { error( "Error determining size of `INCBIN` file \"%s\": %s", name.c_str(), strerror(errno) ); } // The file isn't seekable, so we'll just skip bytes one at a time while (startPos--) { if (fgetc(file) == EOF) { error( "Specified start position is greater than length of file \"%s\"", name.c_str() ); return false; } } // LCOV_EXCL_STOP } for (int byte; (byte = fgetc(file)) != EOF;) { writeByte(byte); } if (ferror(file)) { // LCOV_EXCL_START error("Error reading `INCBIN` file \"%s\": %s", name.c_str(), strerror(errno)); // LCOV_EXCL_STOP } return false; } bool sect_BinaryFileSlice(std::string const &name, uint32_t startPos, uint32_t length) { if (!requireCodeSection()) { return false; } if (length == 0) { // Don't even bother with 0-byte slices return false; } FILE *file = nullptr; if (std::optional fullPath = fstk_FindFile(name); fullPath) { file = fopen(fullPath->c_str(), "rb"); } if (!file) { return fstk_FileError(name, "`INCBIN`"); } Defer closeFile{[&] { fclose(file); }}; if (fseek(file, 0, SEEK_END) == 0) { if (long fsize = ftell(file); startPos > fsize) { error("Specified start position is greater than length of file \"%s\"", name.c_str()); return false; } else if (startPos + length > fsize) { error( "Specified range in `INCBIN` file \"%s\" is out of bounds (%" PRIu32 " + %" PRIu32 " > %ld)", name.c_str(), startPos, length, fsize ); return false; } // The file is seekable; skip to the specified start position fseek(file, startPos, SEEK_SET); } else { // LCOV_EXCL_START if (errno != ESPIPE) { error( "Error determining size of `INCBIN` file \"%s\": %s", name.c_str(), strerror(errno) ); } // The file isn't seekable, so we'll just skip bytes one at a time while (startPos--) { if (fgetc(file) == EOF) { error( "Specified start position is greater than length of file \"%s\"", name.c_str() ); return false; } } // LCOV_EXCL_STOP } while (length--) { if (int byte = fgetc(file); byte != EOF) { writeByte(byte); // LCOV_EXCL_START } else if (ferror(file)) { error("Error reading `INCBIN` file \"%s\": %s", name.c_str(), strerror(errno)); } else { error( "Premature end of `INCBIN` file \"%s\" (%" PRId32 " bytes left to read)", name.c_str(), length + 1 ); // LCOV_EXCL_STOP } } return false; } void sect_PushSection() { sectionStack.push_front({ .section = currentSection, .loadSection = currentLoadSection, .labelScopes = sym_GetCurrentLabelScopes(), .offset = curOffset, .loadOffset = loadOffset, .unionStack = {}, }); // Reset the section scope currentSection = nullptr; currentLoadSection = nullptr; sym_ResetCurrentLabelScopes(); std::swap(currentUnionStack, sectionStack.front().unionStack); } void sect_PopSection() { if (sectionStack.empty()) { fatal("No entries in the section stack"); } if (currentLoadSection) { sect_EndLoadSection("POPS"); } SectionStackEntry entry = sectionStack.front(); sectionStack.pop_front(); changeSection(); currentSection = entry.section; currentLoadSection = entry.loadSection; sym_SetCurrentLabelScopes(entry.labelScopes); curOffset = entry.offset; loadOffset = entry.loadOffset; std::swap(currentUnionStack, entry.unionStack); } void sect_CheckStack() { if (!sectionStack.empty()) { warning(WARNING_UNMATCHED_DIRECTIVE, "`PUSHS` without corresponding `POPS`"); } } void sect_EndSection() { if (!currentSection) { fatal("Cannot end the section outside of a `SECTION`"); } if (!currentUnionStack.empty()) { fatal("Cannot end the section within a `UNION`"); } if (currentLoadSection) { sect_EndLoadSection("ENDSECTION"); } // Reset the section scope currentSection = nullptr; sym_ResetCurrentLabelScopes(); } std::string sect_PushSectionFragmentLiteral() { static uint64_t nextFragmentLiteralID = 0; // Like `requireCodeSection` but fatal if (!currentSection) { fatal("Cannot output fragment literals outside of a `SECTION`"); } if (!sectTypeHasData(currentSection->type)) { fatal( "Section \"%s\" cannot contain fragment literals (not `ROM0` or `ROMX`)", currentSection->name.c_str() ); } // This section has data (ROM0 or ROMX), so it cannot be a UNION assume(currentSection->modifier != SECTION_UNION); if (currentLoadSection) { fatal("`LOAD` blocks cannot contain fragment literals"); } // A section containing a fragment literal has to become a fragment too currentSection->modifier = SECTION_FRAGMENT; Section *parent = currentSection; sect_PushSection(); // Resets `currentSection` Section *sect = createSectionFragmentLiteral(*parent); changeSection(); curOffset = sect->size; currentSection = sect; // Return a symbol ID to use for the address of this section fragment return "$"s + std::to_string(nextFragmentLiteralID++); } gbdev-rgbds-92bfe5d/src/asm/symbol.cpp000066400000000000000000000514221512540461700177730ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "asm/symbol.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "diagnostics.hpp" #include "helpers.hpp" // assume #include "util.hpp" #include "version.hpp" #include "asm/fstack.hpp" #include "asm/lexer.hpp" #include "asm/macro.hpp" #include "asm/main.hpp" #include "asm/output.hpp" #include "asm/section.hpp" #include "asm/warning.hpp" using namespace std::literals; static std::unordered_map symbols; static std::unordered_set purgedSymbols; static Symbol const *globalScope = nullptr; // Current section's global label scope static Symbol const *localScope = nullptr; // Current section's local label scope static Symbol *PCSymbol; static Symbol *NARGSymbol; static Symbol *SCOPESymbol; static Symbol *globalScopeSymbol; static Symbol *localScopeSymbol; static Symbol *RSSymbol; static char savedTIME[256]; static char savedDATE[256]; static char savedTIMESTAMP_ISO8601_LOCAL[256]; static char savedTIMESTAMP_ISO8601_UTC[256]; bool sym_IsPC(Symbol const *sym) { return sym == PCSymbol; } bool sym_IsDotScope(std::string const &symName) { // Label scopes `.` and `..` are the only nonlocal identifiers that start with a dot. // Three or more dots are considered a nonsensical local label. return symName == "." || symName == ".."; } void sym_ForEach(void (*callback)(Symbol &)) { for (auto &it : symbols) { callback(it.second); } } static int32_t NARGCallback() { if (MacroArgs const *macroArgs = fstk_GetCurrentMacroArgs(); macroArgs) { return macroArgs->nbArgs(); } else { error("`_NARG` has no value outside of a macro"); return 0; } } static std::shared_ptr SCOPECallback() { if (localScope) { return std::make_shared(".."); } else if (globalScope) { return std::make_shared("."); } else { if (!sect_GetSymbolSection()) { error("`__SCOPE__` has no value outside of a section"); } return std::make_shared(""); } } static std::shared_ptr globalScopeCallback() { if (!globalScope) { error("`.` has no value outside of a label scope"); return std::make_shared(""); } return std::make_shared(globalScope->name); } static std::shared_ptr localScopeCallback() { if (!localScope) { error("`..` has no value outside of a local label scope"); return std::make_shared(""); } return std::make_shared(localScope->name); } static int32_t PCCallback() { return sect_GetSymbolSection()->org + sect_GetSymbolOffset(); } int32_t Symbol::getValue() const { assume(std::holds_alternative(data) || std::holds_alternative(data)); if (auto *value = std::get_if(&data); value) { return type == SYM_LABEL ? *value + getSection()->org : *value; } return getOutputValue(); } int32_t Symbol::getOutputValue() const { if (auto *value = std::get_if(&data); value) { return *value; } else if (auto *callback = std::get_if(&data); callback) { return (*callback)(); } else { return 0; } } ContentSpan const &Symbol::getMacro() const { assume(std::holds_alternative(data)); return std::get(data); } std::shared_ptr Symbol::getEqus() const { assume( std::holds_alternative>(data) || std::holds_alternative (*)()>(data) ); if (auto *callback = std::get_if (*)()>(&data); callback) { return (*callback)(); } return std::get>(data); } // Meant to be called last in an `errorNoTrace` callback static void printBacktraces(Symbol const &sym) { putc('\n', stderr); fstk_TraceCurrent(); fputs(" and also:\n", stderr); if (sym.src) { sym.src->printBacktrace(sym.fileLine); } else { fprintf(stderr, " at <%s>\n", sym.isBuiltin ? "builtin" : "command-line"); } } static void updateSymbolFilename(Symbol &sym) { std::shared_ptr oldSrc = std::move(sym.src); sym.src = fstk_GetFileStack(); sym.fileLine = sym.src ? lexer_GetLineNo() : 0; // If the old node was registered, ensure the new one is too if (oldSrc && oldSrc->ID != UINT32_MAX) { out_RegisterNode(sym.src); } } static bool isValidIdentifier(std::string const &s) { return !s.empty() && startsIdentifier(s[0]) && std::all_of(s.begin() + 1, s.end(), [](char c) { return continuesIdentifier(c); }); } static void alreadyDefinedError(Symbol const &sym, char const *asType) { auto suggestion = [&]() { std::string s; if (auto const &contents = sym.type == SYM_EQUS ? sym.getEqus() : nullptr; contents && isValidIdentifier(*contents)) { s.append(" (should it be {interpolated} to define its contents \""); s.append(*contents); s.append("\"?)"); } return s; }; if (sym.isBuiltin) { if (sym_FindScopedValidSymbol(sym.name)) { if (std::string s = suggestion(); asType) { error("`%s` already defined as built-in %s%s", sym.name.c_str(), asType, s.c_str()); } else { error("`%s` already defined as built-in%s", sym.name.c_str(), s.c_str()); } } else { // `DEF()` would return false, so we should not claim the symbol is already defined, // nor suggest to interpolate it if (asType) { error("`%s` is reserved for a built-in %s symbol", sym.name.c_str(), asType); } else { error("`%s` is reserved for a built-in symbol", sym.name.c_str()); } } } else { errorNoTrace([&]() { fprintf(stderr, "`%s` already defined", sym.name.c_str()); if (asType) { fprintf(stderr, " as %s", asType); } fputs(suggestion().c_str(), stderr); printBacktraces(sym); }); } } static void redefinedError(Symbol const &sym) { assume(sym.isBuiltin); if (sym_FindScopedValidSymbol(sym.name)) { error("Built-in symbol `%s` cannot be redefined", sym.name.c_str()); } else { // `DEF()` would return false, so we should not imply the symbol is already defined error("`%s` is reserved for a built-in symbol", sym.name.c_str()); } } static void assumeAlreadyExpanded(std::string const &symName) { // Either the symbol name is `Global.local` or entirely '.'s (for scopes `.` and `..`), // but cannot be unqualified `.local` or more than two '.'s assume(!symName.starts_with('.') || sym_IsDotScope(symName)); } static Symbol &createSymbol(std::string const &symName) { assumeAlreadyExpanded(symName); static uint32_t nextDefIndex = 0; Symbol &sym = symbols[symName]; sym.name = symName; sym.isBuiltin = false; sym.isExported = false; sym.isQuiet = false; sym.section = nullptr; sym.src = fstk_GetFileStack(); sym.fileLine = sym.src ? lexer_GetLineNo() : 0; sym.ID = UINT32_MAX; sym.defIndex = nextDefIndex++; return sym; } static bool isAutoScoped(std::string const &symName) { // `globalScope` should be global if it's defined assume(!globalScope || globalScope->name.find('.') == std::string::npos); // `localScope` should be qualified local if it's defined assume(!localScope || localScope->name.find('.') != std::string::npos); size_t dotPos = symName.find('.'); // If there are no dots, it's not a local label if (dotPos == std::string::npos) { return false; } // Label scopes `.` and `..` are the only nonlocal identifiers that start with a dot if (sym_IsDotScope(symName)) { return false; } // Check for nothing after the dot if (dotPos == symName.length() - 1) { fatal("`%s` is a nonsensical reference to an empty local label", symName.c_str()); } // Check for more than one dot if (symName.find('.', dotPos + 1) != std::string::npos) { fatal("`%s` is a nonsensical reference to a nested local label", symName.c_str()); } // Check for already-qualified local label if (dotPos > 0) { return false; } // Check for unqualifiable local label if (!globalScope) { fatal("Unqualified local label `%s` in main scope", symName.c_str()); } return true; } Symbol *sym_FindExactSymbol(std::string const &symName) { assumeAlreadyExpanded(symName); auto search = symbols.find(symName); return search != symbols.end() ? &search->second : nullptr; } Symbol *sym_FindScopedSymbol(std::string const &symName) { return sym_FindExactSymbol(isAutoScoped(symName) ? globalScope->name + symName : symName); } Symbol *sym_FindScopedValidSymbol(std::string const &symName) { Symbol *sym = sym_FindScopedSymbol(symName); // `@` has no value outside of a section if (sym_IsPC(sym) && !sect_GetSymbolSection()) { return nullptr; } // `_NARG` has no value outside of a macro if (sym == NARGSymbol && !fstk_GetCurrentMacroArgs()) { return nullptr; } // `.` has no value outside of a global label scope if (sym == globalScopeSymbol && !globalScope) { return nullptr; } // `..` has no value outside of a local label scope if (sym == localScopeSymbol && !localScope) { return nullptr; } // `__SCOPE__` has no value outside of a section if (sym == SCOPESymbol && !sect_GetSymbolSection()) { return nullptr; } return sym; } Symbol const *sym_GetPC() { return PCSymbol; } void sym_Purge(std::string const &symName) { Symbol *sym = sym_FindScopedValidSymbol(symName); if (!sym) { if (sym_IsPurgedScoped(symName)) { error("Undefined symbol `%s` was already purged", symName.c_str()); } else { error("Undefined symbol `%s`", symName.c_str()); } } else if (sym->isBuiltin) { error("Built-in symbol `%s` cannot be purged", symName.c_str()); } else if (sym->ID != UINT32_MAX) { error("Symbol `%s` is referenced and thus cannot be purged", symName.c_str()); } else { if (sym->isExported) { warning(WARNING_PURGE_1, "Purging an exported symbol `%s`", symName.c_str()); } else if (sym->isLabel()) { warning(WARNING_PURGE_2, "Purging a label `%s`", symName.c_str()); } // Do not keep a reference to the label after purging it if (sym == globalScope) { globalScope = nullptr; } if (sym == localScope) { localScope = nullptr; } purgedSymbols.emplace(sym->name); symbols.erase(sym->name); } } bool sym_IsPurgedExact(std::string const &symName) { assumeAlreadyExpanded(symName); return purgedSymbols.find(symName) != purgedSymbols.end(); } bool sym_IsPurgedScoped(std::string const &symName) { return sym_IsPurgedExact(isAutoScoped(symName) ? globalScope->name + symName : symName); } int32_t sym_GetRSValue() { return RSSymbol->getOutputValue(); } void sym_SetRSValue(int32_t value) { updateSymbolFilename(*RSSymbol); RSSymbol->data = value; } uint32_t Symbol::getConstantValue() const { if (isConstant()) { return getValue(); } if (sym_IsPC(this)) { assume(getSection()); // There's no way to reach here from outside of a section error("PC does not have a constant value; the current section is not fixed"); } else { error("`%s` does not have a constant value", name.c_str()); } return 0; } std::pair sym_GetCurrentLabelScopes() { return {globalScope, localScope}; } void sym_SetCurrentLabelScopes(std::pair newScopes) { globalScope = std::get<0>(newScopes); localScope = std::get<1>(newScopes); // `globalScope` should be global if it's defined assume(!globalScope || globalScope->name.find('.') == std::string::npos); // `localScope` should be qualified local if it's defined assume(!localScope || localScope->name.find('.') != std::string::npos); } void sym_ResetCurrentLabelScopes() { globalScope = nullptr; localScope = nullptr; } static Symbol *createNonrelocSymbol(std::string const &symName, bool numeric) { Symbol *sym = sym_FindExactSymbol(symName); if (!sym) { sym = &createSymbol(symName); purgedSymbols.erase(sym->name); } else if (sym->isDefined()) { alreadyDefinedError(*sym, nullptr); return nullptr; // Don't allow overriding the symbol, that'd be bad! } else if (!numeric) { // The symbol has already been referenced, but it's not allowed errorNoTrace([&]() { fprintf(stderr, "`%s` already referenced", symName.c_str()); printBacktraces(*sym); }); return nullptr; // Don't allow overriding the symbol, that'd be bad! } return sym; } Symbol *sym_AddEqu(std::string const &symName, int32_t value) { Symbol *sym = createNonrelocSymbol(symName, true); if (!sym) { return nullptr; } sym->type = SYM_EQU; sym->data = value; return sym; } Symbol *sym_RedefEqu(std::string const &symName, int32_t value) { Symbol *sym = sym_FindExactSymbol(symName); if (!sym) { return sym_AddEqu(symName, value); } if (sym->isDefined() && sym->type != SYM_EQU) { alreadyDefinedError(*sym, "non-`EQU`"); return nullptr; } else if (sym->isBuiltin) { redefinedError(*sym); return nullptr; } updateSymbolFilename(*sym); sym->type = SYM_EQU; sym->data = value; return sym; } Symbol *sym_AddString(std::string const &symName, std::shared_ptr str) { Symbol *sym = createNonrelocSymbol(symName, false); if (!sym) { return nullptr; } sym->type = SYM_EQUS; sym->data = str; return sym; } Symbol *sym_RedefString(std::string const &symName, std::shared_ptr str) { Symbol *sym = sym_FindExactSymbol(symName); if (!sym) { return sym_AddString(symName, str); } if (sym->type != SYM_EQUS) { if (sym->isDefined()) { alreadyDefinedError(*sym, "non-`EQUS`"); } else { errorNoTrace([&]() { fprintf(stderr, "`%s` already referenced", symName.c_str()); printBacktraces(*sym); }); } return nullptr; } else if (sym->isBuiltin) { redefinedError(*sym); return nullptr; } updateSymbolFilename(*sym); sym->data = str; return sym; } Symbol *sym_AddVar(std::string const &symName, int32_t value) { Symbol *sym = sym_FindExactSymbol(symName); if (!sym) { sym = &createSymbol(symName); } else if (sym->isDefined() && sym->type != SYM_VAR) { alreadyDefinedError(*sym, sym->type == SYM_LABEL ? "label" : "constant"); return sym; } else { updateSymbolFilename(*sym); } sym->type = SYM_VAR; sym->data = value; return sym; } static Symbol *addLabel(std::string const &symName) { assumeAlreadyExpanded(symName); Symbol *sym = sym_FindExactSymbol(symName); if (!sym) { sym = &createSymbol(symName); } else if (sym->isDefined()) { alreadyDefinedError(*sym, nullptr); return nullptr; } else { updateSymbolFilename(*sym); } // If the symbol already exists as a ref, just "take over" it sym->type = SYM_LABEL; sym->data = static_cast(sect_GetSymbolOffset()); // Don't export anonymous labels if (options.exportAll && !symName.starts_with('!')) { sym->isExported = true; } sym->section = sect_GetSymbolSection(); if (sym && !sym->section) { error("Label `%s` created outside of a `SECTION`", symName.c_str()); } return sym; } Symbol *sym_AddLocalLabel(std::string const &symName) { // The symbol name should be local, qualified or not assume(symName.find('.') != std::string::npos); Symbol *sym = addLabel(isAutoScoped(symName) ? globalScope->name + symName : symName); if (sym) { localScope = sym; } return sym; } Symbol *sym_AddLabel(std::string const &symName) { // The symbol name should be global assume(symName.find('.') == std::string::npos); Symbol *sym = addLabel(symName); if (sym) { globalScope = sym; // A new global scope resets the local scope localScope = nullptr; } return sym; } static uint32_t anonLabelID = 0; Symbol *sym_AddAnonLabel() { if (anonLabelID == UINT32_MAX) { // LCOV_EXCL_START error("Only %" PRIu32 " anonymous labels can be created", anonLabelID); return nullptr; // LCOV_EXCL_STOP } std::string anon = sym_MakeAnonLabelName(0, true); // The direction is important! ++anonLabelID; return addLabel(anon); } std::string sym_MakeAnonLabelName(uint32_t ofs, bool neg) { uint32_t id = 0; if (neg) { if (ofs > anonLabelID) { error( "Reference to anonymous label %" PRIu32 " before, when only %" PRIu32 " ha%s been created so far", ofs, anonLabelID, anonLabelID == 1 ? "s" : "ve" ); } else { id = anonLabelID - ofs; } } else { // We're referencing symbols that haven't been created yet... if (--ofs > UINT32_MAX - anonLabelID) { // LCOV_EXCL_START error( "Reference to anonymous label %" PRIu32 " after, when only %" PRIu32 " can still be created", ofs + 1, UINT32_MAX - anonLabelID ); } else { // LCOV_EXCL_STOP id = anonLabelID + ofs; } } return "!"s + std::to_string(id); } void sym_Export(std::string const &symName) { if (symName.starts_with('!')) { // LCOV_EXCL_START // The parser does not accept anonymous labels for an `EXPORT` directive error("Cannot export anonymous label"); return; // LCOV_EXCL_STOP } Symbol *sym = sym_FindScopedSymbol(symName); // If the symbol doesn't exist, create a ref that can be purged if (!sym) { warning(WARNING_EXPORT_UNDEFINED, "Exporting an undefined symbol `%s`", symName.c_str()); sym = sym_Ref(symName); } sym->isExported = true; } Symbol *sym_AddMacro( std::string const &symName, int32_t defLineNo, ContentSpan const &span, bool isQuiet ) { Symbol *sym = createNonrelocSymbol(symName, false); if (!sym) { return nullptr; } sym->type = SYM_MACRO; sym->data = span; sym->isQuiet = isQuiet; sym->src = fstk_GetFileStack(); // The symbol is created at the line after the `ENDM`, // override this with the actual definition line sym->fileLine = defLineNo; return sym; } // Flag that a symbol is referenced in an RPN expression // and create it if it doesn't exist yet Symbol *sym_Ref(std::string const &symName) { Symbol *sym = sym_FindScopedSymbol(symName); if (!sym) { sym = &createSymbol(isAutoScoped(symName) ? globalScope->name + symName : symName); sym->type = SYM_REF; } return sym; } // Define the built-in symbols void sym_Init(time_t now) { PCSymbol = &createSymbol("@"s); PCSymbol->type = SYM_LABEL; PCSymbol->data = PCCallback; PCSymbol->isBuiltin = true; NARGSymbol = &createSymbol("_NARG"s); NARGSymbol->type = SYM_EQU; NARGSymbol->data = NARGCallback; NARGSymbol->isBuiltin = true; globalScopeSymbol = &createSymbol("."s); globalScopeSymbol->type = SYM_EQUS; globalScopeSymbol->data = globalScopeCallback; globalScopeSymbol->isBuiltin = true; localScopeSymbol = &createSymbol(".."s); localScopeSymbol->type = SYM_EQUS; localScopeSymbol->data = localScopeCallback; localScopeSymbol->isBuiltin = true; SCOPESymbol = &createSymbol("__SCOPE__"s); SCOPESymbol->type = SYM_EQUS; SCOPESymbol->data = SCOPECallback; SCOPESymbol->isBuiltin = true; RSSymbol = sym_AddVar("_RS"s, 0); RSSymbol->isBuiltin = true; sym_AddString("__RGBDS_VERSION__"s, std::make_shared(get_package_version_string())) ->isBuiltin = true; sym_AddEqu("__RGBDS_MAJOR__"s, PACKAGE_VERSION_MAJOR)->isBuiltin = true; sym_AddEqu("__RGBDS_MINOR__"s, PACKAGE_VERSION_MINOR)->isBuiltin = true; sym_AddEqu("__RGBDS_PATCH__"s, PACKAGE_VERSION_PATCH)->isBuiltin = true; #ifdef PACKAGE_VERSION_RC sym_AddEqu("__RGBDS_RC__"s, PACKAGE_VERSION_RC)->isBuiltin = true; #endif // LCOV_EXCL_START if (now == static_cast(-1)) { warnx("Failed to determine current time: %s", strerror(errno)); // Fall back by pretending we are at the Epoch now = 0; } // LCOV_EXCL_STOP tm const *time_local = localtime(&now); strftime(savedTIME, sizeof(savedTIME), "\"%H:%M:%S\"", time_local); strftime(savedDATE, sizeof(savedDATE), "\"%d %B %Y\"", time_local); strftime( savedTIMESTAMP_ISO8601_LOCAL, sizeof(savedTIMESTAMP_ISO8601_LOCAL), "\"%Y-%m-%dT%H:%M:%S%z\"", time_local ); tm const *time_utc = gmtime(&now); strftime( savedTIMESTAMP_ISO8601_UTC, sizeof(savedTIMESTAMP_ISO8601_UTC), "\"%Y-%m-%dT%H:%M:%SZ\"", time_utc ); Symbol *timeSymbol = &createSymbol("__TIME__"s); timeSymbol->type = SYM_EQUS; timeSymbol->data = []() { warning(WARNING_OBSOLETE, "`__TIME__` is deprecated; use `__ISO_8601_LOCAL__`"); return std::make_shared(savedTIME); }; timeSymbol->isBuiltin = true; Symbol *dateSymbol = &createSymbol("__DATE__"s); dateSymbol->type = SYM_EQUS; dateSymbol->data = []() { warning(WARNING_OBSOLETE, "`__DATE__` is deprecated; use `__ISO_8601_LOCAL__`"); return std::make_shared(savedDATE); }; dateSymbol->isBuiltin = true; sym_AddString( "__ISO_8601_LOCAL__"s, std::make_shared(savedTIMESTAMP_ISO8601_LOCAL) ) ->isBuiltin = true; sym_AddString("__ISO_8601_UTC__"s, std::make_shared(savedTIMESTAMP_ISO8601_UTC)) ->isBuiltin = true; sym_AddEqu("__UTC_YEAR__"s, time_utc->tm_year + 1900)->isBuiltin = true; sym_AddEqu("__UTC_MONTH__"s, time_utc->tm_mon + 1)->isBuiltin = true; sym_AddEqu("__UTC_DAY__"s, time_utc->tm_mday)->isBuiltin = true; sym_AddEqu("__UTC_HOUR__"s, time_utc->tm_hour)->isBuiltin = true; sym_AddEqu("__UTC_MINUTE__"s, time_utc->tm_min)->isBuiltin = true; sym_AddEqu("__UTC_SECOND__"s, time_utc->tm_sec)->isBuiltin = true; } gbdev-rgbds-92bfe5d/src/asm/warning.cpp000066400000000000000000000106521512540461700201330ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "asm/warning.hpp" #include #include #include #include #include #include "diagnostics.hpp" #include "style.hpp" #include "asm/fstack.hpp" #include "asm/main.hpp" // clang-format off: nested initializers Diagnostics warnings = { .metaWarnings = { {"all", LEVEL_ALL }, {"extra", LEVEL_EXTRA }, {"everything", LEVEL_EVERYTHING}, }, .warningFlags = { {"assert", LEVEL_DEFAULT }, {"backwards-for", LEVEL_ALL }, {"builtin-args", LEVEL_ALL }, {"charmap-redef", LEVEL_ALL }, {"div", LEVEL_EVERYTHING}, {"empty-data-directive", LEVEL_ALL }, {"empty-macro-arg", LEVEL_EXTRA }, {"empty-strrpl", LEVEL_ALL }, {"export-undefined", LEVEL_ALL }, {"large-constant", LEVEL_DEFAULT }, {"macro-shift", LEVEL_EXTRA }, {"nested-comment", LEVEL_DEFAULT }, {"obsolete", LEVEL_DEFAULT }, {"shift", LEVEL_EVERYTHING}, {"shift-amount", LEVEL_EVERYTHING}, {"unmatched-directive", LEVEL_EXTRA }, {"unterminated-load", LEVEL_EXTRA }, {"user", LEVEL_DEFAULT }, // Parametric warnings {"numeric-string", LEVEL_EVERYTHING}, {"numeric-string", LEVEL_EVERYTHING}, {"purge", LEVEL_DEFAULT }, {"purge", LEVEL_ALL }, {"truncation", LEVEL_DEFAULT }, {"truncation", LEVEL_EXTRA }, {"unmapped-char", LEVEL_DEFAULT }, {"unmapped-char", LEVEL_ALL }, }, .paramWarnings = { {WARNING_NUMERIC_STRING_1, WARNING_NUMERIC_STRING_2, 1}, {WARNING_PURGE_1, WARNING_PURGE_2, 2}, {WARNING_TRUNCATION_1, WARNING_TRUNCATION_2, 1}, {WARNING_UNMAPPED_CHAR_1, WARNING_UNMAPPED_CHAR_2, 1}, }, .state = DiagnosticsState(), .nbErrors = 0, }; // clang-format on static void printDiag( char const *fmt, va_list args, char const *type, StyleColor color, char const *flagfmt, char const *flag ) { style_Set(stderr, color, true); fprintf(stderr, "%s: ", type); style_Reset(stderr); vfprintf(stderr, fmt, args); if (flagfmt) { style_Set(stderr, color, true); putc(' ', stderr); fprintf(stderr, flagfmt, flag); } putc('\n', stderr); fstk_TraceCurrent(); } static void incrementErrors() { // This intentionally makes 0 act as "unlimited" warnings.incrementErrors(); if (warnings.nbErrors == options.maxErrors) { style_Set(stderr, STYLE_RED, true); fprintf( stderr, "Assembly aborted after the maximum of %" PRIu64 " error%s", warnings.nbErrors, warnings.nbErrors == 1 ? "" : "s" ); style_Set(stderr, STYLE_RED, false); fputs(" (configure with '-X/--max-errors')\n", stderr); style_Reset(stderr); exit(1); } } void error(char const *fmt, ...) { va_list args; va_start(args, fmt); printDiag(fmt, args, "error", STYLE_RED, nullptr, nullptr); va_end(args); incrementErrors(); } void errorNoTrace(std::function callback) { style_Set(stderr, STYLE_RED, true); fputs("error: ", stderr); style_Reset(stderr); callback(); incrementErrors(); } [[noreturn]] void fatal(char const *fmt, ...) { va_list args; va_start(args, fmt); printDiag(fmt, args, "FATAL", STYLE_RED, nullptr, nullptr); va_end(args); exit(1); } void requireZeroErrors() { if (warnings.nbErrors != 0) { style_Set(stderr, STYLE_RED, true); fprintf( stderr, "Assembly aborted with %" PRIu64 " error%s\n", warnings.nbErrors, warnings.nbErrors == 1 ? "" : "s" ); style_Reset(stderr); exit(1); } } void warning(WarningID id, char const *fmt, ...) { char const *flag = warnings.warningFlags[id].name; va_list args; va_start(args, fmt); switch (warnings.getWarningBehavior(id)) { case WarningBehavior::DISABLED: break; case WarningBehavior::ENABLED: printDiag(fmt, args, "warning", STYLE_YELLOW, "[-W%s]", flag); break; case WarningBehavior::ERROR: printDiag(fmt, args, "error", STYLE_RED, "[-Werror=%s]", flag); break; } va_end(args); } gbdev-rgbds-92bfe5d/src/backtrace.cpp000066400000000000000000000013201512540461700176150ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "backtrace.hpp" #include #include #include "platform.hpp" // strcasecmp #include "util.hpp" // parseWholeNumber Tracing tracing; bool trace_ParseTraceDepth(char const *arg) { if (!strcasecmp(arg, "collapse")) { tracing.collapse = true; return true; } else if (!strcasecmp(arg, "no-collapse")) { tracing.collapse = false; return true; } else if (!strcasecmp(arg, "all")) { tracing.loud = true; return true; } else if (!strcasecmp(arg, "no-all")) { tracing.loud = false; return true; } else { std::optional depth = parseWholeNumber(arg); if (depth) { tracing.depth = *depth; } return depth.has_value(); } } gbdev-rgbds-92bfe5d/src/bison.sh000077500000000000000000000020241512540461700166450ustar00rootroot00000000000000#!/bin/sh set -eu OUTPUT_CPP="${1:?}" INPUT_Y="${2:?}" BISON_MAJOR=$(bison -V | sed -E 's/^.+ ([0-9]+)\..*$/\1/g;q') BISON_MINOR=$(bison -V | sed -E 's/^.+ [0-9]+\.([0-9]+)(\..*)?$/\1/g;q') if [ "$BISON_MAJOR" -lt 3 ]; then echo "Bison $BISON_MAJOR.$BISON_MINOR is not supported" 1>&2 exit 1 fi BISON_FLAGS="-Wall -Dlr.type=ielr" # Set some optimization flags on versions that support them if [ "$BISON_MAJOR" -ge 4 ] || [ "$BISON_MAJOR" -eq 3 ] && [ "$BISON_MINOR" -ge 5 ]; then BISON_FLAGS="$BISON_FLAGS -Dparse.lac=full -Dapi.token.raw=true" fi if [ "$BISON_MAJOR" -ge 4 ] || [ "$BISON_MAJOR" -eq 3 ] && [ "$BISON_MINOR" -ge 6 ]; then BISON_FLAGS="$BISON_FLAGS -Dparse.error=detailed" else BISON_FLAGS="$BISON_FLAGS -Dparse.error=verbose" fi if [ "$BISON_MAJOR" -ge 4 ] || [ "$BISON_MAJOR" -eq 3 ] && [ "$BISON_MINOR" -ge 7 ]; then BISON_FLAGS="$BISON_FLAGS -Wcounterexamples" fi # Replace the arguments to this script ($@) with the ones in $BISON_FLAGS eval "set -- $BISON_FLAGS" exec bison "$@" -d -o "$OUTPUT_CPP" "$INPUT_Y" gbdev-rgbds-92bfe5d/src/cli.cpp000066400000000000000000000113751512540461700164600ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "cli.hpp" #include #include #include #include #include #include #include "extern/getopt.hpp" #include "style.hpp" #include "usage.hpp" #include "util.hpp" // isBlankSpace using namespace std::literals; // Turn an at-file's contents into an argv that `getopt` can handle, appending them to `argPool`. static std::vector readAtFile(std::string const &path, std::vector &argPool, Usage usage) { std::vector argvOfs; std::filebuf file; if (!file.open(path, std::ios_base::in)) { int errnum = errno; style_Set(stderr, STYLE_RED, true); fputs("FATAL: ", stderr); style_Reset(stderr); fprintf(stderr, "Failed to open at-file \"%s\": %s\n", path.c_str(), strerror(errnum)); usage.printAndExit(1); } for (;;) { int c = file.sbumpc(); // First, discard any leading blank space while (isBlankSpace(c)) { c = file.sbumpc(); } // If it's a comment, discard everything until EOL if (c == '#') { c = file.sbumpc(); while (c != EOF && !isNewline(c)) { c = file.sbumpc(); } } if (c == EOF) { return argvOfs; } else if (isNewline(c)) { continue; // Start processing the next line } // Alright, now we can parse the line do { argvOfs.push_back(argPool.size()); // Read one argument (until the next whitespace char). // We know there is one because we already have its first character in `c`. for (; c != EOF && !isWhitespace(c); c = file.sbumpc()) { argPool.push_back(c); } argPool.push_back('\0'); // Discard blank space until the next argument (candidate) while (isBlankSpace(c)) { c = file.sbumpc(); } } while (c != EOF && !isNewline(c)); // End if we reached EOL } } void cli_ParseArgs( int argc, char *argv[], char const *shortOpts, option const *longOpts, void (*parseArg)(int, char *), Usage usage ) { struct AtFileStackEntry { int parentInd; // Saved offset into parent argv std::vector argv; // This context's arg pointer vec AtFileStackEntry(int parentInd_, std::vector argv_) : parentInd(parentInd_), argv(argv_) {} }; std::vector atFileStack; int curArgc = argc; char **curArgv = argv; std::string optString = "-"s + shortOpts; // Request position arguments with a leading '-' std::vector> argPools; for (;;) { char *atFileName = nullptr; for (int ch; (ch = musl_getopt_long_only(curArgc, curArgv, optString.c_str(), longOpts)) != -1;) { if (ch == 1 && musl_optarg[0] == '@') { atFileName = &musl_optarg[1]; break; } else { parseArg(ch, musl_optarg); } } if (atFileName) { // We need to allocate a new arg pool for each at-file, so as not to invalidate pointers // previous at-files may have generated to their own arg pools. // But for the same reason, the arg pool must also outlive the at-file's stack entry! std::vector &argPool = argPools.emplace_back(); // Copy `argv[0]` for error reporting, and because option parsing skips it AtFileStackEntry &stackEntry = atFileStack.emplace_back(musl_optind, std::vector{atFileName}); // It would be nice to compute the char pointers on the fly, but reallocs don't allow // that; so we must compute the offsets after the pool is fixed std::vector offsets = readAtFile(&musl_optarg[1], argPool, usage); stackEntry.argv.reserve(offsets.size() + 2); // Avoid a bunch of reallocs for (size_t ofs : offsets) { stackEntry.argv.push_back(&argPool.data()[ofs]); } stackEntry.argv.push_back(nullptr); // Don't forget the arg vector terminator! curArgc = stackEntry.argv.size() - 1; curArgv = stackEntry.argv.data(); musl_optind = 1; // Don't use 0 because we're not scanning a different argv per se } else { if (musl_optind != curArgc) { // This happens if `--` is passed, process the remaining arg(s) as positional assume(musl_optind < curArgc); for (int i = musl_optind; i < curArgc; ++i) { parseArg(1, argv[i]); // Positional argument } } // Pop off the top stack entry, or end parsing if none if (atFileStack.empty()) { break; } // OK to restore `optind` directly, because `optpos` must be 0 right now. // (Providing 0 would be a "proper" reset, but we want to resume parsing) musl_optind = atFileStack.back().parentInd; atFileStack.pop_back(); if (atFileStack.empty()) { curArgc = argc; curArgv = argv; } else { std::vector &vec = atFileStack.back().argv; curArgc = vec.size(); curArgv = vec.data(); } } } } gbdev-rgbds-92bfe5d/src/diagnostics.cpp000066400000000000000000000046621512540461700202210ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "diagnostics.hpp" #include #include #include #include #include #include #include "helpers.hpp" #include "style.hpp" #include "util.hpp" // isDigit void warnx(char const *fmt, ...) { va_list ap; style_Set(stderr, STYLE_YELLOW, true); fputs("warning: ", stderr); style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); putc('\n', stderr); } void WarningState::update(WarningState other) { if (other.state != WARNING_DEFAULT) { state = other.state; } if (other.error != WARNING_DEFAULT) { error = other.error; } } std::pair> getInitialWarningState(std::string &flag) { // Check for prefixes that affect what the flag does WarningState state; if (flag.starts_with("error=")) { // `-Werror=` enables the flag as an error state = {.state = WARNING_ENABLED, .error = WARNING_ENABLED}; flag.erase(0, literal_strlen("error=")); } else if (flag.starts_with("no-error=")) { // `-Wno-error=` prevents the flag from being an error, // without affecting whether it is enabled state = {.state = WARNING_DEFAULT, .error = WARNING_DISABLED}; flag.erase(0, literal_strlen("no-error=")); } else if (flag.starts_with("no-")) { // `-Wno-` disables the flag state = {.state = WARNING_DISABLED, .error = WARNING_DEFAULT}; flag.erase(0, literal_strlen("no-")); } else { // `-W` enables the flag state = {.state = WARNING_ENABLED, .error = WARNING_DEFAULT}; } // Check if there is an "equals" sign followed by a decimal number // Ignore an equals sign at the very end of the string auto equals = flag.find('='); // `-Wno-` and `-Wno-error=` negation cannot have an `=` parameter, but without // one, the 0 value will apply to all levels of a parametric warning if (state.state != WARNING_ENABLED || equals == flag.npos || equals == flag.size() - 1) { return {state, std::nullopt}; } // If the rest of the string is a decimal number, it's the parameter value char const *ptr = flag.c_str() + equals + 1; uint64_t param = parseNumber(ptr, BASE_10).value_or(0); // If we reached the end of the string, truncate it at the '=' if (*ptr == '\0') { flag.resize(equals); // `-W=0` is equivalent to `-Wno-` if (param == 0) { state.state = WARNING_DISABLED; } } return {state, param > UINT32_MAX ? UINT32_MAX : param}; } gbdev-rgbds-92bfe5d/src/extern/000077500000000000000000000000001512540461700165035ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/src/extern/getopt.cpp000066400000000000000000000117011512540461700205110ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // This implementation was taken from musl and modified for RGBDS. #include "extern/getopt.hpp" #include #include #include #include #include #include #include "style.hpp" char *musl_optarg; int musl_optind = 1, musl_optopt; static int musl_optpos; static void musl_getopt_msg(char const *msg, char const *param) { style_Set(stderr, STYLE_RED, true); fputs("error: ", stderr); style_Reset(stderr); fputs(msg, stderr); fputs(param, stderr); putc('\n', stderr); } static int musl_getopt(int argc, char *argv[], char const *optstring) { if (!musl_optind) { musl_optpos = 0; musl_optind = 1; } if (musl_optind >= argc || !argv[musl_optind]) { return -1; } char *argi = argv[musl_optind]; if (argi[0] != '-') { if (optstring[0] == '-') { musl_optarg = argv[musl_optind++]; return 1; } return -1; } if (!argi[1]) { return -1; } if (argi[1] == '-' && !argi[2]) { ++musl_optind; return -1; } if (!musl_optpos) { ++musl_optpos; } wchar_t c; int k = mbtowc(&c, argi + musl_optpos, MB_LEN_MAX); if (k < 0) { k = 1; c = 0xFFFD; // replacement char } char *optchar = argi + musl_optpos; musl_optpos += k; if (!argi[musl_optpos]) { ++musl_optind; musl_optpos = 0; } if (optstring[0] == '-' || optstring[0] == '+') { ++optstring; } int i = 0; wchar_t d = 0; int l; do { l = mbtowc(&d, optstring + i, MB_LEN_MAX); if (l > 0) { i += l; } else { ++i; } } while (l && d != c); if (d != c || c == ':') { musl_optopt = c; if (optstring[0] != ':') { musl_getopt_msg("unrecognized option: ", optchar); } return '?'; } if (optstring[i] == ':') { musl_optarg = 0; if (optstring[i + 1] != ':' || musl_optpos) { musl_optarg = argv[musl_optind++] + musl_optpos; musl_optpos = 0; } if (musl_optind > argc) { musl_optopt = c; if (optstring[0] == ':') { return ':'; } musl_getopt_msg("option requires an argument: ", optchar); return '?'; } } return c; } static void permute(char **argv, int dest, int src) { char *tmp = argv[src]; for (int i = src; i > dest; --i) { argv[i] = argv[i - 1]; } argv[dest] = tmp; } static int musl_getopt_long_core(int argc, char **argv, char const *optstring, option const *longopts) { musl_optarg = 0; if (char *argi = argv[musl_optind]; !longopts || argi[0] != '-' || ((!argi[1] || argi[1] == '-') && (argi[1] != '-' || !argi[2]))) { return musl_getopt(argc, argv, optstring); } bool colon = optstring[optstring[0] == '+' || optstring[0] == '-'] == ':'; int i = 0, cnt = 0, match = 0; char *arg = 0, *opt, *start = argv[musl_optind] + 1; for (; longopts[i].name; ++i) { char const *name = longopts[i].name; opt = start; if (*opt == '-') { ++opt; } while (*opt && *opt != '=' && *opt == *name) { ++name; ++opt; } if (*opt && *opt != '=') { continue; } arg = opt; match = i; if (!*name) { cnt = 1; break; } ++cnt; } if (cnt == 1 && arg - start == mblen(start, MB_LEN_MAX)) { int l = arg - start; for (i = 0; optstring[i]; ++i) { int j = 0; while (j < l && start[j] == optstring[i + j]) { ++j; } if (j == l) { ++cnt; break; } } } if (cnt == 1) { i = match; opt = arg; ++musl_optind; if (*opt == '=') { if (!longopts[i].has_arg) { musl_optopt = longopts[i].val; if (colon) { return '?'; } musl_getopt_msg("option does not take an argument: ", longopts[i].name); return '?'; } musl_optarg = opt + 1; } else if (longopts[i].has_arg == required_argument) { musl_optarg = argv[musl_optind]; if (!musl_optarg) { musl_optopt = longopts[i].val; if (colon) { return ':'; } musl_getopt_msg("option requires an argument: ", longopts[i].name); return '?'; } ++musl_optind; } if (longopts[i].flag) { *longopts[i].flag = longopts[i].val; return 0; } return longopts[i].val; } if (argv[musl_optind][1] == '-') { musl_optopt = 0; if (!colon) { musl_getopt_msg( cnt ? "option is ambiguous: " : "unrecognized option: ", argv[musl_optind] + 2 ); } ++musl_optind; return '?'; } return musl_getopt(argc, argv, optstring); } int musl_getopt_long_only(int argc, char **argv, char const *optstring, option const *longopts) { if (!musl_optind) { musl_optpos = 0; musl_optind = 1; } if (musl_optind >= argc || !argv[musl_optind]) { return -1; } int skipped = musl_optind; if (optstring[0] != '+' && optstring[0] != '-') { int i = musl_optind; for (;; ++i) { if (i >= argc || !argv[i]) { return -1; } if (argv[i][0] == '-' && argv[i][1]) { break; } } musl_optind = i; } int resumed = musl_optind; int ret = musl_getopt_long_core(argc, argv, optstring, longopts); if (resumed > skipped) { int cnt = musl_optind - resumed; for (int i = 0; i < cnt; ++i) { permute(argv, skipped, musl_optind - 1); } musl_optind = skipped + cnt; } return ret; } gbdev-rgbds-92bfe5d/src/extern/utf8decoder.cpp000066400000000000000000000044721512540461700214320ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // This implementation was taken from // http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ // and modified for RGBDS. #include "extern/utf8decoder.hpp" #include // clang-format off: vertically align values static uint8_t const utf8d[] = { // The first part of the table maps bytes to character classes that // to reduce the size of the transition table and create bitmasks. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..0f 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 10..1f 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..2f 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 30..3f 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..4f 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 50..5f 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..6f 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 70..7f 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 80..8f 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 90..9f 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // a0..af 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // b0..bf 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // c0..cf 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // d0..df 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, // e0..ef 11, 6, 6, 6, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // f0..ff // The second part is a transition table that maps a combination // of a state of the automaton and a character class to a state. 0, 12, 24, 36, 60, 96, 84, 12, 12, 12, 48, 72, // s0 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, // s1 12, 0, 12, 12, 12, 12, 12, 0, 12, 0, 12, 12, // s2 12, 24, 12, 12, 12, 12, 12, 24, 12, 24, 12, 12, // s3 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, // s4 12, 24, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, // s5 12, 12, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, // s6 12, 36, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, // s7 12, 36, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, // s8 }; // clang-format on uint32_t decode(uint32_t *state, uint32_t *codep, uint8_t byte) { uint8_t type = utf8d[byte]; *codep = *state != UTF8_ACCEPT ? (byte & 0b111111) | (*codep << 6) : (0xff >> type) & byte; *state = utf8d[0x100 + *state + type]; return *state; } gbdev-rgbds-92bfe5d/src/fix/000077500000000000000000000000001512540461700157645ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/src/fix/fix.cpp000066400000000000000000000356101512540461700172630ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "fix/fix.hpp" #include #include #include #include #include #include #include #include #include #include "diagnostics.hpp" #include "helpers.hpp" #include "platform.hpp" #include "fix/main.hpp" #include "fix/mbc.hpp" #include "fix/warning.hpp" static constexpr off_t BANK_SIZE = 0x4000; static ssize_t readBytes(int fd, uint8_t *buf, size_t len) { // POSIX specifies that lengths greater than SSIZE_MAX yield implementation-defined results assume(len <= SSIZE_MAX); ssize_t total = 0; while (len) { ssize_t ret = read(fd, buf, len); // Return errors, unless we only were interrupted if (ret == -1 && errno != EINTR) { return -1; // LCOV_EXCL_LINE } // EOF reached if (ret == 0) { return total; } // If anything was read, accumulate it, and continue if (ret != -1) { total += ret; len -= ret; buf += ret; } } return total; } static ssize_t writeBytes(int fd, uint8_t *buf, size_t len) { // POSIX specifies that lengths greater than SSIZE_MAX yield implementation-defined results assume(len <= SSIZE_MAX); ssize_t total = 0; while (len) { ssize_t ret = write(fd, buf, len); // Return errors, unless we only were interrupted if (ret == -1 && errno != EINTR) { return -1; // LCOV_EXCL_LINE } // If anything was written, accumulate it, and continue if (ret != -1) { total += ret; len -= ret; buf += ret; } } return total; } static void overwriteByte(uint8_t *rom0, uint16_t addr, uint8_t fixedByte, char const *areaName) { uint8_t origByte = rom0[addr]; if (origByte != 0 && origByte != fixedByte) { warning(WARNING_OVERWRITE, "Overwrote a non-zero byte in the %s", areaName); } rom0[addr] = fixedByte; } static void overwriteBytes( uint8_t *rom0, uint16_t startAddr, uint8_t const *fixed, uint8_t size, char const *areaName ) { for (uint8_t i = 0; i < size; ++i) { uint8_t origByte = rom0[i + startAddr]; if (origByte != 0 && origByte != fixed[i]) { warning(WARNING_OVERWRITE, "Overwrote a non-zero byte in the %s", areaName); break; } } memcpy(&rom0[startAddr], fixed, size); } static void processFile(int input, int output, char const *name, off_t fileSize, bool expectFileSize) { if (expectFileSize) { assume(fileSize != 0); } else { assume(fileSize == 0); } uint8_t rom0[BANK_SIZE]; ssize_t rom0Len = readBytes(input, rom0, sizeof(rom0)); // Also used as how many bytes to write back when fixing in-place ssize_t headerSize = (options.cartridgeType & 0xFF00) == TPP1 ? 0x154 : 0x150; if (rom0Len == -1) { // LCOV_EXCL_START error("Failed to read \"%s\"'s header: %s", name, strerror(errno)); return; // LCOV_EXCL_STOP } else if (rom0Len < headerSize) { error( "\"%s\" too short, expected at least %jd ($%jx) bytes, got only %jd", name, static_cast(headerSize), static_cast(headerSize), static_cast(rom0Len) ); return; } // Accept partial reads if the file contains at least the header if (options.fixSpec & (FIX_LOGO | TRASH_LOGO)) { overwriteBytes( rom0, 0x0104, options.logo, sizeof(options.logo), options.logoFilename ? "logo" : "Nintendo logo" ); } if (options.title) { overwriteBytes( rom0, 0x134, reinterpret_cast(options.title->c_str()), options.titleLen, "title" ); } if (options.gameID) { overwriteBytes( rom0, 0x13F, reinterpret_cast(options.gameID->c_str()), options.gameIDLen, "manufacturer code" ); } if (options.model != DMG) { overwriteByte(rom0, 0x143, options.model == BOTH ? 0x80 : 0xC0, "CGB flag"); } if (options.newLicensee) { overwriteBytes( rom0, 0x144, reinterpret_cast(options.newLicensee->c_str()), options.newLicenseeLen, "new licensee code" ); } if (options.sgb) { overwriteByte(rom0, 0x146, 0x03, "SGB flag"); } // If a valid MBC was specified... if (options.cartridgeType < MBC_NONE) { uint8_t byte = options.cartridgeType; if ((options.cartridgeType & 0xFF00) == TPP1) { // Cartridge type isn't directly actionable, translate it byte = 0xBC; // The other TPP1 identification bytes will be written below } overwriteByte(rom0, 0x147, byte, "cartridge type"); } // ROM size will be written last, after evaluating the file's size if ((options.cartridgeType & 0xFF00) == TPP1) { uint8_t const tpp1Code[2] = {0xC1, 0x65}; overwriteBytes(rom0, 0x149, tpp1Code, sizeof(tpp1Code), "TPP1 identification code"); overwriteBytes( rom0, 0x150, options.tpp1Rev, sizeof(options.tpp1Rev), "TPP1 revision number" ); if (options.ramSize != UNSPECIFIED) { overwriteByte(rom0, 0x152, options.ramSize, "RAM size"); } overwriteByte(rom0, 0x153, options.cartridgeType & 0xFF, "TPP1 feature flags"); } else { // Regular mappers if (options.ramSize != UNSPECIFIED) { overwriteByte(rom0, 0x149, options.ramSize, "RAM size"); } if (!options.japanese) { overwriteByte(rom0, 0x14A, 0x01, "destination code"); } } if (options.oldLicensee != UNSPECIFIED) { overwriteByte(rom0, 0x14B, options.oldLicensee, "old licensee code"); } else if (options.sgb && rom0[0x14B] != 0x33) { warning( WARNING_SGB, "SGB compatibility enabled, but old licensee was 0x%02x, not 0x33", rom0[0x14B] ); } if (options.romVersion != UNSPECIFIED) { overwriteByte(rom0, 0x14C, options.romVersion, "mask ROM version number"); } // Remain to be handled the ROM size, and header checksum. // The latter depends on the former, and so will be handled after it. // The former requires knowledge of the file's total size, so read that first. uint16_t globalSum = 0; // To keep file sizes fairly reasonable, we'll cap the amount of banks at 65536. // Official mappers only go up to 512 banks, but at least the TPP1 spec allows up to // 65536 banks = 1 GiB. // This should be reasonable for the time being, and may be extended later. std::vector romx; // Buffer of ROMX bank data uint32_t nbBanks = 1; // Number of banks *targeted*, including ROM0 size_t totalRomxLen = 0; // *Actual* size of ROMX data // Handle ROMX auto errorTooLarge = [&name]() { error("\"%s\" has more than 65536 banks", name); // LCOV_EXCL_LINE }; static constexpr off_t NB_BANKS_LIMIT = 0x10000; static_assert(NB_BANKS_LIMIT * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS"); if (input == output) { if (fileSize >= NB_BANKS_LIMIT * BANK_SIZE) { return errorTooLarge(); // LCOV_EXCL_LINE } // Compute number of banks and ROMX len from file size nbBanks = (fileSize + (BANK_SIZE - 1)) / BANK_SIZE; // ceil(fileSize / BANK_SIZE) totalRomxLen = fileSize >= BANK_SIZE ? fileSize - BANK_SIZE : 0; } else if (rom0Len == BANK_SIZE) { // Copy ROMX when reading a pipe, and we're not at EOF yet for (;;) { romx.resize(nbBanks * BANK_SIZE); ssize_t bankLen = readBytes(input, &romx[(nbBanks - 1) * BANK_SIZE], BANK_SIZE); // Update bank count, ONLY IF at least one byte was read if (bankLen) { // We're going to read another bank, check that it won't be too much if (nbBanks == NB_BANKS_LIMIT) { return errorTooLarge(); // LCOV_EXCL_LINE } ++nbBanks; // Update global checksum, too for (uint16_t i = 0; i < bankLen; ++i) { globalSum += romx[totalRomxLen + i]; } totalRomxLen += bankLen; } // Stop when an incomplete bank has been read if (bankLen != BANK_SIZE) { break; } } } // Handle setting the ROM size if padding was requested // Pad to the next valid power of 2. This is because padding is required by flashers, which // flash to ROM chips, whose size is always a power of 2... so there'd be no point in // padding to something else. // Additionally, a ROM must be at least 32k, so we guarantee a whole amount of banks... if (options.padValue != UNSPECIFIED) { // We want at least 2 banks if (nbBanks == 1) { if (rom0Len != sizeof(rom0)) { memset(&rom0[rom0Len], options.padValue, sizeof(rom0) - rom0Len); // The global checksum hasn't taken ROM0 into consideration yet! // ROM0 was padded, so treat it as entirely written: update its size // Update how many bytes were read in total, too rom0Len = sizeof(rom0); } nbBanks = 2; } else { assume(rom0Len == sizeof(rom0)); } assume(nbBanks >= 2); // Alter number of banks to reflect required value // x&(x-1) is zero iff x is a power of 2, or 0; we know for sure it's non-zero, // so this is true (non-zero) when we don't have a power of 2 if (nbBanks & (nbBanks - 1)) { nbBanks = 1 << (CHAR_BIT * sizeof(nbBanks) - clz(nbBanks)); } // Write final ROM size rom0[0x148] = ctz(nbBanks / 2); // Alter global checksum based on how many bytes will be added (not counting ROM0) globalSum += options.padValue * ((nbBanks - 1) * BANK_SIZE - totalRomxLen); } // Handle the header checksum after the ROM size has been written if (options.fixSpec & (FIX_HEADER_SUM | TRASH_HEADER_SUM)) { uint8_t sum = 0; for (uint16_t i = 0x134; i < 0x14D; ++i) { sum -= rom0[i] + 1; } overwriteByte( rom0, 0x14D, options.fixSpec & TRASH_HEADER_SUM ? ~sum : sum, "header checksum" ); } if (options.fixSpec & (FIX_GLOBAL_SUM | TRASH_GLOBAL_SUM)) { // Computation of the global checksum does not include the checksum bytes assume(rom0Len >= 0x14E); for (uint16_t i = 0; i < 0x14E; ++i) { globalSum += rom0[i]; } for (uint16_t i = 0x150; i < rom0Len; ++i) { globalSum += rom0[i]; } // Pipes have already read ROMX and updated globalSum, but not regular files if (input == output) { for (;;) { uint8_t bank[BANK_SIZE]; ssize_t bankLen = readBytes(input, bank, sizeof(bank)); for (uint16_t i = 0; i < bankLen; ++i) { globalSum += bank[i]; } if (bankLen != sizeof(bank)) { break; } } } if (options.fixSpec & TRASH_GLOBAL_SUM) { globalSum = ~globalSum; } uint8_t bytes[2] = { static_cast(globalSum >> 8), static_cast(globalSum & 0xFF) }; overwriteBytes(rom0, 0x14E, bytes, sizeof(bytes), "global checksum"); } ssize_t writeLen; // In case the output depends on the input, reset to the beginning of the file, and only // write the header if (input == output) { if (lseek(output, 0, SEEK_SET) == static_cast(-1)) { // LCOV_EXCL_START error("Failed to rewind \"%s\": %s", name, strerror(errno)); return; // LCOV_EXCL_STOP } // If modifying the file in-place, we only need to edit the header // However, padding may have modified ROM0 (added padding), so don't in that case if (options.padValue == UNSPECIFIED) { rom0Len = headerSize; } } writeLen = writeBytes(output, rom0, rom0Len); if (writeLen == -1) { // LCOV_EXCL_START error("Failed to write \"%s\"'s ROM0: %s", name, strerror(errno)); return; // LCOV_EXCL_STOP } else if (writeLen < rom0Len) { // LCOV_EXCL_START error( "Could only write %jd of \"%s\"'s %jd ROM0 bytes", static_cast(writeLen), name, static_cast(rom0Len) ); return; // LCOV_EXCL_STOP } // Output ROMX if it was buffered if (!romx.empty()) { // The value returned is either -1, or smaller than `totalRomxLen`, // so it's fine to cast to `size_t` writeLen = writeBytes(output, romx.data(), totalRomxLen); if (writeLen == -1) { // LCOV_EXCL_START error("Failed to write \"%s\"'s ROMX: %s", name, strerror(errno)); return; // LCOV_EXCL_STOP } else if (static_cast(writeLen) < totalRomxLen) { // LCOV_EXCL_START error( "Could only write %jd of \"%s\"'s %zu ROMX bytes", static_cast(writeLen), name, totalRomxLen ); return; // LCOV_EXCL_STOP } } // Output padding if (options.padValue != UNSPECIFIED) { if (input == output) { if (lseek(output, 0, SEEK_END) == static_cast(-1)) { // LCOV_EXCL_START error("Failed to seek to end of \"%s\": %s", name, strerror(errno)); return; // LCOV_EXCL_STOP } } uint8_t bank[BANK_SIZE]; memset(bank, options.padValue, sizeof(bank)); size_t len = (nbBanks - 1) * sizeof(bank) - totalRomxLen; // Don't count ROM0! while (len) { static_assert(sizeof(bank) <= SSIZE_MAX, "Bank too large for reading"); size_t thisLen = len > sizeof(bank) ? sizeof(bank) : len; ssize_t ret = writeBytes(output, bank, thisLen); // The return value is either -1, or at most `thisLen`, // so it's fine to cast to `size_t` if (static_cast(ret) != thisLen) { // LCOV_EXCL_START error("Failed to write \"%s\"'s padding: %s", name, strerror(errno)); break; // LCOV_EXCL_STOP } len -= thisLen; } } } bool fix_ProcessFile(char const *name, char const *outputName) { warnings.nbErrors = 0; bool inputStdin = !strcmp(name, "-"); if (inputStdin && !outputName) { outputName = "-"; } int output = -1; bool openedOutput = false; if (outputName) { if (!strcmp(outputName, "-")) { output = STDOUT_FILENO; (void)setmode(STDOUT_FILENO, O_BINARY); } else { output = open(outputName, O_WRONLY | O_BINARY | O_CREAT, 0600); if (output == -1) { // LCOV_EXCL_START error("Failed to open \"%s\" for writing: %s", outputName, strerror(errno)); return true; // LCOV_EXCL_STOP } openedOutput = true; } } Defer closeOutput{[&] { if (openedOutput) { close(output); } }}; if (inputStdin) { name = ""; (void)setmode(STDIN_FILENO, O_BINARY); processFile(STDIN_FILENO, output, name, 0, false); } else if (int input = open(name, (outputName ? O_RDONLY : O_RDWR) | O_BINARY); input == -1) { // POSIX specifies that the results of O_RDWR on a FIFO are undefined. // However, this is necessary to avoid a TOCTTOU, if the file was changed between // `stat()` and `open(O_RDWR)`, which could trigger the UB anyway. // Thus, we're going to hope that either the `open` fails, or it succeeds but I/O // operations may fail, all of which we handle. error("Failed to open \"%s\" for reading+writing: %s", name, strerror(errno)); } else { Defer closeInput{[&] { close(input); }}; struct stat stat; if (fstat(input, &stat) == -1) { error("Failed to stat \"%s\": %s", name, strerror(errno)); // LCOV_EXCL_LINE } else if (!S_ISREG(stat.st_mode)) { // We do not support FIFOs or symlinks // LCOV_EXCL_START error("\"%s\" is not a regular file, and thus cannot be modified in-place", name); // LCOV_EXCL_STOP } else if (stat.st_size < 0x150) { // This check is in theory redundant with the one in `processFile`, but it // prevents passing a file size of 0, which usually indicates pipes error( "\"%s\" too short, expected at least 336 ($150) bytes, got only %jd", name, static_cast(stat.st_size) ); } else { if (!outputName) { output = input; } processFile(input, output, name, stat.st_size, true); } } return checkErrors(name); } gbdev-rgbds-92bfe5d/src/fix/main.cpp000066400000000000000000000275271512540461700174310ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "fix/main.hpp" #include #include #include #include #include #include #include #include #include #include "cli.hpp" #include "diagnostics.hpp" #include "helpers.hpp" #include "platform.hpp" #include "style.hpp" #include "usage.hpp" #include "util.hpp" #include "fix/fix.hpp" #include "fix/mbc.hpp" #include "fix/warning.hpp" Options options; // Flags which must be processed after the option parsing finishes static struct LocalOptions { std::optional outputFileName; // -o std::vector inputFileNames; // ... } localOptions; // Short options static char const *optstring = "Ccf:hi:jk:L:l:m:n:Oo:p:r:st:VvW:w"; // Long-only option variable static int longOpt; // `--color` // Equivalent long options // Please keep in the same order as short opts. // Also, make sure long opts don't create ambiguity: // A long opt's name should start with the same letter as its short opt, // except if it doesn't create any ambiguity (`verbose` versus `version`). // This is because long opt matching, even to a single char, is prioritized // over short opt matching. static option const longopts[] = { {"color-only", no_argument, nullptr, 'C'}, {"color-compatible", no_argument, nullptr, 'c'}, {"fix-spec", required_argument, nullptr, 'f'}, {"help", no_argument, nullptr, 'h'}, {"game-id", required_argument, nullptr, 'i'}, {"non-japanese", no_argument, nullptr, 'j'}, {"new-licensee", required_argument, nullptr, 'k'}, {"logo", required_argument, nullptr, 'L'}, {"old-licensee", required_argument, nullptr, 'l'}, {"mbc-type", required_argument, nullptr, 'm'}, {"rom-version", required_argument, nullptr, 'n'}, {"overwrite", no_argument, nullptr, 'O'}, {"output", required_argument, nullptr, 'o'}, {"pad-value", required_argument, nullptr, 'p'}, {"ram-size", required_argument, nullptr, 'r'}, {"sgb-compatible", no_argument, nullptr, 's'}, {"title", required_argument, nullptr, 't'}, {"version", no_argument, nullptr, 'V'}, {"validate", no_argument, nullptr, 'v'}, {"warning", required_argument, nullptr, 'W'}, {"color", required_argument, &longOpt, 'c'}, {nullptr, no_argument, nullptr, 0 }, }; // clang-format off: nested initializers static Usage usage = { .name = "rgbfix", .flags = { "[-hjOsVvw]", "[-C | -c]", "[-f ]", "[-i ]", "[-k ]", "[-L ]", "[-l ]", "[-m ]", "[-n ]", "[-p ]", "[-r ]", "[-t ]", "[-W warning]", " ...", }, .options = { { {"-m", "--mbc-type "}, { "set the MBC type byte to this value; \"-m help\"", "or \"-m list\" prints the accepted values", }, }, {{"-p", "--pad-value "}, {"pad to the next valid size using this value"}}, {{"-r", "--ram-size "}, {"set the cart RAM size byte to this value"}}, {{"-o", "--output "}, {"set the output file"}}, {{"-V", "--version"}, {"print RGBFIX version and exit"}}, {{"-v", "--validate"}, {"fix the header logo and both checksums (\"-f lhg\")"}}, {{"-W", "--warning "}, {"enable or disable warnings"}}, }, }; // clang-format on static uint16_t parseByte(char const *input, char name) { if (std::optional value = parseWholeNumber(input); !value) { fatal("Invalid argument for option '-%c'", name); } else if (*value > 0xFF) { fatal("Argument for option '-%c' must be between 0 and 0xFF", name); } else { return *value; } } static void parseArg(int ch, char *arg) { switch (ch) { case 'C': case 'c': options.model = ch == 'c' ? BOTH : CGB; if (options.titleLen > 15) { options.titleLen = 15; assume(options.title.has_value()); warning( WARNING_TRUNCATION, "Truncating title \"%s\" to 15 chars", options.title->c_str() ); } break; case 'f': options.fixSpec = 0; while (*arg) { switch (*arg) { #define overrideSpec(cur, bad, curFlag, badFlag) \ case cur: \ if (options.fixSpec & badFlag) { \ warnx("'%c' overriding '%c' in fix spec", cur, bad); \ } \ options.fixSpec = (options.fixSpec & ~badFlag) | curFlag; \ break #define overrideSpecPair(fix, fixFlag, trash, trashFlag) \ overrideSpec(fix, trash, fixFlag, trashFlag); \ overrideSpec(trash, fix, trashFlag, fixFlag) overrideSpecPair('l', FIX_LOGO, 'L', TRASH_LOGO); overrideSpecPair('h', FIX_HEADER_SUM, 'H', TRASH_HEADER_SUM); overrideSpecPair('g', FIX_GLOBAL_SUM, 'G', TRASH_GLOBAL_SUM); #undef overrideSpec #undef overrideSpecPair default: fatal("Invalid character '%c' in fix spec", *arg); } ++arg; } break; // LCOV_EXCL_START case 'h': usage.printAndExit(0); // LCOV_EXCL_STOP case 'i': { options.gameID = arg; size_t len = options.gameID->length(); if (len > 4) { len = 4; warning( WARNING_TRUNCATION, "Truncating game ID \"%s\" to 4 chars", options.gameID->c_str() ); } options.gameIDLen = len; if (options.titleLen > 11) { options.titleLen = 11; assume(options.title.has_value()); warning( WARNING_TRUNCATION, "Truncating title \"%s\" to 11 chars", options.title->c_str() ); } break; } case 'j': options.japanese = false; break; case 'k': { options.newLicensee = arg; size_t len = options.newLicensee->length(); if (len > 2) { len = 2; warning( WARNING_TRUNCATION, "Truncating new licensee \"%s\" to 2 chars", options.newLicensee->c_str() ); } options.newLicenseeLen = len; break; } case 'L': options.logoFilename = arg; break; case 'l': options.oldLicensee = parseByte(arg, 'l'); break; case 'm': options.cartridgeType = mbc_ParseName(arg, options.tpp1Rev[0], options.tpp1Rev[1]); if (options.cartridgeType == ROM_RAM || options.cartridgeType == ROM_RAM_BATTERY) { warning(WARNING_MBC, "MBC \"%s\" is under-specified and poorly supported", arg); } break; case 'n': options.romVersion = parseByte(arg, 'n'); break; case 'O': warning(WARNING_OBSOLETE, "'-O' is deprecated; use '-Wno-overwrite' instead"); warnings.processWarningFlag("no-overwrite"); break; case 'o': localOptions.outputFileName = arg; break; case 'p': options.padValue = parseByte(arg, 'p'); break; case 'r': options.ramSize = parseByte(arg, 'r'); break; case 's': options.sgb = true; break; case 't': { options.title = arg; size_t len = options.title->length(); uint8_t maxLen = options.gameID ? 11 : options.model != DMG ? 15 : 16; if (len > maxLen) { len = maxLen; warning( WARNING_TRUNCATION, "Truncating title \"%s\" to %u chars", options.title->c_str(), maxLen ); } options.titleLen = len; break; } // LCOV_EXCL_START case 'V': usage.printVersion(false); exit(0); case 'v': options.fixSpec = FIX_LOGO | FIX_HEADER_SUM | FIX_GLOBAL_SUM; break; // LCOV_EXCL_STOP case 'W': warnings.processWarningFlag(arg); break; case 'w': warnings.state.warningsEnabled = false; break; case 0: // Long-only options if (longOpt == 'c' && !style_Parse(arg)) { fatal("Invalid argument for option '--color'"); } break; case 1: // Positional arguments localOptions.inputFileNames.push_back(arg); break; // LCOV_EXCL_START default: usage.printAndExit(1); // LCOV_EXCL_STOP } } static uint8_t const nintendoLogo[] = { 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E, }; static void initLogo() { if (options.logoFilename) { FILE *logoFile; char const *logoFilename = options.logoFilename->c_str(); if (*options.logoFilename != "-") { logoFile = fopen(logoFilename, "rb"); } else { // LCOV_EXCL_START logoFilename = ""; (void)setmode(STDIN_FILENO, O_BINARY); logoFile = stdin; // LCOV_EXCL_STOP } if (!logoFile) { // LCOV_EXCL_START fatal("Failed to open \"%s\" for reading: %s", logoFilename, strerror(errno)); // LCOV_EXCL_STOP } Defer closeLogo{[&] { fclose(logoFile); }}; uint8_t logoBpp[sizeof(options.logo)]; if (size_t nbRead = fread(logoBpp, 1, sizeof(logoBpp), logoFile); nbRead != sizeof(options.logo) || fgetc(logoFile) != EOF || ferror(logoFile)) { fatal("\"%s\" is not %zu bytes", logoFilename, sizeof(options.logo)); } auto highs = [&logoBpp](size_t i) { return (logoBpp[i * 2] & 0xF0) | ((logoBpp[i * 2 + 1] & 0xF0) >> 4); }; auto lows = [&logoBpp](size_t i) { return ((logoBpp[i * 2] & 0x0F) << 4) | (logoBpp[i * 2 + 1] & 0x0F); }; constexpr size_t mid = sizeof(options.logo) / 2; for (size_t i = 0; i < mid; i += 4) { options.logo[i + 0] = highs(i + 0); options.logo[i + 1] = highs(i + 1); options.logo[i + 2] = lows(i + 0); options.logo[i + 3] = lows(i + 1); options.logo[mid + i + 0] = highs(i + 2); options.logo[mid + i + 1] = highs(i + 3); options.logo[mid + i + 2] = lows(i + 2); options.logo[mid + i + 3] = lows(i + 3); } } else { static_assert(sizeof(options.logo) == sizeof(nintendoLogo)); memcpy(options.logo, nintendoLogo, sizeof(nintendoLogo)); } if (options.fixSpec & TRASH_LOGO) { for (uint16_t i = 0; i < sizeof(options.logo); ++i) { options.logo[i] = 0xFF ^ options.logo[i]; } } } int main(int argc, char *argv[]) { cli_ParseArgs(argc, argv, optstring, longopts, parseArg, usage); if ((options.cartridgeType & 0xFF00) == TPP1 && !options.japanese) { warning( WARNING_MBC, "TPP1 overwrites region flag for its identification code, ignoring '-j'" ); } // Check that RAM size is correct for "standard" mappers if (options.ramSize != UNSPECIFIED && (options.cartridgeType & 0xFF00) == 0) { if (options.cartridgeType == ROM_RAM || options.cartridgeType == ROM_RAM_BATTERY) { if (options.ramSize != 1) { warning( WARNING_MBC, "MBC \"%s\" should have 2 KiB of RAM (\"-r 1\")", mbc_Name(options.cartridgeType) ); } } else if (mbc_HasRAM(options.cartridgeType)) { if (!options.ramSize) { warning( WARNING_MBC, "MBC \"%s\" has RAM, but RAM size was set to 0", mbc_Name(options.cartridgeType) ); } else if (options.ramSize == 1) { warning( WARNING_MBC, "RAM size 1 (2 KiB) was specified for MBC \"%s\"", mbc_Name(options.cartridgeType) ); } } else if (options.ramSize) { warning( WARNING_MBC, "MBC \"%s\" has no RAM, but RAM size was set to %u", mbc_Name(options.cartridgeType), options.ramSize ); } } if (options.sgb && options.oldLicensee != UNSPECIFIED && options.oldLicensee != 0x33) { warning( WARNING_SGB, "SGB compatibility enabled, but old licensee is 0x%02x, not 0x33", options.oldLicensee ); } initLogo(); if (localOptions.inputFileNames.empty()) { usage.printAndExit("No input file specified (pass \"-\" to read from standard input)"); } if (localOptions.outputFileName && localOptions.inputFileNames.size() != 1) { usage.printAndExit("If '-o' is set then only a single input file may be specified"); } char const *outputFileName = localOptions.outputFileName ? localOptions.outputFileName->c_str() : nullptr; bool failed = warnings.nbErrors > 0; for (std::string const &inputFileName : localOptions.inputFileNames) { failed |= fix_ProcessFile(inputFileName.c_str(), outputFileName); } return failed; } gbdev-rgbds-92bfe5d/src/fix/mbc.cpp000066400000000000000000000326061512540461700172400ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "fix/mbc.hpp" #include #include #include #include #include #include #include #include "helpers.hpp" // unreachable_ #include "platform.hpp" // strcasecmp #include "util.hpp" #include "fix/warning.hpp" // Associate every MBC type with its name and whether it has RAM static std::unordered_map> mbcData{ {ROM, {"ROM", false} }, {ROM_RAM, {"ROM+RAM", true} }, {ROM_RAM_BATTERY, {"ROM+RAM+BATTERY", true} }, {MBC1, {"MBC1", false} }, {MBC1_RAM, {"MBC1+RAM", true} }, {MBC1_RAM_BATTERY, {"MBC1+RAM+BATTERY", true} }, // MBC2 technically has RAM, but is not marked as such {MBC2, {"MBC2", false} }, {MBC2_BATTERY, {"MBC2+BATTERY", false} }, {MMM01, {"MMM01", false} }, {MMM01_RAM, {"MMM01+RAM", true} }, {MMM01_RAM_BATTERY, {"MMM01+RAM+BATTERY", true} }, {MBC3, {"MBC3", false} }, {MBC3_TIMER_BATTERY, {"MBC3+TIMER+BATTERY", false} }, {MBC3_TIMER_RAM_BATTERY, {"MBC3+TIMER+RAM+BATTERY", true} }, {MBC3_RAM, {"MBC3+RAM", true} }, {MBC3_RAM_BATTERY, {"MBC3+RAM+BATTERY", true} }, {MBC5, {"MBC5", false} }, {MBC5_RAM, {"MBC5+RAM", true} }, {MBC5_RAM_BATTERY, {"MBC5+RAM+BATTERY", true} }, {MBC5_RUMBLE, {"MBC5+RUMBLE", false} }, {MBC5_RUMBLE_RAM, {"MBC5+RUMBLE+RAM", true} }, {MBC5_RUMBLE_RAM_BATTERY, {"MBC5+RUMBLE+RAM+BATTERY", true} }, // MBC6 "Net de Get - Minigame @ 100" has RAM size 3 (32 KiB) {MBC6, {"MBC6", true} }, {MBC7_SENSOR_RUMBLE_RAM_BATTERY, {"MBC7+SENSOR+RUMBLE+RAM+BATTERY", true} }, {POCKET_CAMERA, {"POCKET CAMERA", true} }, // Bandai TAMA5 "Game de Hakken!! Tamagotchi - Osutchi to Mesutchi" has RAM size 0 {BANDAI_TAMA5, {"BANDAI TAMA5", false} }, {HUC3, {"HUC3", true} }, {HUC1_RAM_BATTERY, {"HUC1+RAM+BATTERY", true} }, // TPP1 may or may not have RAM, don't use these flags for it {TPP1, {"TPP1", false} }, {TPP1_RUMBLE, {"TPP1+RUMBLE", false} }, {TPP1_MULTIRUMBLE_RUMBLE, {"TPP1+MULTIRUMBLE", false} }, {TPP1_TIMER, {"TPP1+TIMER", false} }, {TPP1_TIMER_RUMBLE, {"TPP1+TIMER+RUMBLE", false} }, {TPP1_TIMER_MULTIRUMBLE_RUMBLE, {"TPP1+TIMER+MULTIRUMBLE", false} }, {TPP1_BATTERY, {"TPP1+BATTERY", false} }, {TPP1_BATTERY_RUMBLE, {"TPP1+BATTERY+RUMBLE", false} }, {TPP1_BATTERY_MULTIRUMBLE_RUMBLE, {"TPP1+BATTERY+MULTIRUMBLE", false} }, {TPP1_BATTERY_TIMER, {"TPP1+BATTERY+TIMER", false} }, {TPP1_BATTERY_TIMER_RUMBLE, {"TPP1+BATTERY+TIMER+RUMBLE", false} }, {TPP1_BATTERY_TIMER_MULTIRUMBLE_RUMBLE, {"TPP1+BATTERY+TIMER+MULTIRUMBLE", false}}, }; static char const *acceptedMBCNames = "Accepted MBC names:\n" "\tROM ($00) [aka ROM_ONLY]\n" "\tMBC1 ($01), MBC1+RAM ($02), MBC1+RAM+BATTERY ($03)\n" "\tMBC2 ($05), MBC2+BATTERY ($06)\n" "\tROM+RAM ($08) [deprecated], ROM+RAM+BATTERY ($09) [deprecated]\n" "\tMMM01 ($0B), MMM01+RAM ($0C), MMM01+RAM+BATTERY ($0D)\n" "\tMBC3+TIMER+BATTERY ($0F), MBC3+TIMER+RAM+BATTERY ($10)\n" "\tMBC3 ($11), MBC3+RAM ($12), MBC3+RAM+BATTERY ($13)\n" "\tMBC5 ($19), MBC5+RAM ($1A), MBC5+RAM+BATTERY ($1B)\n" "\tMBC5+RUMBLE ($1C), MBC5+RUMBLE+RAM ($1D), MBC5+RUMBLE+RAM+BATTERY ($1E)\n" "\tMBC6 ($20)\n" "\tMBC7+SENSOR+RUMBLE+RAM+BATTERY ($22)\n" "\tPOCKET_CAMERA ($FC)\n" "\tBANDAI_TAMA5 ($FD) [aka TAMA5]\n" "\tHUC3 ($FE)\n" "\tHUC1+RAM+BATTERY ($FF)\n" "\n" "\tTPP1_1.0, TPP1_1.0+RUMBLE, TPP1_1.0+MULTIRUMBLE, TPP1_1.0+TIMER,\n" "\tTPP1_1.0+TIMER+RUMBLE, TPP1_1.0+TIMER+MULTIRUMBLE, TPP1_1.0+BATTERY,\n" "\tTPP1_1.0+BATTERY+RUMBLE, TPP1_1.0+BATTERY+MULTIRUMBLE,\n" "\tTPP1_1.0+BATTERY+TIMER, TPP1_1.0+BATTERY+TIMER+RUMBLE,\n" "\tTPP1_1.0+BATTERY+TIMER+MULTIRUMBLE"; // No trailing newline char const *mbc_Name(MbcType type) { auto search = mbcData.find(type); return search != mbcData.end() ? search->second.first : "(unknown)"; } bool mbc_HasRAM(MbcType type) { auto search = mbcData.find(type); return search != mbcData.end() && search->second.second; } static void skipMBCSpace(char const *&ptr) { ptr += strspn(ptr, " \t_"); } static char normalizeMBCChar(char c) { if (c == '_') { return ' '; // Treat underscores as spaces } return toUpper(c); // Uppercase for comparison with `mbc_Name`s } [[noreturn]] static void fatalUnknownMBC(char const *name) { fatal("Unknown MBC \"%s\"\n%s", name, acceptedMBCNames); } [[noreturn]] static void fatalWrongMBCFeatures(char const *name) { fatal("Features incompatible with MBC (\"%s\")\n%s", name, acceptedMBCNames); } MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor) { char const *ptr = name + strspn(name, " \t"); // Skip leading blank space if (!strcasecmp(ptr, "help") || !strcasecmp(ptr, "list")) { puts(acceptedMBCNames); // Outputs to stdout and appends a newline exit(0); } // Parse numeric MBC and return it as-is (unless it's too large) if (char c = *ptr; isDigit(c) || c == '$' || c == '&' || c == '%') { if (std::optional mbc = parseWholeNumber(ptr); !mbc) { fatalUnknownMBC(name); } else if (*mbc > 0xFF) { fatal("Specified MBC ID out of range 0-255: \"%s\"", name); } else { return static_cast(*mbc); } } // Begin by reading the MBC type: uint16_t mbc = UINT16_MAX; auto tryReadSlice = [&ptr, &name](char const *expected) { while (*expected) { // If `name` is too short, the character will be '\0' and this will return `false` if (normalizeMBCChar(*ptr++) != *expected++) { fatalUnknownMBC(name); } } }; switch (*ptr++) { case 'R': // ROM / ROM_ONLY case 'r': tryReadSlice("OM"); // Handle optional " ONLY" skipMBCSpace(ptr); if (*ptr == 'O' || *ptr == 'o') { ++ptr; tryReadSlice("NLY"); } mbc = ROM; break; case 'M': // MBC{1, 2, 3, 5, 6, 7} / MMM01 case 'm': switch (*ptr++) { case 'B': case 'b': tryReadSlice("C"); switch (*ptr++) { case '1': mbc = MBC1; break; case '2': mbc = MBC2; break; case '3': mbc = MBC3; break; case '5': mbc = MBC5; break; case '6': mbc = MBC6; break; case '7': mbc = MBC7_SENSOR_RUMBLE_RAM_BATTERY; break; } break; case 'M': case 'm': tryReadSlice("M01"); mbc = MMM01; break; } break; case 'P': // POCKET_CAMERA case 'p': tryReadSlice("OCKET CAMERA"); mbc = POCKET_CAMERA; break; case 'B': // BANDAI_TAMA5 case 'b': tryReadSlice("ANDAI TAMA5"); mbc = BANDAI_TAMA5; break; case 'T': // TAMA5 / TPP1 case 't': switch (*ptr++) { case 'A': tryReadSlice("MA5"); mbc = BANDAI_TAMA5; break; case 'P': tryReadSlice("P1"); // Parse version skipMBCSpace(ptr); // Major if (std::optional major = parseNumber(ptr, BASE_10); !major) { fatal("Failed to parse TPP1 major revision number"); } else if (*major != 1) { fatal("RGBFIX only supports TPP1 version 1.0"); } else { tpp1Major = *major; } tryReadSlice("."); // Minor if (std::optional minor = parseNumber(ptr, BASE_10); !minor) { fatal("Failed to parse TPP1 minor revision number"); } else if (*minor > 0xFF) { fatal("TPP1 minor revision number must be 8-bit"); } else { tpp1Minor = *minor; } mbc = TPP1; break; } break; case 'H': // HuC{1, 3} case 'h': tryReadSlice("UC"); switch (*ptr++) { case '1': mbc = HUC1_RAM_BATTERY; break; case '3': mbc = HUC3; break; } break; } if (mbc == UINT16_MAX) { fatalUnknownMBC(name); } // Read "additional features" uint8_t features = 0; // clang-format off: vertically align values static constexpr uint8_t RAM = 1 << 7; static constexpr uint8_t BATTERY = 1 << 6; static constexpr uint8_t TIMER = 1 << 5; static constexpr uint8_t RUMBLE = 1 << 4; static constexpr uint8_t SENSOR = 1 << 3; static constexpr uint8_t MULTIRUMBLE = 1 << 2; // clang-format on while (*ptr) { // We expect a '+' at this point skipMBCSpace(ptr); tryReadSlice("+"); skipMBCSpace(ptr); switch (*ptr++) { case 'B': // BATTERY case 'b': tryReadSlice("ATTERY"); features |= BATTERY; break; case 'M': case 'm': tryReadSlice("ULTIRUMBLE"); features |= MULTIRUMBLE; break; case 'R': // RAM or RUMBLE case 'r': switch (*ptr++) { case 'U': case 'u': tryReadSlice("MBLE"); features |= RUMBLE; break; case 'A': case 'a': tryReadSlice("M"); features |= RAM; break; } break; case 'S': // SENSOR case 's': tryReadSlice("ENSOR"); features |= SENSOR; break; case 'T': // TIMER case 't': tryReadSlice("IMER"); features |= TIMER; break; } } switch (mbc) { case ROM: if (!features) { break; } mbc = ROM_RAM - 1; static_assert(ROM_RAM + 1 == ROM_RAM_BATTERY, "Enum sanity check failed!"); static_assert(MBC1 + 1 == MBC1_RAM, "Enum sanity check failed!"); static_assert(MBC1 + 2 == MBC1_RAM_BATTERY, "Enum sanity check failed!"); static_assert(MMM01 + 1 == MMM01_RAM, "Enum sanity check failed!"); static_assert(MMM01 + 2 == MMM01_RAM_BATTERY, "Enum sanity check failed!"); [[fallthrough]]; case MBC1: case MMM01: if (features == RAM) { ++mbc; } else if (features == (RAM | BATTERY)) { mbc += 2; } else if (features) { fatalWrongMBCFeatures(name); } break; case MBC2: if (features == BATTERY) { mbc = MBC2_BATTERY; } else if (features) { fatalWrongMBCFeatures(name); } break; case MBC3: // Handle timer, which also requires battery if (features & TIMER) { if (!(features & BATTERY)) { warning(WARNING_MBC, "\"MBC3+TIMER\" implies \"BATTERY\""); } features &= ~(TIMER | BATTERY); // Reset those bits mbc = MBC3_TIMER_BATTERY; // RAM is handled below } static_assert(MBC3 + 1 == MBC3_RAM, "Enum sanity check failed!"); static_assert(MBC3 + 2 == MBC3_RAM_BATTERY, "Enum sanity check failed!"); static_assert( MBC3_TIMER_BATTERY + 1 == MBC3_TIMER_RAM_BATTERY, "Enum sanity check failed!" ); if (features == RAM) { ++mbc; } else if (features == (RAM | BATTERY)) { mbc += 2; } else if (features) { fatalWrongMBCFeatures(name); } break; case MBC5: if (features & RUMBLE) { features &= ~RUMBLE; mbc = MBC5_RUMBLE; } static_assert(MBC5 + 1 == MBC5_RAM, "Enum sanity check failed!"); static_assert(MBC5 + 2 == MBC5_RAM_BATTERY, "Enum sanity check failed!"); static_assert(MBC5_RUMBLE + 1 == MBC5_RUMBLE_RAM, "Enum sanity check failed!"); static_assert(MBC5_RUMBLE + 2 == MBC5_RUMBLE_RAM_BATTERY, "Enum sanity check failed!"); if (features == RAM) { ++mbc; } else if (features == (RAM | BATTERY)) { mbc += 2; } else if (features) { fatalWrongMBCFeatures(name); } break; case MBC6: case POCKET_CAMERA: case BANDAI_TAMA5: case HUC3: // No extra features accepted if (features) { fatalWrongMBCFeatures(name); } break; case MBC7_SENSOR_RUMBLE_RAM_BATTERY: if (features != (SENSOR | RUMBLE | RAM | BATTERY)) { fatalWrongMBCFeatures(name); } break; case HUC1_RAM_BATTERY: if (features != (RAM | BATTERY)) { // HuC1 expects RAM+BATTERY fatalWrongMBCFeatures(name); } break; case TPP1: { // clang-format off: vertically align values static constexpr uint8_t BATTERY_TPP1 = 1 << 3; static constexpr uint8_t TIMER_TPP1 = 1 << 2; static constexpr uint8_t MULTIRUMBLE_TPP1 = 1 << 1; static constexpr uint8_t RUMBLE_TPP1 = 1 << 0; // clang-format on if (features & RAM) { warning(WARNING_MBC, "TPP1 requests RAM implicitly if given a non-zero RAM size"); } if (features & BATTERY) { mbc |= BATTERY_TPP1; } if (features & TIMER) { mbc |= TIMER_TPP1; } if (features & RUMBLE) { mbc |= RUMBLE_TPP1; } if (features & SENSOR) { fatalWrongMBCFeatures(name); } if (features & MULTIRUMBLE) { mbc |= MULTIRUMBLE_TPP1 | RUMBLE_TPP1; // Multiple rumble speeds imply rumble } break; } } return static_cast(mbc); } gbdev-rgbds-92bfe5d/src/fix/warning.cpp000066400000000000000000000045651512540461700201470ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "fix/warning.hpp" #include #include #include #include #include #include "diagnostics.hpp" #include "style.hpp" // clang-format off: nested initializers Diagnostics warnings = { .metaWarnings = { {"all", LEVEL_ALL }, {"everything", LEVEL_EVERYTHING}, }, .warningFlags = { {"mbc", LEVEL_DEFAULT }, {"obsolete", LEVEL_DEFAULT }, {"overwrite", LEVEL_DEFAULT }, {"sgb", LEVEL_DEFAULT }, {"truncation", LEVEL_DEFAULT }, }, .paramWarnings = {}, .state = DiagnosticsState(), .nbErrors = 0, }; // clang-format on uint32_t checkErrors(char const *filename) { if (warnings.nbErrors > 0) { style_Set(stderr, STYLE_RED, true); fprintf( stderr, "Fixing \"%s\" failed with %" PRIu64 " error%s\n", filename, warnings.nbErrors, warnings.nbErrors == 1 ? "" : "s" ); style_Reset(stderr); } return warnings.nbErrors; } void error(char const *fmt, ...) { va_list ap; style_Set(stderr, STYLE_RED, true); fputs("error: ", stderr); style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); putc('\n', stderr); warnings.incrementErrors(); } void fatal(char const *fmt, ...) { va_list ap; style_Set(stderr, STYLE_RED, true); fputs("FATAL: ", stderr); style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); putc('\n', stderr); exit(1); } void warning(WarningID id, char const *fmt, ...) { char const *flag = warnings.warningFlags[id].name; va_list ap; switch (warnings.getWarningBehavior(id)) { case WarningBehavior::DISABLED: break; case WarningBehavior::ENABLED: style_Set(stderr, STYLE_YELLOW, true); fputs("warning: ", stderr); style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); style_Set(stderr, STYLE_YELLOW, true); fprintf(stderr, " [-W%s]\n", flag); style_Reset(stderr); break; case WarningBehavior::ERROR: style_Set(stderr, STYLE_RED, true); fputs("error: ", stderr); style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); style_Set(stderr, STYLE_RED, true); fprintf(stderr, " [-Werror=%s]\n", flag); style_Reset(stderr); warnings.incrementErrors(); break; } } gbdev-rgbds-92bfe5d/src/gfx/000077500000000000000000000000001512540461700157625ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/src/gfx/color_set.cpp000066400000000000000000000040441512540461700204610ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "gfx/color_set.hpp" #include #include #include #include #include #include "helpers.hpp" void ColorSet::add(uint16_t color) { size_t i = 0; // Seek the first slot greater than the new color // (A linear search is better because we don't store the array size, // and there are very few slots anyway) while (_colorIndices[i] < color) { ++i; if (i == _colorIndices.size()) { // We reached the end of the array without finding the color, so it's a new one. return; } } // If we found it, great! Nothing else to do. if (_colorIndices[i] == color) { return; } // Swap entries until the end while (_colorIndices[i] != UINT16_MAX) { std::swap(_colorIndices[i], color); ++i; if (i == _colorIndices.size()) { // The set is full, but doesn't include the new color. return; } } // Write that last one into the new slot _colorIndices[i] = color; } ColorSet::ComparisonResult ColorSet::compare(ColorSet const &other) const { // This works because the sets are sorted numerically assume(std::is_sorted(RANGE(_colorIndices))); assume(std::is_sorted(RANGE(other._colorIndices))); auto ours = _colorIndices.begin(), theirs = other._colorIndices.begin(); bool weBigger = true, theyBigger = true; while (ours != end() && theirs != other.end()) { if (*ours == *theirs) { ++ours; ++theirs; } else if (*ours < *theirs) { ++ours; theyBigger = false; } else { // *ours > *theirs ++theirs; weBigger = false; } } weBigger &= theirs == other.end(); theyBigger &= ours == end(); return theyBigger ? THEY_BIGGER : (weBigger ? WE_BIGGER : NEITHER); } size_t ColorSet::size() const { return std::distance(RANGE(*this)); } bool ColorSet::empty() const { return _colorIndices[0] == UINT16_MAX; } auto ColorSet::begin() const -> decltype(_colorIndices)::const_iterator { return _colorIndices.begin(); } auto ColorSet::end() const -> decltype(_colorIndices)::const_iterator { return std::find(RANGE(_colorIndices), UINT16_MAX); } gbdev-rgbds-92bfe5d/src/gfx/main.cpp000066400000000000000000000505351512540461700174220ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "gfx/main.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "cli.hpp" #include "diagnostics.hpp" #include "file.hpp" #include "helpers.hpp" #include "platform.hpp" #include "style.hpp" #include "usage.hpp" #include "util.hpp" #include "verbosity.hpp" #include "gfx/pal_spec.hpp" #include "gfx/process.hpp" #include "gfx/reverse.hpp" #include "gfx/rgba.hpp" #include "gfx/warning.hpp" using namespace std::literals::string_view_literals; Options options; // Flags which must be processed after the option parsing finishes static struct LocalOptions { std::optional externalPalSpec; // -c bool autoAttrmap; // -A bool autoTilemap; // -T bool autoPalettes; // -P bool autoPalmap; // -Q bool groupOutputs; // -O bool reverse; // -r bool autoAny() const { return autoAttrmap || autoTilemap || autoPalettes || autoPalmap; } } localOptions; // Short options static char const *optstring = "Aa:B:b:Cc:d:hi:L:l:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvW:wXx:YZ"; // Long-only option variable static int longOpt; // `--color` // Equivalent long options // Please keep in the same order as short opts. // Also, make sure long opts don't create ambiguity: // A long opt's name should start with the same letter as its short opt, // except if it doesn't create any ambiguity (`verbose` versus `version`). // This is because long opt matching, even to a single char, is prioritized // over short opt matching. static option const longopts[] = { {"auto-attr-map", no_argument, nullptr, 'A'}, {"attr-map", required_argument, nullptr, 'a'}, {"background-color", required_argument, nullptr, 'B'}, {"base-tiles", required_argument, nullptr, 'b'}, {"color-curve", no_argument, nullptr, 'C'}, {"colors", required_argument, nullptr, 'c'}, {"depth", required_argument, nullptr, 'd'}, {"help", no_argument, nullptr, 'h'}, {"input-tileset", required_argument, nullptr, 'i'}, {"slice", required_argument, nullptr, 'L'}, {"base-palette", required_argument, nullptr, 'l'}, {"mirror-tiles", no_argument, nullptr, 'm'}, {"nb-tiles", required_argument, nullptr, 'N'}, {"nb-palettes", required_argument, nullptr, 'n'}, {"group-outputs", no_argument, nullptr, 'O'}, {"output", required_argument, nullptr, 'o'}, {"auto-palette", no_argument, nullptr, 'P'}, {"palette", required_argument, nullptr, 'p'}, {"auto-palette-map", no_argument, nullptr, 'Q'}, {"palette-map", required_argument, nullptr, 'q'}, {"reverse", required_argument, nullptr, 'r'}, {"palette-size", required_argument, nullptr, 's'}, {"auto-tilemap", no_argument, nullptr, 'T'}, {"tilemap", required_argument, nullptr, 't'}, {"unit-size", required_argument, nullptr, 'U'}, {"unique-tiles", no_argument, nullptr, 'u'}, {"version", no_argument, nullptr, 'V'}, {"verbose", no_argument, nullptr, 'v'}, {"warning", required_argument, nullptr, 'W'}, {"mirror-x", no_argument, nullptr, 'X'}, {"trim-end", required_argument, nullptr, 'x'}, {"mirror-y", no_argument, nullptr, 'Y'}, {"columns", no_argument, nullptr, 'Z'}, {"color", required_argument, &longOpt, 'c'}, {nullptr, no_argument, nullptr, 0 }, }; // clang-format off: nested initializers static Usage usage = { .name = "rgbgfx", .flags = { "[-r stride]", "[-ChmOuVXYZ]", "[-v [-v ...]]", "[-a | -A]", "[-b ]", "[-c ]", "[-d ]", "[-i ]", "[-L ]", "[-l ]", "[-N ]", "[-n ]", "[-o ]", "[-p | -P]", "[-q | -Q]", "[-s ]", "[-t | -T]", "[-x ]", "", }, .options = { {{"-m", "--mirror-tiles"}, {"optimize out mirrored tiles"}}, {{"-o", "--output "}, {"output the tile data to this path"}}, {{"-t", "--tilemap "}, {"output the tile map to this path"}}, {{"-u", "--unique-tiles"}, {"optimize out identical tiles"}}, {{"-V", "--version"}, {"print RGBGFX version and exit"}}, {{"-W", "--warning "}, {"enable or disable warnings"}}, }, }; // clang-format on // Parses a number at the beginning of a string, moving the pointer to skip the parsed characters. // Returns the provided errVal on error. static uint16_t readNumber(char const *&str, char const *errPrefix, uint16_t errVal = UINT16_MAX) { if (std::optional number = parseNumber(str); !number) { error("%s: expected number, but found nothing", errPrefix); return errVal; } else if (*number > UINT16_MAX) { error("%s: the number is too large", errPrefix); return errVal; } else { return *number; } } static void skipBlankSpace(char const *&arg) { arg += strspn(arg, " \t"); } static void parseArg(int ch, char *arg) { char const *argPtr = arg; // Make a copy for scanning switch (ch) { case 'A': localOptions.autoAttrmap = true; break; case 'a': localOptions.autoAttrmap = false; if (!options.attrmap.empty()) { warnx("Overriding attrmap file \"%s\"", options.attrmap.c_str()); } options.attrmap = arg; break; case 'B': parseBackgroundPalSpec(arg); break; case 'b': { uint16_t number = readNumber(argPtr, "Bank 0 base tile ID", 0); if (number >= 256) { error("Bank 0 base tile ID must be below 256"); } else { options.baseTileIDs[0] = number; } if (*argPtr == '\0') { options.baseTileIDs[1] = 0; break; } skipBlankSpace(argPtr); if (*argPtr != ',') { error("Base tile IDs must be one or two comma-separated numbers, not \"%s\"", arg); break; } ++argPtr; // Skip comma skipBlankSpace(argPtr); number = readNumber(argPtr, "Bank 1 base tile ID", 0); if (number >= 256) { error("Bank 1 base tile ID must be below 256"); } else { options.baseTileIDs[1] = number; } if (*argPtr != '\0') { error("Base tile IDs must be one or two comma-separated numbers, not \"%s\"", arg); break; } break; } case 'C': options.useColorCurve = true; break; case 'c': localOptions.externalPalSpec = std::nullopt; // Allow overriding a previous pal spec if (arg[0] == '#') { options.palSpecType = Options::EXPLICIT; parseInlinePalSpec(arg); } else if (strcasecmp(arg, "embedded") == 0) { // Use PLTE, error out if missing options.palSpecType = Options::EMBEDDED; } else if (strcasecmp(arg, "auto") == 0) { options.palSpecType = Options::NO_SPEC; } else if (strcasecmp(arg, "dmg") == 0) { options.palSpecType = Options::DMG; parseDmgPalSpec(0xE4); // Same darkest-first order as `sortGrayscale` } else if (strncasecmp(arg, "dmg=", literal_strlen("dmg=")) == 0) { options.palSpecType = Options::DMG; parseDmgPalSpec(&arg[literal_strlen("dmg=")]); } else { options.palSpecType = Options::EXPLICIT; localOptions.externalPalSpec = arg; } break; case 'd': options.bitDepth = readNumber(argPtr, "Bit depth", 2); if (*argPtr != '\0') { error("Bit depth ('-b') argument must be a valid number, not \"%s\"", arg); } else if (options.bitDepth != 1 && options.bitDepth != 2) { error("Bit depth must be 1 or 2, not %" PRIu8, options.bitDepth); options.bitDepth = 2; } break; // LCOV_EXCL_START case 'h': usage.printAndExit(0); // LCOV_EXCL_STOP case 'i': if (!options.inputTileset.empty()) { warnx("Overriding input tileset file \"%s\"", options.inputTileset.c_str()); } options.inputTileset = arg; break; case 'L': options.inputSlice.left = readNumber(argPtr, "Input slice left coordinate"); if (options.inputSlice.left > INT16_MAX) { error("Input slice left coordinate is out of range"); break; } skipBlankSpace(argPtr); if (*argPtr != ',') { error("Missing comma after left coordinate in \"%s\"", arg); break; } ++argPtr; skipBlankSpace(argPtr); options.inputSlice.top = readNumber(argPtr, "Input slice upper coordinate"); skipBlankSpace(argPtr); if (*argPtr != ':') { error("Missing colon after upper coordinate in \"%s\"", arg); break; } ++argPtr; skipBlankSpace(argPtr); options.inputSlice.width = readNumber(argPtr, "Input slice width"); skipBlankSpace(argPtr); if (options.inputSlice.width == 0) { error("Input slice width may not be 0"); } if (*argPtr != ',') { error("Missing comma after width in \"%s\"", arg); break; } ++argPtr; skipBlankSpace(argPtr); options.inputSlice.height = readNumber(argPtr, "Input slice height"); if (options.inputSlice.height == 0) { error("Input slice height may not be 0"); } if (*argPtr != '\0') { error("Unexpected extra characters after slice spec in \"%s\"", arg); } break; case 'l': { uint16_t number = readNumber(argPtr, "Base palette ID", 0); if (*argPtr != '\0') { error("Base palette ID must be a valid number, not \"%s\"", arg); } else if (number >= 256) { error("Base palette ID must be below 256"); } else { options.basePalID = number; } break; } case 'm': options.allowMirroringX = true; // Imply `-X` options.allowMirroringY = true; // Imply `-Y` [[fallthrough]]; // Imply `-u` case 'u': options.allowDedup = true; break; case 'N': options.maxNbTiles[0] = readNumber(argPtr, "Number of tiles in bank 0", 256); if (options.maxNbTiles[0] > 256) { error("Bank 0 cannot contain more than 256 tiles"); } if (*argPtr == '\0') { options.maxNbTiles[1] = 0; break; } skipBlankSpace(argPtr); if (*argPtr != ',') { error("Bank capacity must be one or two comma-separated numbers, not \"%s\"", arg); break; } ++argPtr; // Skip comma skipBlankSpace(argPtr); options.maxNbTiles[1] = readNumber(argPtr, "Number of tiles in bank 1", 256); if (options.maxNbTiles[1] > 256) { error("Bank 1 cannot contain more than 256 tiles"); } if (*argPtr != '\0') { error("Bank capacity must be one or two comma-separated numbers, not \"%s\"", arg); break; } break; case 'n': { uint16_t number = readNumber(argPtr, "Number of palettes", 256); if (*argPtr != '\0') { error("Number of palettes ('-n') must be a valid number, not \"%s\"", arg); } if (number > 256) { error("Number of palettes ('-n') must not exceed 256"); } else if (number == 0) { error("Number of palettes ('-n') may not be 0"); } else { options.nbPalettes = number; } break; } case 'O': localOptions.groupOutputs = true; break; case 'o': if (!options.output.empty()) { warnx("Overriding tile data file %s", options.output.c_str()); } options.output = arg; break; case 'P': localOptions.autoPalettes = true; break; case 'p': localOptions.autoPalettes = false; if (!options.palettes.empty()) { warnx("Overriding palettes file %s", options.palettes.c_str()); } options.palettes = arg; break; case 'Q': localOptions.autoPalmap = true; break; case 'q': localOptions.autoPalmap = false; if (!options.palmap.empty()) { warnx("Overriding palette map file %s", options.palmap.c_str()); } options.palmap = arg; break; case 'r': localOptions.reverse = true; options.reversedWidth = readNumber(argPtr, "Reversed image stride"); if (*argPtr != '\0') { error("Reversed image stride ('-r') must be a valid number, not \"%s\"", arg); } break; case 's': options.nbColorsPerPal = readNumber(argPtr, "Number of colors per palette", 4); if (*argPtr != '\0') { error("Palette size ('-s') must be a valid number, not \"%s\"", arg); } if (options.nbColorsPerPal > 4) { error("Palette size ('-s') must not exceed 4"); } else if (options.nbColorsPerPal == 0) { error("Palette size ('-s') may not be 0"); } break; case 'T': localOptions.autoTilemap = true; break; case 't': localOptions.autoTilemap = false; if (!options.tilemap.empty()) { warnx("Overriding tilemap file %s", options.tilemap.c_str()); } options.tilemap = arg; break; // LCOV_EXCL_START case 'V': usage.printVersion(false); exit(0); case 'v': incrementVerbosity(); break; // LCOV_EXCL_STOP case 'W': warnings.processWarningFlag(arg); break; case 'w': warnings.state.warningsEnabled = false; break; case 'x': options.trim = readNumber(argPtr, "Number of tiles to trim", 0); if (*argPtr != '\0') { error("Tile trim ('-x') argument must be a valid number, not \"%s\"", arg); } break; case 'X': options.allowMirroringX = true; options.allowDedup = true; // Imply `-u` break; case 'Y': options.allowMirroringY = true; options.allowDedup = true; // Imply `-u` break; case 'Z': options.columnMajor = true; break; case 0: // Long-only options if (longOpt == 'c' && !style_Parse(arg)) { fatal("Invalid argument for option '--color'"); } break; case 1: // Positional argument if (!options.input.empty()) { usage.printAndExit( "Input image specified more than once! (first \"%s\", then \"%s\")", options.input.c_str(), arg ); } else if (arg[0] == '\0') { // Empty input path usage.printAndExit("Input image path cannot be empty"); } else { options.input = arg; } break; // LCOV_EXCL_START default: usage.printAndExit(1); // LCOV_EXCL_STOP } } // LCOV_EXCL_START static void verboseOutputConfig() { if (!checkVerbosity(VERB_CONFIG)) { return; } style_Set(stderr, STYLE_MAGENTA, false); usage.printVersion(true); printVVVVVVerbosity(); fputs("Options:\n", stderr); // -Z/--columns if (options.columnMajor) { fputs("\tVisit image in column-major order\n", stderr); } // -u/--unique-tiles if (options.allowDedup) { fputs("\tAllow deduplicating identical tiles\n", stderr); } // -m/--mirror-tiles if (options.allowMirroringX && options.allowMirroringY) { fputs("\tAllow deduplicating mirrored tiles\n", stderr); } // -X/--mirror-x else if (options.allowMirroringX) { fputs("\tAllow deduplicating horizontally mirrored tiles\n", stderr); } // -Y/--mirror-y else if (options.allowMirroringY) { fputs("\tAllow deduplicating vertically mirrored tiles\n", stderr); } // -C/--color-curve if (options.useColorCurve) { fputs("\tUse color curve\n", stderr); } // -d/--depth fprintf(stderr, "\tBit depth: %" PRIu8 "bpp\n", options.bitDepth); // -x/--trim-end if (options.trim != 0) { fprintf(stderr, "\tTrim the last %" PRIu64 " tiles\n", options.trim); } // -n/--nb-palettes fprintf(stderr, "\tMaximum %" PRIu16 " palettes\n", options.nbPalettes); // -s/--palette-size fprintf(stderr, "\tPalettes contain %" PRIu8 " colors\n", options.nbColorsPerPal); // -c/--colors if (options.palSpecType == Options::NO_SPEC) { fputs("\tAutomatic palette generation\n", stderr); } else { fprintf(stderr, "\t%s palette spec\n", [] { switch (options.palSpecType) { case Options::EXPLICIT: return "Explicit"; case Options::EMBEDDED: return "Embedded"; case Options::DMG: return "DMG"; default: return "???"; } }()); } if (options.palSpecType == Options::EXPLICIT) { fputs("\t[\n", stderr); for (auto const &pal : options.palSpec) { fputs("\t\t", stderr); for (auto const &color : pal) { if (color) { fprintf(stderr, "#%06x, ", color->toCSS() >> 8); } else { fputs("#none, ", stderr); } } putc('\n', stderr); } fputs("\t]\n", stderr); } // -L/--slice if (options.inputSlice.width || options.inputSlice.height || options.inputSlice.left || options.inputSlice.top) { fprintf( stderr, "\tInput image slice: %" PRIu16 "x%" PRIu16 " pixels starting at (%" PRIu16 ", %" PRIu16 ")\n", options.inputSlice.width, options.inputSlice.height, options.inputSlice.left, options.inputSlice.top ); } // -b/--base-tiles if (options.baseTileIDs[0] || options.baseTileIDs[1]) { fprintf( stderr, "\tBase tile IDs: bank 0 = 0x%02" PRIx8 ", bank 1 = 0x%02" PRIx8 "\n", options.baseTileIDs[0], options.baseTileIDs[1] ); } // -l/--base-palette if (options.basePalID) { fprintf(stderr, "\tBase palette ID: %" PRIu8 "\n", options.basePalID); } // -N/--nb-tiles fprintf( stderr, "\tMaximum %" PRIu16 " tiles in bank 0, and %" PRIu16 " in bank 1\n", options.maxNbTiles[0], options.maxNbTiles[1] ); // -O/--group-outputs (influences other options) auto printPath = [](char const *name, std::string const &path) { if (!path.empty()) { fprintf(stderr, "\t%s: %s\n", name, path.c_str()); } }; // file printPath("Input image", options.input); // -i/--input-tileset printPath("Input tileset", options.inputTileset); // -o/--output printPath("Output tile data", options.output); // -t/--tilemap or -T/--auto-tilemap printPath("Output tilemap", options.tilemap); // -a/--attrmap or -A/--auto-attrmap printPath("Output attrmap", options.attrmap); // -p/--palette or -P/--auto-palette printPath("Output palettes", options.palettes); // -q/--palette-map or -Q/--auto-palette-map printPath("Output palette map", options.palmap); // -r/--reverse if (localOptions.reverse) { fprintf(stderr, "\tReverse image width: %" PRIu16 " tiles\n", options.reversedWidth); } fputs("Ready for conversion\n", stderr); style_Reset(stderr); } // LCOV_EXCL_STOP // Manual implementation of std::filesystem::path.replace_extension(). // macOS <10.15 did not support std::filesystem::path. static void replaceExtension(std::string &path, char const *extension) { constexpr std::string_view chars = // Both must start with a dot! #if defined(_MSC_VER) || defined(__MINGW32__) "./\\"sv; #else "./"sv; #endif size_t len = path.npos; if (size_t i = path.find_last_of(chars); i != path.npos && path[i] == '.') { // We found the last dot, but check if it's part of a stem // (There must be a non-path separator character before it) if (i != 0 && chars.find(path[i - 1], 1) == chars.npos) { // We can replace the extension len = i; } } path.assign(path, 0, len); path.append(extension); } int main(int argc, char *argv[]) { cli_ParseArgs(argc, argv, optstring, longopts, parseArg, usage); if (options.nbColorsPerPal == 0) { options.nbColorsPerPal = 1u << options.bitDepth; } else if (options.nbColorsPerPal > 1u << options.bitDepth) { error( "%" PRIu8 "bpp palettes can only contain %u colors, not %" PRIu8, options.bitDepth, 1u << options.bitDepth, options.nbColorsPerPal ); } if (localOptions.groupOutputs) { if (!localOptions.autoAny()) { warnx("Grouping outputs ('-O') is enabled, but without any automatic output paths " "('-A', '-P', '-Q', or '-T')"); } if (options.output.empty()) { warnx("Grouping outputs ('-O') is enabled, but without an output tile data file ('-o')" ); } } auto autoOutPath = [](bool autoOptEnabled, std::string &path, char const *extension) { if (!autoOptEnabled) { return; } path = localOptions.groupOutputs ? options.output : options.input; if (path.empty()) { usage.printAndExit( "No %s specified", localOptions.groupOutputs ? "output tile data file" : "input image" ); } replaceExtension(path, extension); }; autoOutPath(localOptions.autoAttrmap, options.attrmap, ".attrmap"); autoOutPath(localOptions.autoTilemap, options.tilemap, ".tilemap"); autoOutPath(localOptions.autoPalettes, options.palettes, ".pal"); autoOutPath(localOptions.autoPalmap, options.palmap, ".palmap"); // Execute deferred external pal spec parsing, now that all other params are known if (localOptions.externalPalSpec) { parseExternalPalSpec(localOptions.externalPalSpec->c_str()); } verboseOutputConfig(); // LCOV_EXCL_LINE // Do not do anything if option parsing went wrong. requireZeroErrors(); if (!options.input.empty()) { if (localOptions.reverse) { reverse(); } else { process(); } } else if (!options.palettes.empty() && options.palSpecType == Options::EXPLICIT && !localOptions.reverse) { processPalettes(); } else { usage.printAndExit("No input file specified (pass \"-\" to read from standard input)"); } requireZeroErrors(); return 0; } gbdev-rgbds-92bfe5d/src/gfx/pal_packing.cpp000066400000000000000000000461211512540461700207420ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "gfx/pal_packing.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "helpers.hpp" #include "style.hpp" #include "verbosity.hpp" #include "gfx/color_set.hpp" #include "gfx/main.hpp" // The solvers here are picked from the paper at https://arxiv.org/abs/1605.00558: // "Algorithms for the Pagination Problem, a Bin Packing with Overlapping Items" // Their formulation of the problem consists in packing "tiles" into "pages". // Here is a correspondence table for our application of it: // // Paper | RGBGFX // -------+---------- // Symbol | Color // Tile | Color set // Page | Palette // A reference to a color set, and attached attributes for sorting purposes struct ColorSetAttrs { size_t colorSetIndex; // Pages from which we are banned (to prevent infinite loops) // This is dynamic because we wish not to hard-cap the amount of palettes std::vector bannedPages; explicit ColorSetAttrs(size_t index) : colorSetIndex(index) {} bool isBannedFrom(size_t index) const { return index < bannedPages.size() && bannedPages[index]; } void banFrom(size_t index) { if (bannedPages.size() <= index) { bannedPages.resize(index + 1); } bannedPages[index] = true; } }; // A collection of color sets assigned to a palette // Does not contain the actual color indices because we need to be able to remove elements class AssignedSets { // We leave room for emptied slots to avoid copying the structs around on removal std::vector> _assigned; // For resolving color set indices std::vector const *_colorSets; public: AssignedSets(std::vector const &colorSets, std::optional &&attrs) : _assigned{attrs}, _colorSets{&colorSets} {} private: // Template class for both const and non-const iterators over the non-empty `_assigned` slots template typename Constness> class AssignedSetsIter { public: friend class AssignedSets; // For `iterator_traits` using value_type = ColorSetAttrs; using difference_type = ptrdiff_t; using reference = Constness &; using pointer = Constness *; using iterator_category = std::forward_iterator_tag; private: Constness *_array = nullptr; IteratorT _iter{}; AssignedSetsIter(decltype(_array) array, decltype(_iter) &&iter) : _array(array), _iter(iter) {} AssignedSetsIter &skipEmpty() { while (_iter != _array->end() && !_iter->has_value()) { ++_iter; } return *this; } public: AssignedSetsIter() = default; bool operator==(AssignedSetsIter const &rhs) const { return _iter == rhs._iter; } AssignedSetsIter &operator++() { ++_iter; skipEmpty(); return *this; } AssignedSetsIter operator++(int) { AssignedSetsIter it = *this; ++(*this); return it; } reference operator*() const { assume((*_iter).has_value()); return **_iter; } pointer operator->() const { return &(**this); // Invokes the operator above, not quite a no-op! } friend void swap(AssignedSetsIter &lhs, AssignedSetsIter &rhs) { std::swap(lhs._array, rhs._array); std::swap(lhs._iter, rhs._iter); } }; public: using iterator = AssignedSetsIter; iterator begin() { return iterator{&_assigned, _assigned.begin()}.skipEmpty(); } iterator end() { return iterator{&_assigned, _assigned.end()}; } using const_iterator = AssignedSetsIter; const_iterator begin() const { return const_iterator{&_assigned, _assigned.begin()}.skipEmpty(); } const_iterator end() const { return const_iterator{&_assigned, _assigned.end()}; } void assign(ColorSetAttrs const &&attrs) { auto freeSlot = std::find_if_not(RANGE(_assigned), [](std::optional const &slot) { return slot.has_value(); }); if (freeSlot == _assigned.end()) { _assigned.emplace_back(attrs); // We are full, use a new slot } else { freeSlot->emplace(attrs); // Reuse a free slot } } void remove(iterator const &iter) { iter._iter->reset(); // This time, we want to access the `optional` itself } void clear() { _assigned.clear(); } bool empty() const { return std::none_of(RANGE(_assigned), [](std::optional const &slot) { return slot.has_value(); }); } size_t nbColorSets() const { return std::distance(RANGE(*this)); } private: template static void addUniqueColors( std::unordered_set &colors, IteratorT iter, IteratorT const &end, std::vector const &colorSets ) { for (; iter != end; ++iter) { ColorSet const &colorSet = colorSets[iter->colorSetIndex]; colors.insert(RANGE(colorSet)); } } public: // Returns the set of distinct colors std::unordered_set uniqueColors() const { std::unordered_set colors; addUniqueColors(colors, RANGE(*this), *_colorSets); return colors; } // Returns the number of distinct colors size_t volume() const { return uniqueColors().size(); } bool canFit(ColorSet const &colorSet) const { std::unordered_set colors = uniqueColors(); colors.insert(RANGE(colorSet)); return colors.size() <= options.maxOpaqueColors(); } // Counts how many of our color sets this color also belongs to uint32_t multiplicity(uint16_t color) const { return std::count_if(RANGE(*this), [this, &color](ColorSetAttrs const &attrs) { ColorSet const &pal = (*_colorSets)[attrs.colorSetIndex]; return std::find(RANGE(pal), color) != pal.end(); }); } // The `relSizeOf` method below should compute the sum, for each color in `colorSet`, of // the reciprocal of the "multiplicity" of the color across "our" color sets. // However, literally computing the reciprocals would involve floating-point division, which // leads to imprecision and even platform-specific differences. // We avoid this by multiplying the reciprocals by a factor such that division always produces // an integer; the LCM of all values the denominator can take is the smallest suitable factor. static constexpr uint32_t scaleFactor = [] { // Fold over 1..=17 with the associative LCM function // (17 is the largest the denominator in `relSizeOf` below can be) uint32_t factor = 1; for (uint32_t n = 2; n <= 17; ++n) { factor = std::lcm(factor, n); } return factor; }(); // Computes the "relative size" of a color set on this palette; // it's a measure of how much this color set would "cost" to introduce. uint32_t relSizeOf(ColorSet const &colorSet) const { uint32_t relSize = 0; for (uint16_t color : colorSet) { uint32_t n = multiplicity(color); // We increase the denominator by 1 here; the reference code does this, // but the paper does not. Not adding 1 makes a multiplicity of 0 cause a division by 0 // (that is, if the color is not found in any color set), and adding 1 still seems // to preserve the paper's reasoning. // // The scale factor should ensure integer divisions only. assume(scaleFactor % (n + 1) == 0); relSize += scaleFactor / (n + 1); } return relSize; } // Computes the "relative size" of a set of color sets on this palette template size_t combinedVolume( IteratorT &&begin, IteratorT const &end, std::vector const &colorSets ) const { std::unordered_set colors = uniqueColors(); addUniqueColors(colors, std::forward(begin), end, colorSets); return colors.size(); } // Computes the "relative size" of a set of colors on this palette template size_t combinedVolume(IteratorT &&begin, IteratorT &&end) const { std::unordered_set colors = uniqueColors(); colors.insert(std::forward(begin), std::forward(end)); return colors.size(); } }; // LCOV_EXCL_START static void verboseOutputAssignments( std::vector const &assignments, std::vector const &colorSets ) { if (!checkVerbosity(VERB_INFO)) { return; } style_Set(stderr, STYLE_MAGENTA, false); for (AssignedSets const &assignment : assignments) { fputs("{ ", stderr); for (ColorSetAttrs const &attrs : assignment) { fprintf(stderr, "[%zu] ", attrs.colorSetIndex); for (uint16_t colorIndex : colorSets[attrs.colorSetIndex]) { fprintf(stderr, "%04" PRIx16 ", ", colorIndex); } } fprintf(stderr, "} (volume = %zu)\n", assignment.volume()); } style_Reset(stderr); } // LCOV_EXCL_STOP static void decant(std::vector &assignments, std::vector const &colorSets) { // "Decanting" is the process of moving all *things* that can fit in a lower index there auto decantOn = [&assignments](auto const &tryDecanting) { // No need to attempt decanting on palette #0, as there are no palettes to decant to for (size_t from = assignments.size(); --from;) { // Scan all palettes before this one for (size_t to = 0; to < from; ++to) { tryDecanting(assignments[to], assignments[from]); } // If the color set is now empty, remove it // Doing this now reduces the number of iterations performed by later steps // NB: order is intentionally preserved so as not to alter the "decantation"'s // properties // NB: this does mean that the first step might get empty palettes as its input! // NB: this is safe to do because we go towards the beginning of the vector, thereby not // invalidating our iteration (thus, iterators should not be used to drivethe outer // loop) if (assignments[from].empty()) { assignments.erase(assignments.begin() + from); } } }; verbosePrint(VERB_DEBUG, "%zu palettes before decanting\n", assignments.size()); // Decant on palettes decantOn([&colorSets](AssignedSets &to, AssignedSets &from) { // If the entire palettes can be merged, move all of `from`'s color sets if (to.combinedVolume(RANGE(from), colorSets) <= options.maxOpaqueColors()) { for (ColorSetAttrs &attrs : from) { to.assign(std::move(attrs)); } from.clear(); } }); verbosePrint(VERB_DEBUG, "%zu palettes after decanting on palettes\n", assignments.size()); // Decant on "components" (color sets sharing colors) decantOn([&colorSets](AssignedSets &to, AssignedSets &from) { // We need to iterate on all the "components", which are groups of color sets sharing at // least one color with another color set in the group. // We do this by adding the first available color set, and then looking for palettes with // common colors. (As an optimization, we know we can skip palettes already scanned.) std::vector processed(from.nbColorSets(), false); for (std::vector::iterator wasProcessed; (wasProcessed = std::find(RANGE(processed), false)) != processed.end();) { auto attrs = from.begin(); std::advance(attrs, wasProcessed - processed.begin()); std::unordered_set colors(RANGE(colorSets[attrs->colorSetIndex])); std::vector members = {static_cast(wasProcessed - processed.begin())}; *wasProcessed = true; // Mark the first color set as processed // Build up the "component"... for (; ++wasProcessed != processed.end(); ++attrs) { // If at least one color matches, add it if (ColorSet const &colorSet = colorSets[attrs->colorSetIndex]; std::find_first_of(RANGE(colors), RANGE(colorSet)) != colors.end()) { colors.insert(RANGE(colorSet)); members.push_back(wasProcessed - processed.begin()); *wasProcessed = true; // Mark that color set as processed } } if (to.combinedVolume(RANGE(colors)) > options.maxOpaqueColors()) { continue; } // Iterate through the component's color sets, and transfer them auto member = from.begin(); size_t curIndex = 0; for (size_t index : members) { std::advance(member, index - curIndex); curIndex = index; to.assign(std::move(*member)); from.remove(member); // Removing does not shift elements, so it's cheap } } }); verbosePrint( VERB_DEBUG, "%zu palettes after decanting on \"components\"\n", assignments.size() ); // Decant on individual color sets decantOn([&colorSets](AssignedSets &to, AssignedSets &from) { for (auto it = from.begin(); it != from.end(); ++it) { if (to.canFit(colorSets[it->colorSetIndex])) { to.assign(std::move(*it)); from.remove(it); } } }); verbosePrint(VERB_DEBUG, "%zu palettes after decanting on color sets\n", assignments.size()); } std::pair, size_t> overloadAndRemove(std::vector const &colorSets) { verbosePrint(VERB_NOTICE, "Paginating palettes using \"overload-and-remove\" strategy...\n"); // Sort the color sets by size, which improves the packing algorithm's efficiency auto const indexOfLargestColorSetFirst = [&colorSets](size_t left, size_t right) { ColorSet const &lhs = colorSets[left]; ColorSet const &rhs = colorSets[right]; return lhs.size() > rhs.size(); // We want the color sets to be sorted *largest first*! }; std::vector sortedColorSetIDs; sortedColorSetIDs.reserve(colorSets.size()); for (size_t i = 0; i < colorSets.size(); ++i) { sortedColorSetIDs.insert( std::lower_bound(RANGE(sortedColorSetIDs), i, indexOfLargestColorSetFirst), i ); } // Begin with no pages std::vector assignments; // Begin with all color sets queued up for insertion for (std::queue queue(std::deque(RANGE(sortedColorSetIDs))); !queue.empty(); queue.pop()) { ColorSetAttrs const &attrs = queue.front(); // Valid until the `queue.pop()` verbosePrint(VERB_TRACE, "Handling color set %zu\n", attrs.colorSetIndex); ColorSet const &colorSet = colorSets[attrs.colorSetIndex]; size_t bestPalIndex = assignments.size(); // We're looking for a palette where the color set's relative size is less than // its actual size; so only overwrite the "not found" index on meeting that criterion uint32_t bestRelSize = colorSet.size() * AssignedSets::scaleFactor; for (size_t i = 0; i < assignments.size(); ++i) { // Skip the page if this one is banned from it if (attrs.isBannedFrom(i)) { continue; } uint32_t relSize = assignments[i].relSizeOf(colorSet); verbosePrint( VERB_TRACE, " Relative size to palette %zu (of %zu): %" PRIu32 " (size = %zu)\n", i, assignments.size(), relSize, colorSet.size() ); if (relSize < bestRelSize) { bestPalIndex = i; bestRelSize = relSize; } } if (bestPalIndex == assignments.size()) { // Found nowhere to put it, create a new page containing just that one verbosePrint( VERB_TRACE, "Assigning color set %zu to new palette %zu\n", attrs.colorSetIndex, bestPalIndex ); assignments.emplace_back(colorSets, std::move(attrs)); continue; } verbosePrint( VERB_TRACE, "Assigning color set %zu to palette %zu\n", attrs.colorSetIndex, bestPalIndex ); AssignedSets &bestPal = assignments[bestPalIndex]; // Add the color to that palette bestPal.assign(std::move(attrs)); auto compareEfficiency = [&](ColorSetAttrs const &attrs1, ColorSetAttrs const &attrs2) { ColorSet const &colorSet1 = colorSets[attrs1.colorSetIndex]; ColorSet const &colorSet2 = colorSets[attrs2.colorSetIndex]; size_t size1 = colorSet1.size(); size_t size2 = colorSet2.size(); uint32_t relSize1 = bestPal.relSizeOf(colorSet1); uint32_t relSize2 = bestPal.relSizeOf(colorSet2); verbosePrint( VERB_TRACE, " Color sets %zu <=> %zu: Efficiency: %zu / %" PRIu32 " <=> %zu / " "%" PRIu32 "\n", attrs1.colorSetIndex, attrs2.colorSetIndex, size1, relSize1, size2, relSize2 ); // This comparison is algebraically equivalent to // `size1 / relSize1 <=> size2 / relSize2`, // but without potential precision loss from floating-point division. size_t efficiency1 = size1 * relSize2; size_t efficiency2 = size2 * relSize1; return (efficiency1 > efficiency2) - (efficiency1 < efficiency2); }; // If this overloads the palette, get it back to normal (if possible) while (bestPal.volume() > options.maxOpaqueColors()) { verbosePrint( VERB_TRACE, "Palette %zu is overloaded! (%zu > %" PRIu8 ")\n", bestPalIndex, bestPal.volume(), options.maxOpaqueColors() ); // Look for a color set minimizing "efficiency" (size / relSize) auto [minEfficiencyIter, maxEfficiencyIter] = std::minmax_element( RANGE(bestPal), [&compareEfficiency](ColorSetAttrs const &lhs, ColorSetAttrs const &rhs) { return compareEfficiency(lhs, rhs) < 0; } ); // All efficiencies are identical iff min equals max if (compareEfficiency(*minEfficiencyIter, *maxEfficiencyIter) == 0) { verbosePrint(VERB_TRACE, " All efficiencies are identical\n"); break; } // Remove the color set with minimal efficiency verbosePrint( VERB_TRACE, " Removing color set %zu\n", minEfficiencyIter->colorSetIndex ); queue.emplace(std::move(*minEfficiencyIter)); queue.back().banFrom(bestPalIndex); // Ban it from this palette bestPal.remove(minEfficiencyIter); } } // Deal with palettes still overloaded, by emptying them auto const &largestColorSetFirst = [&colorSets](ColorSetAttrs const &lhs, ColorSetAttrs const &rhs) { return colorSets[lhs.colorSetIndex].size() > colorSets[rhs.colorSetIndex].size(); }; std::vector overloadQueue{}; for (AssignedSets &pal : assignments) { if (pal.volume() > options.maxOpaqueColors()) { for (ColorSetAttrs &attrs : pal) { overloadQueue.emplace( std::lower_bound(RANGE(overloadQueue), attrs, largestColorSetFirst), std::move(attrs) ); } pal.clear(); } } // Place back any color sets now in the queue via first-fit for (ColorSetAttrs const &attrs : overloadQueue) { ColorSet const &colorSet = colorSets[attrs.colorSetIndex]; auto palette = std::find_if(RANGE(assignments), [&colorSet](AssignedSets const &pal) { return pal.canFit(colorSet); }); if (palette == assignments.end()) { // No such page, create a new one verbosePrint( VERB_DEBUG, "Adding new palette (%zu) for overflowing color set %zu\n", assignments.size(), attrs.colorSetIndex ); assignments.emplace_back(colorSets, std::move(attrs)); } else { verbosePrint( VERB_DEBUG, "Assigning overflowing color set %zu to palette %zu\n", attrs.colorSetIndex, palette - assignments.begin() ); palette->assign(std::move(attrs)); } } verboseOutputAssignments(assignments, colorSets); // LCOV_EXCL_LINE // "Decant" the result decant(assignments, colorSets); // Note that the result does not contain any empty palettes verboseOutputAssignments(assignments, colorSets); // LCOV_EXCL_LINE std::vector mappings(colorSets.size()); for (size_t i = 0; i < assignments.size(); ++i) { for (ColorSetAttrs const &attrs : assignments[i]) { mappings[attrs.colorSetIndex] = i; } } return {mappings, assignments.size()}; } gbdev-rgbds-92bfe5d/src/gfx/pal_sorting.cpp000066400000000000000000000037441512540461700210170ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "gfx/pal_sorting.hpp" #include #include #include #include #include #include "helpers.hpp" #include "verbosity.hpp" #include "gfx/palette.hpp" #include "gfx/rgba.hpp" void sortIndexed(std::vector &palettes, std::vector const &embPal) { verbosePrint(VERB_NOTICE, "Sorting palettes using embedded palette...\n"); for (Palette &pal : palettes) { std::sort(RANGE(pal), [&](uint16_t lhs, uint16_t rhs) { // Iterate through the PNG's palette, looking for either of the two for (Rgba const &rgba : embPal) { uint16_t color = rgba.cgbColor(); if (color == Rgba::transparent) { continue; } // Return whether lhs < rhs if (color == rhs) { return false; } if (color == lhs) { return true; } } unreachable_(); // LCOV_EXCL_LINE }); } } void sortGrayscale( std::vector &palettes, std::array, NB_COLOR_SLOTS> const &colors ) { verbosePrint(VERB_NOTICE, "Sorting palette by grayscale bins...\n"); // This method is only applicable if there are at most as many colors as colors per palette, so // we should only have a single palette. assume(palettes.size() == 1); Palette &palette = palettes[0]; std::fill(RANGE(palette.colors), Rgba::transparent); for (std::optional const &slot : colors) { if (!slot.has_value() || slot->isTransparent()) { continue; } palette[slot->grayIndex()] = slot->cgbColor(); } } static unsigned int luminance(uint16_t color) { uint8_t red = color & 0b11111; uint8_t green = color >> 5 & 0b11111; uint8_t blue = color >> 10; return 2126 * red + 7152 * green + 722 * blue; } void sortRgb(std::vector &palettes) { verbosePrint(VERB_NOTICE, "Sorting palettes by luminance...\n"); for (Palette &pal : palettes) { // Sort from lightest to darkest std::sort(RANGE(pal), [](uint16_t lhs, uint16_t rhs) { return luminance(lhs) > luminance(rhs); }); } } gbdev-rgbds-92bfe5d/src/gfx/pal_spec.cpp000066400000000000000000000450401512540461700202570ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "gfx/pal_spec.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "diagnostics.hpp" #include "helpers.hpp" #include "platform.hpp" #include "util.hpp" // UpperMap, isDigit #include "gfx/main.hpp" #include "gfx/png.hpp" #include "gfx/rgba.hpp" #include "gfx/warning.hpp" using namespace std::string_view_literals; static char const *hexDigits = "0123456789ABCDEFabcdef"; static void skipBlankSpace(std::string_view const &str, size_t &pos) { pos = std::min(str.find_first_not_of(" \t"sv, pos), str.length()); } static uint8_t toHex(char c1, char c2) { return parseHexDigit(c1) * 16 + parseHexDigit(c2); } static uint8_t singleToHex(char c) { return toHex(c, c); } static uint16_t toWord(uint8_t low, uint8_t high) { return high << 8 | low; } void parseInlinePalSpec(char const * const rawArg) { // List of #rrggbb/#rgb colors (or #none); comma-separated. // Palettes are separated by colons. std::string_view arg(rawArg); auto parseError = [&rawArg, &arg](size_t ofs, size_t len, char const *msg) { (void)arg; // With NDEBUG, `arg` is otherwise not used assume(ofs <= arg.length()); assume(len <= arg.length()); error("%s", msg); // `format_` and `-Wformat-security` would complain about `error(msg);` fprintf( stderr, "In inline palette spec: \"%s\"\n%*c", rawArg, static_cast(literal_strlen("In inline palette spec: \"") + ofs), ' ' ); for (size_t i = len; i; --i) { putc('^', stderr); } putc('\n', stderr); }; options.palSpec.clear(); options.palSpec.emplace_back(); // Value-initialized, not default-init'd, so we get zeros size_t n = 0; // Index into the argument size_t nbColors = 0; // Number of colors in the current palette for (;;) { ++n; // Ignore the '#' (checked either by caller or previous loop iteration) std::optional &color = options.palSpec.back()[nbColors]; // Check for "#none" first. if (strncasecmp(&rawArg[n], "none", literal_strlen("none")) == 0) { color = {}; n += literal_strlen("none"); } else { size_t pos = std::min(arg.find_first_not_of(hexDigits, n), arg.length()); switch (pos - n) { case 3: color = Rgba( singleToHex(arg[n + 0]), singleToHex(arg[n + 1]), singleToHex(arg[n + 2]), 0xFF ); break; case 6: color = Rgba( toHex(arg[n + 0], arg[n + 1]), toHex(arg[n + 2], arg[n + 3]), toHex(arg[n + 4], arg[n + 5]), 0xFF ); break; case 0: parseError(n - 1, 1, "Missing color after '#'"); return; default: parseError(n, pos - n, "Unknown color specification"); return; } n = pos; } // Skip trailing space, if any skipBlankSpace(arg, n); // Skip comma/semicolon, or end if (n == arg.length()) { break; } switch (arg[n]) { case ',': ++n; // Skip it ++nbColors; // A trailing comma may be followed by a semicolon skipBlankSpace(arg, n); if (n == arg.length()) { break; } else if (arg[n] != ';' && arg[n] != ':') { if (nbColors == 4) { parseError(n, 1, "Each palette can only contain up to 4 colors"); return; } break; } [[fallthrough]]; case ':': case ';': ++n; skipBlankSpace(arg, n); nbColors = 0; // Start a new palette // Avoid creating a spurious empty palette if (n != arg.length()) { options.palSpec.emplace_back(); } break; default: parseError(n, 1, "Unexpected character, expected ',', ';', or end of argument"); return; } // Check again to allow trailing a comma/semicolon if (n == arg.length()) { break; } if (arg[n] != '#') { parseError(n, 1, "Unexpected character, expected '#'"); return; } } } // Appends the first line read from `file` to the end of the provided `buffer`. // Returns true if a line was read. [[gnu::warn_unused_result]] static bool readLine(std::filebuf &file, std::string &buffer) { assume(buffer.empty()); for (;;) { int c = file.sbumpc(); if (c == std::filebuf::traits_type::eof()) { return !buffer.empty(); } if (c == '\n') { // Discard a trailing CRLF if (!buffer.empty() && buffer.back() == '\r') { buffer.pop_back(); } return true; } buffer.push_back(c); } } static void warnExtraColors( char const *kind, char const *filename, uint16_t nbColors, uint16_t maxNbColors ) { warnx( "%s file \"%s\" contains %" PRIu16 " colors, but there can only be %" PRIu16 "; ignoring extra", kind, filename, nbColors, maxNbColors ); } // Parses the initial part of a string_view, advancing the "read index" as it does template // Should be uint*_t static std::optional parseDec(std::string const &str, size_t &n) { uintmax_t value = 0; auto result = std::from_chars(str.data() + n, str.data() + str.length(), value); if (static_cast(result.ec)) { return std::nullopt; } n = result.ptr - str.data(); return std::optional{value}; } static std::optional parseColor(std::string const &str, size_t &n, uint16_t i) { std::optional r = parseDec(str, n); if (!r) { error("Failed to parse color #%d (\"%s\"): invalid red component", i + 1, str.c_str()); return std::nullopt; } skipBlankSpace(str, n); if (n == str.length()) { error("Failed to parse color #%d (\"%s\"): missing green component", i + 1, str.c_str()); return std::nullopt; } std::optional g = parseDec(str, n); if (!g) { error("Failed to parse color #%d (\"%s\"): invalid green component", i + 1, str.c_str()); return std::nullopt; } skipBlankSpace(str, n); if (n == str.length()) { error("Failed to parse color #%d (\"%s\"): missing blue component", i + 1, str.c_str()); return std::nullopt; } std::optional b = parseDec(str, n); if (!b) { error("Failed to parse color #%d (\"%s\"): invalid blue component", i + 1, str.c_str()); return std::nullopt; } return std::optional{Rgba(*r, *g, *b, 0xFF)}; } static void parsePSPFile(char const *filename, std::filebuf &file) { // https://www.selapa.net/swatches/colors/fileformats.php#psp_pal #define requireLine() \ do { \ line.clear(); \ if (!readLine(file, line)) { \ error("PSP palette file \"%s\" is shorter than expected", filename); \ return; \ } \ } while (0) std::string line; if (!readLine(file, line) || line != "JASC-PAL") { error("File \"%s\" is not a valid PSP palette file", filename); return; } requireLine(); if (line != "0100") { error("Unsupported PSP palette file version \"%s\"", line.c_str()); return; } requireLine(); size_t n = 0; std::optional nbColors = parseDec(line, n); if (!nbColors || n != line.length()) { error("Invalid \"number of colors\" line in PSP file (\"%s\")", line.c_str()); return; } if (uint16_t maxNbColors = options.maxNbColors(); *nbColors > maxNbColors) { warnExtraColors("PSP", filename, *nbColors, maxNbColors); nbColors = maxNbColors; } options.palSpec.clear(); for (uint16_t i = 0; i < *nbColors; ++i) { requireLine(); n = 0; std::optional color = parseColor(line, n, i + 1); if (!color) { return; } if (n != line.length()) { error( "Failed to parse color #%d (\"%s\"): trailing characters after blue component", i + 1, line.c_str() ); return; } if (i % options.nbColorsPerPal == 0) { options.palSpec.emplace_back(); } options.palSpec.back()[i % options.nbColorsPerPal] = *color; } #undef requireLine } static void parseGPLFile(char const *filename, std::filebuf &file) { // https://gitlab.gnome.org/GNOME/gimp/-/blob/gimp-2-10/app/core/gimppalette-load.c#L39 std::string line; if (!readLine(file, line) || !line.starts_with("GIMP Palette")) { error("File \"%s\" is not a valid GPL palette file", filename); return; } uint16_t nbColors = 0; uint16_t const maxNbColors = options.maxNbColors(); for (;;) { line.clear(); if (!readLine(file, line)) { break; } if (line.starts_with("Name:") || line.starts_with("Columns:")) { continue; } size_t n = 0; skipBlankSpace(line, n); // Skip empty lines, or lines that contain just a comment. if (line.length() == n || line[n] == '#') { continue; } std::optional color = parseColor(line, n, nbColors + 1); if (!color) { return; } // Ignore anything following the three components // (sometimes it's a comment, sometimes it's the color in CSS hex format, sometimes there's // nothing...). if (nbColors < maxNbColors) { if (nbColors % options.nbColorsPerPal == 0) { options.palSpec.emplace_back(); } options.palSpec.back()[nbColors % options.nbColorsPerPal] = *color; } ++nbColors; } if (nbColors > maxNbColors) { warnExtraColors("GPL", filename, nbColors, maxNbColors); } } static void parseHEXFile(char const *filename, std::filebuf &file) { // https://lospec.com/palette-list/tag/gbc uint16_t nbColors = 0; uint16_t const maxNbColors = options.maxNbColors(); for (;;) { std::string line; if (!readLine(file, line)) { break; } // Ignore empty lines. if (line.length() == 0) { continue; } if (line.length() != 6 || line.find_first_not_of(hexDigits) != std::string::npos) { error( "Failed to parse color #%d (\"%s\"): invalid \"rrggbb\" line", nbColors + 1, line.c_str() ); return; } Rgba color = Rgba(toHex(line[0], line[1]), toHex(line[2], line[3]), toHex(line[4], line[5]), 0xFF); if (nbColors < maxNbColors) { if (nbColors % options.nbColorsPerPal == 0) { options.palSpec.emplace_back(); } options.palSpec.back()[nbColors % options.nbColorsPerPal] = color; } ++nbColors; } if (nbColors > maxNbColors) { warnExtraColors("HEX", filename, nbColors, maxNbColors); } } static void parseACTFile(char const *filename, std::filebuf &file) { // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1070626 std::array buf{}; size_t len = file.sgetn(buf.data(), buf.size()); uint16_t nbColors = 256; if (len == 772) { nbColors = toWord(buf[769], buf[768]); if (nbColors > 256 || nbColors == 0) { error("Invalid number of colors in ACT file \"%s\" (%" PRIu16 ")", filename, nbColors); return; } } else if (len != 768) { error( "Invalid file size for ACT file \"%s\" (expected 768 or 772 bytes, got %zu)", filename, len ); return; } if (uint16_t maxNbColors = options.maxNbColors(); nbColors > maxNbColors) { warnExtraColors("ACT", filename, nbColors, maxNbColors); nbColors = maxNbColors; } options.palSpec.clear(); options.palSpec.emplace_back(); char const *ptr = buf.data(); size_t colorIdx = 0; for (uint16_t i = 0; i < nbColors; ++i) { std::optional &color = options.palSpec.back()[colorIdx]; color = Rgba(ptr[0], ptr[1], ptr[2], 0xFF); ptr += 3; ++colorIdx; if (colorIdx == options.nbColorsPerPal) { options.palSpec.emplace_back(); colorIdx = 0; } } // Remove the spurious empty palette if there is one if (colorIdx == 0) { options.palSpec.pop_back(); } } static void parseACOFile(char const *filename, std::filebuf &file) { // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1055819 char buf[10]; if (file.sgetn(buf, 2) != 2) { error("Failed to read ACO file version"); return; } if (toWord(buf[1], buf[0]) != 1) { error("File \"%s\" is not a valid ACO v1 file", filename); return; } if (file.sgetn(buf, 2) != 2) { error("Failed to read number of colors in palette file"); return; } uint16_t nbColors = toWord(buf[1], buf[0]); if (uint16_t maxNbColors = options.maxNbColors(); nbColors > maxNbColors) { warnExtraColors("ACO", filename, nbColors, maxNbColors); nbColors = maxNbColors; } options.palSpec.clear(); for (uint16_t i = 0; i < nbColors; ++i) { if (file.sgetn(buf, 10) != 10) { error("Failed to read color #%d from palette file", i + 1); return; } if (i % options.nbColorsPerPal == 0) { options.palSpec.emplace_back(); } std::optional &color = options.palSpec.back()[i % options.nbColorsPerPal]; uint16_t colorType = toWord(buf[1], buf[0]); switch (colorType) { case 0: // RGB // Only keep the MSB of the (big-endian) 16-bit values. color = Rgba(buf[2], buf[4], buf[6], 0xFF); break; case 1: // HSB error("Unsupported color type (HSB) for ACO file"); return; case 2: // CMYK error("Unsupported color type (CMYK) for ACO file"); return; case 7: // Lab error("Unsupported color type (Lab) for ACO file"); return; case 8: // Grayscale error("Unsupported color type (grayscale) for ACO file"); return; default: error("Unknown color type (%" PRIu16 ") for ACO file", colorType); return; } } } static void parseGBCFile(char const *filename, std::filebuf &file) { // This only needs to be able to read back files generated by `rgbgfx -p` options.palSpec.clear(); for (;;) { char buf[2 * 4]; if (size_t len = file.sgetn(buf, sizeof(buf)); len == 0) { break; } else if (len != sizeof(buf)) { error( "GBC palette file \"%s\" contains %zu 8-byte palette%s, plus %zu byte%s", filename, options.palSpec.size(), options.palSpec.size() == 1 ? "" : "s", len, len == 1 ? "" : "s" ); break; } options.palSpec.push_back({ Rgba::fromCGBColor(toWord(buf[0], buf[1])), Rgba::fromCGBColor(toWord(buf[2], buf[3])), Rgba::fromCGBColor(toWord(buf[4], buf[5])), Rgba::fromCGBColor(toWord(buf[6], buf[7])), }); } } static bool checkPngSwatch(std::vector const &pixels, uint32_t base, uint32_t swatchSize) { for (uint32_t y = 0; y < swatchSize; ++y) { uint32_t yOffset = y * swatchSize * options.nbColorsPerPal + base; for (uint32_t x = 0; x < swatchSize; ++x) { if (x == 0 && y == 0) { continue; } if (pixels[yOffset + x] != pixels[base]) { return false; } } } return true; } static void parsePNGFile(char const *filename, std::filebuf &file) { Png png{filename, file}; // The image width must evenly divide into a color swatch for each color per palette if (png.width % options.nbColorsPerPal != 0) { error( "PNG palette file is %" PRIu32 "x%" PRIu32 ", which is not a multiple of %" PRIu8 " color swatches wide", png.width, png.height, options.nbColorsPerPal ); return; } // Infer the color swatch size (width and height) from the image width uint32_t swatchSize = png.width / options.nbColorsPerPal; // The image height must evenly divide into a color swatch for each palette if (png.height % swatchSize != 0) { error( "PNG palette file is %" PRIu32 "x%" PRIu32 ", which is not a multiple of %" PRIu32 " pixels high", png.width, png.height, swatchSize ); return; } // More palettes than the maximum are a warning, not an error uint32_t nbPals = png.height / swatchSize; if (nbPals > options.nbPalettes) { warnExtraColors( "PNG palette", filename, nbPals * options.nbColorsPerPal, options.maxNbColors() ); nbPals = options.nbPalettes; } options.palSpec.clear(); // Get each color from the top-left pixel of each swatch for (uint32_t y = 0; y < nbPals; ++y) { uint32_t yOffset = y * swatchSize * swatchSize * options.nbColorsPerPal; options.palSpec.emplace_back(); for (uint32_t x = 0; x < options.nbColorsPerPal; ++x) { uint32_t offset = yOffset + x * swatchSize; options.palSpec.back()[x] = png.pixels[offset]; // Check that each swatch is completely one color if (!checkPngSwatch(png.pixels, offset, swatchSize)) { error("PNG palette file uses multiple colors in one color swatch"); return; } } } } void parseExternalPalSpec(char const *arg) { // `fmt:path`, parse the file according to the given format // Split both parts, error out if malformed char const *ptr = strchr(arg, ':'); if (ptr == nullptr) { error("External palette spec must have format \"fmt:path\" (missing colon)"); return; } char const *path = ptr + 1; static UpperMap> const parsers{ {"PSP", std::pair{&parsePSPFile, false}}, {"GPL", std::pair{&parseGPLFile, false}}, {"HEX", std::pair{&parseHEXFile, false}}, {"ACT", std::pair{&parseACTFile, true} }, {"ACO", std::pair{&parseACOFile, true} }, {"GBC", std::pair{&parseGBCFile, true} }, {"PNG", std::pair{&parsePNGFile, true} }, }; std::string format{arg, ptr}; auto search = parsers.find(format); if (search == parsers.end()) { error("Unknown external palette format \"%s\"", format.c_str()); return; } std::filebuf file; // Some parsers read the file in text mode, others in binary mode if (!file.open(path, search->second.second ? std::ios::in | std::ios::binary : std::ios::in)) { fatal("Failed to open palette file \"%s\": %s", path, strerror(errno)); return; } search->second.first(path, file); } void parseDmgPalSpec(char const * const rawArg) { // Two hex digit DMG palette spec std::string_view arg(rawArg); if (arg.length() != 2 || arg.find_first_not_of(hexDigits) != std::string_view::npos) { error("Unknown DMG palette specification \"%s\"", rawArg); return; } parseDmgPalSpec(toHex(arg[0], arg[1])); } void parseDmgPalSpec(uint8_t palSpecDmg) { options.palSpecDmg = palSpecDmg; // Map gray shades to their DMG color indexes for fast lookup by `Rgba::grayIndex` for (uint8_t i = 0; i < 4; ++i) { options.dmgColors[options.dmgValue(i)] = i; } // Validate that DMG palette spec does not have conflicting colors for (uint8_t i = 0; i < 3; ++i) { for (uint8_t j = i + 1; j < 4; ++j) { if (options.dmgValue(i) == options.dmgValue(j)) { error("DMG palette specification maps two gray shades to the same color index"); return; } } } } void parseBackgroundPalSpec(char const *arg) { if (strcasecmp(arg, "transparent") == 0) { options.bgColor = Rgba(0x00, 0x00, 0x00, 0x00); return; } if (arg[0] != '#') { error("Background color specification must be \"#rgb\", \"#rrggbb\", or \"transparent\""); return; } size_t size = strspn(&arg[1], hexDigits); switch (size) { case 3: options.bgColor = Rgba(singleToHex(arg[1]), singleToHex(arg[2]), singleToHex(arg[3]), 0xFF); break; case 6: options.bgColor = Rgba(toHex(arg[1], arg[2]), toHex(arg[3], arg[4]), toHex(arg[5], arg[6]), 0xFF); break; default: error("Unknown background color specification \"%s\"", arg); } if (arg[size + 1] != '\0') { error("Unexpected text \"%s\" after background color specification", &arg[size + 1]); } } gbdev-rgbds-92bfe5d/src/gfx/palette.cpp000066400000000000000000000032121512540461700201220ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "gfx/palette.hpp" #include #include #include "helpers.hpp" #include "gfx/main.hpp" #include "gfx/rgba.hpp" void Palette::addColor(uint16_t color) { for (size_t i = 0; true; ++i) { assume(i < colors.size()); // The packing should guarantee this if (colors[i] == color) { // The color is already present break; } else if (colors[i] == UINT16_MAX) { // Empty slot colors[i] = color; break; } } } // Returns the ID of the color in the palette, or `size()` if the color is not in uint8_t Palette::indexOf(uint16_t color) const { return color == Rgba::transparent ? 0 : std::find(begin(), colors.end(), color) - begin() + options.hasTransparentPixels; } auto Palette::begin() -> decltype(colors)::iterator { // Skip the first slot if reserved for transparency return colors.begin() + options.hasTransparentPixels; } auto Palette::end() -> decltype(colors)::iterator { // Return an iterator pointing past the last non-empty element. // Since the palette may contain gaps, we must scan from the end. return std::find_if(RRANGE(colors), [](uint16_t c) { return c != UINT16_MAX; }).base(); } auto Palette::begin() const -> decltype(colors)::const_iterator { // Same as the non-const begin(). return colors.begin() + options.hasTransparentPixels; } auto Palette::end() const -> decltype(colors)::const_iterator { // Same as the non-const end(). return std::find_if(RRANGE(colors), [](uint16_t c) { return c != UINT16_MAX; }).base(); } uint8_t Palette::size() const { return end() - colors.begin(); } gbdev-rgbds-92bfe5d/src/gfx/png.cpp000066400000000000000000000152461512540461700172620ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "gfx/png.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "diagnostics.hpp" #include "helpers.hpp" #include "style.hpp" #include "verbosity.hpp" #include "gfx/rgba.hpp" #include "gfx/warning.hpp" struct Input { char const *filename; std::streambuf &file; Input(char const *filename_, std::streambuf &file_) : filename(filename_), file(file_) {} }; [[noreturn]] static void handleError(png_structp png, char const *msg) { fatal( "libpng error while reading PNG image (\"%s\"): %s", reinterpret_cast(png_get_error_ptr(png))->filename, msg ); } static void handleWarning(png_structp png, char const *msg) { warnx( "libpng found while reading PNG image (\"%s\"): %s", reinterpret_cast(png_get_error_ptr(png))->filename, msg ); } static void readData(png_structp png, png_bytep data, size_t length) { Input &input = *reinterpret_cast(png_get_io_ptr(png)); std::streamsize expectedLen = length; std::streamsize nbBytesRead = input.file.sgetn(reinterpret_cast(data), expectedLen); if (nbBytesRead != expectedLen) { fatal( "Error reading PNG image (\"%s\"): file too short (expected at least %zd more " "bytes after reading %zu)", input.filename, length - nbBytesRead, static_cast(input.file.pubseekoff(0, std::ios_base::cur)) ); } } Png::Png(char const *filename, std::streambuf &file) { Input input(filename, file); verbosePrint(VERB_NOTICE, "Reading PNG file \"%s\"\n", input.filename); std::array pngHeader; if (input.file.sgetn(reinterpret_cast(pngHeader.data()), pngHeader.size()) != static_cast(pngHeader.size()) // Not enough bytes? || png_sig_cmp(pngHeader.data(), 0, pngHeader.size()) != 0) { fatal("File \"%s\" is not a valid PNG image", input.filename); // LCOV_EXCL_LINE } verbosePrint(VERB_INFO, "PNG header signature is OK\n"); png_structp png = png_create_read_struct( PNG_LIBPNG_VER_STRING, static_cast(&input), handleError, handleWarning ); if (!png) { fatal("Failed to create PNG read structure: %s", strerror(errno)); // LCOV_EXCL_LINE } png_infop info = png_create_info_struct(png); Defer destroyPng{[&] { png_destroy_read_struct(&png, info ? &info : nullptr, nullptr); }}; if (!info) { fatal("Failed to create PNG info structure: %s", strerror(errno)); // LCOV_EXCL_LINE } png_set_read_fn(png, &input, readData); png_set_sig_bytes(png, pngHeader.size()); // Process all chunks up to but not including the image data png_read_info(png, info); int bitDepth, colorType, interlaceType; png_get_IHDR( png, info, &width, &height, &bitDepth, &colorType, &interlaceType, nullptr, nullptr ); pixels.resize(static_cast(width) * static_cast(height)); auto colorTypeName = [](int type) { switch (type) { case PNG_COLOR_TYPE_GRAY: return "grayscale"; case PNG_COLOR_TYPE_GRAY_ALPHA: return "grayscale + alpha"; case PNG_COLOR_TYPE_PALETTE: return "palette"; case PNG_COLOR_TYPE_RGB: return "RGB"; case PNG_COLOR_TYPE_RGB_ALPHA: return "RGB + alpha"; default: return "unknown color type"; } }; auto interlaceTypeName = [](int type) { switch (type) { case PNG_INTERLACE_NONE: return "not interlaced"; case PNG_INTERLACE_ADAM7: return "interlaced (Adam7)"; default: return "unknown interlace type"; } }; verbosePrint( VERB_INFO, "PNG image: %" PRIu32 "x%" PRIu32 " pixels, %dbpp %s, %s\n", width, height, bitDepth, colorTypeName(colorType), interlaceTypeName(interlaceType) ); int nbColors = 0; png_colorp embeddedPal = nullptr; if (png_get_PLTE(png, info, &embeddedPal, &nbColors) != 0) { int nbTransparentEntries = 0; png_bytep transparencyPal = nullptr; if (png_get_tRNS(png, info, &transparencyPal, &nbTransparentEntries, nullptr)) { assume(nbTransparentEntries <= nbColors); } for (int i = 0; i < nbColors; ++i) { png_color const &color = embeddedPal[i]; palette.emplace_back( color.red, color.green, color.blue, transparencyPal && i < nbTransparentEntries ? transparencyPal[i] : 0xFF ); } if (checkVerbosity(VERB_INFO)) { style_Set(stderr, STYLE_MAGENTA, false); fprintf(stderr, "Embedded PNG palette has %d colors: [", nbColors); for (int i = 0; i < nbColors; ++i) { fprintf(stderr, "%s#%08x", i > 0 ? ", " : "", palette[i].toCSS()); } fprintf(stderr, "]\n"); style_Reset(stderr); } } else { verbosePrint(VERB_INFO, "No embedded PNG palette\n"); } // Set up transformations to turn everything into RGBA8888 for simplicity of handling // Convert grayscale to RGB switch (colorType & ~PNG_COLOR_MASK_ALPHA) { case PNG_COLOR_TYPE_GRAY: png_set_gray_to_rgb(png); // This also converts tRNS to alpha break; case PNG_COLOR_TYPE_PALETTE: png_set_palette_to_rgb(png); break; } if (png_get_valid(png, info, PNG_INFO_tRNS)) { // If we read a tRNS chunk, convert it to alpha png_set_tRNS_to_alpha(png); } else if (!(colorType & PNG_COLOR_MASK_ALPHA)) { // Otherwise, if we lack an alpha channel, default to full opacity png_set_add_alpha(png, 0xFFFF, PNG_FILLER_AFTER); } // Scale 16bpp back to 8 (we don't need all of that precision anyway) if (bitDepth == 16) { png_set_scale_16(png); } else if (bitDepth < 8) { png_set_packing(png); } // Deinterlace rows so they can trivially be read in order if (interlaceType != PNG_INTERLACE_NONE) { png_set_interlace_handling(png); } // Update `info` with the transformations png_read_update_info(png, info); // These shouldn't have changed assume(png_get_image_width(png, info) == width); assume(png_get_image_height(png, info) == height); // These should have changed, however assume(png_get_color_type(png, info) == PNG_COLOR_TYPE_RGBA); assume(png_get_bit_depth(png, info) == 8); // Now that metadata has been read, we can read the image data std::vector image(width * height * 4); std::vector rowPtrs(height); for (uint32_t y = 0; y < height; ++y) { rowPtrs[y] = image.data() + y * width * 4; } png_read_image(png, rowPtrs.data()); // We don't care about chunks after the image data (comments, etc.) png_read_end(png, nullptr); // Finally, process the image data from RGBA8888 bytes into `Rgba` colors for (uint32_t y = 0; y < height; ++y) { for (uint32_t x = 0; x < width; ++x) { uint32_t idx = y * width + x; uint32_t off = idx * 4; pixels[idx] = Rgba(image[off], image[off + 1], image[off + 2], image[off + 3]); } } } gbdev-rgbds-92bfe5d/src/gfx/process.cpp000066400000000000000000001067321512540461700201550ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "gfx/process.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "diagnostics.hpp" #include "file.hpp" #include "helpers.hpp" #include "itertools.hpp" #include "style.hpp" #include "verbosity.hpp" #include "gfx/color_set.hpp" #include "gfx/flip.hpp" #include "gfx/main.hpp" #include "gfx/pal_packing.hpp" #include "gfx/pal_sorting.hpp" #include "gfx/palette.hpp" #include "gfx/png.hpp" #include "gfx/rgba.hpp" #include "gfx/warning.hpp" static bool isBgColorTransparent() { return options.bgColor.has_value() && options.bgColor->isTransparent(); } class ImagePalette { std::array, NB_COLOR_SLOTS> _colors; public: ImagePalette() = default; // Registers a color in the palette. // If the newly inserted color "conflicts" with another one (different color, but same CGB // color), then the other color is returned. Otherwise, `nullptr` is returned. [[nodiscard]] Rgba const *registerColor(Rgba const &rgba) { uint16_t color = rgba.cgbColor(); std::optional &slot = _colors[color]; if (color == Rgba::transparent && !isBgColorTransparent()) { options.hasTransparentPixels = true; } if (!slot.has_value()) { slot.emplace(rgba); } else if (*slot != rgba) { assume(slot->cgbColor() != UINT16_MAX); return &*slot; } return nullptr; } size_t size() const { return std::count_if(RANGE(_colors), [](std::optional const &slot) { return slot.has_value() && slot->isOpaque(); }); } decltype(_colors) const &raw() const { return _colors; } auto begin() const { return _colors.begin(); } auto end() const { return _colors.end(); } }; struct Image { Png png{}; ImagePalette colors{}; Rgba &pixel(uint32_t x, uint32_t y) { return png.pixels[y * png.width + x]; } Rgba const &pixel(uint32_t x, uint32_t y) const { return png.pixels[y * png.width + x]; } enum GrayscaleResult { GRAY_OK, GRAY_TOO_MANY, GRAY_NONGRAY, GRAY_CONFLICT, }; std::pair> isSuitableForGrayscale() const { // Check that all of the grays don't fall into the same "bin" if (colors.size() > options.maxOpaqueColors()) { // Apply the Pigeonhole Principle verbosePrint( VERB_DEBUG, "Too many colors for grayscale sorting (%zu > %" PRIu8 ")\n", colors.size(), options.maxOpaqueColors() ); return {GrayscaleResult::GRAY_TOO_MANY, std::nullopt}; } uint8_t bins = 0; for (std::optional const &color : colors) { if (!color.has_value() || color->isTransparent()) { continue; } if (!color->isGray()) { verbosePrint( VERB_DEBUG, "Found non-gray color #%08x, not using grayscale sorting\n", color->toCSS() ); return {GrayscaleResult::GRAY_NONGRAY, color}; } uint8_t mask = 1 << color->grayIndex(); if (bins & mask) { // Two in the same bin! verbosePrint( VERB_DEBUG, "Color #%08x conflicts with another one, not using grayscale sorting\n", color->toCSS() ); return {GrayscaleResult::GRAY_CONFLICT, color}; } bins |= mask; } return {GrayscaleResult::GRAY_OK, std::nullopt}; } explicit Image(std::string const &path) { File input; if (input.open(path, std::ios_base::in | std::ios_base::binary) == nullptr) { fatal("Failed to open input image (\"%s\"): %s", input.c_str(path), strerror(errno)); } png = Png(input.c_str(path), *input); // Validate input slice if (options.inputSlice.width == 0 && png.width % 8 != 0) { fatal("Image width (%" PRIu32 " pixels) is not a multiple of 8", png.width); } if (options.inputSlice.height == 0 && png.height % 8 != 0) { fatal("Image height (%" PRIu32 " pixels) is not a multiple of 8", png.height); } if (options.inputSlice.right() > png.width || options.inputSlice.bottom() > png.height) { error( "Image slice ((%" PRIu16 ", %" PRIu16 ") to (%" PRIu32 ", %" PRIu32 ")) is outside the image bounds (%" PRIu32 "x%" PRIu32 ")", options.inputSlice.left, options.inputSlice.top, options.inputSlice.right(), options.inputSlice.bottom(), png.width, png.height ); if (options.inputSlice.width % 8 == 0 && options.inputSlice.height % 8 == 0) { fprintf( stderr, " (Did you mean the slice \"%" PRIu32 ",%" PRIu32 ":%" PRId32 ",%" PRId32 "\"? The width and height are in tiles, not pixels!)\n", options.inputSlice.left, options.inputSlice.top, options.inputSlice.width / 8, options.inputSlice.height / 8 ); } giveUp(); } // Holds colors whose alpha value is ambiguous to avoid erroring about them twice. std::unordered_set ambiguous; // Holds fused color pairs to avoid warning about them twice. // We don't need to worry about transitivity, as ImagePalette slots are immutable once // assigned, and conflicts always occur between that and another color. // For the same reason, we don't need to worry about order, either. auto hashPair = [](std::pair const &pair) { return pair.first * 31 + pair.second; }; std::unordered_set, decltype(hashPair)> fusions; // Register colors from `png` into `colors` for (uint32_t y = 0; y < png.height; ++y) { for (uint32_t x = 0; x < png.width; ++x) { if (Rgba const &color = pixel(x, y); color.isTransparent() == color.isOpaque()) { // Report ambiguously transparent or opaque colors if (uint32_t css = color.toCSS(); ambiguous.find(css) == ambiguous.end()) { error( "Color #%08x is neither transparent (alpha < %u) nor opaque (alpha >= " "%u) (first seen at (%" PRIu32 ", %" PRIu32 "))", css, Rgba::transparency_threshold, Rgba::opacity_threshold, x, y ); ambiguous.insert(css); // Do not report this color again } } else if (Rgba const *other = colors.registerColor(color); other) { // Report fused colors that reduce to the same RGB555 value if (std::pair fused{color.toCSS(), other->toCSS()}; fusions.find(fused) == fusions.end()) { warnx( "Colors #%08x and #%08x both reduce to the same Game Boy color $%04x " "(first seen at (%" PRIu32 ", %" PRIu32 "))", fused.first, fused.second, color.cgbColor(), x, y ); fusions.insert(fused); // Do not report this fusion again } } } } } class TilesVisitor { Image const &_image; bool const _columnMajor; uint32_t const _width, _height; uint32_t const _limit = _columnMajor ? _height : _width; public: TilesVisitor(Image const &image, bool columnMajor, uint32_t width, uint32_t height) : _image(image), _columnMajor(columnMajor), _width(width), _height(height) {} class Tile { Image const &_image; public: uint32_t const x, y; Tile(Image const &image, uint32_t x_, uint32_t y_) : _image(image), x(x_), y(y_) {} Rgba pixel(uint32_t xOfs, uint32_t yOfs) const { return _image.pixel(x + xOfs, y + yOfs); } }; private: struct Iterator { TilesVisitor const &parent; uint32_t const limit; uint32_t x, y; std::pair coords() const { return {x + options.inputSlice.left, y + options.inputSlice.top}; } Tile operator*() const { return {parent._image, x + options.inputSlice.left, y + options.inputSlice.top}; } Iterator &operator++() { auto [major, minor] = parent._columnMajor ? std::tie(y, x) : std::tie(x, y); major += 8; if (major == limit) { minor += 8; major = 0; } return *this; } bool operator==(Iterator const &rhs) const { return coords() == rhs.coords(); } }; public: Iterator begin() const { return {*this, _limit, 0, 0}; } Iterator end() const { Iterator it{*this, _limit, _width - 8, _height - 8}; // Last valid one... return ++it; // ...now one-past-last! } }; public: TilesVisitor visitAsTiles() const { return { *this, options.columnMajor, options.inputSlice.width ? options.inputSlice.width * 8 : png.width, options.inputSlice.height ? options.inputSlice.height * 8 : png.height, }; } }; class RawTiles { // A tile which only contains indices into the image's global palette class RawTile { std::array, 8> _pixelIndices{}; public: // Not super clean, but it's closer to matrix notation size_t &operator()(size_t x, size_t y) { return _pixelIndices[y][x]; } }; private: std::vector _tiles; public: // Creates a new raw tile, and returns a reference to it so it can be filled in RawTile &newTile() { return _tiles.emplace_back(); } }; struct AttrmapEntry { // This field can either be a color set ID, or `transparent` to indicate that the // corresponding tile is fully transparent. If you are looking to get the palette ID for this // attrmap entry while correctly handling the above, use `getPalID`. size_t colorSetID; // Only this field is used when outputting "unoptimized" data uint8_t tileID; // This is the ID as it will be output to the tilemap bool bank; bool yFlip; bool xFlip; static constexpr size_t transparent = static_cast(-1); static constexpr size_t background = static_cast(-2); bool isBackgroundTile() const { return colorSetID == background; } size_t getPalID(std::vector const &mappings) const { return mappings[isBackgroundTile() || colorSetID == transparent ? 0 : colorSetID]; } }; static void generatePalSpec(Image const &image) { // Generate a palette spec from the first few colors in the embedded palette std::vector const &embPal = image.png.palette; if (embPal.empty()) { fatal("\"-c embedded\" was given, but the PNG does not have an embedded palette"); } // Ignore extraneous colors if they are unused size_t nbColors = embPal.size(); if (nbColors > options.maxOpaqueColors()) { nbColors = options.maxOpaqueColors(); } // Fill in the palette spec options.palSpec.clear(); auto &palette = options.palSpec.emplace_back(); assume(nbColors <= palette.size()); for (size_t i = 0; i < nbColors; ++i) { palette[i] = embPal[i]; } } static std::pair, std::vector> generatePalettes(std::vector const &colorSets, Image const &image) { // Run a "pagination" problem solver auto [mappings, nbPalettes] = overloadAndRemove(colorSets); assume(mappings.size() == colorSets.size()); // LCOV_EXCL_START if (checkVerbosity(VERB_INFO)) { style_Set(stderr, STYLE_MAGENTA, false); fprintf( stderr, "Color set mappings: (%zu palette%s)\n", nbPalettes, nbPalettes != 1 ? "s" : "" ); for (size_t i = 0; i < mappings.size(); ++i) { fprintf(stderr, "%zu -> %zu\n", i, mappings[i]); } style_Reset(stderr); } // LCOV_EXCL_STOP std::vector palettes(nbPalettes); // If the image contains at least one transparent pixel, force transparency in the first slot of // all palettes if (options.hasTransparentPixels) { for (Palette &pal : palettes) { pal.colors[0] = Rgba::transparent; } } // Generate the actual palettes from the mappings for (size_t colorSetID = 0; colorSetID < mappings.size(); ++colorSetID) { Palette &pal = palettes[mappings[colorSetID]]; for (uint16_t color : colorSets[colorSetID]) { pal.addColor(color); } } // "Sort" colors in the generated palettes, see the man page for the flowchart if (options.palSpecType == Options::DMG) { sortGrayscale(palettes, image.colors.raw()); } else if (!image.png.palette.empty()) { warning( WARNING_EMBEDDED, "Sorting palette colors by PNG's embedded PLTE chunk without '-c/--colors embedded'" ); sortIndexed(palettes, image.png.palette); } else if (image.isSuitableForGrayscale().first == Image::GRAY_OK) { sortGrayscale(palettes, image.colors.raw()); } else { sortRgb(palettes); } return {mappings, palettes}; } static std::pair, std::vector> makePalsAsSpecified(std::vector const &colorSets) { // Convert the palette spec to actual palettes std::vector palettes(options.palSpec.size()); for (auto [spec, pal] : zip(options.palSpec, palettes)) { for (size_t i = 0; i < options.nbColorsPerPal; ++i) { // If the spec has a gap, there's no need to copy anything. if (spec[i].has_value() && spec[i]->isOpaque()) { pal[i] = spec[i]->cgbColor(); } } } auto listColors = [](auto const &list) { static char buf[sizeof(", $XXXX, $XXXX, $XXXX, $XXXX")]; char *ptr = buf; for (uint16_t color : list) { ptr += snprintf(ptr, sizeof(", $XXXX"), ", $%04x", color); } return &buf[literal_strlen(", ")]; }; // Iterate through color sets, and try mapping them to the specified palettes std::vector mappings(colorSets.size()); bool bad = false; for (size_t i = 0; i < colorSets.size(); ++i) { ColorSet const &colorSet = colorSets[i]; // Find the palette... auto iter = std::find_if(RANGE(palettes), [&colorSet](Palette const &pal) { // ...which contains all colors in this color set return std::all_of(RANGE(colorSet), [&pal](uint16_t color) { return std::find(RANGE(pal), color) != pal.end(); }); }); if (iter == palettes.end()) { assume(!colorSet.empty()); error("Failed to fit tile colors [%s] in specified palettes", listColors(colorSet)); bad = true; } mappings[i] = iter - palettes.begin(); // Bogus value, but whatever } if (bad) { fprintf( stderr, "note: The following palette%s specified:\n", palettes.size() == 1 ? " was" : "s were" ); for (Palette const &pal : palettes) { fprintf(stderr, " [%s]\n", listColors(pal)); } giveUp(); } return {mappings, palettes}; } static void outputPalettes(std::vector const &palettes) { // LCOV_EXCL_START if (checkVerbosity(VERB_INFO)) { style_Set(stderr, STYLE_MAGENTA, false); for (Palette const &palette : palettes) { fputs("{ ", stderr); for (uint16_t colorIndex : palette) { fprintf(stderr, "%04" PRIx16 ", ", colorIndex); } fputs("}\n", stderr); } style_Reset(stderr); } // LCOV_EXCL_STOP if (palettes.size() > options.nbPalettes) { // If the palette generation is wrong, other (dependee) operations are likely to be // nonsensical, so fatal-error outright fatal( "Generated %zu palettes, over the maximum of %" PRIu16, palettes.size(), options.nbPalettes ); } if (!options.palettes.empty()) { File output; if (!output.open(options.palettes, std::ios_base::out | std::ios_base::binary)) { // LCOV_EXCL_START fatal("Failed to create \"%s\": %s", output.c_str(options.palettes), strerror(errno)); // LCOV_EXCL_STOP } for (Palette const &palette : palettes) { for (uint8_t i = 0; i < options.nbColorsPerPal; ++i) { // Will output `UINT16_MAX` for unused slots uint16_t color = palette.colors[i]; output->sputc(color & 0xFF); output->sputc(color >> 8); } } } } static void hashBitplanes(uint16_t bitplanes, uint16_t &hash) { hash ^= bitplanes; if (options.allowMirroringX) { // Count the line itself as mirrored, which ensures the same hash as the tile's horizontal // flip; vertical mirroring is already taken care of because the symmetric line will be // XOR'd the same way. (This can trivially create some collisions, but real-world tile data // generally doesn't trigger them.) hash ^= flipTable[bitplanes >> 8] << 8 | flipTable[bitplanes & 0xFF]; } } class TileData { // Importantly, `TileData` is **always** 2bpp. // If the active bit depth is 1bpp, all tiles are processed as 2bpp nonetheless, but emitted as // 1bpp. This massively simplifies internal processing, since bit depth is always identical // outside of I/O / serialization boundaries. std::array _data; // The hash is a bit lax: it's the XOR of all lines, and every other nibble is identical // if horizontal mirroring is in effect. It should still be a reasonable tie-breaker in // non-pathological cases. uint16_t _hash; public: // This is an index within the "global" pool; no bank info is encoded here // It's marked as `mutable` so that it can be modified even on a `const` object; // this is necessary because the `set` in which it's inserted refuses any modification for fear // of altering the element's hash, but the tile ID is not part of it. mutable uint16_t tileID; static uint16_t rowBitplanes(Image::TilesVisitor::Tile const &tile, Palette const &palette, uint32_t y) { uint16_t row = 0; for (uint32_t x = 0; x < 8; ++x) { row <<= 1; uint8_t index = palette.indexOf(tile.pixel(x, y).cgbColor()); assume(index < palette.size()); // The color should be in the palette if (index & 1) { row |= 1; } if (index & 2) { row |= 0x100; } } return row; } TileData(std::array &&raw) : _data(raw), _hash(0) { for (uint8_t y = 0; y < 8; ++y) { uint16_t bitplanes = _data[y * 2] | _data[y * 2 + 1] << 8; hashBitplanes(bitplanes, _hash); } } TileData(Image::TilesVisitor::Tile const &tile, Palette const &palette) : _hash(0) { size_t writeIndex = 0; for (uint32_t y = 0; y < 8; ++y) { uint16_t bitplanes = rowBitplanes(tile, palette, y); hashBitplanes(bitplanes, _hash); _data[writeIndex++] = bitplanes & 0xFF; _data[writeIndex++] = bitplanes >> 8; } } std::array const &data() const { return _data; } uint16_t hash() const { return _hash; } enum MatchType { NOPE, EXACT, HFLIP, VFLIP, VHFLIP, }; MatchType tryMatching(TileData const &other) const { // Check for strict equality first, as that can typically be optimized, and it allows // hoisting the mirroring check out of the loop if (_data == other._data) { return MatchType::EXACT; } // Check if we have horizontal mirroring, which scans the array forward again if (options.allowMirroringX && std::equal(RANGE(_data), other._data.begin(), [](uint8_t lhs, uint8_t rhs) { return lhs == flipTable[rhs]; })) { return MatchType::HFLIP; } // The remaining possibilities for matching all require vertical mirroring if (!options.allowMirroringY) { return MatchType::NOPE; } // Check if we have vertical or vertical+horizontal mirroring, for which we have to read // bitplane *pairs* backwards bool hasVFlip = true, hasVHFlip = true; for (uint8_t i = 0; i < _data.size(); ++i) { // Flip the bottom bit to get the corresponding row's bitplane 0/1 // (This works because the array size is even) uint8_t lhs = _data[i], rhs = other._data[(15 - i) ^ 1]; if (lhs != rhs) { hasVFlip = false; } if (lhs != flipTable[rhs]) { hasVHFlip = false; } if (!hasVFlip && !hasVHFlip) { return MatchType::NOPE; // If both have been eliminated, all hope is lost! } } // If we have both (i.e. we have symmetry), default to vflip only if (hasVFlip) { return MatchType::VFLIP; } // If we allow both and have both, then use both if (options.allowMirroringX && hasVHFlip) { return MatchType::VHFLIP; } return MatchType::NOPE; } bool operator==(TileData const &rhs) const { return tryMatching(rhs) != MatchType::NOPE; } }; template<> struct std::hash { size_t operator()(TileData const &tile) const { return tile.hash(); } }; static void outputUnoptimizedTileData( Image const &image, std::vector const &attrmap, std::vector const &palettes, std::vector const &mappings ) { File output; if (!output.open(options.output, std::ios_base::out | std::ios_base::binary)) { // LCOV_EXCL_START fatal("Failed to create \"%s\": %s", output.c_str(options.output), strerror(errno)); // LCOV_EXCL_STOP } uint16_t widthTiles = options.inputSlice.width ? options.inputSlice.width : image.png.width / 8; uint16_t heightTiles = options.inputSlice.height ? options.inputSlice.height : image.png.height / 8; uint64_t nbTiles = widthTiles * heightTiles; uint64_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0; uint64_t tileIdx = 0; for (auto const &[tile, attr] : zip(image.visitAsTiles(), attrmap)) { // Do not emit fully-background tiles. if (attr.isBackgroundTile()) { ++tileIdx; continue; } // If the tile is fully transparent, this defaults to palette 0. Palette const &palette = palettes[attr.getPalID(mappings)]; bool empty = true; for (uint32_t y = 0; y < 8; ++y) { uint16_t bitplanes = TileData::rowBitplanes(tile, palette, y); if (bitplanes != 0) { empty = false; } if (tileIdx < nbKeptTiles) { output->sputc(bitplanes & 0xFF); if (options.bitDepth == 2) { output->sputc(bitplanes >> 8); } } } if (!empty && tileIdx >= nbKeptTiles) { warning( WARNING_TRIM_NONEMPTY, "Trimming a nonempty tile (configure with '-x/--trim-end')" ); break; // Don't repeat the warning for subsequent tiles } ++tileIdx; } assume(nbKeptTiles <= tileIdx && tileIdx <= nbTiles); } static void outputUnoptimizedMaps( std::vector const &attrmap, std::vector const &mappings ) { std::optional tilemapOutput, attrmapOutput, palmapOutput; auto autoOpenPath = [](std::string const &path, std::optional &file) { if (!path.empty()) { file.emplace(); if (!file->open(path, std::ios_base::out | std::ios_base::binary)) { // LCOV_EXCL_START fatal("Failed to create \"%s\": %s", file->c_str(options.tilemap), strerror(errno)); // LCOV_EXCL_STOP } } }; autoOpenPath(options.tilemap, tilemapOutput); autoOpenPath(options.attrmap, attrmapOutput); autoOpenPath(options.palmap, palmapOutput); uint8_t tileID = 0; uint8_t bank = 0; for (AttrmapEntry const &attr : attrmap) { if (tilemapOutput.has_value()) { (*tilemapOutput) ->sputc((attr.isBackgroundTile() ? 0 : tileID) + options.baseTileIDs[bank]); } uint8_t palID = attr.getPalID(mappings) + options.basePalID; if (attrmapOutput.has_value()) { (*attrmapOutput)->sputc((palID & 0b111) | bank << 3); // The other flags are all 0 } if (palmapOutput.has_value()) { (*palmapOutput)->sputc(palID); } // Background tiles are skipped in the tile data, so they should be skipped in the maps too. if (attr.isBackgroundTile()) { continue; } // Compare with `maxNbTiles` *before* incrementing, due to unsigned overflow! if (tileID + 1 < options.maxNbTiles[bank]) { ++tileID; } else { assume(bank == 0); bank = 1; tileID = 0; } } } struct UniqueTiles { std::unordered_set tileset; std::vector tiles; UniqueTiles() = default; // Copies are likely to break pointers, so we really don't want those. // Copy elision should be relied on to be more sure that refs won't be invalidated, too! UniqueTiles(UniqueTiles const &) = delete; UniqueTiles(UniqueTiles &&) = default; // Adds a tile to the collection, and returns its ID std::pair addTile(TileData newTile) { if (auto [tileData, inserted] = tileset.insert(newTile); inserted) { // Give the new tile the next available unique ID tileData->tileID = static_cast(tiles.size()); tiles.emplace_back(&*tileData); // Pointers are never invalidated! return {tileData->tileID, TileData::NOPE}; } else { return {tileData->tileID, tileData->tryMatching(newTile)}; } } size_t size() const { return tiles.size(); } auto begin() const { return tiles.begin(); } auto end() const { return tiles.end(); } }; // Generate tile data while deduplicating unique tiles (via mirroring if enabled) // Additionally, while we have the info handy, convert from the 16-bit "global" tile IDs to // 8-bit tile IDs + the bank bit; this will save the work when we output the data later (potentially // twice) static UniqueTiles dedupTiles( Image const &image, std::vector &attrmap, std::vector const &palettes, std::vector const &mappings ) { // Iterate throughout the image, generating tile data as we go // (We don't need the full tile data to be able to dedup tiles, but we don't lose anything // by caching the full tile data anyway, so we might as well.) UniqueTiles tiles; if (!options.inputTileset.empty()) { File inputTileset; if (!inputTileset.open(options.inputTileset, std::ios::in | std::ios::binary)) { fatal("Failed to open \"%s\": %s", options.inputTileset.c_str(), strerror(errno)); } std::array tile; size_t const tileSize = options.bitDepth * 8; for (;;) { // It's okay to cast between character types. size_t len = inputTileset->sgetn(reinterpret_cast(tile.data()), tileSize); if (len == 0) { // EOF! break; } else if (len != tileSize) { fatal( "\"%s\" does not contain a multiple of %zu bytes; is it actually tile data?", options.inputTileset.c_str(), tileSize ); } else if (len == 8) { // Expand the tile data to 2bpp. for (size_t i = 8; i--;) { tile[i * 2 + 1] = 0; tile[i * 2] = tile[i]; } } auto [tileID, matchType] = tiles.addTile(std::move(tile)); if (matchType != TileData::NOPE) { error( "The input tileset's tile #%hu was deduplicated; please check that your " "deduplication flags ('-u', '-m') are consistent with what was used to " "generate the input tileset", tileID ); } } } bool inputWithoutOutput = !options.inputTileset.empty() && options.output.empty(); for (auto const &[tile, attr] : zip(image.visitAsTiles(), attrmap)) { if (attr.isBackgroundTile()) { attr.xFlip = false; attr.yFlip = false; attr.bank = 0; attr.tileID = 0; } else { auto [tileID, matchType] = tiles.addTile({tile, palettes[mappings[attr.colorSetID]]}); if (inputWithoutOutput && matchType == TileData::NOPE) { error( "Tile at (%" PRIu32 ", %" PRIu32 ") is not within the input tileset, and '-o' was not given", tile.x, tile.y ); } attr.xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP; attr.yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP; attr.bank = tileID >= options.maxNbTiles[0]; attr.tileID = (attr.bank ? tileID - options.maxNbTiles[0] : tileID) + options.baseTileIDs[attr.bank]; } } // Copy elision should prevent the contained `unordered_set` from being re-constructed return tiles; } static void outputTileData(UniqueTiles const &tiles) { File output; if (!output.open(options.output, std::ios_base::out | std::ios_base::binary)) { // LCOV_EXCL_START fatal("Failed to create \"%s\": %s", output.c_str(options.output), strerror(errno)); // LCOV_EXCL_STOP } uint16_t tileID = 0; for (auto iter = tiles.begin(), end = tiles.end() - options.trim; iter != end; ++iter) { TileData const *tile = *iter; assume(tile->tileID == tileID); ++tileID; if (options.bitDepth == 2) { output->sputn(reinterpret_cast(tile->data().data()), 16); } else { assume(options.bitDepth == 1); for (size_t y = 0; y < 8; ++y) { output->sputc(tile->data()[y * 2]); } } } } static void outputTilemap(std::vector const &attrmap) { File output; if (!output.open(options.tilemap, std::ios_base::out | std::ios_base::binary)) { // LCOV_EXCL_START fatal("Failed to create \"%s\": %s", output.c_str(options.tilemap), strerror(errno)); // LCOV_EXCL_STOP } for (AttrmapEntry const &entry : attrmap) { output->sputc(entry.tileID); // The tile ID has already been converted } } static void outputAttrmap(std::vector const &attrmap, std::vector const &mappings) { File output; if (!output.open(options.attrmap, std::ios_base::out | std::ios_base::binary)) { // LCOV_EXCL_START fatal("Failed to create \"%s\": %s", output.c_str(options.attrmap), strerror(errno)); // LCOV_EXCL_STOP } for (AttrmapEntry const &entry : attrmap) { uint8_t attr = entry.xFlip << 5 | entry.yFlip << 6; attr |= entry.bank << 3; attr |= (entry.getPalID(mappings) + options.basePalID) & 0b111; output->sputc(attr); } } static void outputPalmap(std::vector const &attrmap, std::vector const &mappings) { File output; if (!output.open(options.palmap, std::ios_base::out | std::ios_base::binary)) { // LCOV_EXCL_START fatal("Failed to create \"%s\": %s", output.c_str(options.palmap), strerror(errno)); // LCOV_EXCL_STOP } for (AttrmapEntry const &entry : attrmap) { output->sputc(entry.getPalID(mappings) + options.basePalID); } } void processPalettes() { verbosePrint(VERB_CONFIG, "Using libpng %s\n", png_get_libpng_ver(nullptr)); std::vector colorSets; std::vector palettes; std::tie(std::ignore, palettes) = makePalsAsSpecified(colorSets); outputPalettes(palettes); } void process() { verbosePrint(VERB_CONFIG, "Using libpng %s\n", png_get_libpng_ver(nullptr)); verbosePrint(VERB_NOTICE, "Reading tiles...\n"); Image image(options.input); // This also sets `hasTransparentPixels` as a side effect // LCOV_EXCL_START if (checkVerbosity(VERB_INFO)) { style_Set(stderr, STYLE_MAGENTA, false); fputs("Image colors: [ ", stderr); for (std::optional const &slot : image.colors) { if (!slot.has_value()) { continue; } fprintf(stderr, "#%08x, ", slot->toCSS()); } fputs("]\n", stderr); style_Reset(stderr); } // LCOV_EXCL_STOP if (options.palSpecType == Options::DMG) { char const *prefix = "Image is not compatible with a DMG palette specification: it contains"; if (options.hasTransparentPixels) { fatal("%s transparent pixels", prefix); } switch (auto const [result, color] = image.isSuitableForGrayscale(); result) { case Image::GRAY_OK: break; case Image::GRAY_TOO_MANY: fatal("%s too many colors (%zu)", prefix, image.colors.size()); case Image::GRAY_NONGRAY: fatal("%s a non-gray color #%08x", prefix, color->toCSS()); case Image::GRAY_CONFLICT: fatal( "%s a color #%08x that reduces to the same gray shade as another one", prefix, color->toCSS() ); } } // Now, iterate through the tiles, generating color sets as we go // We do this unconditionally because this performs the image validation (which we want to // perform even if no output is requested), and because it's necessary to generate any // output (with the exception of an un-duplicated tilemap, but that's an acceptable loss.) std::vector colorSets; std::vector attrmap{}; for (auto tile : image.visitAsTiles()) { AttrmapEntry &attrs = attrmap.emplace_back(); // Count the unique non-transparent colors for packing std::unordered_set tileColors; for (uint32_t y = 0; y < 8; ++y) { for (uint32_t x = 0; x < 8; ++x) { if (Rgba color = tile.pixel(x, y); color.isOpaque() || !options.hasTransparentPixels) { tileColors.insert(color.cgbColor()); } } } if (tileColors.size() > options.maxOpaqueColors()) { fatal( "Tile at (%" PRIu32 ", %" PRIu32 ") has %zu colors, more than %" PRIu8, tile.x, tile.y, tileColors.size(), options.maxOpaqueColors() ); } if (tileColors.empty()) { // "Empty" color sets screw with the packing process, so discard those assume(!isBgColorTransparent()); attrs.colorSetID = AttrmapEntry::transparent; continue; } ColorSet colorSet; for (uint16_t color : tileColors) { colorSet.add(color); } if (options.bgColor.has_value() && std::find(RANGE(tileColors), options.bgColor->cgbColor()) != tileColors.end()) { if (tileColors.size() == 1) { // The tile contains just the background color, skip it. attrs.colorSetID = AttrmapEntry::background; continue; } fatal( "Tile (%" PRIu32 ", %" PRIu32 ") contains the background color (#%08x)", tile.x, tile.y, options.bgColor->toCSS() ); } // Insert the color set, making sure to avoid overlaps for (size_t n = 0; n < colorSets.size(); ++n) { switch (colorSet.compare(colorSets[n])) { case ColorSet::WE_BIGGER: colorSets[n] = colorSet; // Override them // Remove any other color sets that we encompass // (Example [(0, 1), (0, 2)], inserting (0, 1, 2)) [[fallthrough]]; case ColorSet::THEY_BIGGER: // Do nothing, they already contain us attrs.colorSetID = n; goto continue_visiting_tiles; // Can't `continue` from within a nested loop case ColorSet::NEITHER: break; // Keep going } } attrs.colorSetID = colorSets.size(); if (colorSets.size() == AttrmapEntry::background) { // Check for overflow fatal( "Reached %zu color sets... sorry, this image is too much for me to handle :(", AttrmapEntry::transparent ); } colorSets.push_back(colorSet); continue_visiting_tiles:; } verbosePrint( VERB_INFO, "Image contains %zu color set%s\n", colorSets.size(), colorSets.size() != 1 ? "s" : "" ); // LCOV_EXCL_START if (checkVerbosity(VERB_INFO)) { style_Set(stderr, STYLE_MAGENTA, false); for (ColorSet const &colorSet : colorSets) { fputs("[ ", stderr); for (uint16_t color : colorSet) { fprintf(stderr, "$%04x, ", color); } fputs("]\n", stderr); } style_Reset(stderr); } // LCOV_EXCL_STOP if (options.palSpecType == Options::EMBEDDED) { generatePalSpec(image); } auto [mappings, palettes] = options.palSpecType == Options::NO_SPEC || options.palSpecType == Options::DMG ? generatePalettes(colorSets, image) : makePalsAsSpecified(colorSets); outputPalettes(palettes); // If deduplication is not happening, we just need to output the tile data and/or maps as-is if (!options.allowDedup) { uint32_t const nbTilesH = image.png.height / 8, nbTilesW = image.png.width / 8; // Check the tile count if (uint32_t nbTiles = nbTilesW * nbTilesH; nbTiles > options.maxNbTiles[0] + options.maxNbTiles[1]) { fatal( "Image contains %" PRIu32 " tiles, exceeding the limit of %" PRIu16 " + %" PRIu16, nbTiles, options.maxNbTiles[0], options.maxNbTiles[1] ); } // I currently cannot figure out useful semantics for this combination of flags. if (!options.inputTileset.empty()) { fatal("Input tilesets are not supported without '-u'"); } if (!options.output.empty()) { verbosePrint(VERB_NOTICE, "Generating unoptimized tile data...\n"); outputUnoptimizedTileData(image, attrmap, palettes, mappings); } if (!options.tilemap.empty() || !options.attrmap.empty() || !options.palmap.empty()) { verbosePrint( VERB_NOTICE, "Generating unoptimized tilemap and/or attrmap and/or palmap...\n" ); outputUnoptimizedMaps(attrmap, mappings); } } else { // All of these require the deduplication process to be performed to be output verbosePrint(VERB_NOTICE, "Deduplicating tiles...\n"); UniqueTiles tiles = dedupTiles(image, attrmap, palettes, mappings); if (size_t nbTiles = tiles.size(); nbTiles > options.maxNbTiles[0] + options.maxNbTiles[1]) { fatal( "Image contains %zu tiles, exceeding the limit of %" PRIu16 " + %" PRIu16, nbTiles, options.maxNbTiles[0], options.maxNbTiles[1] ); } if (!options.output.empty()) { verbosePrint(VERB_NOTICE, "Generating optimized tile data...\n"); outputTileData(tiles); } if (!options.tilemap.empty()) { verbosePrint(VERB_NOTICE, "Generating optimized tilemap...\n"); outputTilemap(attrmap); } if (!options.attrmap.empty()) { verbosePrint(VERB_NOTICE, "Generating optimized attrmap...\n"); outputAttrmap(attrmap, mappings); } if (!options.palmap.empty()) { verbosePrint(VERB_NOTICE, "Generating optimized palmap...\n"); outputPalmap(attrmap, mappings); } } } gbdev-rgbds-92bfe5d/src/gfx/reverse.cpp000066400000000000000000000423621512540461700201500ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "gfx/reverse.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "diagnostics.hpp" #include "file.hpp" #include "helpers.hpp" // assume #include "verbosity.hpp" #include "gfx/flip.hpp" #include "gfx/main.hpp" #include "gfx/rgba.hpp" #include "gfx/warning.hpp" static std::vector readInto(std::string const &path) { File file; if (!file.open(path, std::ios::in | std::ios::binary)) { fatal("Failed to open \"%s\": %s", file.c_str(path), strerror(errno)); } std::vector data(128 * 16); // Begin with some room pre-allocated size_t curSize = 0; for (;;) { size_t oldSize = curSize; curSize = data.size(); // Fill the new area ([oldSize; curSize[) with bytes size_t nbRead = file->sgetn(reinterpret_cast(&data.data()[oldSize]), curSize - oldSize); if (nbRead != curSize - oldSize) { // Shrink the vector to discard bytes that weren't read data.resize(oldSize + nbRead); break; } // If the vector has some capacity left, use it; otherwise, double the current size // Arbitrary, but if you got a better idea... size_t newSize = oldSize != data.capacity() ? data.capacity() : oldSize * 2; assume(oldSize != newSize); data.resize(newSize); } return data; } [[noreturn]] static void pngError(png_structp png, char const *msg) { fatal( "libpng error while writing reversed image (\"%s\"): %s", reinterpret_cast(png_get_error_ptr(png)), msg ); } static void pngWarning(png_structp png, char const *msg) { warnx( "libpng found while writing reversed image (\"%s\"): %s", reinterpret_cast(png_get_error_ptr(png)), msg ); } static void writePng(png_structp png, png_bytep data, size_t length) { File &pngFile = *static_cast(png_get_io_ptr(png)); pngFile->sputn(reinterpret_cast(data), length); } static void flushPng(png_structp png) { File &pngFile = *static_cast(png_get_io_ptr(png)); pngFile->pubsync(); } static void printColor(std::optional const &color) { if (color) { fprintf(stderr, "#%08x", color->toCSS()); } else { fputs(" ", stderr); } } static void printPalette(std::array, 4> const &palette) { putc('[', stderr); printColor(palette[0]); fputs(", ", stderr); printColor(palette[1]); fputs(", ", stderr); printColor(palette[2]); fputs(", ", stderr); printColor(palette[3]); putc(']', stderr); } void reverse() { verbosePrint(VERB_CONFIG, "Using libpng %s\n", png_get_libpng_ver(nullptr)); // Check for weird flag combinations if (options.output.empty()) { fatal("Tile data must be provided when reversing an image"); } if (options.allowDedup && options.tilemap.empty()) { warnx("Tile deduplication is enabled, but no tilemap is provided"); } if (options.useColorCurve) { warnx("The color curve is not yet supported in reverse mode"); } if (options.inputSlice.left != 0 || options.inputSlice.top != 0 || options.inputSlice.height != 0) { warnx("\"Sliced-off\" pixels are ignored in reverse mode"); } if (options.inputSlice.width != 0 && options.inputSlice.width != options.reversedWidth * 8) { warnx( "Specified input slice width (%" PRIu16 ") does not match provided reversing width (%" PRIu16 " * 8)", options.inputSlice.width, options.reversedWidth ); } verbosePrint(VERB_NOTICE, "Reading tiles...\n"); std::vector const tiles = readInto(options.output); uint8_t tileSize = 8 * options.bitDepth; if (tiles.size() % tileSize != 0) { fatal( "Tile data size (%zu bytes) is not a multiple of %" PRIu8 " bytes", tiles.size(), tileSize ); } // By default, assume tiles are not deduplicated, and add the (allegedly) trimmed tiles size_t const nbTiles = tiles.size() / tileSize; verbosePrint(VERB_INFO, "Read %zu tiles\n", nbTiles); size_t mapSize = nbTiles + options.trim; // Image size in tiles std::optional> tilemap; if (!options.tilemap.empty()) { tilemap = readInto(options.tilemap); mapSize = tilemap->size(); verbosePrint(VERB_INFO, "Read %zu tilemap entries\n", mapSize); } if (mapSize == 0) { fatal("Cannot generate empty image"); } if (mapSize > options.maxNbTiles[0] + options.maxNbTiles[1]) { warnx( "Total number of tiles (%zu) is more than the limit of %" PRIu16 " + %" PRIu16, mapSize, options.maxNbTiles[0], options.maxNbTiles[1] ); } size_t width = options.reversedWidth, height; // In tiles if (width == 0) { // Pick the smallest width that will result in a landscape-aspect rectangular image. // Thus a prime number of tiles will result in a horizontal row. // This avoids redundancy with `-r 1` which results in a vertical column. width = static_cast(ceil(sqrt(mapSize))); for (; width < mapSize; ++width) { if (mapSize % width == 0) { break; } } verbosePrint(VERB_INFO, "Picked reversing width of %zu tiles\n", width); } if (mapSize % width != 0) { if (options.trim == 0 && !tilemap) { fatal( "Total number of tiles (%zu) cannot be divided by image width (%zu tiles)\n" "(To proceed anyway with this image width, try passing \"-x %zu\")", mapSize, width, width - mapSize % width ); } fatal( "Total number of tiles (%zu) cannot be divided by image width (%zu tiles)", mapSize, width ); } height = mapSize / width; verbosePrint(VERB_INFO, "Reversed image dimensions: %zux%zu tiles\n", width, height); Rgba const grayColors[4] = { Rgba(0xFFFFFFFF), Rgba(0xAAAAAAFF), Rgba(0x555555FF), Rgba(0x000000FF) }; std::vector, 4>> palettes{ {grayColors[0], grayColors[1], grayColors[2], grayColors[3]} }; // If a palette file or palette spec is used as input, it overrides the default colors. if (!options.palettes.empty()) { File file; if (!file.open(options.palettes, std::ios::in | std::ios::binary)) { fatal("Failed to open \"%s\": %s", file.c_str(options.palettes), strerror(errno)); } palettes.clear(); std::array buf; // 4 colors for (;;) { if (size_t nbRead = file->sgetn(reinterpret_cast(buf.data()), buf.size()); nbRead == 0) { break; } else if (nbRead != buf.size()) { fatal( "Palette data size (%zu) is not a multiple of %zu bytes\n", palettes.size() * buf.size() + nbRead, buf.size() ); } // Expand the colors auto &palette = palettes.emplace_back(); std::generate( palette.begin(), palette.begin() + options.nbColorsPerPal, [&buf, i = 0]() mutable { i += 2; return Rgba::fromCGBColor(buf[i - 2] | buf[i - 1] << 8); // little-endian } ); } if (palettes.size() > options.nbPalettes) { warnx( "Read %zu palettes, more than the specified limit of %" PRIu16, palettes.size(), options.nbPalettes ); } if (options.palSpecType == Options::EXPLICIT && palettes != options.palSpec) { warnx("Colors in the palette file do not match those specified with '-c'"); // This spacing aligns "...versus with `-c`" above the column of `-c` palettes fputs("Colors specified in the palette file: ...versus with '-c':\n", stderr); for (size_t i = 0; i < palettes.size() && i < options.palSpec.size(); ++i) { if (i < palettes.size()) { printPalette(palettes[i]); } else { fputs(" ", stderr); } if (i < options.palSpec.size()) { fputs(" ", stderr); printPalette(options.palSpec[i]); } putc('\n', stderr); } } } else if (options.palSpecType == Options::DMG) { for (size_t i = 0; i < palettes[0].size(); ++i) { palettes[0][i] = grayColors[options.dmgValue(i)]; } } else if (options.palSpecType == Options::EMBEDDED) { warnx("An embedded palette was requested, but no palette file was specified; ignoring " "request"); } else if (options.palSpecType == Options::EXPLICIT) { palettes = std::move(options.palSpec); // We won't be using it again. } std::optional> attrmap; uint16_t nbTilesInBank[2] = {0, 0}; // Only used if there is an attrmap. if (!options.attrmap.empty()) { attrmap = readInto(options.attrmap); if (attrmap->size() != mapSize) { fatal( "Attribute map size (%zu tiles) does not match image size (%zu tiles)", attrmap->size(), mapSize ); } // Scan through the attributes for inconsistencies // We do this now for two reasons: // 1. Checking those during the main loop is harmful to optimization, and // 2. It clutters the code more, and it's not in great shape to begin with for (size_t index = 0; index < mapSize; ++index) { uint8_t attr = (*attrmap)[index]; size_t tx = index % width, ty = index / width; if (uint8_t palID = (attr & 0b111) - options.basePalID; palID > palettes.size()) { error( "Attribute map references palette #%u at (%zu, %zu), but there are only %zu " "palettes", palID, tx, ty, palettes.size() ); } bool bank = attr & 0b1000; if (!tilemap) { if (bank) { warnx( "Attribute map assigns tile at (%zu, %zu) to bank 1, but no tilemap " "specified; " "ignoring the bank bit", tx, ty ); } } else { if (uint8_t tileOfs = (*tilemap)[index] - options.baseTileIDs[bank]; tileOfs >= nbTilesInBank[bank]) { nbTilesInBank[bank] = tileOfs + 1; } } } verbosePrint( VERB_INFO, "Number of tiles in bank {0: %" PRIu16 ", 1: %" PRIu16 "}\n", nbTilesInBank[0], nbTilesInBank[1] ); for (int bank = 0; bank < 2; ++bank) { if (nbTilesInBank[bank] > options.maxNbTiles[bank]) { error( "Bank %d contains %" PRIu16 " tiles, but the specified limit is %" PRIu16, bank, nbTilesInBank[bank], options.maxNbTiles[bank] ); } } if (nbTilesInBank[0] + nbTilesInBank[1] > nbTiles) { fatal( "The tilemap references %" PRIu16 " tiles in bank 0 and %" PRIu16 " in bank 1, but only %zu have been read in total", nbTilesInBank[0], nbTilesInBank[1], nbTiles ); } requireZeroErrors(); } if (tilemap) { if (attrmap) { for (size_t index = 0; index < mapSize; ++index) { size_t tx = index % width, ty = index / width; uint8_t tileID = (*tilemap)[index]; uint8_t attr = (*attrmap)[index]; bool bank = attr & 0b1000; if (uint8_t tileOfs = tileID - options.baseTileIDs[bank]; tileOfs >= options.maxNbTiles[bank]) { error( "Tilemap references tile #%" PRIu8 " at (%zu, %zu), but the limit for bank %u is %" PRIu16, tileID, tx, ty, bank, options.maxNbTiles[bank] ); } } } else { size_t const limit = std::min(nbTiles, options.maxNbTiles[0]); for (size_t index = 0; index < mapSize; ++index) { if (uint8_t tileID = (*tilemap)[index]; static_cast(tileID - options.baseTileIDs[0]) >= limit) { size_t tx = index % width, ty = index / width; error( "Tilemap references tile #%" PRIu8 " at (%zu, %zu), but the limit is %zu", tileID, tx, ty, limit ); } } } requireZeroErrors(); } std::optional> palmap; if (!options.palmap.empty()) { palmap = readInto(options.palmap); if (palmap->size() != mapSize) { fatal( "Palette map size (%zu tiles) does not match image size (%zu tiles)", palmap->size(), mapSize ); } } verbosePrint(VERB_NOTICE, "Writing image...\n"); File pngFile; if (!pngFile.open(options.input, std::ios::out | std::ios::binary)) { // LCOV_EXCL_START fatal("Failed to create \"%s\": %s", pngFile.c_str(options.input), strerror(errno)); // LCOV_EXCL_STOP } png_structp png = png_create_write_struct( PNG_LIBPNG_VER_STRING, const_cast(static_cast(pngFile.c_str(options.input))), pngError, pngWarning ); if (!png) { // LCOV_EXCL_START fatal("Failed to create PNG write struct: %s", strerror(errno)); // LCOV_EXCL_STOP } png_infop pngInfo = png_create_info_struct(png); if (!pngInfo) { // LCOV_EXCL_START fatal("Failed to create PNG info structure: %s", strerror(errno)); // LCOV_EXCL_STOP } png_set_write_fn(png, &pngFile, writePng, flushPng); int pngColorType = options.palettes.empty() ? PNG_COLOR_TYPE_GRAY : palettes.size() == 1 ? PNG_COLOR_TYPE_PALETTE : PNG_COLOR_TYPE_RGB_ALPHA; int pngDepth = options.palettes.empty() ? options.bitDepth : 8; png_set_IHDR( png, pngInfo, width * 8, height * 8, pngDepth, pngColorType, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT ); if (pngColorType != PNG_COLOR_TYPE_GRAY) { png_color_8 sbitChunk; sbitChunk.red = 5; sbitChunk.green = 5; sbitChunk.blue = 5; if (pngColorType == PNG_COLOR_TYPE_RGB_ALPHA) { sbitChunk.alpha = 1; } png_set_sBIT(png, pngInfo, &sbitChunk); } if (pngColorType == PNG_COLOR_TYPE_PALETTE) { assume(palettes.size() == 1); png_color pngPalette[4] = {}; png_byte pngTrans[4] = {}; int nbPngColors = 0, nbPngTrans = 0; for (auto const &color : palettes[0]) { if (!color.has_value()) { continue; } pngPalette[nbPngColors].red = color->red; pngPalette[nbPngColors].green = color->green; pngPalette[nbPngColors].blue = color->blue; pngTrans[nbPngColors] = color->alpha; ++nbPngColors; if (color->alpha < 255) { nbPngTrans = nbPngColors; } } png_set_PLTE(png, pngInfo, pngPalette, nbPngColors); if (nbPngTrans > 0) { png_set_tRNS(png, pngInfo, pngTrans, nbPngTrans, nullptr); } } png_write_info(png, pngInfo); // N bits/pixel * 8 pixels/tile row / 8 bits/byte = N bytes/tile row uint8_t const bytesPerTileRow = pngColorType == PNG_COLOR_TYPE_RGB_ALPHA ? 32 : pngDepth; size_t const bytesPerRow = width * bytesPerTileRow; std::vector tileRow(8 * bytesPerRow, 0xFF); // Data for 8 rows of pixels uint8_t * const rowPtrs[8] = { &tileRow.data()[0 * bytesPerRow], &tileRow.data()[1 * bytesPerRow], &tileRow.data()[2 * bytesPerRow], &tileRow.data()[3 * bytesPerRow], &tileRow.data()[4 * bytesPerRow], &tileRow.data()[5 * bytesPerRow], &tileRow.data()[6 * bytesPerRow], &tileRow.data()[7 * bytesPerRow], }; for (size_t ty = 0; ty < height; ++ty) { for (size_t tx = 0; tx < width; ++tx) { size_t index = options.columnMajor ? ty + tx * height : ty * width + tx; // By default, a tile is unflipped, in bank 0, and uses palette #0 uint8_t attribute = attrmap ? (*attrmap)[index] : 0b0000; bool bank = attribute & 0b1000; // Get the tile ID at this location size_t tileOfs = tilemap ? static_cast((*tilemap)[index] - options.baseTileIDs[bank]) + (bank ? nbTilesInBank[0] : 0) : index; // This should have been enforced by the earlier checking. assume(tileOfs < nbTiles + options.trim); size_t palID = (palmap ? (*palmap)[index] : attribute & 0b111) - options.basePalID; assume(palID < palettes.size()); // Should be ensured on data read // We do not have data for tiles trimmed with `-x`, so assume they are "blank" static std::array const trimmedTile{0x00}; uint8_t const *tileData = tileOfs >= nbTiles ? trimmedTile.data() : &tiles[tileOfs * tileSize]; auto const &palette = palettes[palID]; for (uint8_t y = 0; y < 8; ++y) { // If vertically mirrored, fetch the bytes from the other end uint8_t realY = (attribute & 0x40 ? 7 - y : y) * options.bitDepth; uint8_t bitplane0 = tileData[realY]; uint8_t bitplane1 = tileData[realY + 1 % options.bitDepth]; if (attribute & 0x20) { // Handle horizontal flip bitplane0 = flipTable[bitplane0]; bitplane1 = flipTable[bitplane1]; } uint8_t *ptr = &rowPtrs[y][tx * bytesPerTileRow]; uint16_t gray = 0; for (uint8_t x = 0; x < 8; ++x) { uint8_t bit0 = bitplane0 & 0x80, bit1 = bitplane1 & 0x80; uint8_t colorID = bit0 >> 7 | bit1 >> 6; Rgba const &pixel = *palette[colorID]; if (pngColorType == PNG_COLOR_TYPE_GRAY) { gray = gray << pngDepth | (pixel.red & ((1 << pngDepth) - 1)); } else if (pngColorType == PNG_COLOR_TYPE_PALETTE) { *ptr++ = palID * 4 + colorID; } else { *ptr++ = pixel.red; *ptr++ = pixel.green; *ptr++ = pixel.blue; *ptr++ = pixel.alpha; } // Shift the pixel out bitplane0 <<= 1; bitplane1 <<= 1; } if (pngDepth == 1) { *ptr = gray; } else if (pngDepth == 2) { *ptr++ = gray >> 8; *ptr = gray & 0xff; } } } // We never modify the pointers, and neither should libpng, despite the overly lax function // signature. // (AIUI, casting away const-ness is okay as long as you don't actually modify the // pointed-to data) png_write_rows(png, const_cast(rowPtrs), 8); } // Finalize the write png_write_end(png, pngInfo); png_destroy_write_struct(&png, &pngInfo); } gbdev-rgbds-92bfe5d/src/gfx/rgba.cpp000066400000000000000000000053601512540461700174050ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "gfx/rgba.hpp" #include #include #include #include #include "helpers.hpp" // assume #include "gfx/main.hpp" // options // Based on inverting the "Modern - Accurate" formula used by SameBoy // since commit b5a611c5db46d6a0649d04d24d8d6339200f9ca1 (Dec 2020), // with gaps in the scale curve filled by polynomial interpolation. // clang-format off: vertically align columns of values static std::array reverse_curve{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, }; // clang-format on uint16_t Rgba::cgbColor() const { if (isTransparent()) { return transparent; } assume(isOpaque()); uint8_t r = red, g = green, b = blue; if (options.useColorCurve) { double g_linear = pow(g / 255.0, 2.2), b_linear = pow(b / 255.0, 2.2); double g_adjusted = std::clamp((g_linear * 4 - b_linear) / 3, 0.0, 1.0); g = round(pow(g_adjusted, 1 / 2.2) * 255); r = reverse_curve[r]; g = reverse_curve[g]; b = reverse_curve[b]; } else { r >>= 3; g >>= 3; b >>= 3; } return r | g << 5 | b << 10; } uint8_t Rgba::grayIndex() const { assume(isGray()); // 2bpp shades are inverted from RGB PNG; %00 = white, %11 = black uint8_t gray = 255 - red; if (options.palSpecType == Options::DMG) { assume(!options.hasTransparentPixels); // Reduce gray shade from 0..<256 to 0..<4, then map to color index, // then reduce to 0.. #include #include #include #include "diagnostics.hpp" #include "style.hpp" // clang-format off: nested initializers Diagnostics warnings = { .metaWarnings = { {"all", LEVEL_ALL }, {"everything", LEVEL_EVERYTHING}, }, .warningFlags = { {"embedded", LEVEL_EVERYTHING}, {"obsolete", LEVEL_DEFAULT }, {"trim-nonempty", LEVEL_ALL }, }, .paramWarnings = {}, .state = DiagnosticsState(), .nbErrors = 0, }; // clang-format on [[noreturn]] void giveUp() { style_Set(stderr, STYLE_RED, true); fprintf( stderr, "Conversion aborted after %" PRIu64 " error%s\n", warnings.nbErrors, warnings.nbErrors == 1 ? "" : "s" ); style_Reset(stderr); exit(1); } void requireZeroErrors() { if (warnings.nbErrors != 0) { giveUp(); } } void error(char const *fmt, ...) { va_list ap; style_Set(stderr, STYLE_RED, true); fputs("error: ", stderr); style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); putc('\n', stderr); warnings.incrementErrors(); } [[noreturn]] void fatal(char const *fmt, ...) { va_list ap; style_Set(stderr, STYLE_RED, true); fputs("FATAL: ", stderr); style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); putc('\n', stderr); warnings.incrementErrors(); giveUp(); } void warning(WarningID id, char const *fmt, ...) { char const *flag = warnings.warningFlags[id].name; va_list ap; switch (warnings.getWarningBehavior(id)) { case WarningBehavior::DISABLED: break; case WarningBehavior::ENABLED: style_Set(stderr, STYLE_YELLOW, true); fputs("warning: ", stderr); style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); style_Set(stderr, STYLE_YELLOW, true); fprintf(stderr, " [-W%s]\n", flag); style_Reset(stderr); break; case WarningBehavior::ERROR: style_Set(stderr, STYLE_RED, true); fputs("error: ", stderr); style_Reset(stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); style_Set(stderr, STYLE_RED, true); fprintf(stderr, " [-Werror=%s]\n", flag); style_Reset(stderr); warnings.incrementErrors(); break; } } gbdev-rgbds-92bfe5d/src/link/000077500000000000000000000000001512540461700161335ustar00rootroot00000000000000gbdev-rgbds-92bfe5d/src/link/.gitignore000066400000000000000000000000421512540461700201170ustar00rootroot00000000000000/script.cpp /script.hpp /stack.hh gbdev-rgbds-92bfe5d/src/link/assign.cpp000066400000000000000000000347531512540461700201370ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "link/assign.hpp" #include #include #include #include #include #include #include #include #include #include "helpers.hpp" #include "itertools.hpp" #include "linkdefs.hpp" #include "verbosity.hpp" #include "link/main.hpp" #include "link/output.hpp" #include "link/section.hpp" #include "link/symbol.hpp" #include "link/warning.hpp" struct MemoryLocation { uint16_t address; uint32_t bank; }; struct FreeSpace { uint16_t address; uint16_t size; }; // Table of free space for each bank static std::vector> memory[SECTTYPE_INVALID]; // Assigns a section to a given memory location static void assignSection(Section §ion, MemoryLocation const &location) { // Propagate the assigned location to all UNIONs/FRAGMENTs // so `jr` patches in them will have the correct offset for (Section &piece : section.pieces()) { piece.org = location.address; piece.bank = location.bank; } out_AddSection(section); } // Checks whether a given location is suitable for placing a given section // This checks not only that the location has enough room for the section, but // also that the constraints (alignment...) are respected. static bool isLocationSuitable( Section const §ion, FreeSpace const &freeSpace, MemoryLocation const &location ) { if (section.isAddressFixed && section.org != location.address) { return false; } if (section.isAlignFixed && ((location.address - section.alignOfs) & section.alignMask)) { return false; } if (location.address < freeSpace.address) { return false; } return location.address + section.size <= freeSpace.address + freeSpace.size; } static MemoryLocation getStartLocation(Section const §ion) { static uint16_t curScrambleROM = 0; static uint8_t curScrambleWRAM = 0; static int8_t curScrambleSRAM = 0; MemoryLocation location; // Determine which bank we should start searching in if (section.isBankFixed) { location.bank = section.bank; } else if (options.scrambleROMX && section.type == SECTTYPE_ROMX) { if (curScrambleROM < 1) { curScrambleROM = options.scrambleROMX; } location.bank = curScrambleROM--; } else if (options.scrambleWRAMX && section.type == SECTTYPE_WRAMX) { if (curScrambleWRAM < 1) { curScrambleWRAM = options.scrambleWRAMX; } location.bank = curScrambleWRAM--; } else if (options.scrambleSRAM && section.type == SECTTYPE_SRAM) { if (curScrambleSRAM < 0) { curScrambleSRAM = options.scrambleSRAM; } location.bank = curScrambleSRAM--; } else { location.bank = sectionTypeInfo[section.type].firstBank; } return location; } // Returns a suitable free space index into `memory[section->type]` at which to place the given // section, or `std::nullopt` if none was found. static std::optional getPlacement(Section const §ion, MemoryLocation &location) { SectionTypeInfo const &typeInfo = sectionTypeInfo[section.type]; for (;;) { if (location.bank < typeInfo.firstBank || location.bank >= memory[section.type].size() + typeInfo.firstBank) { fatal( "Invalid bank for %s section \"%s\": %" PRIu32, sectionTypeInfo[section.type].name.c_str(), section.name.c_str(), location.bank ); } // Switch to the beginning of the next bank std::deque &bankMem = memory[section.type][location.bank - typeInfo.firstBank]; size_t spaceIdx = 0; if (spaceIdx < bankMem.size()) { location.address = bankMem[spaceIdx].address; } // Process locations in that bank while (spaceIdx < bankMem.size()) { // If that location is OK, return it if (isLocationSuitable(section, bankMem[spaceIdx], location)) { return spaceIdx; } // Go to the next *possible* location if (section.isAddressFixed) { // If the address is fixed, there can be only one candidate block per bank; // if we already reached it, give up and try again in the next bank. if (location.address >= section.org) { break; } location.address = section.org; } else if (section.isAlignFixed) { // Move to next aligned location // Move back to alignment boundary location.address -= section.alignOfs; // Ensure we're there (e.g. on first check) location.address &= ~section.alignMask; // Go to next align boundary and add offset location.address += section.alignMask + 1 + section.alignOfs; } else if (++spaceIdx < bankMem.size()) { // Any location is fine, so, next free block location.address = bankMem[spaceIdx].address; } // If that location is past the current block's end, // go forwards until that is no longer the case. while (spaceIdx < bankMem.size() && location.address >= bankMem[spaceIdx].address + bankMem[spaceIdx].size) { ++spaceIdx; } // Try again with the new location/free space combo } // Try again in the next bank, if one is available. // Try scrambled banks in descending order until no bank in the scrambled range is // available. Otherwise, try in ascending order. if (section.isBankFixed) { return std::nullopt; } else if (options.scrambleROMX && section.type == SECTTYPE_ROMX && location.bank <= options.scrambleROMX) { if (location.bank > typeInfo.firstBank) { --location.bank; } else if (options.scrambleROMX < typeInfo.lastBank) { location.bank = options.scrambleROMX + 1; } else { return std::nullopt; } } else if (options.scrambleWRAMX && section.type == SECTTYPE_WRAMX && location.bank <= options.scrambleWRAMX) { if (location.bank > typeInfo.firstBank) { --location.bank; } else if (options.scrambleWRAMX < typeInfo.lastBank) { location.bank = options.scrambleWRAMX + 1; } else { return std::nullopt; } } else if (options.scrambleSRAM && section.type == SECTTYPE_SRAM && location.bank <= options.scrambleSRAM) { if (location.bank > typeInfo.firstBank) { --location.bank; } else if (options.scrambleSRAM < typeInfo.lastBank) { location.bank = options.scrambleSRAM + 1; } else { return std::nullopt; } } else if (location.bank < typeInfo.lastBank) { ++location.bank; } else { return std::nullopt; } // Try again in the next iteration. } } static std::string getSectionDescription(Section const §ion) { std::string description = "\"" + section.name + "\" (" + sectionTypeInfo[section.type].name + " section) "; if (section.isBankFixed && sectTypeBanks(section.type) != 1) { char bank[8]; snprintf(bank, sizeof(bank), "%02" PRIx32, section.bank); if (section.isAddressFixed) { char addr[8]; snprintf(addr, sizeof(addr), "%04" PRIx16, section.org); description = description + "at $" + bank + ":" + addr; } else if (section.isAlignFixed) { char mask[8]; snprintf(mask, sizeof(mask), "%" PRIx16, static_cast(~section.alignMask)); description = description + "in bank $" + bank + " with align mask $" + mask; } else { description = description + "in bank $" + bank; } } else { if (section.isAddressFixed) { char addr[8]; snprintf(addr, sizeof(addr), "%04" PRIx16, section.org); description = description + "at address $" + addr; } else if (section.isAlignFixed) { char mask[8], offset[8]; snprintf(mask, sizeof(mask), "%" PRIx16, static_cast(~section.alignMask)); snprintf(offset, sizeof(offset), "%" PRIx16, section.alignOfs); description = description + "with align mask $" + mask + " and offset $" + offset; } else { description = description + "anywhere"; } } return description; } // Places a section in a suitable location, or error out if it fails to. // Due to the implemented algorithm, this should be called with sections of decreasing size! static void placeSection(Section §ion) { // Specially handle 0-byte SECTIONs, as they can't overlap anything if (section.size == 0) { // Unless the SECTION's address was fixed, the starting address // is fine for any alignment, as checked in sect_DoSanityChecks. MemoryLocation location = { .address = section.isAddressFixed ? section.org : sectionTypeInfo[section.type].startAddr, .bank = section.isBankFixed ? section.bank : sectionTypeInfo[section.type].firstBank, }; assignSection(section, location); return; } // Place section using first-fit decreasing algorithm // https://en.wikipedia.org/wiki/Bin_packing_problem#First-fit_algorithm MemoryLocation location = getStartLocation(section); if (std::optional spaceIdx = getPlacement(section, location); spaceIdx) { std::deque &bankMem = memory[section.type][location.bank - sectionTypeInfo[section.type].firstBank]; FreeSpace &freeSpace = bankMem[*spaceIdx]; assignSection(section, location); // Update the free space uint16_t sectionEnd = section.org + section.size; bool noLeftSpace = freeSpace.address == section.org; bool noRightSpace = freeSpace.address + freeSpace.size == sectionEnd; if (noLeftSpace && noRightSpace) { // The free space is entirely deleted bankMem.erase(bankMem.begin() + *spaceIdx); } else if (!noLeftSpace && !noRightSpace) { // The free space is split in two // Append the new space after the original one uint16_t size = static_cast(freeSpace.address + freeSpace.size - sectionEnd); bankMem.insert(bankMem.begin() + *spaceIdx + 1, {.address = sectionEnd, .size = size}); // **`freeSpace` cannot be reused from this point on, because `bankMem.insert` // invalidates all references to itself!** // Resize the original space (address is unmodified) bankMem[*spaceIdx].size = section.org - bankMem[*spaceIdx].address; } else { // The amount of free spaces doesn't change: resize! freeSpace.size -= section.size; if (noLeftSpace) { // The free space is moved *and* resized freeSpace.address += section.size; } } return; } if (!section.isBankFixed || !section.isAddressFixed) { // If a section failed to go to several places, nothing we can report fatal("Unable to place %s", getSectionDescription(section).c_str()); } else if (section.org + section.size > sectTypeEndAddr(section.type) + 1) { // If the section just can't fit the bank, report that fatal( "Unable to place %s: section runs past end of region ($%04x > $%04x)", getSectionDescription(section).c_str(), section.org + section.size, sectTypeEndAddr(section.type) + 1 ); } else { // Otherwise there is overlap with another section fatal( "Unable to place %s: section overlaps with \"%s\"", getSectionDescription(section).c_str(), out_OverlappingSection(section)->name.c_str() ); } } static std::deque
unassignedSections[1 << 3]; // clang-format off: vertically align values static constexpr uint8_t BANK_CONSTRAINED = 1 << 2; static constexpr uint8_t ORG_CONSTRAINED = 1 << 1; static constexpr uint8_t ALIGN_CONSTRAINED = 1 << 0; // clang-format on static char const * const constraintNames[] = { "un", "align-", "org-", nullptr, // align+org (impossible) "bank-", "bank+align-", "bank+org-", nullptr, // bank+align+org (impossible) }; // Categorize a section depending on how constrained it is. // This is so the most-constrained sections are placed first. static void categorizeSection(Section §ion) { uint8_t constraints = 0; if (section.isBankFixed) { constraints |= BANK_CONSTRAINED; } // Can't have both! if (section.isAddressFixed) { constraints |= ORG_CONSTRAINED; } else if (section.isAlignFixed) { constraints |= ALIGN_CONSTRAINED; } std::deque
§ions = unassignedSections[constraints]; // Insert section while keeping the list sorted by decreasing size auto pos = sections.begin(); while (pos != sections.end() && (*pos)->size > section.size) { ++pos; } sections.insert(pos, §ion); } static void checkOverlayCompat() { auto isFixed = [](uint8_t constraints) { return (constraints & BANK_CONSTRAINED) && (constraints & ORG_CONSTRAINED); }; std::string unfixedList; size_t nbUnfixedSections = 0; for (uint8_t constraints = std::size(unassignedSections); constraints--;) { if (!isFixed(constraints)) { nbUnfixedSections += unassignedSections[constraints].size(); } } if (nbUnfixedSections == 0) { return; } size_t nbListed = 0; for (uint8_t constraints = std::size(unassignedSections); constraints--;) { if (isFixed(constraints)) { continue; } for (Section const *section : unassignedSections[constraints]) { if (nbListed == 10) { unfixedList += "\n- and "; unfixedList += std::to_string(nbUnfixedSections - nbListed); unfixedList += " more"; break; } unfixedList += "\n- \""; unfixedList += section->name; unfixedList += "\" ("; if (!(constraints & (BANK_CONSTRAINED | ORG_CONSTRAINED))) { unfixedList += "bank and address"; } else if (!(constraints & BANK_CONSTRAINED)) { unfixedList += "bank"; } else { assume(!(constraints & ORG_CONSTRAINED)); unfixedList += "address"; } unfixedList += " not specified)"; ++nbListed; } } fatal( "All sections must be fixed when using an overlay file; %zu %s not:%s", nbUnfixedSections, nbUnfixedSections == 1 ? "is" : "are", unfixedList.c_str() ); } void assign_AssignSections() { verbosePrint(VERB_NOTICE, "Beginning assignment...\n"); // Initialize the free space-modelling structs for (SectionType type : EnumSeq(SECTTYPE_INVALID)) { memory[type].resize(sectTypeBanks(type)); for (std::deque &bankMem : memory[type]) { bankMem.push_back({ .address = sectionTypeInfo[type].startAddr, .size = sectionTypeInfo[type].size, }); } } // Generate linked lists of sections to assign static uint64_t nbSectionsToAssign = 0; // `static` so `sect_ForEach` callback can see it sect_ForEach([](Section §ion) { categorizeSection(section); ++nbSectionsToAssign; }); // Overlaying requires only fully-constrained sections if (options.overlayFileName) { checkOverlayCompat(); } // Assign sections in decreasing constraint order for (uint8_t constraints = std::size(unassignedSections); constraints--;) { if (char const *constraintName = constraintNames[constraints]; constraintName) { verbosePrint(VERB_INFO, "Assigning %sconstrained sections...\n", constraintName); } else { assume(unassignedSections[constraints].empty()); } for (Section *section : unassignedSections[constraints]) { placeSection(*section); // If all sections were fully constrained, we have nothing left to do if (!--nbSectionsToAssign) { return; } } } assume(nbSectionsToAssign == 0); } gbdev-rgbds-92bfe5d/src/link/fstack.cpp000066400000000000000000000035461512540461700201220ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "link/fstack.hpp" #include #include #include #include #include #include "backtrace.hpp" #include "helpers.hpp" #include "linkdefs.hpp" #include "link/warning.hpp" void FileStackNode::printBacktrace(uint32_t curLineNo) const { using TraceItem = std::pair; std::vector items; for (TraceItem item{this, curLineNo};;) { auto &[node, itemLineNo] = item; bool loud = !node->isQuiet || tracing.loud; if (loud) { items.emplace_back(node, itemLineNo); } if (!node->parent) { assume(node->type != NODE_REPT && std::holds_alternative(node->data)); break; } if (loud || node->type != NODE_REPT) { // Quiet REPT nodes will pass their interior line number up to their parent, // which is more precise than the parent's own line number (since that will be // the line number of the "REPT?" or "FOR?" itself). itemLineNo = node->lineNo; } node = &*node->parent; } using TraceNode = std::pair; std::vector traceNodes; traceNodes.reserve(items.size()); for (auto &[node, itemLineNo] : reversed(items)) { if (std::holds_alternative>(node->data)) { assume(!traceNodes.empty()); // REPT nodes use their parent's name std::string reptName = traceNodes.back().first; if (std::vector const &nodeIters = node->iters(); !nodeIters.empty()) { reptName.append(NODE_SEPARATOR REPT_NODE_PREFIX); reptName.append(std::to_string(nodeIters.back())); } traceNodes.emplace_back(reptName, itemLineNo); } else { traceNodes.emplace_back(node->name(), itemLineNo); } } trace_PrintBacktrace( traceNodes, [](TraceNode const &node) { return node.first.c_str(); }, [](TraceNode const &node) { return node.second; } ); } gbdev-rgbds-92bfe5d/src/link/layout.cpp000066400000000000000000000232151512540461700201570ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "link/layout.hpp" #include #include #include #include #include #include "helpers.hpp" #include "linkdefs.hpp" #include "link/section.hpp" #include "link/warning.hpp" static std::array, SECTTYPE_INVALID> curAddr; static SectionType activeType = SECTTYPE_INVALID; // Index into curAddr static uint32_t activeBankIdx; // Index into curAddr[activeType] static bool isPcFloating; static uint16_t floatingAlignMask; static uint16_t floatingAlignOffset; static void setActiveTypeAndIdx(SectionType type, uint32_t idx) { activeType = type; activeBankIdx = idx; isPcFloating = false; if (curAddr[activeType].size() <= activeBankIdx) { curAddr[activeType].resize(activeBankIdx + 1, sectionTypeInfo[type].startAddr); } } void layout_SetFloatingSectionType(SectionType type) { if (sectTypeBanks(type) == 1) { // There is only a single bank anyway, so just set the index to 0. setActiveTypeAndIdx(type, 0); } else { activeType = type; activeBankIdx = UINT32_MAX; // Force PC to be floating for this kind of section. // Because we wouldn't know how to index into `curAddr[activeType]`! isPcFloating = true; floatingAlignMask = 0; floatingAlignOffset = 0; } } void layout_SetSectionType(SectionType type) { if (sectTypeBanks(type) != 1) { scriptError("A bank number must be specified for %s", sectionTypeInfo[type].name.c_str()); // Keep going with a default value for the bank index. } setActiveTypeAndIdx(type, 0); // There is only a single bank anyway, so just set the index to 0. } void layout_SetSectionType(SectionType type, uint32_t bank) { SectionTypeInfo const &typeInfo = sectionTypeInfo[type]; if (bank < typeInfo.firstBank) { scriptError( "%s bank %" PRIu32 " does not exist (the minimum is %" PRIu32 ")", typeInfo.name.c_str(), bank, typeInfo.firstBank ); bank = typeInfo.firstBank; } else if (bank > typeInfo.lastBank) { scriptError( "%s bank %" PRIu32 " does not exist (the maximum is %" PRIu32 ")", typeInfo.name.c_str(), bank, typeInfo.lastBank ); } setActiveTypeAndIdx(type, bank - typeInfo.firstBank); } void layout_SetAddr(uint32_t addr) { if (activeType == SECTTYPE_INVALID) { scriptError("Cannot set the current address: no memory region is active"); return; } if (activeBankIdx == UINT32_MAX) { scriptError("Cannot set the current address: the bank is floating"); return; } uint16_t &pc = curAddr[activeType][activeBankIdx]; SectionTypeInfo const &typeInfo = sectionTypeInfo[activeType]; if (addr < pc) { scriptError("Cannot decrease the current address (from $%04x to $%04x)", pc, addr); } else if (addr > sectTypeEndAddr(activeType)) { // Allow "one past the end" sections. scriptError( "Cannot set the current address to $%04" PRIx32 ": %s ends at $%04" PRIx16, addr, typeInfo.name.c_str(), sectTypeEndAddr(activeType) ); pc = sectTypeEndAddr(activeType); } else { pc = addr; } isPcFloating = false; } void layout_MakeAddrFloating() { if (activeType == SECTTYPE_INVALID) { scriptError("Cannot make the current address floating: no memory region is active"); return; } isPcFloating = true; floatingAlignMask = 0; floatingAlignOffset = 0; } void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) { if (activeType == SECTTYPE_INVALID) { scriptError("Cannot align: no memory region is active"); return; } if (isPcFloating) { if (alignment >= 16) { layout_SetAddr(floatingAlignOffset); } else { uint32_t alignSize = 1u << alignment; uint32_t alignMask = alignSize - 1; if (alignOfs >= alignSize) { scriptError( "Cannot align: The alignment offset (%" PRIu32 ") must be less than alignment size (%" PRIu32 ")", alignOfs, alignSize ); return; } floatingAlignMask = alignMask; floatingAlignOffset = alignOfs & alignMask; } return; } SectionTypeInfo const &typeInfo = sectionTypeInfo[activeType]; uint16_t &pc = curAddr[activeType][activeBankIdx]; if (alignment > 16) { scriptError("Cannot align: The alignment (%" PRIu32 ") must be less than 16", alignment); return; } // Let it wrap around, this'll trip the final check if alignment == 16. uint16_t length = alignOfs - pc; if (alignment < 16) { uint32_t alignSize = 1u << alignment; uint32_t alignMask = alignSize - 1; if (alignOfs >= alignSize) { scriptError( "Cannot align: The alignment offset (%" PRIu32 ") must be less than alignment size (%" PRIu32 ")", alignOfs, alignSize ); return; } assume(pc >= typeInfo.startAddr); length &= alignMask; } if (uint16_t offset = pc - typeInfo.startAddr; length > typeInfo.size - offset) { scriptError( "Cannot align: the next suitable address after $%04" PRIx16 " is $%04" PRIx16 ", past $%04" PRIx16, pc, static_cast(pc + length), static_cast(sectTypeEndAddr(activeType) + 1) ); return; } pc += length; } void layout_Pad(uint32_t length) { if (activeType == SECTTYPE_INVALID) { scriptError("Cannot increase the current address: no memory region is active"); return; } if (isPcFloating) { floatingAlignOffset = (floatingAlignOffset + length) & floatingAlignMask; return; } SectionTypeInfo const &typeInfo = sectionTypeInfo[activeType]; uint16_t &pc = curAddr[activeType][activeBankIdx]; assume(pc >= typeInfo.startAddr); if (uint16_t offset = pc - typeInfo.startAddr; length + offset > typeInfo.size) { scriptError( "Cannot increase the current address by %u bytes: only %u bytes to $%04" PRIx16, length, typeInfo.size - offset, static_cast(sectTypeEndAddr(activeType) + 1) ); } else { pc += length; } } void layout_PlaceSection(std::string const &name, bool isOptional) { if (activeType == SECTTYPE_INVALID) { scriptError("No memory region has been specified to place section \"%s\" in", name.c_str()); return; } Section *section = sect_GetSection(name.c_str()); if (!section) { if (!isOptional) { scriptError("Undefined section \"%s\"", name.c_str()); } return; } SectionTypeInfo const &typeInfo = sectionTypeInfo[activeType]; assume(section->offset == 0); // Check that the linker script doesn't contradict what the code says. if (section->type == SECTTYPE_INVALID) { // A section that has data must get assigned a type that requires data. if (!sectTypeHasData(activeType) && !section->data.empty()) { scriptError( "\"%s\" is specified to be a %s section, but it contains data", name.c_str(), typeInfo.name.c_str() ); } else if (sectTypeHasData(activeType) && section->data.empty() && section->size != 0) { // A section that lacks data can only be assigned to a type that requires data // if it's empty. scriptError( "\"%s\" is specified to be a %s section, but it does not contain data", name.c_str(), typeInfo.name.c_str() ); } else { // SDCC areas don't have a type assigned yet, so the linker script gives them one. for (Section &piece : section->pieces()) { piece.type = activeType; } } } else if (section->type != activeType) { scriptError( "\"%s\" is specified to be a %s section, but it is already a %s section", name.c_str(), typeInfo.name.c_str(), sectionTypeInfo[section->type].name.c_str() ); } if (activeBankIdx == UINT32_MAX) { section->isBankFixed = false; } else { uint32_t bank = activeBankIdx + typeInfo.firstBank; if (section->isBankFixed && bank != section->bank) { scriptError( "The linker script places section \"%s\" in %s bank %" PRIu32 ", but it was already defined in bank %" PRIu32, name.c_str(), sectionTypeInfo[section->type].name.c_str(), bank, section->bank ); } section->isBankFixed = true; section->bank = bank; } if (!isPcFloating) { uint16_t &org = curAddr[activeType][activeBankIdx]; if (section->isAddressFixed && org != section->org) { scriptError( "The linker script assigns section \"%s\" to address $%04" PRIx16 ", but it was already at $%04" PRIx16, name.c_str(), org, section->org ); } else if (section->isAlignFixed && (org & section->alignMask) != section->alignOfs) { uint8_t alignment = std::countr_one(section->alignMask); scriptError( "The linker script assigns section \"%s\" to address $%04" PRIx16 ", but that would be ALIGN[%" PRIu8 ", %" PRIu16 "] instead of the requested ALIGN[%" PRIu8 ", %" PRIu16 "]", name.c_str(), org, alignment, static_cast(org & section->alignMask), alignment, section->alignOfs ); } section->isAddressFixed = true; section->isAlignFixed = false; // This can't be set when the above is. section->org = org; uint16_t curOfs = org - typeInfo.startAddr; if (section->size > typeInfo.size - curOfs) { uint16_t overflowSize = section->size - (typeInfo.size - curOfs); scriptError( "The linker script assigns section \"%s\" to address $%04" PRIx16 ", but then it would overflow %s by %" PRIu16 " byte%s", name.c_str(), org, typeInfo.name.c_str(), overflowSize, overflowSize == 1 ? "" : "s" ); // Fill as much as possible without going out of bounds. org = typeInfo.startAddr + typeInfo.size; } else { org += section->size; } } else { section->isAddressFixed = false; section->isAlignFixed = floatingAlignMask != 0; section->alignMask = floatingAlignMask; section->alignOfs = floatingAlignOffset; floatingAlignOffset = (floatingAlignOffset + section->size) & floatingAlignMask; } } gbdev-rgbds-92bfe5d/src/link/lexer.cpp000066400000000000000000000211421512540461700177560ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "link/lexer.hpp" #include #include #include #include #include #include #include #include #include #include "backtrace.hpp" #include "linkdefs.hpp" #include "util.hpp" #include "link/warning.hpp" // Include this last so it gets all type & constant definitions #include "script.hpp" // For token definitions, generated from script.y struct LexerStackEntry { std::filebuf file; std::string path; uint32_t lineNo; explicit LexerStackEntry(std::string &&path_) : file(), path(path_), lineNo(1) {} }; static std::vector lexerStack; void lexer_TraceCurrent() { trace_PrintBacktrace( lexerStack, [](LexerStackEntry const &context) { return context.path.c_str(); }, [](LexerStackEntry const &context) { return context.lineNo; } ); } void lexer_IncludeFile(std::string &&path) { // `.emplace_back` can invalidate references to the stack's elements! // This is why `newContext` must be gotten before `prevContext`. LexerStackEntry &newContext = lexerStack.emplace_back(std::move(path)); LexerStackEntry &prevContext = lexerStack[lexerStack.size() - 2]; if (!newContext.file.open(newContext.path, std::ios_base::in)) { // `.pop_back()` will invalidate `newContext`, which is why `path` must be moved first. std::string badPath = std::move(newContext.path); lexerStack.pop_back(); // This error will occur in `prevContext`, *before* incrementing the line number! scriptError( "Failed to open included linker script \"%s\": %s", badPath.c_str(), strerror(errno) ); } // `.pop_back()` cannot invalidate an unpopped reference, so `prevContext` // is still valid even if `.open()` failed. ++prevContext.lineNo; } void lexer_IncLineNo() { ++lexerStack.back().lineNo; } yy::parser::symbol_type yylex(); // Forward declaration for `yywrap` static yy::parser::symbol_type yywrap() { static bool atEof = false; if (lexerStack.size() != 1) { if (!atEof) { // Inject a newline at EOF to simplify parsing. atEof = true; return yy::parser::make_newline(); } lexerStack.pop_back(); return yylex(); } if (!atEof) { // Inject a newline at EOF to simplify parsing. atEof = true; return yy::parser::make_newline(); } return yy::parser::make_YYEOF(); } static std::string readKeyword(int c) { LexerStackEntry &context = lexerStack.back(); std::string keyword; keyword.push_back(c); for (c = context.file.sgetc(); isAlphanumeric(c); c = context.file.snextc()) { keyword.push_back(c); } return keyword; } static yy::parser::symbol_type parseDecNumber(int c) { LexerStackEntry &context = lexerStack.back(); uint32_t number = c - '0'; for (c = context.file.sgetc(); isDigit(c) || c == '_'; c = context.file.sgetc()) { if (c != '_') { number = number * 10 + (c - '0'); } context.file.sbumpc(); } return yy::parser::make_number(number); } static yy::parser::symbol_type parseBinNumber(char const *prefix) { LexerStackEntry &context = lexerStack.back(); int c = context.file.sgetc(); if (!isBinDigit(c)) { scriptError("No binary digits found after %s", prefix); return yy::parser::make_number(0); } uint32_t number = c - '0'; context.file.sbumpc(); for (c = context.file.sgetc(); isBinDigit(c) || c == '_'; c = context.file.sgetc()) { if (c != '_') { number = number * 2 + (c - '0'); } context.file.sbumpc(); } return yy::parser::make_number(number); } static yy::parser::symbol_type parseOctNumber(char const *prefix) { LexerStackEntry &context = lexerStack.back(); int c = context.file.sgetc(); if (!isOctDigit(c)) { scriptError("No octal digits found after %s", prefix); return yy::parser::make_number(0); } uint32_t number = c - '0'; context.file.sbumpc(); for (c = context.file.sgetc(); isOctDigit(c) || c == '_'; c = context.file.sgetc()) { if (c != '_') { number = number * 8 + (c - '0'); } context.file.sbumpc(); } return yy::parser::make_number(number); } static yy::parser::symbol_type parseHexNumber(char const *prefix) { LexerStackEntry &context = lexerStack.back(); int c = context.file.sgetc(); if (!isHexDigit(c)) { scriptError("No hexadecimal digits found after %s", prefix); return yy::parser::make_number(0); } uint32_t number = parseHexDigit(c); context.file.sbumpc(); for (c = context.file.sgetc(); isHexDigit(c) || c == '_'; c = context.file.sgetc()) { if (c != '_') { number = number * 16 + parseHexDigit(c); } context.file.sbumpc(); } return yy::parser::make_number(number); } static yy::parser::symbol_type parseAnyNumber(int c) { LexerStackEntry &context = lexerStack.back(); if (c == '0') { switch (context.file.sgetc()) { case 'x': context.file.sbumpc(); return parseHexNumber("\"0x\""); case 'X': context.file.sbumpc(); return parseHexNumber("\"0X\""); case 'o': context.file.sbumpc(); return parseOctNumber("\"0o\""); case 'O': context.file.sbumpc(); return parseOctNumber("\"0O\""); case 'b': context.file.sbumpc(); return parseBinNumber("\"0b\""); case 'B': context.file.sbumpc(); return parseBinNumber("\"0B"); } } return parseDecNumber(c); } static yy::parser::symbol_type parseString() { LexerStackEntry &context = lexerStack.back(); int c = context.file.sgetc(); std::string str; for (; c != '"'; c = context.file.sgetc()) { if (c == EOF || isNewline(c)) { scriptError("Unterminated string"); break; } context.file.sbumpc(); if (c == '\\') { c = context.file.sgetc(); if (c == EOF || isNewline(c)) { scriptError("Unterminated string"); break; } else if (c == 'n') { c = '\n'; } else if (c == 'r') { c = '\r'; } else if (c == 't') { c = '\t'; } else if (c == '0') { c = '\0'; } else if (c != '\\' && c != '"' && c != '\'') { scriptError("Cannot escape character %s", printChar(c)); } context.file.sbumpc(); } str.push_back(c); } if (c == '"') { context.file.sbumpc(); } return yy::parser::make_string(std::move(str)); } yy::parser::symbol_type yylex() { LexerStackEntry &context = lexerStack.back(); int c = context.file.sbumpc(); // First, skip leading blank space. while (isBlankSpace(c)) { c = context.file.sbumpc(); } // Then, skip a comment if applicable. if (c == ';') { while (c != EOF && !isNewline(c)) { c = context.file.sbumpc(); } } // Alright, what token should we return? if (c == EOF) { return yywrap(); } else if (c == ',') { return yy::parser::make_COMMA(); } else if (isNewline(c)) { // Handle CRLF. if (c == '\r' && context.file.sgetc() == '\n') { context.file.sbumpc(); } return yy::parser::make_newline(); } else if (c == '"') { return parseString(); } else if (c == '$') { return parseHexNumber("'$'"); } else if (c == '%') { return parseBinNumber("'%'"); } else if (c == '&') { return parseOctNumber("'&'"); } else if (isDigit(c)) { return parseAnyNumber(c); } else if (isLetter(c)) { std::string keyword = readKeyword(c); static UpperMap const sectTypes{ {"WRAM0", SECTTYPE_WRAM0}, {"VRAM", SECTTYPE_VRAM }, {"ROMX", SECTTYPE_ROMX }, {"ROM0", SECTTYPE_ROM0 }, {"HRAM", SECTTYPE_HRAM }, {"WRAMX", SECTTYPE_WRAMX}, {"SRAM", SECTTYPE_SRAM }, {"OAM", SECTTYPE_OAM }, }; if (auto search = sectTypes.find(keyword); search != sectTypes.end()) { return yy::parser::make_sect_type(search->second); } static UpperMap const keywords{ {"ORG", yy::parser::make_ORG }, {"FLOATING", yy::parser::make_FLOATING}, {"INCLUDE", yy::parser::make_INCLUDE }, {"ALIGN", yy::parser::make_ALIGN }, {"DS", yy::parser::make_DS }, {"OPTIONAL", yy::parser::make_OPTIONAL}, }; if (auto search = keywords.find(keyword); search != keywords.end()) { return search->second(); } scriptError("Unknown keyword `%s`", keyword.c_str()); return yylex(); } else { scriptError("Unexpected character %s", printChar(c)); // Keep reading characters until the EOL, to avoid reporting too many errors. for (c = context.file.sgetc(); !isNewline(c); c = context.file.sgetc()) { if (c == EOF) { break; } context.file.sbumpc(); } return yylex(); } // Not marking as unreachable; this will generate a warning if any codepath forgets to return. } bool lexer_Init(std::string const &linkerScriptName) { if (LexerStackEntry &newContext = lexerStack.emplace_back(std::string(linkerScriptName)); !newContext.file.open(newContext.path, std::ios_base::in)) { error("Failed to open linker script \"%s\"", linkerScriptName.c_str()); lexerStack.clear(); return false; } return true; } gbdev-rgbds-92bfe5d/src/link/main.cpp000066400000000000000000000327051512540461700175720ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "link/main.hpp" #include #include #include #include #include #include #include #include #include #include "backtrace.hpp" #include "cli.hpp" #include "diagnostics.hpp" #include "linkdefs.hpp" #include "script.hpp" // Generated from script.y #include "style.hpp" #include "usage.hpp" #include "util.hpp" // UpperMap, printChar #include "verbosity.hpp" #include "link/assign.hpp" #include "link/lexer.hpp" #include "link/object.hpp" #include "link/output.hpp" #include "link/patch.hpp" #include "link/section.hpp" #include "link/warning.hpp" Options options; // Flags which must be processed after the option parsing finishes static struct LocalOptions { std::optional linkerScriptName; // -l std::vector inputFileNames; // ... } localOptions; // Short options static char const *optstring = "B:dhl:m:Mn:O:o:p:S:tVvW:wx"; // Long-only option variable static int longOpt; // `--color` // Equivalent long options // Please keep in the same order as short opts. // Also, make sure long opts don't create ambiguity: // A long opt's name should start with the same letter as its short opt, // except if it doesn't create any ambiguity (`verbose` versus `version`). // This is because long opt matching, even to a single char, is prioritized // over short opt matching. static option const longopts[] = { {"backtrace", required_argument, nullptr, 'B'}, {"dmg", no_argument, nullptr, 'd'}, {"help", no_argument, nullptr, 'h'}, {"linkerscript", required_argument, nullptr, 'l'}, {"map", required_argument, nullptr, 'm'}, {"no-sym-in-map", no_argument, nullptr, 'M'}, {"sym", required_argument, nullptr, 'n'}, {"overlay", required_argument, nullptr, 'O'}, {"output", required_argument, nullptr, 'o'}, {"pad", required_argument, nullptr, 'p'}, {"scramble", required_argument, nullptr, 'S'}, {"tiny", no_argument, nullptr, 't'}, {"version", no_argument, nullptr, 'V'}, {"verbose", no_argument, nullptr, 'v'}, {"warning", required_argument, nullptr, 'W'}, {"wramx", no_argument, nullptr, 'w'}, {"nopad", no_argument, nullptr, 'x'}, {"color", required_argument, &longOpt, 'c'}, {nullptr, no_argument, nullptr, 0 }, }; // clang-format off: nested initializers static Usage usage = { .name = "rgblink", .flags = { "[-dhMtVvwx]", "[-B depth]", "[-l script]", "[-m map_file]", "[-n sym_file]", "[-O overlay_file]", "[-o out_file]", "[-p pad_value]", "[-S spec]", " ...", }, .options = { {{"-l", "--linkerscript "}, {"set the input linker script"}}, {{"-m", "--map "}, {"set the output map file"}}, {{"-n", "--sym "}, {"set the output symbol list file"}}, {{"-o", "--output "}, {"set the output file"}}, {{"-p", "--pad "}, {"set the value to pad between sections with"}}, {{"-x", "--nopad"}, {"disable padding of output binary"}}, {{"-V", "--version"}, {"print RGBLINK version and exit"}}, {{"-W", "--warning "}, {"enable or disable warnings"}}, }, }; // clang-format on static size_t skipBlankSpace(char const *str) { return strspn(str, " \t"); } static void parseScrambleSpec(char *spec) { // clang-format off: vertically align nested initializers static UpperMap> scrambleSpecs{ {"ROMX", std::pair{&options.scrambleROMX, 65535}}, {"SRAM", std::pair{&options.scrambleSRAM, 255 }}, {"WRAMX", std::pair{&options.scrambleWRAMX, 7 }}, }; // clang-format on // Skip leading blank space before the regions. spec += skipBlankSpace(spec); // The argument to `-S` should be a comma-separated list of regions, allowing a trailing comma. // Each region name is optionally followed by an '=' and a region size. while (*spec) { char *regionName = spec; // The region name continues (skipping any blank space) until a ',' (next region), // '=' (region size), or the end of the string. size_t regionNameLen = strcspn(regionName, "=, \t"); // Skip trailing blank space after the region name. size_t regionNameSkipLen = regionNameLen + skipBlankSpace(regionName + regionNameLen); spec = regionName + regionNameSkipLen; if (*spec != '=' && *spec != ',' && *spec != '\0') { fatal("Unexpected character %s in spec for option '-S'", printChar(*spec)); } char *regionSize = nullptr; size_t regionSizeLen = 0; // The '=' region size limit is optional. if (*spec == '=') { regionSize = spec + 1; // Skip the '=' // Skip leading blank space before the region size. regionSize += skipBlankSpace(regionSize); // The region size continues (skipping any blank space) until a ',' (next region) // or the end of the string. regionSizeLen = strcspn(regionSize, ", \t"); // Skip trailing blank space after the region size. size_t regionSizeSkipLen = regionSizeLen + skipBlankSpace(regionSize + regionSizeLen); spec = regionSize + regionSizeSkipLen; if (*spec != ',' && *spec != '\0') { fatal("Unexpected character %s in spec for option '-S'", printChar(*spec)); } } // Skip trailing comma after the region. if (*spec == ',') { ++spec; } // Skip trailing blank space after the region. // `spec` will be the next region name, or the end of the string. spec += skipBlankSpace(spec); // Terminate the `regionName` and `regionSize` strings. regionName[regionNameLen] = '\0'; if (regionSize) { regionSize[regionSizeLen] = '\0'; } // Check for an empty region name or limit. // Note that by skipping leading blank space before the loop, and skipping a trailing comma // and blank space before the next iteration, we guarantee that the region name will not be // empty if it is present at all. if (*regionName == '\0') { fatal("Empty region name in spec for option '-S'"); } if (regionSize && *regionSize == '\0') { fatal("Empty region size limit in spec for option '-S'"); } // Determine which region type this is. auto search = scrambleSpecs.find(regionName); if (search == scrambleSpecs.end()) { fatal("Unknown region name \"%s\" in spec for option '-S'", regionName); } uint16_t limit = search->second.second; if (regionSize) { char const *ptr = regionSize + skipBlankSpace(regionSize); if (std::optional value = parseWholeNumber(ptr); !value) { fatal("Invalid region size limit \"%s\" for option '-S'", regionSize); } else if (*value > limit) { fatal( "%s region size for option '-S' must be between 0 and %" PRIu16, search->first.c_str(), limit ); } else { limit = *value; } } else if (search->second.first != &options.scrambleWRAMX) { // Only WRAMX limit can be implied, since ROMX and SRAM size may vary. fatal("Missing %s region size limit for option '-S'", search->first.c_str()); } if (*search->second.first != limit && *search->second.first != 0) { warnx("Overriding %s region size limit for option '-S'", search->first.c_str()); } // Update the scrambling region size limit. *search->second.first = limit; } } static void parseArg(int ch, char *arg) { switch (ch) { case 'B': if (!trace_ParseTraceDepth(arg)) { fatal("Invalid argument for option '-B'"); } break; case 'd': options.isDmgMode = true; options.isWRAM0Mode = true; break; // LCOV_EXCL_START case 'h': usage.printAndExit(0); // LCOV_EXCL_STOP case 'l': if (localOptions.linkerScriptName) { warnx("Overriding linker script file \"%s\"", localOptions.linkerScriptName->c_str()); } localOptions.linkerScriptName = arg; break; case 'M': options.noSymInMap = true; break; case 'm': if (options.mapFileName) { warnx("Overriding map file \"%s\"", options.mapFileName->c_str()); } options.mapFileName = arg; break; case 'n': if (options.symFileName) { warnx("Overriding sym file \"%s\"", options.symFileName->c_str()); } options.symFileName = arg; break; case 'O': if (options.overlayFileName) { warnx("Overriding overlay file \"%s\"", options.overlayFileName->c_str()); } options.overlayFileName = arg; break; case 'o': if (options.outputFileName) { warnx("Overriding output file \"%s\"", options.outputFileName->c_str()); } options.outputFileName = arg; break; case 'p': if (std::optional value = parseWholeNumber(arg); !value) { fatal("Invalid argument for option '-p'"); } else if (*value > 0xFF) { fatal("Argument for option '-p' must be between 0 and 0xFF"); } else { options.padValue = *value; options.hasPadValue = true; } break; case 'S': parseScrambleSpec(arg); break; case 't': options.is32kMode = true; break; // LCOV_EXCL_START case 'V': usage.printVersion(false); exit(0); case 'v': incrementVerbosity(); break; // LCOV_EXCL_STOP case 'W': warnings.processWarningFlag(arg); break; case 'w': options.isWRAM0Mode = true; break; case 'x': options.disablePadding = true; // implies tiny mode options.is32kMode = true; break; case 0: // Long-only options if (longOpt == 'c' && !style_Parse(arg)) { fatal("Invalid argument for option '--color'"); } break; case 1: // Positional argument localOptions.inputFileNames.push_back(arg); break; // LCOV_EXCL_START default: usage.printAndExit(1); // LCOV_EXCL_STOP } } // LCOV_EXCL_START static void verboseOutputConfig() { if (!checkVerbosity(VERB_CONFIG)) { return; } style_Set(stderr, STYLE_MAGENTA, false); usage.printVersion(true); printVVVVVVerbosity(); fputs("Options:\n", stderr); // -d/--dmg if (options.isDmgMode) { fputs("\tDMG mode prohibits non-DMG section types\n", stderr); } // -t/--tiny if (options.is32kMode) { fputs("\tROM0 covers the full 32 KiB of ROM\n", stderr); } // -w/--wramx if (options.isWRAM0Mode) { fputs("\tWRAM0 covers the full 8 KiB of WRAM\n", stderr); } // -x/--nopad if (options.disablePadding) { fputs("\tNo padding at the end of the ROM file\n", stderr); } // -p/--pad fprintf(stderr, "\tPad value: 0x%02" PRIx8 "\n", options.padValue); // -S/--scramble if (options.scrambleROMX || options.scrambleWRAMX || options.scrambleSRAM) { fputs("\tScramble: ", stderr); if (options.scrambleROMX) { fprintf(stderr, "ROMX = %" PRIu16, options.scrambleROMX); if (options.scrambleWRAMX || options.scrambleSRAM) { fputs(", ", stderr); } } if (options.scrambleWRAMX) { fprintf(stderr, "WRAMX = %" PRIu16, options.scrambleWRAMX); if (options.scrambleSRAM) { fputs(", ", stderr); } } if (options.scrambleSRAM) { fprintf(stderr, "SRAM = %" PRIu16, options.scrambleSRAM); } putc('\n', stderr); } // file ... if (!localOptions.inputFileNames.empty()) { fprintf(stderr, "\tInput object files: "); size_t nbFiles = localOptions.inputFileNames.size(); for (size_t i = 0; i < nbFiles; ++i) { if (i > 0) { fputs(", ", stderr); } if (i == 10) { fprintf(stderr, "and %zu more", nbFiles - i); break; } fputs(localOptions.inputFileNames[i].c_str(), stderr); } putc('\n', stderr); } auto printPath = [](char const *name, std::optional const &path) { if (path) { fprintf(stderr, "\t%s: %s\n", name, path->c_str()); } }; // -O/--overlay printPath("Overlay file", options.overlayFileName); // -l/--linkerscript printPath("Linker script", localOptions.linkerScriptName); // -o/--output printPath("Output ROM file", options.outputFileName); // -m/--map printPath("Output map file", options.mapFileName); // -M/--no-sym-in-map if (options.mapFileName && options.noSymInMap) { fputs("\tNo symbols in map file\n", stderr); } // -n/--sym printPath("Output sym file", options.symFileName); fputs("Ready for linking\n", stderr); style_Reset(stderr); } // LCOV_EXCL_STOP int main(int argc, char *argv[]) { cli_ParseArgs(argc, argv, optstring, longopts, parseArg, usage); verboseOutputConfig(); if (localOptions.inputFileNames.empty()) { usage.printAndExit("No input file specified (pass \"-\" to read from standard input)"); } // Patch the size array depending on command-line options if (!options.is32kMode) { sectionTypeInfo[SECTTYPE_ROM0].size = 0x4000; } if (!options.isWRAM0Mode) { sectionTypeInfo[SECTTYPE_WRAM0].size = 0x1000; } // Patch the bank ranges array depending on command-line options if (options.isDmgMode) { sectionTypeInfo[SECTTYPE_VRAM].lastBank = 0; } // Read all object files first, size_t nbFiles = localOptions.inputFileNames.size(); obj_Setup(nbFiles); for (size_t i = 0; i < nbFiles; ++i) { obj_ReadFile(localOptions.inputFileNames[i], nbFiles - i - 1); } // apply the linker script's modifications, if (localOptions.linkerScriptName) { verbosePrint(VERB_NOTICE, "Reading linker script...\n"); if (yy::parser parser; lexer_Init(*localOptions.linkerScriptName) && parser.parse() != 0) { // Exited due to YYABORT or YYNOMEM fatal("Unrecoverable error while reading linker script"); // LCOV_EXCL_LINE } // If the linker script produced any errors, some sections may be in an invalid state requireZeroErrors(); } // then process them, sect_DoSanityChecks(); requireZeroErrors(); assign_AssignSections(); patch_CheckAssertions(); // and finally output the result. patch_ApplyPatches(); requireZeroErrors(); out_WriteFiles(); return 0; } gbdev-rgbds-92bfe5d/src/link/object.cpp000066400000000000000000000426531512540461700201170ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include "link/object.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "helpers.hpp" #include "linkdefs.hpp" #include "platform.hpp" #include "verbosity.hpp" #include "version.hpp" #include "link/fstack.hpp" #include "link/patch.hpp" #include "link/sdas_obj.hpp" #include "link/section.hpp" #include "link/symbol.hpp" #include "link/warning.hpp" static std::deque> symbolLists; static std::vector> nodes; // Helper functions for reading object files // For internal use only by `tryReadLong` and `tryGetc`! #define tryRead(func, type, errval, vartype, var, file, ...) \ do { \ FILE *tmpFile = file; \ type tmpVal = func(tmpFile); \ if (tmpVal == (errval)) { \ fatal(__VA_ARGS__, feof(tmpFile) ? "Unexpected end of file" : strerror(errno)); \ } \ var = static_cast(tmpVal); \ } while (0) // Reads an unsigned long (32-bit) value from a file, or `INT64_MAX` on failure. static int64_t readLong(FILE *file) { uint32_t value = 0; // Read the little-endian value byte by byte for (uint8_t shift = 0; shift < sizeof(value) * CHAR_BIT; shift += 8) { int byte = getc(file); if (byte == EOF) { return INT64_MAX; } // This must be casted to `unsigned`, not `uint8_t`. Rationale: // the type of the shift is the type of `byte` after undergoing // integer promotion, which would be `int` if this was casted to // `uint8_t`, because int is large enough to hold a byte. This // however causes values larger than 127 to be too large when // shifted, potentially triggering undefined behavior. value |= static_cast(byte) << shift; } return value; } // Helper macro to read a long from a file to a var, or error out if it fails to. #define tryReadLong(var, file, ...) \ tryRead(readLong, int64_t, INT64_MAX, long, var, file, __VA_ARGS__) // Helper macro to read a byte from a file to a var, or error out if it fails to. #define tryGetc(var, file, ...) tryRead(getc, int, EOF, uint8_t, var, file, __VA_ARGS__) // Helper macro to read a '\0'-terminated string from a file, or error out if it fails to. #define tryReadString(var, file, ...) \ do { \ FILE *tmpFile = file; \ std::string &tmpVal = var; \ for (int tmpByte; (tmpByte = getc(tmpFile)) != '\0';) { \ if (tmpByte == EOF) { \ fatal(__VA_ARGS__, feof(tmpFile) ? "Unexpected end of file" : strerror(errno)); \ } else { \ tmpVal.push_back(tmpByte); \ } \ } \ } while (0) // Functions to parse object files // Reads a file stack node from a file. static void readFileStackNode( FILE *file, std::vector &fileNodes, uint32_t nodeID, char const *fileName ) { FileStackNode &node = fileNodes[nodeID]; uint32_t parentID; tryReadLong( parentID, file, "%s: Cannot read node #%" PRIu32 "'s parent ID: %s", fileName, nodeID ); if (parentID == UINT32_MAX) { node.parent = nullptr; } else if (parentID >= fileNodes.size()) { fatal("%s: Node #%" PRIu32 " has invalid parent ID #%" PRIu32, fileName, nodeID, parentID); } else { node.parent = &fileNodes[parentID]; } tryReadLong( node.lineNo, file, "%s: Cannot read node #%" PRIu32 "'s line number: %s", fileName, nodeID ); uint8_t type; tryGetc(type, file, "%s: Cannot read node #%" PRIu32 "'s type: %s", fileName, nodeID); switch (type & ~(1 << FSTACKNODE_QUIET_BIT)) { case NODE_FILE: case NODE_MACRO: node.type = FileStackNodeType(type); node.data = ""; tryReadString( node.name(), file, "%s: Cannot read node #%" PRIu32 "'s file name: %s", fileName, nodeID ); break; case NODE_REPT: { node.type = NODE_REPT; uint32_t depth; tryReadLong( depth, file, "%s: Cannot read node #%" PRIu32 "'s REPT depth: %s", fileName, nodeID ); node.data = std::vector(depth); for (uint32_t i = 0; i < depth; ++i) { tryReadLong( node.iters()[i], file, "%s: Cannot read node #%" PRIu32 "'s iter #%" PRIu32 ": %s", fileName, nodeID, i ); } if (!node.parent) { fatal( "%s: Invalid object file: root node (#%" PRIu32 ") may not be REPT", fileName, nodeID ); } break; } default: fatal("%s: Node #%" PRIu32 " has unknown type 0x%02x", fileName, nodeID, type); } node.isQuiet = (type & (1 << FSTACKNODE_QUIET_BIT)) != 0; } // Reads a symbol from a file. static void readSymbol( FILE *file, Symbol &symbol, char const *fileName, std::vector const &fileNodes ) { tryReadString(symbol.name, file, "%s: Cannot read symbol name: %s", fileName); uint8_t type; tryGetc(type, file, "%s: Cannot read `%s`'s type: %s", fileName, symbol.name.c_str()); if (type >= SYMTYPE_INVALID) { fatal("%s: `%s` has unknown type 0x%02x", fileName, symbol.name.c_str(), type); } else { symbol.type = ExportLevel(type); } // If the symbol is defined in this file, read its definition if (symbol.type != SYMTYPE_IMPORT) { uint32_t nodeID; tryReadLong( nodeID, file, "%s: Cannot read `%s`'s node ID: %s", fileName, symbol.name.c_str() ); if (nodeID >= fileNodes.size()) { fatal("%s: `%s` has invalid node ID #%" PRIu32, fileName, symbol.name.c_str(), nodeID); } symbol.src = &fileNodes[nodeID]; tryReadLong( symbol.lineNo, file, "%s: Cannot read `%s`'s line number: %s", fileName, symbol.name.c_str() ); int32_t sectionID, value; tryReadLong( sectionID, file, "%s: Cannot read `%s`'s section ID: %s", fileName, symbol.name.c_str() ); tryReadLong(value, file, "%s: Cannot read `%s`'s value: %s", fileName, symbol.name.c_str()); if (sectionID == -1) { symbol.data = value; } else { symbol.data = Label{ .sectionID = sectionID, .offset = value, // Set the `.section` later based on the `.sectionID` .section = nullptr, }; } } else { symbol.data = -1; } } // Reads a patch from a file. static void readPatch( FILE *file, Patch &patch, char const *fileName, std::string const §Name, uint32_t patchID, std::vector const &fileNodes ) { uint32_t nodeID; tryReadLong( nodeID, file, "%s: Cannot read \"%s\"'s patch #%" PRIu32 "'s node ID: %s", fileName, sectName.c_str(), patchID ); if (nodeID >= fileNodes.size()) { fatal( "%s: \"%s\"'s patch #%" PRIu32 " has invalid node ID #%" PRIu32, fileName, sectName.c_str(), patchID, nodeID ); } patch.src = &fileNodes[nodeID]; tryReadLong( patch.lineNo, file, "%s: Cannot read \"%s\"'s patch #%" PRIu32 "'s line number: %s", fileName, sectName.c_str(), patchID ); tryReadLong( patch.offset, file, "%s: Cannot read \"%s\"'s patch #%" PRIu32 "'s offset: %s", fileName, sectName.c_str(), patchID ); tryReadLong( patch.pcSectionID, file, "%s: Cannot read \"%s\"'s patch #%" PRIu32 "'s PC offset: %s", fileName, sectName.c_str(), patchID ); tryReadLong( patch.pcOffset, file, "%s: Cannot read \"%s\"'s patch #%" PRIu32 "'s PC offset: %s", fileName, sectName.c_str(), patchID ); uint8_t type; tryGetc( type, file, "%s: Cannot read \"%s\"'s patch #%" PRIu32 "'s type: %s", fileName, sectName.c_str(), patchID ); if (type >= PATCHTYPE_INVALID) { fatal( "%s: \"%s\"'s patch #%" PRIu32 " has unknown type 0x%02x", fileName, sectName.c_str(), patchID, type ); } else { patch.type = PatchType(type); } uint32_t rpnSize; tryReadLong( rpnSize, file, "%s: Cannot read \"%s\"'s patch #%" PRIu32 "'s RPN size: %s", fileName, sectName.c_str(), patchID ); patch.rpnExpression.resize(rpnSize); if (fread(patch.rpnExpression.data(), 1, rpnSize, file) != rpnSize) { fatal( "%s: Cannot read \"%s\"'s patch #%" PRIu32 "'s RPN expression: %s", fileName, sectName.c_str(), patchID, feof(file) ? "Unexpected end of file" : strerror(errno) ); } } // Reads a section from a file. static void readSection( FILE *file, Section §ion, char const *fileName, std::vector const &fileNodes ) { int32_t tmp; uint8_t byte; tryReadString(section.name, file, "%s: Cannot read section name: %s", fileName); uint32_t nodeID; tryReadLong( nodeID, file, "%s: Cannot read \"%s\"'s node ID: %s", fileName, section.name.c_str() ); if (nodeID >= fileNodes.size()) { fatal("%s: \"%s\" has invalid node ID #%" PRIu32, fileName, section.name.c_str(), nodeID); } section.src = &fileNodes[nodeID]; tryReadLong( section.lineNo, file, "%s: Cannot read \"%s\"'s line number: %s", fileName, section.name.c_str() ); tryReadLong(tmp, file, "%s: Cannot read \"%s\"'s' size: %s", fileName, section.name.c_str()); if (tmp < 0 || tmp > UINT16_MAX) { fatal( "%s: \"%s\"'s section size ($%" PRIx32 ") is invalid", fileName, section.name.c_str(), tmp ); } section.size = tmp; section.offset = 0; tryGetc(byte, file, "%s: Cannot read \"%s\"'s type: %s", fileName, section.name.c_str()); if (uint8_t type = byte & SECTTYPE_TYPE_MASK; type >= SECTTYPE_INVALID) { fatal("%s: \"%s\" has unknown section type 0x%02x", fileName, section.name.c_str(), type); } else { section.type = SectionType(type); } if (byte & (1 << SECTTYPE_UNION_BIT)) { section.modifier = SECTION_UNION; } else if (byte & (1 << SECTTYPE_FRAGMENT_BIT)) { section.modifier = SECTION_FRAGMENT; } else { section.modifier = SECTION_NORMAL; } tryReadLong(tmp, file, "%s: Cannot read \"%s\"'s org: %s", fileName, section.name.c_str()); section.isAddressFixed = tmp >= 0; if (tmp > UINT16_MAX) { error("\"%s\"'s org is too large ($%" PRIx32 ")", section.name.c_str(), tmp); tmp = UINT16_MAX; } section.org = tmp; tryReadLong(tmp, file, "%s: Cannot read \"%s\"'s bank: %s", fileName, section.name.c_str()); section.isBankFixed = tmp >= 0; section.bank = tmp; tryGetc(byte, file, "%s: Cannot read \"%s\"'s alignment: %s", fileName, section.name.c_str()); if (byte > 16) { byte = 16; } section.isAlignFixed = byte != 0; section.alignMask = (1 << byte) - 1; tryReadLong( tmp, file, "%s: Cannot read \"%s\"'s alignment offset: %s", fileName, section.name.c_str() ); if (tmp > UINT16_MAX) { error("\"%s\"'s alignment offset is too large ($%" PRIx32 ")", section.name.c_str(), tmp); tmp = UINT16_MAX; } section.alignOfs = tmp; if (sectTypeHasData(section.type)) { if (section.size) { section.data.resize(section.size); if (fread(section.data.data(), 1, section.size, file) != section.size) { fatal( "%s: Cannot read \"%s\"'s data: %s", fileName, section.name.c_str(), feof(file) ? "Unexpected end of file" : strerror(errno) ); } } uint32_t nbPatches; tryReadLong( nbPatches, file, "%s: Cannot read \"%s\"'s number of patches: %s", fileName, section.name.c_str() ); section.patches.resize(nbPatches); for (uint32_t i = 0; i < nbPatches; ++i) { readPatch(file, section.patches[i], fileName, section.name, i, fileNodes); } } } // Reads an assertion from a file. static void readAssertion( FILE *file, Assertion &assert, char const *fileName, uint32_t assertID, std::vector const &fileNodes ) { std::string assertName("Assertion #"); assertName += std::to_string(assertID); readPatch(file, assert.patch, fileName, assertName, 0, fileNodes); tryReadString(assert.message, file, "%s: Cannot read assertion's message: %s", fileName); } void obj_ReadFile(std::string const &filePath, size_t fileID) { FILE *file; char const *fileName = filePath.c_str(); if (filePath != "-") { file = fopen(fileName, "rb"); } else { fileName = ""; (void)setmode(STDIN_FILENO, O_BINARY); file = stdin; } if (!file) { fatal("Failed to open file \"%s\": %s", fileName, strerror(errno)); } Defer closeFile{[&] { fclose(file); }}; // First, check if the object is a RGBDS object, a SDCC one, or neither. // A single `ungetc` is guaranteed to work. switch (ungetc(getc(file), file)) { case EOF: fatal("File \"%s\" is empty", fileName); case 'X': case 'D': case 'Q': { // This is (probably) a SDCC object file, defer the rest of detection to it. // Since SDCC does not provide line info, everything will be reported as coming from the // object file. It's better than nothing. nodes[fileID].push_back({ .type = NODE_FILE, .data = std::variant, std::string>(fileName), .isQuiet = false, .parent = nullptr, .lineNo = 0, }); std::vector &fileSymbols = symbolLists.emplace_front(); sdobj_ReadFile(nodes[fileID].back(), file, fileSymbols); return; } case 'R': // Check the magic byte signature for a RGB object file. if (char magic[literal_strlen(RGBDS_OBJECT_VERSION_STRING)]; fread(magic, 1, sizeof(magic), file) == sizeof(magic) && !memcmp(magic, RGBDS_OBJECT_VERSION_STRING, sizeof(magic))) { break; } [[fallthrough]]; default: fatal("%s: Not a RGBDS object file", fileName); } verbosePrint(VERB_NOTICE, "Reading object file %s\n", fileName); uint32_t revNum; tryReadLong(revNum, file, "%s: Cannot read revision number: %s", fileName); if (revNum != RGBDS_OBJECT_REV) { fatal( "%s: Unsupported object file for rgblink %s; try rebuilding \"%s\"%s" " (expected revision %d, got %d)", fileName, get_package_version_string(), fileName, revNum > RGBDS_OBJECT_REV ? " or updating rgblink" : "", RGBDS_OBJECT_REV, revNum ); } uint32_t nbSymbols; tryReadLong(nbSymbols, file, "%s: Cannot read number of symbols: %s", fileName); uint32_t nbSections; tryReadLong(nbSections, file, "%s: Cannot read number of sections: %s", fileName); uint32_t nbNodes; tryReadLong(nbNodes, file, "%s: Cannot read number of nodes: %s", fileName); nodes[fileID].resize(nbNodes); verbosePrint(VERB_INFO, "Reading %u nodes...\n", nbNodes); for (uint32_t nodeID = nbNodes; nodeID--;) { readFileStackNode(file, nodes[fileID], nodeID, fileName); } // This file's symbols, kept to link sections to them std::vector &fileSymbols = symbolLists.emplace_front(nbSymbols); std::vector nbSymPerSect(nbSections, 0); verbosePrint(VERB_INFO, "Reading %" PRIu32 " symbols...\n", nbSymbols); for (Symbol &sym : fileSymbols) { readSymbol(file, sym, fileName, nodes[fileID]); sym_AddSymbol(sym); if (std::holds_alternative