mirror of
https://github.com/gbdev/rgbds.git
synced 2026-09-23 14:17:06 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7b8207322 | ||
|
|
33ef57ea49 | ||
|
|
6e5d7a9823 | ||
|
|
4d5b38ebfe | ||
|
|
eca8c1f3e7 | ||
|
|
bc1ed831ec | ||
|
|
4eba73d7b2 | ||
|
|
970342f564 | ||
|
|
c43b123183 | ||
|
|
306a83a4b4 | ||
|
|
6da3c3c38d | ||
|
|
629074bc28 | ||
|
|
63a06f02c3 | ||
|
|
c8b05a6a46 | ||
|
|
2882424407 | ||
|
|
04bbe80ae0 | ||
|
|
84c1671cb3 | ||
|
|
a2dd318f1d | ||
|
|
ace8db1b73 | ||
|
|
408e861ecc | ||
|
|
e1ed304e6b | ||
|
|
bb0fc08980 | ||
|
|
b7f6c641f1 | ||
|
|
b80bbe0fe6 | ||
|
|
c151450d9f | ||
|
|
76bba2809b | ||
|
|
44328d522b | ||
|
|
142eae4558 | ||
|
|
89403ca23b | ||
|
|
0286d4119e | ||
|
|
c803a20259 | ||
|
|
aab22852d3 | ||
|
|
0e41ace1e3 | ||
|
|
ce5c8ce8ac | ||
|
|
c49828f7c9 | ||
|
|
5b9903f358 | ||
|
|
d43ef53745 | ||
|
|
ef709fade6 | ||
|
|
8685bcbfbf | ||
|
|
1ca4282e31 | ||
|
|
afe1f01d00 | ||
|
|
8bf5957c83 | ||
|
|
f234796428 | ||
|
|
0b9f8ab523 | ||
|
|
3b250d5386 | ||
|
|
b4988afc8a | ||
|
|
6f69867d04 | ||
|
|
960f82126f | ||
|
|
b9b5c9a2b4 | ||
|
|
aa657cd481 | ||
|
|
e39dbe1752 | ||
|
|
9f175f7923 | ||
|
|
b2967ff509 | ||
|
|
e8d9eb4511 | ||
|
|
589616338f | ||
|
|
6587941512 | ||
|
|
004749ea75 | ||
|
|
f4c6221eb1 | ||
|
|
d81ab2fb23 | ||
|
|
febdf63896 | ||
|
|
b0f49a36b7 | ||
|
|
ed1ac7309b | ||
|
|
d082f24a2d | ||
|
|
7746ddbf2b | ||
|
|
0fe43bf0cb | ||
|
|
c8e8a2f522 | ||
|
|
de1e1ff688 | ||
|
|
f8a1da71c1 | ||
|
|
49b3f566b1 | ||
|
|
82f58b95ac | ||
|
|
e9fc7b3135 | ||
|
|
47bbd30f2c | ||
|
|
520cc2a80d | ||
|
|
a703b8c704 | ||
|
|
e9c800b3be | ||
|
|
8a7019e1f8 | ||
|
|
439a7a1828 | ||
|
|
438b943a1a | ||
|
|
5a2927778f | ||
|
|
9e785495ff | ||
|
|
9d69a08909 | ||
|
|
970bb84450 | ||
|
|
a662cad7ce | ||
|
|
e85375f3e1 | ||
|
|
429a2325fb | ||
|
|
9c5e872f9c | ||
|
|
25240e9fc0 | ||
|
|
3112239174 | ||
|
|
8a6b1946e3 | ||
|
|
631ef003e7 | ||
|
|
fdd6cece30 | ||
|
|
db9509fba0 | ||
|
|
7d0cb000e8 | ||
|
|
a0ab1802ff | ||
|
|
bf3ab7cb18 | ||
|
|
8988cdde46 | ||
|
|
b36028d1d1 | ||
|
|
760cc4d464 | ||
|
|
cebf57630a | ||
|
|
eb2b7c1842 | ||
|
|
a2c52caca2 | ||
|
|
21a4b85a4f | ||
|
|
9fa5058add | ||
|
|
e287ee2724 | ||
|
|
0888600cb7 | ||
|
|
8bc7de35f9 | ||
|
|
ed0a2d1075 | ||
|
|
71d9e236bc | ||
|
|
d9003f633a | ||
|
|
cda77721d4 |
@@ -10,10 +10,10 @@ if [[ "$tag" = v*-rc* ]]; then
|
||||
fi
|
||||
|
||||
files=(
|
||||
win64/rgbds-win64.zip
|
||||
win32/rgbds-win32.zip
|
||||
macos/rgbds-macos.zip
|
||||
linux/rgbds-linux-x86_64.tar.xz
|
||||
rgbds-win64.zip
|
||||
rgbds-win32.zip
|
||||
rgbds-macos.zip
|
||||
rgbds-linux-x86_64.tar.xz
|
||||
rgbds-source.tar.gz
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ on:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{github.ref}}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
|
||||
TERM: xterm-256color
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
name: Contrib script syntax checking
|
||||
on:
|
||||
- push
|
||||
- pull_request
|
||||
push:
|
||||
paths: [ 'contrib/**', .github/scripts/check-contrib.sh ]
|
||||
pull_request:
|
||||
paths: [ 'contrib/**', .github/scripts/check-contrib.sh ]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{github.event.pull_request.number || github.ref}}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
|
||||
@@ -15,6 +22,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: | # Avoid checking out other files, we don't use them here.
|
||||
contrib
|
||||
.github/scripts
|
||||
- name: Install zsh
|
||||
run: |
|
||||
sudo apt install -Uyq zsh
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
name: Diff completeness check
|
||||
on: pull_request
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{github.event.pull_request.number || github.ref}}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
|
||||
TERM: xterm-256color
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
name: Code format checking
|
||||
on: pull_request
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{github.event.pull_request.number || github.ref}}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
|
||||
TERM: xterm-256color
|
||||
|
||||
@@ -3,6 +3,10 @@ on:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{github.event.pull_request.number || github.ref}}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
|
||||
TERM: xterm-256color
|
||||
|
||||
@@ -4,6 +4,10 @@ on:
|
||||
tags:
|
||||
- v[0-9]*
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{github.ref}}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
|
||||
TERM: xterm-256color
|
||||
@@ -38,7 +42,9 @@ jobs:
|
||||
shell: cmd
|
||||
run: |
|
||||
call .github\scripts\msvc-env.bat ${{ matrix.bits }}
|
||||
cmake -B build -G Ninja -DFETCHCONTENT_BASE_DIR="${{ env.DEPS_ROOT_DIR }}" -DCMAKE_C_FLAGS="/nologo"
|
||||
cmake -B build -G Ninja -DFETCHCONTENT_BASE_DIR="${{ env.DEPS_ROOT_DIR }}" ^
|
||||
-DCMAKE_COMPILE_WARNING_AS_ERROR=YES -DCMAKE_LINK_WARNING_AS_ERROR=YES ^
|
||||
-DCMAKE_C_FLAGS="/nologo" -DCMAKE_INSTALL_BINDIR=. -DCPACK_COMPONENTS_ALL=binaries
|
||||
cmake --build build -- -k 0
|
||||
- name: Package binaries
|
||||
working-directory: build
|
||||
@@ -47,8 +53,8 @@ jobs:
|
||||
- name: Upload Windows binaries
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: win${{ matrix.bits }}
|
||||
path: build/rgbds-win${{ matrix.bits }}.zip
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
macos:
|
||||
@@ -61,7 +67,8 @@ jobs:
|
||||
./.github/scripts/install-deps.sh macos lld
|
||||
- name: Build binaries
|
||||
run: |
|
||||
cmake -B build -G Ninja --preset macos-static -DFETCHCONTENT_BASE_DIR="${{ env.DEPS_ROOT_DIR }}"
|
||||
cmake -B build -G Ninja --preset macos-static -DFETCHCONTENT_BASE_DIR="${{ env.DEPS_ROOT_DIR }}" \
|
||||
-DCMAKE_COMPILE_WARNING_AS_ERROR=YES -DCMAKE_LINK_WARNING_AS_ERROR=YES
|
||||
cmake --build build -- -k 0
|
||||
strip rgb{asm,link,fix,gfx}
|
||||
env:
|
||||
@@ -75,8 +82,8 @@ jobs:
|
||||
- name: Upload macOS binaries
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: macos
|
||||
path: rgbds-macos.zip
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
linux:
|
||||
@@ -89,7 +96,7 @@ jobs:
|
||||
./.github/scripts/install-deps.sh ubuntu-22.04
|
||||
- name: Build binaries
|
||||
run: |
|
||||
make -k -j "$(getconf _NPROCESSORS_ONLN)" WARNFLAGS="-Wall -Wextra -pedantic -static" PKG_CONFIG="pkg-config --static" Q=
|
||||
make -k -j "$(getconf _NPROCESSORS_ONLN)" WARNFLAGS="-Werror -Wall -Wextra -pedantic -static" PKG_CONFIG="pkg-config --static" Q=
|
||||
strip rgb{asm,link,fix,gfx}
|
||||
- name: Create install script
|
||||
run: |
|
||||
@@ -100,8 +107,8 @@ jobs:
|
||||
- name: Upload Linux binaries
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: linux
|
||||
path: rgbds-linux-x86_64.tar.xz
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
@@ -117,6 +124,9 @@ jobs:
|
||||
make dist Q=
|
||||
- name: Download all binary packages
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
skip-decompress: true # Do not decompress `.zip`s, we want them as-is.
|
||||
merge-multiple: true # Do not download non-`.zip`s into separate directories.
|
||||
- name: Draft the release
|
||||
run: |
|
||||
.github/scripts/draft-release.sh "${ref#refs/tags/}"
|
||||
|
||||
@@ -3,6 +3,10 @@ on:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{github.event.pull_request.number || github.ref}}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
|
||||
TERM: xterm-256color
|
||||
@@ -197,7 +201,7 @@ jobs:
|
||||
run: | # ASan seems to be broken on Windows, so we disable it.
|
||||
call .github\scripts\msvc-env.bat ${{ matrix.bits }}
|
||||
cmake -B build -G Ninja --preset develop -DFETCHCONTENT_BASE_DIR="${{ env.DEPS_ROOT_DIR }}" ^
|
||||
-DSANITIZERS=OFF -DCMAKE_C_FLAGS="/nologo"
|
||||
-DSANITIZERS=OFF -DCMAKE_C_FLAGS="/nologo" -DCMAKE_INSTALL_BINDIR=. -DCPACK_COMPONENTS_ALL=binaries
|
||||
cmake --build build -- -k 0
|
||||
- name: Install
|
||||
shell: cmd
|
||||
@@ -207,12 +211,12 @@ jobs:
|
||||
- name: Package binaries
|
||||
working-directory: build
|
||||
run: |
|
||||
cpack -DCPACK_PACKAGE_FILE_NAME=rgbds-win${{ matrix.bits }} -G ZIP --verbose
|
||||
cpack -DCPACK_PACKAGE_FILE_NAME=rgbds-canary-w${{ matrix.bits }}-${{ matrix.os }} -G ZIP --verbose
|
||||
- name: Upload Windows binaries
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: rgbds-canary-w${{ matrix.bits }}-${{ matrix.os }}
|
||||
path: build/rgbds-win${{ matrix.bits }}.zip
|
||||
path: build/rgbds-canary-w${{ matrix.bits }}-${{ matrix.os }}.zip
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
- name: Compute test dependency cache params
|
||||
id: test-deps-cache-params
|
||||
@@ -363,8 +367,8 @@ jobs:
|
||||
make
|
||||
ninja
|
||||
pkg-config
|
||||
python3
|
||||
python-imaging
|
||||
python312
|
||||
python312-imaging
|
||||
- name: Build using Make
|
||||
if: matrix.arch == 'x86'
|
||||
run: | # Cygwin doesn't support `make develop` sanitizers ASan or UBSan, so we don't use it.
|
||||
@@ -387,6 +391,7 @@ jobs:
|
||||
type rgbasm rgblink rgbfix rgbgfx
|
||||
man -w 1 rgbasm rgblink rgbfix rgbgfx
|
||||
- name: Compute test dependency cache params
|
||||
if: matrix.arch == 'x86_64'
|
||||
id: test-deps-cache-params
|
||||
run: |
|
||||
paths=$(test/external/fetch-repos.sh --get-paths)
|
||||
@@ -394,13 +399,14 @@ jobs:
|
||||
tee -a <<<"paths=\"${paths//,/\\n}\"" $GITHUB_OUTPUT
|
||||
tee -a <<<"hash=${hash%-}" $GITHUB_OUTPUT
|
||||
- name: Check test dependency repositories cache
|
||||
if: matrix.arch == 'x86_64'
|
||||
id: test-deps-cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ fromJSON(steps.test-deps-cache-params.outputs.paths) }}
|
||||
key: cygwin-${{ matrix.bits }}-${{ steps.test-deps-cache-params.outputs.hash }}
|
||||
- name: Fetch test dependency repositories
|
||||
if: steps.test-deps-cache.outputs.cache-hit != 'true'
|
||||
if: matrix.arch == 'x86_64' && steps.test-deps-cache.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
test/external/fetch-repos.sh
|
||||
|
||||
@@ -7,6 +7,10 @@ on:
|
||||
- man/*
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{github.ref}}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
|
||||
TERM: xterm-256color
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*.o
|
||||
*.exe
|
||||
*.dll
|
||||
*.pdb
|
||||
*.gcno
|
||||
*.gcda
|
||||
*.gcov
|
||||
|
||||
+39
-18
@@ -11,21 +11,21 @@ file(STRINGS "include/version.hpp" version_defines REGEX "^[ \t]*#define[ \t]+PA
|
||||
foreach(line IN LISTS version_defines)
|
||||
# We want the `CMAKE_MATCH_n` variables, so we just need to run *some* regex op.
|
||||
string(REGEX MATCH "PACKAGE_(VERSION_[^ \t]+)[ \t]+([0-9]+)" dummy "${line}")
|
||||
set("${CMAKE_MATCH_1}" "${CMAKE_MATCH_2}")
|
||||
set("RGBDS_${CMAKE_MATCH_1}" "${CMAKE_MATCH_2}")
|
||||
endforeach()
|
||||
|
||||
project(rgbds
|
||||
VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}"
|
||||
VERSION "${RGBDS_VERSION_MAJOR}.${RGBDS_VERSION_MINOR}.${RGBDS_VERSION_PATCH}"
|
||||
LANGUAGES CXX
|
||||
DESCRIPTION "Game Boy assembly toolchain"
|
||||
HOMEPAGE_URL "https://rgbds.gbdev.io")
|
||||
if(DEFINED VERSION_RC)
|
||||
string(APPEND CMAKE_PROJECT_VERSION "-rc${VERSION_RC}")
|
||||
string(APPEND PROJECT_VERSION "-rc${RGBDS_VERSION_RC}")
|
||||
endif()
|
||||
|
||||
# Reject in-source builds, as they may conflict with the Makefile.
|
||||
get_filename_component(srcdir "${CMAKE_SOURCE_DIR}" REALPATH)
|
||||
get_filename_component(bindir "${CMAKE_BINARY_DIR}" REALPATH)
|
||||
get_filename_component(srcdir "${CMAKE_CURRENT_SOURCE_DIR}" REALPATH)
|
||||
get_filename_component(bindir "${CMAKE_CURRENT_BINARY_DIR}" REALPATH)
|
||||
if(srcdir STREQUAL bindir)
|
||||
message(FATAL_ERROR "RGBDS should not be built in the source directory.
|
||||
Instead, create a separate build directory and specify to CMake the path to the source directory.")
|
||||
@@ -52,14 +52,22 @@ endif()
|
||||
if(SANITIZERS)
|
||||
if(MSVC)
|
||||
message(STATUS "ASan enabled")
|
||||
add_compile_options(/fsanitize=address) # Note that this shouldn't be passed to the linker.
|
||||
add_compile_options(/fsanitize=address /sdl) # Note that this shouldn't be passed to the linker.
|
||||
add_definitions(/_MSVC_STL_HARDENING=1 /_MSVC_STL_DESTRUCTOR_TOMBSTONES=1 /D_ITERATOR_DEBUG_LEVEL=2)
|
||||
else() # We assume a GNU-like compiler.
|
||||
message(STATUS "ASan and UBSan enabled")
|
||||
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)
|
||||
# Consider using `_GLIBCXX_DEBUG_BACKTRACE` (and `-lstdc++exp`) if unable to debug locally.
|
||||
add_definitions(-D_GLIBCXX_DEBUG -D_GLIBCXX_DEBUG_PEDANTIC -D_GLIBCXX_SANITIZE_VECTOR
|
||||
-D_GLIBCXX_VERBOSE_ASSERT
|
||||
# This enables debug-mode checks for `std::string`s, but stdlibc++ says
|
||||
# "this is unsupported and not guaranteed to work", so removing it may be fine.
|
||||
# https://gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode_semantics.html
|
||||
-D_GLIBCXX_EXTERN_TEMPLATE=0
|
||||
-D_LIBCPP_DEBUG -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG)
|
||||
# A non-zero optimization level is desired even in debug mode (especially for Clang),
|
||||
# and the two codegen flags improve the sanitizers' backtraces, but we want the user to
|
||||
# be able to override these easily so we put them first.
|
||||
@@ -82,9 +90,18 @@ endif()
|
||||
# Use versioning consistent with Makefile:
|
||||
# the git revision is used but uses the fallback in an archive.
|
||||
|
||||
if(DEFINED RGBDS_VERSION_STRING) # Possibly stored in the cache, e.g. if specified on the CLI.
|
||||
if(RGBDS_VERSION_STRING STREQUAL "")
|
||||
message(STATUS "Will generate version string from `version.hpp`")
|
||||
else()
|
||||
message(STATUS "Using provided version string - ${RGBDS_VERSION_STRING}")
|
||||
endif()
|
||||
else()
|
||||
message(CHECK_START "Determining RGBDS version from Git history")
|
||||
# Note that we do NOT store this in the cache, since the Git revision is fairly volatile.
|
||||
set(RGBDS_VERSION_STRING "") # An empty value causes `version.cpp` to generate a version string.
|
||||
|
||||
list(APPEND CMAKE_MESSAGE_INDENT " ")
|
||||
set(GIT_REV "") # This fallback is important!
|
||||
find_package(Git)
|
||||
list(POP_BACK CMAKE_MESSAGE_INDENT)
|
||||
if(NOT Git_FOUND)
|
||||
@@ -92,8 +109,8 @@ if(NOT Git_FOUND)
|
||||
else()
|
||||
execute_process(COMMAND "${GIT_EXECUTABLE}" --git-dir=.git -c safe.directory='*'
|
||||
describe --tags --dirty --always --match "v[0-9]*"
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
OUTPUT_VARIABLE GIT_REV OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
OUTPUT_VARIABLE RGBDS_VERSION_STRING OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_VARIABLE git_err ERROR_STRIP_TRAILING_WHITESPACE
|
||||
RESULT_VARIABLE result)
|
||||
if(NOT result EQUAL 0)
|
||||
@@ -103,14 +120,16 @@ else()
|
||||
message("${git_err}")
|
||||
list(POP_BACK CMAKE_MESSAGE_INDENT)
|
||||
else()
|
||||
message(CHECK_PASS "${GIT_REV}")
|
||||
if(NOT "${GIT_REV}" MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+(-rc[0-9]+)?")
|
||||
message(CHECK_PASS "${RGBDS_VERSION_STRING}")
|
||||
if(NOT "${RGBDS_VERSION_STRING}" MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+(-rc[0-9]+)?")
|
||||
# Can't find an ancestor tag! (That passes `--match`, anyway.)
|
||||
message(WARNING "No `v*` Git tag reachable; falling back")
|
||||
elseif(NOT CMAKE_MATCH_0 STREQUAL "v${CMAKE_PROJECT_VERSION}")
|
||||
elseif(NOT CMAKE_MATCH_0 STREQUAL "v${PROJECT_VERSION}")
|
||||
message(SEND_ERROR "\
|
||||
Version mismatch! Git says ${CMAKE_MATCH_0},
|
||||
version.hpp says v${CMAKE_PROJECT_VERSION}!")
|
||||
version.hpp says v${PROJECT_VERSION}
|
||||
Did you forget to consult `RELEASE.md`?")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
@@ -185,7 +204,7 @@ else()
|
||||
# We rely on runtimes more modern than that, so we can ignore those warnings.
|
||||
# If this is a problem for you: use Microsoft's equivalents, or define `__USE_MINGW_ANSI_STDIO=1`.
|
||||
# https://sourceforge.net/p/mingw-w64/wiki2/gnu%20printf/
|
||||
add_compile_options(-Wno-error=format -Wno-error=format-extra-args)
|
||||
add_compile_options(-Wno-format -Wno-format-extra-args)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -215,17 +234,19 @@ endforeach()
|
||||
# the rest is rather convention from our side, and thus more appropriate for presets or CLI flags.
|
||||
## CPACK_PACKAGE_NAME: copied from `project()`
|
||||
set(CPACK_PACKAGE_VENDOR "GBDev")
|
||||
set(CPACK_PACKAGE_VERSION "${CMAKE_PROJECT_VERSION}") # The individual components are defined implicitly.
|
||||
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") # The individual components are defined implicitly.
|
||||
set(CPACK_PACKAGE_DESCRIPTION "An assembly toolchain for the Nintendo Game Boy and Game Boy Color") # Same as our repo's description.
|
||||
## CPACK_PACKAGE_DESCRIPTION_SUMMARY: copied from `project()`
|
||||
set(CPACK_PACKAGE_HOMEPAGE_URL "https://rgbds.gbdev.io")
|
||||
## CPACK_PACKAGE_FILE_NAME: should be provided at runtime (`cpack -P`)
|
||||
set(CPACK_PACKAGE_CHECKSUM SHA256)
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
|
||||
set(CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/README.md")
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE")
|
||||
set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
|
||||
set(CPACK_STRIP_FILES ON) # Only applies to binary packages, not sources.
|
||||
set(CPACK_VERBATIM_VARIABLES ON)
|
||||
set(CPACK_THREADS 0) # Use all available CPU cores.
|
||||
set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF)
|
||||
set(CPACK_COMPONENTS_GROUPING ALL_COMPONENTS_IN_ONE)
|
||||
set(CPACK_ARCHIVE_COMPONENT_INSTALL ON)
|
||||
|
||||
include(CPack)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
FROM debian:13-slim
|
||||
LABEL org.opencontainers.image.source=https://github.com/gbdev/rgbds
|
||||
ARG version=1.0.3
|
||||
ARG version=1.0.4
|
||||
WORKDIR /rgbds
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -34,12 +34,12 @@ WARNFLAGS := -Wall -pedantic -Wno-unknown-warning-option \
|
||||
CXXFLAGS ?= -O3 -flto -DNDEBUG
|
||||
# Non-overridable CXXFLAGS
|
||||
REQUIREDCXXFLAGS := -std=c++20 -I include -fno-exceptions -fno-rtti
|
||||
REALCXXFLAGS := ${CXXFLAGS} ${WARNFLAGS} ${REQUIREDCXXFLAGS}
|
||||
REALCXXFLAGS := ${CXXFLAGS} ${REQUIREDCXXFLAGS}
|
||||
# Overridable LDFLAGS
|
||||
LDFLAGS ?=
|
||||
# Non-overridable LDFLAGS
|
||||
REQUIREDLDFLAGS := -DBUILD_VERSION_STRING=\"${VERSION_STRING}\"
|
||||
REALLDFLAGS := ${LDFLAGS} ${WARNFLAGS} ${REQUIREDLDFLAGS}
|
||||
REALLDFLAGS := ${LDFLAGS} ${REQUIREDLDFLAGS}
|
||||
|
||||
# Wrapper around bison that passes flags depending on what the version supports
|
||||
BISON := src/bison.sh
|
||||
@@ -130,22 +130,22 @@ rgbgfx_obj := \
|
||||
src/verbosity.o
|
||||
|
||||
rgbasm: ${rgbasm_obj}
|
||||
$Q${CXX} ${REALLDFLAGS} -o $@ ${rgbasm_obj} ${REALCXXFLAGS} src/version.cpp
|
||||
$Q${CXX} ${WARNFLAGS} ${REALLDFLAGS} -o $@ ${rgbasm_obj} ${REALCXXFLAGS} src/version.cpp
|
||||
|
||||
rgblink: ${rgblink_obj}
|
||||
$Q${CXX} ${REALLDFLAGS} -o $@ ${rgblink_obj} ${REALCXXFLAGS} src/version.cpp
|
||||
$Q${CXX} ${WARNFLAGS} ${REALLDFLAGS} -o $@ ${rgblink_obj} ${REALCXXFLAGS} src/version.cpp
|
||||
|
||||
rgbfix: ${rgbfix_obj}
|
||||
$Q${CXX} ${REALLDFLAGS} -o $@ ${rgbfix_obj} ${REALCXXFLAGS} src/version.cpp
|
||||
$Q${CXX} ${WARNFLAGS} ${REALLDFLAGS} -o $@ ${rgbfix_obj} ${REALCXXFLAGS} src/version.cpp
|
||||
|
||||
rgbgfx: ${rgbgfx_obj}
|
||||
$Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ ${rgbgfx_obj} ${REALCXXFLAGS} ${PNGLDLIBS} src/version.cpp
|
||||
$Q${CXX} ${WARNFLAGS} ${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}
|
||||
$Q${CXX} ${WARNFLAGS} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCXXFLAGS} ${PNGCFLAGS} ${PNGLDLIBS}
|
||||
|
||||
test/gfx/rgbgfx_test: test/gfx/rgbgfx_test.cpp
|
||||
$Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCXXFLAGS} ${PNGCFLAGS} ${PNGLDLIBS}
|
||||
$Q${CXX} ${WARNFLAGS} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCXXFLAGS} ${PNGCFLAGS} ${PNGLDLIBS}
|
||||
|
||||
# Rules to process files
|
||||
|
||||
@@ -163,29 +163,33 @@ src/link/script.hpp: src/link/script.cpp
|
||||
|
||||
# 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 $@ $<
|
||||
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
src/gfx/main.o: src/gfx/main.cpp
|
||||
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
src/gfx/pal_packing.o: src/gfx/pal_packing.cpp
|
||||
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
src/gfx/pal_sorting.o: src/gfx/pal_sorting.cpp
|
||||
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
src/gfx/pal_spec.o: src/gfx/pal_spec.cpp
|
||||
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
src/gfx/png.o: src/gfx/png.cpp
|
||||
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
src/gfx/process.o: src/gfx/process.cpp
|
||||
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
src/gfx/reverse.o: src/gfx/reverse.cpp
|
||||
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
src/gfx/rgba.o: src/gfx/rgba.cpp
|
||||
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
|
||||
.cpp.o:
|
||||
$Q${CXX} ${REALCXXFLAGS} -c -o $@ $<
|
||||
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} -c -o $@ $<
|
||||
|
||||
# Relying on the Makefile itself to introspect the installation commands.
|
||||
# First pretending to build the executables to exclude their build commands,
|
||||
# and then emitting the install commands without actually executing them.
|
||||
install.sh:
|
||||
$Qecho '#!/usr/bin/env bash' > $@
|
||||
$Q${MAKE} -t all
|
||||
$Q${MAKE} -s -n install MANSRC= >> $@
|
||||
$Qchmod +x $@
|
||||
|
||||
@@ -225,7 +229,9 @@ develop:
|
||||
-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 \
|
||||
-D_GLIBCXX_DEBUG -D_GLIBCXX_DEBUG_PEDANTIC -D_GLIBCXX_SANITIZE_VECTOR \
|
||||
-D_GLIBCXX_VERBOSE_ASSERT -D_GLIBCXX_EXTERN_TEMPLATE=0 \
|
||||
-D_LIBCPP_DEBUG -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"
|
||||
|
||||
|
||||
@@ -6,13 +6,16 @@
|
||||
# triggers some poorly-tested code paths within Apple's linker, which then crashes.
|
||||
# This can be worked around by using LLVM's LLD linker and passing `-fuse-ld=lld` when linking.
|
||||
|
||||
# The `-mmacosx-version-min=10.4` flag ensures that the binary only uses APIs available on Mac OS X 10.4 Tiger.
|
||||
# The `-arch` flags build a "fat binary" that works on both Apple architectures:
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.4 CACHE STRING "Minimum Mac OS X version to target for deployment (at runtime)")
|
||||
# This builds a "fat binary" that works on both Apple architectures:
|
||||
# older Intel x64 Macs and newer ARM "Apple Silicon" ones.
|
||||
set(secret_sauce -mmacosx-version-min=10.4 "SHELL:-arch x86_64" "SHELL:-arch arm64") # Avoid `-arch` being dedup'd.
|
||||
add_compile_options(${secret_sauce})
|
||||
add_link_options(${secret_sauce})
|
||||
set(PNG_HARDWARE_OPTIMIZATIONS OFF) # These do not play well with a dual-arch build.
|
||||
# Due to a libpng build script limitation/bug (as of 1.6.58), the native architecture has to be first...
|
||||
# and since our CI builds this executable on an ARM machine, that's what we're putting first.
|
||||
set(CMAKE_OSX_ARCHITECTURES arm64 x86_64 CACHE STRING "Build architectures for Mac OS X")
|
||||
# This controls the SIMD optimizations, which include architecture-specific headers that get rejected
|
||||
# in a dual-arch build, thus we have to disable them.
|
||||
# This shouldn't be a big deal for RGBGFX, anyway?
|
||||
set(PNG_HARDWARE_OPTIMIZATIONS OFF)
|
||||
|
||||
# Mac OS X has always provided zlib, so we can safely link dynamically against it.
|
||||
# However, libpng is *not* provided by default, so we link it statically, which requires downloading and building it from source.
|
||||
|
||||
@@ -22,4 +22,4 @@ for (( i = 0; i < NB_PALS; i++ )); do
|
||||
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"
|
||||
"${RGBGFX:-${RGBDS+$RGBDS/}rgbgfx}" -o "$TMP/tmp.2bpp" -OTQ -p "$1" -n "$NB_PALS" -r 4 -- "$2"
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@ GitHub.
|
||||
- [Dockerfile](/Dockerfile): update `ARG version`.
|
||||
- [test/external/\*.cfg](/test/external): update test dependency commits
|
||||
(preferably, use the latest available).
|
||||
- [man/\*](/man/): update dates and authors.
|
||||
- [man/\*](/man/): update dates and authors. You can use
|
||||
`sed -Ei '/^\.Dd/ s/ .*/ '"$(LC_ALL=C date +'%B %d, %Y')"/ man/*`.
|
||||
|
||||
2. Create a Git tag formatted as <code>v<i><MAJOR></i>.<i><MINOR></i>.<i><PATCH></i></code>,
|
||||
or <code>v<i><MAJOR></i>.<i><MINOR></i>.<i><PATCH></i>-rc<i><RC></i></code>
|
||||
|
||||
@@ -57,10 +57,12 @@ struct Expression {
|
||||
void addCheckBitIndex(uint8_t mask);
|
||||
|
||||
void checkNBit(uint8_t n) const;
|
||||
void checkSignedNBit(uint8_t n) const;
|
||||
|
||||
void encode(std::vector<uint8_t> &buffer) const;
|
||||
};
|
||||
|
||||
bool checkNBit(int32_t v, uint8_t n, char const *name);
|
||||
bool checkSignedNBit(int32_t v, uint8_t n, char const *name);
|
||||
|
||||
#endif // RGBDS_ASM_RPN_HPP
|
||||
|
||||
@@ -42,6 +42,8 @@ struct Section {
|
||||
std::deque<Patch> patches;
|
||||
std::vector<uint8_t> data;
|
||||
|
||||
SectionTypeInfo const &typeInfo() const { return sectionTypeInfo[type]; }
|
||||
|
||||
uint32_t getID() const; // ID of the section in the object file (`UINT32_MAX` if none)
|
||||
bool isSizeKnown() const;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,8 @@ struct Options {
|
||||
std::array<uint8_t, 2> baseTileIDs{0, 0}; // -b
|
||||
enum {
|
||||
NO_SPEC,
|
||||
EXPLICIT,
|
||||
INLINE,
|
||||
EXTERNAL,
|
||||
EMBEDDED,
|
||||
DMG,
|
||||
} palSpecType = NO_SPEC; // -c
|
||||
@@ -59,6 +60,8 @@ struct Options {
|
||||
|
||||
uint16_t maxNbColors() const { return nbColorsPerPal * nbPalettes; }
|
||||
|
||||
bool hasExplicitPalSpec() const { return palSpecType == INLINE || palSpecType == EXTERNAL; }
|
||||
|
||||
uint8_t dmgColors[4] = {};
|
||||
uint8_t dmgValue(uint8_t i) const {
|
||||
assume(i < 4);
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
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
|
||||
|
||||
@@ -14,6 +14,7 @@ struct Png {
|
||||
uint32_t height = 0;
|
||||
std::vector<Rgba> pixels{};
|
||||
std::vector<Rgba> palette{};
|
||||
bool isIndexed = false;
|
||||
|
||||
Png() {}
|
||||
Png(char const *filename, std::streambuf &file);
|
||||
|
||||
+10
-19
@@ -20,23 +20,6 @@ struct Rgba {
|
||||
explicit constexpr Rgba(uint32_t rgba = 0)
|
||||
: red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {}
|
||||
|
||||
// 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 (1 << 15) exactly.
|
||||
static constexpr uint16_t transparent = 0b1'00000'00000'00000;
|
||||
|
||||
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<uint8_t>(color & transparent ? 0x00 : 0xFF),
|
||||
};
|
||||
}
|
||||
|
||||
// Returns this RGBA as a 32-bit number that can be printed in hex (`#%08x`)
|
||||
// to yield its CSS representation (`#rrggbbaa`).
|
||||
uint32_t toCSS() const {
|
||||
@@ -45,14 +28,22 @@ struct Rgba {
|
||||
};
|
||||
return shl(red, 24) | shl(green, 16) | shl(blue, 8) | shl(alpha, 0);
|
||||
}
|
||||
|
||||
bool operator==(Rgba const &rhs) const { return toCSS() == rhs.toCSS(); }
|
||||
|
||||
// We allow some leeway to consider colors as transparent or opaque,
|
||||
// but intermediate alpha values are still ambiguous.
|
||||
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
|
||||
bool isAmbiguous() const { return isTransparent() == isOpaque(); }
|
||||
|
||||
// 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 (1 << 15) exactly.
|
||||
static constexpr uint16_t transparent = 0b1'00000'00000'00000;
|
||||
// Computes the equivalent RGB888 color; respects the color curve depending on argument
|
||||
static Rgba fromCGBColor(uint16_t color, bool useColorCurve);
|
||||
// Computes the equivalent RGB555 color; respects the color curve depending on options
|
||||
uint16_t cgbColor() const;
|
||||
|
||||
bool isGray() const { return red == green && green == blue; }
|
||||
|
||||
@@ -30,8 +30,12 @@ auto end(ReversedIterable<IterableT> r) {
|
||||
return std::rend(r._iterable);
|
||||
}
|
||||
|
||||
// Prevent creation of a dangling reference to a temporary rvalue
|
||||
template<typename IterableT>
|
||||
ReversedIterable<IterableT> reversed(IterableT &&_iterable) {
|
||||
ReversedIterable<IterableT> reversed(IterableT &&_iterable) = delete;
|
||||
|
||||
template<typename IterableT>
|
||||
ReversedIterable<IterableT> reversed(IterableT &_iterable) {
|
||||
return {_iterable};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ 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();
|
||||
void out_WriteROMIfAny();
|
||||
void out_WriteSymIfAny();
|
||||
void out_WriteMapIfAny();
|
||||
|
||||
#endif // RGBDS_LINK_OUTPUT_HPP
|
||||
|
||||
@@ -51,6 +51,8 @@ struct Section {
|
||||
std::vector<Symbol *> symbols;
|
||||
std::unique_ptr<Section> nextPiece; // The next fragment or union "piece" of this section
|
||||
|
||||
SectionTypeInfo const &typeInfo() const { return sectionTypeInfo[type]; }
|
||||
|
||||
private:
|
||||
// Template class for both const and non-const iterators over the "pieces" of this section
|
||||
template<QualifiedEquivalent<Section> SectionT>
|
||||
|
||||
+17
-18
@@ -4,7 +4,6 @@
|
||||
#define RGBDS_LINKDEFS_HPP
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
|
||||
#include "helpers.hpp" // assume
|
||||
|
||||
@@ -82,6 +81,13 @@ static constexpr uint8_t SECTTYPE_TYPE_MASK = 0b111;
|
||||
static constexpr uint8_t SECTTYPE_UNION_BIT = 7;
|
||||
static constexpr uint8_t SECTTYPE_FRAGMENT_BIT = 6;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
enum FileStackNodeType {
|
||||
NODE_REPT,
|
||||
NODE_FILE,
|
||||
@@ -90,31 +96,24 @@ enum FileStackNodeType {
|
||||
|
||||
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;
|
||||
// Non-`const` members may be patched in RGBLINK depending on CLI flags
|
||||
struct SectionTypeInfo {
|
||||
char 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;
|
||||
}
|
||||
uint16_t endAddr() const { return startAddr + 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;
|
||||
}
|
||||
uint32_t nbBanks() const { return lastBank - firstBank + 1; }
|
||||
|
||||
bool isBanked() const { return nbBanks() != 1; }
|
||||
};
|
||||
|
||||
extern SectionTypeInfo sectionTypeInfo[SECTTYPE_INVALID];
|
||||
|
||||
enum SectionModifier { SECTION_NORMAL, SECTION_UNION, SECTION_FRAGMENT };
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
int32_t op_add(int32_t augend, int32_t addend);
|
||||
int32_t op_sub(int32_t minuend, int32_t subtrahend);
|
||||
int32_t op_mul(int32_t multiplicand, int32_t multiplier);
|
||||
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);
|
||||
|
||||
@@ -66,4 +66,26 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#endif
|
||||
|
||||
// Apple has deprecated `sprintf` since Xcode 14 (for macOS 13), but we use it solely in
|
||||
// contexts where both the size of the buffer *and* max size of the printed string are
|
||||
// known statically, which GCC thus checks for.
|
||||
#ifdef __APPLE__
|
||||
#define sprintf_to_array(array, ...) \
|
||||
do { \
|
||||
static_assert( \
|
||||
std::is_array_v<decltype(array)>, "Only use this macro to print to an array!" \
|
||||
); \
|
||||
snprintf(array, sizeof(array), __VA_ARGS__); \
|
||||
} while (0)
|
||||
|
||||
#else
|
||||
#define sprintf_to_array(array, ...) \
|
||||
do { \
|
||||
static_assert( \
|
||||
std::is_array_v<decltype(array)>, "Only use this macro to print to an array!" \
|
||||
); \
|
||||
sprintf(array, __VA_ARGS__); \
|
||||
} while (0)
|
||||
#endif
|
||||
|
||||
#endif // RGBDS_PLATFORM_HPP
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
|
||||
#define PACKAGE_VERSION_MAJOR 1
|
||||
#define PACKAGE_VERSION_MINOR 0
|
||||
#define PACKAGE_VERSION_PATCH 3
|
||||
#define PACKAGE_VERSION_PATCH 4
|
||||
// #define PACKAGE_VERSION_RC 1
|
||||
|
||||
char const *get_package_version_string();
|
||||
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
.\" SPDX-License-Identifier: MIT
|
||||
.\"
|
||||
.Dd August 1, 2026
|
||||
.Dd September 22, 2026
|
||||
.Dt GBZ80 7
|
||||
.Os
|
||||
.Sh NAME
|
||||
@@ -1525,7 +1525,7 @@ Set according to result.
|
||||
.Ss RLC r8
|
||||
Rotate register
|
||||
.Ar r8
|
||||
left.
|
||||
Left Circularly.
|
||||
.Bd -literal
|
||||
┏━ Flags ━┓ ┏━━━━━━━ r8 ━━━━━━┓
|
||||
┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐
|
||||
@@ -1551,7 +1551,7 @@ Set according to result.
|
||||
.Ss RLC [HL]
|
||||
Rotate the byte pointed to by
|
||||
.Sy HL
|
||||
left.
|
||||
Left Circularly.
|
||||
.Bd -literal
|
||||
┏━ Flags ━┓ ┏━━━━━━ [HL] ━━━━━┓
|
||||
┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐
|
||||
@@ -1568,7 +1568,7 @@ Flags: See
|
||||
.Ss RLCA
|
||||
Rotate register
|
||||
.Sy A
|
||||
left.
|
||||
Left Circularly.
|
||||
.Bd -literal
|
||||
┏━ Flags ━┓ ┏━━━━━━━ A ━━━━━━━┓
|
||||
┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐
|
||||
@@ -1663,7 +1663,7 @@ Set according to result.
|
||||
.Ss RRC r8
|
||||
Rotate register
|
||||
.Ar r8
|
||||
right.
|
||||
Right Circularly.
|
||||
.Bd -literal
|
||||
┏━━━━━━━ r8 ━━━━━━┓ ┏━ Flags ━┓
|
||||
┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃
|
||||
@@ -1689,7 +1689,7 @@ Set according to result.
|
||||
.Ss RRC [HL]
|
||||
Rotate the byte pointed to by
|
||||
.Sy HL
|
||||
right.
|
||||
Right Circularly.
|
||||
.Bd -literal
|
||||
┏━━━━━━ [HL] ━━━━━┓ ┏━ Flags ━┓
|
||||
┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃
|
||||
@@ -1706,7 +1706,7 @@ Flags: See
|
||||
.Ss RRCA
|
||||
Rotate register
|
||||
.Sy A
|
||||
right.
|
||||
Right Circularly.
|
||||
.Bd -literal
|
||||
┏━━━━━━━ A ━━━━━━━┓ ┏━ Flags ━┓
|
||||
┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
.\"
|
||||
.\" SPDX-License-Identifier: MIT
|
||||
.\"
|
||||
.Dd August 1, 2026
|
||||
.Dd September 22, 2026
|
||||
.Dt RGBASM-OLD 5
|
||||
.Os
|
||||
.Sh NAME
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
.\" SPDX-License-Identifier: MIT
|
||||
.\"
|
||||
.Dd August 1, 2026
|
||||
.Dd September 22, 2026
|
||||
.Dt RGBASM 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
@@ -8,7 +8,8 @@
|
||||
.Nd Game Boy assembler
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Op Fl EhVvw
|
||||
.Op Fl EhVw
|
||||
.Op Fl v Op Fl v No ...
|
||||
.Op Fl B Ar param
|
||||
.Op Fl b Ar chars
|
||||
.Op Fl \-color Ar when
|
||||
|
||||
+16
-3
@@ -2,7 +2,7 @@
|
||||
.\"
|
||||
.\" SPDX-License-Identifier: MIT
|
||||
.\"
|
||||
.Dd August 1, 2026
|
||||
.Dd September 22, 2026
|
||||
.Dt RGBASM 5
|
||||
.Os
|
||||
.Sh NAME
|
||||
@@ -729,17 +729,21 @@ String constants are not expanded within the parentheses.
|
||||
.Ic IF ) ,
|
||||
or 0 if only RGBLINK can compute its value.
|
||||
.It Fn BANK arg Ta Returns a bank number.
|
||||
.Pp
|
||||
If
|
||||
.Ar arg
|
||||
is the symbol
|
||||
.Ic @ ,
|
||||
this function returns the bank of the current section.
|
||||
.Pp
|
||||
If
|
||||
.Ar arg
|
||||
is a string, it returns the bank of the section that has that name.
|
||||
.Pp
|
||||
If
|
||||
.Ar arg
|
||||
is a label, it returns the bank number the label is in.
|
||||
.Pp
|
||||
The result may be constant if
|
||||
.Nm
|
||||
is able to compute it.
|
||||
@@ -757,10 +761,17 @@ this function returns the name of the current section.
|
||||
.Ar arg
|
||||
is a string, this function returns the size of the section named
|
||||
.Ar arg .
|
||||
The result is a known constant if the section is already "completed" (i.e. not a
|
||||
.Ic UNION
|
||||
or
|
||||
.Ic FRAGMENT
|
||||
and no longer the current section).
|
||||
.Pp
|
||||
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.
|
||||
.Pp
|
||||
If
|
||||
.Ar arg
|
||||
is an 8-bit or 16-bit register, it returns the size of that register.
|
||||
@@ -768,6 +779,8 @@ is an 8-bit or 16-bit register, it returns the size of that register.
|
||||
.Ar arg
|
||||
is a string, this function returns the starting address of the section named
|
||||
.Ar arg .
|
||||
The result is a known constant if the section has a fixed address.
|
||||
.Pp
|
||||
If
|
||||
.Ar arg
|
||||
is a section type keyword, it returns the starting address of that section type.
|
||||
@@ -2052,8 +2065,8 @@ It's possible to pass arguments to macros as well!
|
||||
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)"
|
||||
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
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
.\" SPDX-License-Identifier: MIT
|
||||
.\"
|
||||
.Dd August 1, 2026
|
||||
.Dd September 22, 2026
|
||||
.Dt RGBDS 5
|
||||
.Os
|
||||
.Sh NAME
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
.\" SPDX-License-Identifier: MIT
|
||||
.\"
|
||||
.Dd August 1, 2026
|
||||
.Dd September 22, 2026
|
||||
.Dt RGBDS 7
|
||||
.Os
|
||||
.Sh NAME
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
.\" SPDX-License-Identifier: MIT
|
||||
.\"
|
||||
.Dd August 1, 2026
|
||||
.Dd September 22, 2026
|
||||
.Dt RGBFIX 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
|
||||
+14
-10
@@ -2,7 +2,7 @@
|
||||
.\"
|
||||
.\" SPDX-License-Identifier: MIT
|
||||
.\"
|
||||
.Dd August 1, 2026
|
||||
.Dd September 22, 2026
|
||||
.Dt RGBGFX 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
@@ -491,9 +491,17 @@ 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
|
||||
.Pp
|
||||
Also note that the tiles that don't get output in the tile data
|
||||
.Em do
|
||||
still count towards
|
||||
.Fl N Ap s
|
||||
limit.
|
||||
limit, since they may get output in the tilemap or attribute map.
|
||||
If you need to omit input tiles from all output in a way that does not count towards
|
||||
.Fl N ,
|
||||
use
|
||||
.Fl B
|
||||
background tiles.
|
||||
.It Fl Y , Fl \-mirror-y
|
||||
Deduplicate tiles that are vertically symmetrical mirror images of each other across the Y axis.
|
||||
Implies
|
||||
@@ -585,15 +593,11 @@ for example because you want to use palette swaps, please use
|
||||
.Fl c
|
||||
to specify the palette explicitly.
|
||||
.Pp
|
||||
First, if the image contains
|
||||
Note that if the image contains
|
||||
.Em any
|
||||
transparent pixel, color #0 of
|
||||
transparent pixels, 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.
|
||||
palettes will be transparent.
|
||||
.Pp
|
||||
After generating palettes,
|
||||
.Nm
|
||||
|
||||
+11
-3
@@ -1,6 +1,6 @@
|
||||
.\" SPDX-License-Identifier: MIT
|
||||
.\"
|
||||
.Dd August 1, 2026
|
||||
.Dd September 22, 2026
|
||||
.Dt RGBLINK 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
@@ -8,7 +8,8 @@
|
||||
.Nd Game Boy linker
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Op Fl dhMtVvwx
|
||||
.Op Fl dhMtVwx
|
||||
.Op Fl v Op Fl v No ...
|
||||
.Op Fl B Ar param
|
||||
.Op Fl \-color Ar when
|
||||
.Op Fl l Ar linker_script
|
||||
@@ -284,7 +285,7 @@ Certain regions allow omitting the size, in which case it defaults to its maximu
|
||||
.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 sram Ta 256 Ta \&No
|
||||
.Cm wramx Ta 7 Ta Yes
|
||||
.El
|
||||
.Pp
|
||||
@@ -307,6 +308,13 @@ Later region specs override earlier ones; for example,
|
||||
disables scrambling for
|
||||
.Cm romx .
|
||||
.Pp
|
||||
The arguments to multiple
|
||||
.Fl S
|
||||
options are processed as if concatenated; for example,
|
||||
.Fl S Cm romx=16 Fl S Cm wramx=8 Fl S Cm sram=4
|
||||
will work the same as
|
||||
.Fl S Cm romx=16,wramx=8,sram=4 .
|
||||
.Pp
|
||||
.Cm wramx
|
||||
scrambling is silently ignored if
|
||||
.Fl w
|
||||
|
||||
+1
-4
@@ -1,6 +1,6 @@
|
||||
.\" SPDX-License-Identifier: MIT
|
||||
.\"
|
||||
.Dd August 1, 2026
|
||||
.Dd September 22, 2026
|
||||
.Dt RGBLINK 5
|
||||
.Os
|
||||
.Sh NAME
|
||||
@@ -73,14 +73,11 @@ 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
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ add_library(common OBJECT
|
||||
"util.cpp"
|
||||
"version.cpp"
|
||||
)
|
||||
target_compile_definitions(common PRIVATE "BUILD_VERSION_STRING=\"${GIT_REV}\"")
|
||||
target_compile_definitions(common PRIVATE "BUILD_VERSION_STRING=\"${RGBDS_VERSION_STRING}\"")
|
||||
|
||||
find_package(BISON 3.0.0 REQUIRED)
|
||||
# BISON 4.0 deprecates passing this BISON_FLAGS string to `bison_target`'s `COMPILE_FLAGS`,
|
||||
|
||||
+1
-1
@@ -555,7 +555,7 @@ std::string act_StringFormat(
|
||||
std::string str;
|
||||
size_t argIndex = 0;
|
||||
|
||||
for (size_t i = 0; spec[i] != '\0';) {
|
||||
for (size_t i = 0; i < spec.length();) {
|
||||
if (int c = spec[i]; c != '%') {
|
||||
str += c;
|
||||
++i;
|
||||
|
||||
+14
-6
@@ -542,7 +542,7 @@ static uint32_t readBracketedMacroArgNum() {
|
||||
}
|
||||
}
|
||||
uint32_t n = readNumber<10>(bumpChar(), nullptr);
|
||||
if (n > INT32_MAX) {
|
||||
if (n > INT32_MAX && !(negative && n == static_cast<uint32_t>(INT32_MAX) + 1)) {
|
||||
error("Number in bracketed macro argument is too large");
|
||||
return 0;
|
||||
}
|
||||
@@ -1005,14 +1005,22 @@ static uint32_t finishReadingFixedPoint(uint32_t integer) {
|
||||
precision = options.fixPrecision;
|
||||
}
|
||||
|
||||
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<uint32_t>(round(static_cast<double>(dividend) / divisor * (1ULL << precision)));
|
||||
// Carry from `fractional` to `integer` if `round` rounded up to the next integer
|
||||
assume(fractional <= 1ULL << precision);
|
||||
bool overflowed = false;
|
||||
if (fractional == 1ULL << precision) {
|
||||
overflowed = integer == UINT32_MAX;
|
||||
++integer; // This may overflow from UINT32_MAX to 0
|
||||
fractional = 0;
|
||||
}
|
||||
|
||||
if (overflowed || integer >= 1ULL << (32 - precision)) {
|
||||
warning(WARNING_LARGE_CONSTANT, "Magnitude of fixed-point constant is too large");
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (integer << precision) | fractional;
|
||||
}
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ void MacroArgs::shiftArgs(int32_t count) {
|
||||
count > 0 && (static_cast<uint32_t>(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<uint32_t>(-count)) {
|
||||
} else if (count < 0 && (count == INT32_MIN || shift < static_cast<uint32_t>(-count))) {
|
||||
warning(WARNING_MACRO_SHIFT, "Cannot shift macro arguments past their beginning");
|
||||
shift = 0;
|
||||
} else {
|
||||
|
||||
@@ -162,6 +162,10 @@ void out_CreatePatch(uint32_t type, Expression const &expr, uint32_t ofs, uint32
|
||||
void out_CreateAssert(
|
||||
AssertionType type, Expression const &expr, std::string const &message, uint32_t ofs
|
||||
) {
|
||||
if (message.find('\0') != std::string::npos) {
|
||||
fatal("Assertion messages cannot contain '\\0' characters");
|
||||
}
|
||||
|
||||
Assertion &assertion = assertions.emplace_front();
|
||||
|
||||
initPatch(assertion.patch, type, expr, ofs);
|
||||
|
||||
+11
-3
@@ -340,6 +340,7 @@
|
||||
%type <Expression> reloc_3bit
|
||||
%type <Expression> reloc_8bit
|
||||
%type <Expression> reloc_16bit
|
||||
%type <Expression> reloc_8bit_signed
|
||||
|
||||
// Constant numbers
|
||||
%type <int32_t> iconst
|
||||
@@ -1241,6 +1242,13 @@ reloc_8bit:
|
||||
}
|
||||
;
|
||||
|
||||
reloc_8bit_signed:
|
||||
relocexpr {
|
||||
$$ = std::move($1);
|
||||
$$.checkSignedNBit(8);
|
||||
}
|
||||
;
|
||||
|
||||
reloc_16bit:
|
||||
relocexpr {
|
||||
$$ = std::move($1);
|
||||
@@ -1886,7 +1894,7 @@ sm83_add:
|
||||
| SM83_ADD MODE_HL COMMA reg_ss {
|
||||
sect_ConstByte(0x09 | ($4 << 4));
|
||||
}
|
||||
| SM83_ADD MODE_SP COMMA reloc_8bit {
|
||||
| SM83_ADD MODE_SP COMMA reloc_8bit_signed {
|
||||
sect_ConstByte(0xE8);
|
||||
sect_RelByte($4, 1);
|
||||
}
|
||||
@@ -2434,11 +2442,11 @@ op_a_n:
|
||||
op_sp_offset:
|
||||
OP_ADD relocexpr {
|
||||
$$ = std::move($2);
|
||||
$$.checkNBit(8);
|
||||
$$.checkSignedNBit(8);
|
||||
}
|
||||
| OP_SUB relocexpr {
|
||||
$$.makeUnaryOp(RPN_NEG, std::move($2));
|
||||
$$.checkNBit(8);
|
||||
$$.checkSignedNBit(8);
|
||||
}
|
||||
| %empty {
|
||||
::error("\"LD HL, SP\" is not a valid instruction; use \"LD HL, SP + 0\"");
|
||||
|
||||
+22
-4
@@ -297,7 +297,6 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
|
||||
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<uint32_t>(lval), urval = static_cast<uint32_t>(rval);
|
||||
|
||||
switch (op) {
|
||||
case RPN_LOGOR:
|
||||
@@ -325,10 +324,10 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
|
||||
data = lval != rval;
|
||||
break;
|
||||
case RPN_ADD:
|
||||
data = static_cast<int32_t>(ulval + urval);
|
||||
data = op_add(lval, rval);
|
||||
break;
|
||||
case RPN_SUB:
|
||||
data = static_cast<int32_t>(ulval - urval);
|
||||
data = op_sub(lval, rval);
|
||||
break;
|
||||
case RPN_XOR:
|
||||
data = lval ^ rval;
|
||||
@@ -370,7 +369,7 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
|
||||
data = op_shift_right_unsigned(lval, rval);
|
||||
break;
|
||||
case RPN_MUL:
|
||||
data = static_cast<int32_t>(ulval * urval);
|
||||
data = op_mul(lval, rval);
|
||||
break;
|
||||
case RPN_DIV:
|
||||
if (rval == 0) {
|
||||
@@ -512,6 +511,25 @@ bool checkNBit(int32_t v, uint8_t n, char const *name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Checks that an RPN expression's value fits within N bits (must be signed)
|
||||
void Expression::checkSignedNBit(uint8_t n) const {
|
||||
if (isKnown()) {
|
||||
::checkSignedNBit(value(), n, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
bool checkSignedNBit(int32_t v, uint8_t n, char const *name) {
|
||||
assume(n != 0); // That doesn't make sense
|
||||
assume(n < CHAR_BIT * sizeof(int) - 1); // Otherwise `1 << n` is UB
|
||||
|
||||
if (v < -(1 << (n - 1)) || v >= 1 << (n - 1)) {
|
||||
warning(WARNING_TRUNCATION_1, "%s must be signed %u-bit", name ? name : "Expression", n);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Expression::encode(std::vector<uint8_t> &buffer) const {
|
||||
assume(buffer.empty());
|
||||
|
||||
|
||||
+64
-47
@@ -100,7 +100,7 @@ void sect_ForEach(void (*callback)(Section &)) {
|
||||
|
||||
void sect_CheckSizes() {
|
||||
for (Section const § : sections) {
|
||||
if (uint32_t maxSize = sectionTypeInfo[sect.type].size; sect.size > maxSize) {
|
||||
if (uint32_t maxSize = sect.typeInfo().size; sect.size > maxSize) {
|
||||
error(
|
||||
"Section \"%s\" grew too big (max size = 0x%" PRIX32 " bytes, reached 0x%" PRIX32
|
||||
")",
|
||||
@@ -214,10 +214,8 @@ static void mergeFragments(Section §, uint32_t org, uint8_t alignment, uint1
|
||||
// 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) {
|
||||
// If both are fixed, they must be compatible
|
||||
if (uint16_t curOrg = org - sect.size; sect.org != UINT32_MAX && sect.org != curOrg) {
|
||||
sectError(
|
||||
"Section \"%s\" already declared as fixed at incompatible address $%04" PRIx32,
|
||||
sect.name.c_str(),
|
||||
@@ -231,6 +229,16 @@ static void mergeFragments(Section §, uint32_t org, uint8_t alignment, uint1
|
||||
sectAlignSize,
|
||||
sect.alignOfs
|
||||
);
|
||||
} else if (org < sect.size) {
|
||||
// Check that `curOrg` did not underflow. Note that it's safe for the above checks to
|
||||
// use an underflowed value, since their reported errors will still be accurate.
|
||||
sectError(
|
||||
"Section \"%s\" already contains %" PRIu32
|
||||
" bytes, higher than this fragment's fixed address $%04" PRIx32,
|
||||
sect.name.c_str(),
|
||||
sect.size,
|
||||
org
|
||||
);
|
||||
} else {
|
||||
// Otherwise, just override
|
||||
sect.org = curOrg;
|
||||
@@ -274,14 +282,6 @@ static void mergeSections(
|
||||
) {
|
||||
sectErrors.clear();
|
||||
|
||||
if (type != sect.type) {
|
||||
sectError(
|
||||
"Section \"%s\" already exists but with type `%s`",
|
||||
sect.name.c_str(),
|
||||
sectionTypeInfo[sect.type].name.c_str()
|
||||
);
|
||||
}
|
||||
|
||||
if (sect.modifier != mod) {
|
||||
sectError(
|
||||
"Section \"%s\" already declared as `SECTION %s`",
|
||||
@@ -290,8 +290,29 @@ static void mergeSections(
|
||||
);
|
||||
} else {
|
||||
switch (mod) {
|
||||
case SECTION_NORMAL:
|
||||
// Only union/fragment sections can end up with multiple errors queued in `sectErrors`,
|
||||
// and they cannot encounter this error, so it's okay for this one to skip the queue.
|
||||
// Queueing it in `sectErrors` would require a sentinel value anyway (e.g. an empty
|
||||
// string) to handle the "no trace" callback.
|
||||
assume(sectErrors.empty());
|
||||
fatalNoTrace([§]() {
|
||||
fprintf(stderr, "Section \"%s\" already defined\n", sect.name.c_str());
|
||||
fstk_TraceCurrent();
|
||||
fputs(" and also:\n", stderr);
|
||||
sect.src->printBacktrace(sect.fileLine);
|
||||
});
|
||||
|
||||
case SECTION_UNION:
|
||||
case SECTION_FRAGMENT: {
|
||||
if (type != sect.type) {
|
||||
sectError(
|
||||
"Section \"%s\" already exists but with type `%s`",
|
||||
sect.name.c_str(),
|
||||
sect.typeInfo().name
|
||||
);
|
||||
}
|
||||
|
||||
void (*merge)(Section &, uint32_t, uint8_t, uint16_t) =
|
||||
mod == SECTION_UNION ? mergeSectUnion : mergeFragments;
|
||||
merge(sect, org, alignment, alignOffset);
|
||||
@@ -310,20 +331,6 @@ static void mergeSections(
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SECTION_NORMAL:
|
||||
// Only union/fragment sections can end up with multiple errors queued in `sectErrors`,
|
||||
// and they cannot encounter this error, so it's okay for this one to skip the queue.
|
||||
// Queueing it in `sectErrors` would require a sentinel value anyway (e.g. an empty
|
||||
// string) to handle the "no trace" callback.
|
||||
assume(sectErrors.empty());
|
||||
fatalNoTrace([§]() {
|
||||
fprintf(stderr, "Section \"%s\" already defined\n", sect.name.c_str());
|
||||
fstk_TraceCurrent();
|
||||
fputs(" and also:\n", stderr);
|
||||
sect.src->printBacktrace(sect.fileLine);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,8 +377,8 @@ static Section *createSection(
|
||||
out_RegisterNode(sect.src);
|
||||
|
||||
// It is only needed to allocate memory for ROM sections.
|
||||
if (sectTypeHasData(type)) {
|
||||
sect.data.resize(sectionTypeInfo[type].size);
|
||||
if (sectTypeHasData(sect.type)) {
|
||||
sect.data.resize(sect.typeInfo().size);
|
||||
}
|
||||
|
||||
return §
|
||||
@@ -396,7 +403,7 @@ static Section *createSectionFragmentLiteral(Section const &parent) {
|
||||
|
||||
// Section fragment literals must be ROM sections.
|
||||
assume(sectTypeHasData(sect.type));
|
||||
sect.data.resize(sectionTypeInfo[sect.type].size);
|
||||
sect.data.resize(sect.typeInfo().size);
|
||||
|
||||
return §
|
||||
}
|
||||
@@ -417,24 +424,24 @@ static Section *getSection(
|
||||
uint32_t alignMask = alignSize - 1;
|
||||
|
||||
// First, validate parameters, and normalize them if applicable
|
||||
SectionTypeInfo const &typeInfo = sectionTypeInfo[type];
|
||||
|
||||
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) {
|
||||
} else if (bank < typeInfo.firstBank || bank > typeInfo.lastBank) {
|
||||
error(
|
||||
"%s bank value $%04" PRIx32 " out of range ($%04" PRIx32 " to $%04" PRIx32 ")",
|
||||
sectionTypeInfo[type].name.c_str(),
|
||||
typeInfo.name,
|
||||
bank,
|
||||
sectionTypeInfo[type].firstBank,
|
||||
sectionTypeInfo[type].lastBank
|
||||
typeInfo.firstBank,
|
||||
typeInfo.lastBank
|
||||
);
|
||||
}
|
||||
} else if (sectTypeBanks(type) == 1) {
|
||||
} else if (!typeInfo.isBanked()) {
|
||||
// If the section type only has a single bank, implicitly force it
|
||||
bank = sectionTypeInfo[type].firstBank;
|
||||
bank = typeInfo.firstBank;
|
||||
}
|
||||
|
||||
// This should be redundant, as the parser guarantees that `AlignmentSpec` will be valid.
|
||||
@@ -450,14 +457,14 @@ static Section *getSection(
|
||||
}
|
||||
|
||||
if (org != UINT32_MAX) {
|
||||
if (org < sectionTypeInfo[type].startAddr || org > sectTypeEndAddr(type)) {
|
||||
if (org < typeInfo.startAddr || org > typeInfo.endAddr()) {
|
||||
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)
|
||||
typeInfo.startAddr,
|
||||
typeInfo.endAddr()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -465,18 +472,15 @@ static Section *getSection(
|
||||
if (alignment != 0) {
|
||||
// It doesn't make sense to have both alignment and org set
|
||||
if (org != UINT32_MAX) {
|
||||
if ((org - alignOffset) & alignMask) {
|
||||
if ((org & alignMask) != alignOffset) {
|
||||
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) {
|
||||
} else if ((typeInfo.startAddr & alignMask) > alignOffset) {
|
||||
error(
|
||||
"Section \"%s\"'s alignment cannot be attained in %s",
|
||||
name.c_str(),
|
||||
sectionTypeInfo[type].name.c_str()
|
||||
"Section \"%s\"'s alignment cannot be attained in %s", name.c_str(), typeInfo.name
|
||||
);
|
||||
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;
|
||||
@@ -544,6 +548,10 @@ void sect_NewSection(
|
||||
SectionSpec const &attrs,
|
||||
SectionModifier mod
|
||||
) {
|
||||
if (name.find('\0') != std::string::npos) {
|
||||
fatal("Section names cannot contain '\\0' characters");
|
||||
}
|
||||
|
||||
for (SectionStackEntry &entry : sectionStack) {
|
||||
if (entry.section && entry.section->name == name) {
|
||||
fatal("Section \"%s\" is already on the stack", name.c_str());
|
||||
@@ -706,8 +714,17 @@ void sect_AlignPC(uint8_t alignment, uint16_t offset) {
|
||||
} 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.
|
||||
if (offset < curOffset) {
|
||||
error(
|
||||
"Section already contains %" PRIu32
|
||||
" bytes, higher than this aligned address $%04" PRIx32,
|
||||
curOffset,
|
||||
offset
|
||||
);
|
||||
} else {
|
||||
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`
|
||||
@@ -1049,7 +1066,7 @@ bool sect_BinaryFileSlice(std::string const &name, uint32_t startPos, uint32_t l
|
||||
*fileSize
|
||||
);
|
||||
return false;
|
||||
} else if (startPos + length > *fileSize) {
|
||||
} else if (length > *fileSize - startPos) {
|
||||
error(
|
||||
"Specified range in `INCBIN` file \"%s\" is out of bounds (%" PRIu32 " + %" PRIu32
|
||||
" > %" PRIu64 ")",
|
||||
|
||||
+2
-2
@@ -251,9 +251,9 @@ static void
|
||||
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");
|
||||
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) {
|
||||
if (fileSize > NB_BANKS_LIMIT * BANK_SIZE) {
|
||||
return errorTooLarge(); // LCOV_EXCL_LINE
|
||||
}
|
||||
// Compute number of banks and ROMX len from file size
|
||||
|
||||
+1
-1
@@ -240,7 +240,7 @@ static void parseArg(int ch, char *arg) {
|
||||
len = maxLen;
|
||||
warning(
|
||||
WARNING_TRUNCATION,
|
||||
"Truncating title \"%s\" to %u chars",
|
||||
"Truncating title \"%s\" to %" PRIu8 " chars",
|
||||
options.title->c_str(),
|
||||
maxLen
|
||||
);
|
||||
|
||||
+48
-31
@@ -36,7 +36,7 @@ Options options;
|
||||
|
||||
// Flags which must be processed after the option parsing finishes
|
||||
static struct LocalOptions {
|
||||
std::optional<std::string> externalPalSpec; // -c
|
||||
std::optional<std::string> palSpec; // -c
|
||||
bool autoAttrmap; // -A
|
||||
bool autoTilemap; // -T
|
||||
bool autoPalettes; // -P
|
||||
@@ -193,10 +193,10 @@ static void parseArg(int ch, char *arg) {
|
||||
break;
|
||||
|
||||
case 'c':
|
||||
localOptions.externalPalSpec = std::nullopt; // Allow overriding a previous pal spec
|
||||
localOptions.palSpec = std::nullopt; // Allow overriding a previous pal spec
|
||||
if (arg[0] == '#') {
|
||||
options.palSpecType = Options::EXPLICIT;
|
||||
parseInlinePalSpec(arg);
|
||||
options.palSpecType = Options::INLINE;
|
||||
localOptions.palSpec = arg;
|
||||
} else if (strcasecmp(arg, "embedded") == 0) {
|
||||
// Use PLTE, error out if missing
|
||||
options.palSpecType = Options::EMBEDDED;
|
||||
@@ -204,13 +204,13 @@ static void parseArg(int ch, char *arg) {
|
||||
options.palSpecType = Options::NO_SPEC;
|
||||
} else if (strcasecmp(arg, "dmg") == 0) {
|
||||
options.palSpecType = Options::DMG;
|
||||
parseDmgPalSpec(0xE4); // Same darkest-first order as `sortGrayscale`
|
||||
localOptions.palSpec = "e4"; // Same darkest-first order as `sortGrayscale`
|
||||
} else if (strncasecmp(arg, "dmg=", literal_strlen("dmg=")) == 0) {
|
||||
options.palSpecType = Options::DMG;
|
||||
parseDmgPalSpec(&arg[literal_strlen("dmg=")]);
|
||||
localOptions.palSpec = &arg[literal_strlen("dmg=")];
|
||||
} else {
|
||||
options.palSpecType = Options::EXPLICIT;
|
||||
localOptions.externalPalSpec = arg;
|
||||
options.palSpecType = Options::EXTERNAL;
|
||||
localOptions.palSpec = arg;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -299,6 +299,10 @@ static void parseArg(int ch, char *arg) {
|
||||
break;
|
||||
|
||||
case 'N':
|
||||
// Explicit numbers for either tile bank cannot be greater than 256.
|
||||
// If they were greater than 256, it would permit tile IDs to be truncated in the tilemap.
|
||||
// We do warn that tile IDs may be truncated for the implicit/default unlimited number of
|
||||
// tiles in bank 0.
|
||||
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");
|
||||
@@ -514,28 +518,17 @@ static void verboseOutputConfig() {
|
||||
// -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);
|
||||
case Options::NO_SPEC:
|
||||
fputs("\tAutomatic palette generation\n", stderr);
|
||||
break;
|
||||
case Options::INLINE:
|
||||
case Options::EXTERNAL:
|
||||
fputs("\tExplicit palette spec\n\t[\n", stderr);
|
||||
for (auto const &pal : options.palSpec) {
|
||||
fputs("\t\t", stderr);
|
||||
for (auto const &color : pal) {
|
||||
if (color) {
|
||||
if (color.has_value()) {
|
||||
fprintf(stderr, "#%06x, ", color->toCSS() >> 8);
|
||||
} else {
|
||||
fputs("#none, ", stderr);
|
||||
@@ -544,6 +537,13 @@ static void verboseOutputConfig() {
|
||||
putc('\n', stderr);
|
||||
}
|
||||
fputs("\t]\n", stderr);
|
||||
break;
|
||||
case Options::EMBEDDED:
|
||||
fputs("\tEmbedded palette spec from PNG indexed PLTE chunk\n", stderr);
|
||||
break;
|
||||
case Options::DMG:
|
||||
fprintf(stderr, "\tDMG palette spec $%02" PRIx8 "\n", options.palSpecDmg);
|
||||
break;
|
||||
}
|
||||
// -L/--slice
|
||||
if (options.inputSlice.width || options.inputSlice.height || options.inputSlice.left
|
||||
@@ -671,9 +671,27 @@ int main(int argc, char *argv[]) {
|
||||
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());
|
||||
// Execute deferred pal spec parsing, now that all other params are known.
|
||||
// Do not parse pal specs if `options.nbColorsPerPal` is invalid.
|
||||
if (options.nbColorsPerPal > 0 && options.nbColorsPerPal <= 4) {
|
||||
switch (options.palSpecType) {
|
||||
case Options::NO_SPEC:
|
||||
case Options::EMBEDDED:
|
||||
assume(!localOptions.palSpec);
|
||||
break;
|
||||
case Options::INLINE:
|
||||
assume(localOptions.palSpec);
|
||||
parseInlinePalSpec(localOptions.palSpec->c_str());
|
||||
break;
|
||||
case Options::EXTERNAL:
|
||||
assume(localOptions.palSpec);
|
||||
parseExternalPalSpec(localOptions.palSpec->c_str());
|
||||
break;
|
||||
case Options::DMG:
|
||||
assume(localOptions.palSpec);
|
||||
parseDmgPalSpec(localOptions.palSpec->c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
verboseDo(VERB_CONFIG, verboseOutputConfig);
|
||||
@@ -687,8 +705,7 @@ int main(int argc, char *argv[]) {
|
||||
} else {
|
||||
process();
|
||||
}
|
||||
} else if (!options.palettes.empty() && options.palSpecType == Options::EXPLICIT
|
||||
&& !localOptions.reverse) {
|
||||
} else if (!options.palettes.empty() && options.hasExplicitPalSpec() && !localOptions.reverse) {
|
||||
processPalettes();
|
||||
} else {
|
||||
usage.printAndExit("No input file specified (pass \"-\" to read from standard input)");
|
||||
|
||||
+14
-11
@@ -29,6 +29,7 @@
|
||||
#include "gfx/rgba.hpp"
|
||||
#include "gfx/warning.hpp"
|
||||
|
||||
using namespace std::literals;
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
static char const *hexDigits = "0123456789ABCDEFabcdef";
|
||||
@@ -131,8 +132,14 @@ void parseInlinePalSpec(char const * const rawArg) {
|
||||
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");
|
||||
if (nbColors == options.nbColorsPerPal) {
|
||||
// `parseError` cannot take variadic arguments, since `format_` and
|
||||
// `-Wformat-security` would complain about passing a template parameter pack
|
||||
// to the C-style variadic `error` function, so we format the error message
|
||||
// before passing it to `parseError`.
|
||||
std::string msg = "Each palette can only contain up to "s
|
||||
+ std::to_string(options.nbColorsPerPal) + " colors";
|
||||
parseError(n, 1, msg.c_str());
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -535,10 +542,10 @@ static void parseGBCFile(char const *filename, std::filebuf &file) {
|
||||
}
|
||||
|
||||
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])),
|
||||
Rgba::fromCGBColor(toWord(buf[0], buf[1]), false),
|
||||
Rgba::fromCGBColor(toWord(buf[2], buf[3]), false),
|
||||
Rgba::fromCGBColor(toWord(buf[4], buf[5]), false),
|
||||
Rgba::fromCGBColor(toWord(buf[6], buf[7]), false),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -664,11 +671,7 @@ void parseDmgPalSpec(char const * const rawArg) {
|
||||
return;
|
||||
}
|
||||
|
||||
parseDmgPalSpec(toHex(arg[0], arg[1]));
|
||||
}
|
||||
|
||||
void parseDmgPalSpec(uint8_t palSpecDmg) {
|
||||
options.palSpecDmg = palSpecDmg;
|
||||
options.palSpecDmg = toHex(arg[0], arg[1]);
|
||||
|
||||
// Map gray shades to their DMG color indexes for fast lookup by `Rgba::grayIndex`
|
||||
for (uint8_t i = 0; i < 4; ++i) {
|
||||
|
||||
@@ -177,6 +177,7 @@ Png::Png(char const *filename, std::streambuf &file) {
|
||||
break;
|
||||
case PNG_COLOR_TYPE_PALETTE:
|
||||
png_set_palette_to_rgb(png);
|
||||
isIndexed = true; // This enables sorting generated palette colors by the PLTE chunk
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
+138
-72
@@ -179,9 +179,14 @@ struct Image {
|
||||
std::unordered_set<std::pair<uint32_t, uint32_t>, 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()) {
|
||||
uint32_t const pxLeft = options.inputSlice.width ? options.inputSlice.left : 0;
|
||||
uint32_t const pxTop = options.inputSlice.height ? options.inputSlice.top : 0;
|
||||
uint32_t const pxRight = options.inputSlice.width ? options.inputSlice.right() : png.width;
|
||||
uint32_t const pxBottom =
|
||||
options.inputSlice.height ? options.inputSlice.bottom() : png.height;
|
||||
for (uint32_t y = pxTop; y < pxBottom; ++y) {
|
||||
for (uint32_t x = pxLeft; x < pxRight; ++x) {
|
||||
if (Rgba const &color = pixel(x, y); color.isAmbiguous()) {
|
||||
// Report ambiguously transparent or opaque colors
|
||||
if (uint32_t css = color.toCSS(); ambiguous.find(css) == ambiguous.end()) {
|
||||
error(
|
||||
@@ -213,6 +218,13 @@ struct Image {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (size_t nbAmbiguousColors = ambiguous.size(); nbAmbiguousColors > 0) {
|
||||
fatal(
|
||||
"Image contains %zu ambiguous color%s (neither transparent nor opaque)",
|
||||
nbAmbiguousColors,
|
||||
nbAmbiguousColors == 1 ? "" : "s"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TilesVisitor {
|
||||
@@ -365,8 +377,9 @@ static std::pair<std::vector<size_t>, std::vector<Palette>>
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
std::vector<Palette> palettes(nbPalettes);
|
||||
|
||||
// If the image contains at least one transparent pixel, force transparency in the first slot of
|
||||
// all palettes
|
||||
// all palettes.
|
||||
if (options.hasTransparentPixels) {
|
||||
for (Palette &pal : palettes) {
|
||||
pal.colors[0] = Rgba::transparent;
|
||||
@@ -384,7 +397,14 @@ static std::pair<std::vector<size_t>, std::vector<Palette>>
|
||||
// "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()) {
|
||||
} else if (image.png.isIndexed) {
|
||||
// A PNG image using PNG_COLOR_TYPE_RGB (2) or PNG_COLOR_TYPE_RGBA (6) can still
|
||||
// contain a PLTE chunk. From the PNG spec: "If present, it provides a suggested set of
|
||||
// from 1 to 256 colors to which the truecolor image can be quantized if the viewer
|
||||
// cannot display truecolor directly." We only sort palette colors by the PLTE chunk's
|
||||
// color order if the image uses PNG_COLOR_TYPE_PALETTE (3), since that guarantees every
|
||||
// color used will also be in the embedded palette.
|
||||
assume(!image.png.palette.empty());
|
||||
warning(
|
||||
WARNING_EMBEDDED,
|
||||
"Sorting palette colors by PNG's embedded PLTE chunk without '-c/--colors embedded'"
|
||||
@@ -400,14 +420,43 @@ static std::pair<std::vector<size_t>, std::vector<Palette>>
|
||||
|
||||
static std::pair<std::vector<size_t>, std::vector<Palette>>
|
||||
makePalsAsSpecified(std::vector<ColorSet> const &colorSets) {
|
||||
// Convert the palette spec to actual palettes
|
||||
std::vector<Palette> palettes(options.palSpec.size());
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the palette spec to actual palettes
|
||||
for (auto [spec, pal] : zip(options.palSpec, palettes)) {
|
||||
bool skipFirst = false;
|
||||
// If the image contains any transparent pixels, color #0 of all palettes is transparent.
|
||||
// Thus, all explicit palette specs should leave color #0 as "#none" or transparent.
|
||||
// If they specify an opaque color #0, we have legacy behavior of implicitly inserting
|
||||
// a transparent color #0, and expecting the spec to only cover the subsequent colors.
|
||||
if (options.hasTransparentPixels && spec.front().has_value() && spec.front()->isOpaque()) {
|
||||
skipFirst = true;
|
||||
}
|
||||
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();
|
||||
if (!spec[i].has_value() || !spec[i]->isOpaque()) {
|
||||
continue;
|
||||
}
|
||||
// If we're skipping color #0 as implicitly transparent, a full spec
|
||||
// plus the implicit transparent color will be too large for a palette.
|
||||
if (i + skipFirst >= options.nbColorsPerPal) {
|
||||
error(
|
||||
"Each palette spec can only contain up to %" PRIu8
|
||||
" color%s plus the implict transparent color",
|
||||
options.nbColorsPerPal - 1,
|
||||
options.nbColorsPerPal - 1 == 1 ? "" : "s"
|
||||
);
|
||||
giveUp();
|
||||
}
|
||||
pal[i + skipFirst] = spec[i]->cgbColor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,14 +507,14 @@ static void outputPalettes(std::vector<Palette> const &palettes) {
|
||||
});
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
if (palettes.size() > options.nbPalettes) {
|
||||
if (size_t nbPals = palettes.size(); nbPals > 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
|
||||
);
|
||||
fatal("Generated %zu palettes, over the maximum of %" PRIu16, nbPals, options.nbPalettes);
|
||||
} else if (nbPals > 8 && !options.attrmap.empty() && options.palmap.empty()) {
|
||||
// With `-n/--nb-palettes` greater than 8, palette IDs may be truncated in the attrmap
|
||||
// (though not in the palmap), so warn about that.
|
||||
warnx("Generated %zu palettes, of which only 8 are representable in the attrmap", nbPals);
|
||||
}
|
||||
|
||||
if (!options.palettes.empty()) {
|
||||
@@ -631,17 +680,15 @@ static void outputUnoptimizedTileData(
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
uint64_t widthTiles = options.inputSlice.width ? options.inputSlice.width : image.png.width / 8;
|
||||
uint64_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;
|
||||
size_t nbTiles = std::count_if(RANGE(attrmap), [](AttrmapEntry const &attr) {
|
||||
return !attr.isBackgroundTile();
|
||||
});
|
||||
size_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0;
|
||||
size_t tileIdx = 0;
|
||||
|
||||
for (auto const &[tile, attr] : zip(image.visitAsTiles(), attrmap)) {
|
||||
// Do not emit fully-background tiles.
|
||||
if (attr.isBackgroundTile()) {
|
||||
++tileIdx;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -691,38 +738,49 @@ static void outputUnoptimizedMaps(
|
||||
autoOpenPath(options.attrmap, attrmapOutput);
|
||||
autoOpenPath(options.palmap, palmapOutput);
|
||||
|
||||
auto const emit = [](std::optional<File> &output, uint8_t byte) {
|
||||
if (output.has_value()) {
|
||||
output.value()->sputc(byte);
|
||||
}
|
||||
};
|
||||
uint16_t tileIdx = 0;
|
||||
uint8_t bank = 0;
|
||||
for (AttrmapEntry const &attr : attrmap) {
|
||||
// The update-increment logic at the end of this loop may increment `bank` from 1 to 2,
|
||||
// if both banks 0 and 1 are full, but by then all the `attrmap` entries should have been
|
||||
// processed, since there cannot be more tiles than could fit in both banks.
|
||||
// A non-zero base ID may make this addition overflow, wrapping around the available
|
||||
// palette IDs. Since the operands are unsigned, this won't cause undefined behavior.
|
||||
// With `-n/--nb-palettes` greater than 8, palette IDs may be truncated in the attrmap
|
||||
// (though not in the palmap), which was already warned about.
|
||||
uint8_t palID = attr.getPalID(mappings) + options.basePalID;
|
||||
if (attr.isBackgroundTile()) {
|
||||
// The tile bank may be 2 here, which is fine since background tiles are emitted as
|
||||
// if they used the base tile ID and bank 0.
|
||||
assume(bank <= 2);
|
||||
|
||||
emit(tilemapOutput, options.baseTileIDs[0]);
|
||||
emit(attrmapOutput, palID & 0b111); // The other flags are all zeros.
|
||||
emit(palmapOutput, palID);
|
||||
// Since background tiles are not in tile data, they do not increment the tile index.
|
||||
} else {
|
||||
// The only valid tile banks are 0 and 1.
|
||||
assume(bank < 2);
|
||||
|
||||
// The unsigned overflow for `tileID` and `palID` is intentional, since
|
||||
// nonzero base IDs may overflow beyond 255 and continue with IDs from 0.
|
||||
if (tilemapOutput.has_value()) {
|
||||
uint8_t tileID = (attr.isBackgroundTile() ? 0 : tileIdx) + options.baseTileIDs[bank];
|
||||
(*tilemapOutput)->sputc(tileID);
|
||||
}
|
||||
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);
|
||||
}
|
||||
// A non-zero base ID may make this addition overflow, wrapping around the available
|
||||
// tile IDs. Since the operands are unsigned, this won't cause undefined behavior.
|
||||
// With `-N/--nb-tiles` unlimited (by default) for bank 0, tile IDs may be truncated in
|
||||
// the tilemap, which was already warned about.
|
||||
uint8_t tileID = tileIdx + options.baseTileIDs[bank];
|
||||
emit(tilemapOutput, tileID);
|
||||
emit(attrmapOutput, (palID & 0b111) | bank << 3); // The other flags are all zeros.
|
||||
emit(palmapOutput, palID);
|
||||
|
||||
// Background tiles were not emitted in the tile data, so their ID and bank do not update.
|
||||
if (attr.isBackgroundTile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tileIdx + 1 < options.maxNbTiles[bank]) {
|
||||
++tileIdx;
|
||||
} else {
|
||||
++bank;
|
||||
// The `bank` may increment from 1 to 2, if banks 0 and 1 are both full. By then all
|
||||
// the tiles should have been emitted, since there cannot be more tiles than could fit
|
||||
// in both banks, but there may still be background tiles to skip.
|
||||
if (tileIdx >= options.maxNbTiles[bank]) {
|
||||
tileIdx = 0;
|
||||
++bank;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -816,9 +874,9 @@ static UniqueTiles dedupTiles(
|
||||
attr.xFlip = false;
|
||||
attr.yFlip = false;
|
||||
attr.bank = 0;
|
||||
attr.tileID = 0;
|
||||
attr.tileID = options.baseTileIDs[attr.bank];
|
||||
} else {
|
||||
auto [tileID, matchType] = tiles.addTile({tile, palettes[attr.getPalID(mappings)]});
|
||||
auto [tileIdx, matchType] = tiles.addTile({tile, palettes[attr.getPalID(mappings)]});
|
||||
|
||||
if (inputWithoutOutput && matchType == TileData::NOPE) {
|
||||
error(
|
||||
@@ -831,8 +889,8 @@ static UniqueTiles dedupTiles(
|
||||
|
||||
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)
|
||||
attr.bank = tileIdx >= options.maxNbTiles[0];
|
||||
attr.tileID = (attr.bank ? tileIdx - options.maxNbTiles[0] : tileIdx)
|
||||
+ options.baseTileIDs[attr.bank];
|
||||
}
|
||||
}
|
||||
@@ -849,9 +907,9 @@ static void outputTileData(UniqueTiles const &tiles) {
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
uint64_t nbTiles = tiles.size();
|
||||
uint64_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0;
|
||||
uint64_t tileIdx = 0;
|
||||
size_t nbTiles = tiles.size();
|
||||
size_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0;
|
||||
size_t tileIdx = 0;
|
||||
|
||||
for (TileData const *tile : tiles) {
|
||||
assume(tile->tileID == tileIdx);
|
||||
@@ -889,6 +947,8 @@ static void outputTilemap(std::vector<AttrmapEntry> const &attrmap) {
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// With `-N/--nb-tiles` unlimited (by default) for bank 0, tile IDs may be truncated in the
|
||||
// tilemap, which was already warned about.
|
||||
for (AttrmapEntry const &entry : attrmap) {
|
||||
output->sputc(entry.tileID); // The tile ID has already been converted
|
||||
}
|
||||
@@ -908,6 +968,8 @@ static void
|
||||
attr |= entry.bank << 3;
|
||||
// The unsigned underflow for the palette ID is intentional, since a
|
||||
// nonzero base palette ID may overflow and continue with IDs from 0.
|
||||
// With `-n/--nb-palettes` greater than 8, palette IDs may be truncated in the attrmap
|
||||
// (though not in the palmap), which was already warned about.
|
||||
attr |= (entry.getPalID(mappings) + options.basePalID) & 0b111;
|
||||
output->sputc(attr);
|
||||
}
|
||||
@@ -990,12 +1052,14 @@ void process() {
|
||||
for (auto tile : image.visitAsTiles()) {
|
||||
AttrmapEntry &attrs = attrmap.emplace_back();
|
||||
|
||||
// Count the unique non-transparent colors for packing
|
||||
// Count the unique opaque colors for packing
|
||||
std::unordered_set<uint16_t> 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) {
|
||||
Rgba color = tile.pixel(x, y);
|
||||
// Ambiguous colors should not be in `tileColors`
|
||||
assume(color.isOpaque() != color.isTransparent());
|
||||
if (color.isOpaque() || !options.hasTransparentPixels) {
|
||||
tileColors.insert(color.cgbColor());
|
||||
}
|
||||
}
|
||||
@@ -1140,20 +1204,31 @@ continue_visiting_tiles:;
|
||||
: 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) {
|
||||
// Check the tile count
|
||||
if (size_t nbTiles = std::count_if(
|
||||
RANGE(attrmap), [](AttrmapEntry const &attr) { return !attr.isBackgroundTile(); }
|
||||
);
|
||||
nbTiles > options.maxNbTiles[0] + options.maxNbTiles[1]) {
|
||||
auto checkTileCountLimit = [](size_t nbTiles) {
|
||||
if (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]
|
||||
);
|
||||
} else if (((nbTiles > 256 && options.maxNbTiles[0] > 256)
|
||||
|| (nbTiles > options.maxNbTiles[0] + 256u && options.maxNbTiles[1] > 256))
|
||||
&& !options.tilemap.empty()) {
|
||||
// With `-N/--nb-tiles` unlimited (by default) for bank 0, tile IDs may be truncated in
|
||||
// the tilemap, so warn about that.
|
||||
warnx(
|
||||
"Image contains %zu tiles, of which only 256 are representable in the tilemap",
|
||||
nbTiles
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// If deduplication is not happening, we just need to output the tile data and/or maps as-is
|
||||
if (!options.allowDedup) {
|
||||
checkTileCountLimit(std::count_if(RANGE(attrmap), [](AttrmapEntry const &attr) {
|
||||
return !attr.isBackgroundTile();
|
||||
}));
|
||||
|
||||
// I currently cannot figure out useful semantics for this combination of flags.
|
||||
if (!options.inputTileset.empty()) {
|
||||
@@ -1176,16 +1251,7 @@ continue_visiting_tiles:;
|
||||
verbosePrint(VERB_NOTICE, "Deduplicating tiles...\n");
|
||||
UniqueTiles tiles = dedupTiles(image, attrmap, palettes, mappings);
|
||||
|
||||
// Check the tile count
|
||||
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]
|
||||
);
|
||||
}
|
||||
checkTileCountLimit(tiles.size());
|
||||
|
||||
if (!options.output.empty()) {
|
||||
verbosePrint(VERB_NOTICE, "Generating optimized tile data...\n");
|
||||
|
||||
+63
-36
@@ -119,18 +119,14 @@ void reverse() {
|
||||
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) {
|
||||
if (options.inputSlice.width != 0 && options.inputSlice.width != options.reversedWidth) {
|
||||
warnx(
|
||||
"Specified input slice width (%" PRIu16
|
||||
") does not match provided reversing width (%" PRIu16 " * 8)",
|
||||
") does not match provided reversing width (%" PRIu16 ")",
|
||||
options.inputSlice.width,
|
||||
options.reversedWidth
|
||||
);
|
||||
@@ -216,26 +212,28 @@ void reverse() {
|
||||
}
|
||||
|
||||
palettes.clear();
|
||||
std::array<uint8_t, sizeof(uint16_t) * 4> buf; // 4 colors
|
||||
std::array<uint8_t, sizeof(uint16_t) * 4> buf; // max 4 colors
|
||||
size_t const palSize = sizeof(uint16_t) * options.nbColorsPerPal;
|
||||
assume(buf.size() >= palSize);
|
||||
for (;;) {
|
||||
if (size_t nbRead = file->sgetn(reinterpret_cast<char *>(buf.data()), buf.size());
|
||||
if (size_t nbRead = file->sgetn(reinterpret_cast<char *>(buf.data()), palSize);
|
||||
nbRead == 0) {
|
||||
break;
|
||||
} else if (nbRead != buf.size()) {
|
||||
} else if (nbRead != palSize) {
|
||||
fatal(
|
||||
"Palette data size (%zu) is not a multiple of %zu bytes\n",
|
||||
palettes.size() * buf.size() + nbRead,
|
||||
buf.size()
|
||||
"Palette data size (%zu) is not a multiple of %zu bytes",
|
||||
palettes.size() * palSize + nbRead,
|
||||
palSize
|
||||
);
|
||||
}
|
||||
// Expand the colors
|
||||
// Expand the little-endian RGB555 colors to RGB888
|
||||
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
|
||||
return Rgba::fromCGBColor(buf[i - 2] | buf[i - 1] << 8, options.useColorCurve);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -248,8 +246,21 @@ void reverse() {
|
||||
);
|
||||
}
|
||||
|
||||
if (options.palSpecType == Options::EXPLICIT && palettes != options.palSpec) {
|
||||
warnx("Colors in the palette file do not match those specified with '-c'");
|
||||
if (options.hasExplicitPalSpec() && palettes != options.palSpec) {
|
||||
// The explicit `-c` pal spec does not match the input `-p` palette file.
|
||||
// Check whether their 8-to-5-bit-reduced GB colors nevertheless match.
|
||||
std::vector<std::array<std::optional<Rgba>, 4>> palSpecQuantized(options.palSpec);
|
||||
for (auto &pal : palSpecQuantized) {
|
||||
for (auto &color : pal) {
|
||||
if (color.has_value()) {
|
||||
color = Rgba::fromCGBColor(color->cgbColor(), options.useColorCurve);
|
||||
}
|
||||
}
|
||||
}
|
||||
warnx(
|
||||
"Colors %s the palette file do not match those specified with '-c'",
|
||||
palettes == palSpecQuantized ? "reversed from" : "in"
|
||||
);
|
||||
// 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) {
|
||||
@@ -272,12 +283,12 @@ void reverse() {
|
||||
} 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) {
|
||||
} else if (options.hasExplicitPalSpec()) {
|
||||
palettes = std::move(options.palSpec); // We won't be using it again.
|
||||
}
|
||||
|
||||
std::optional<std::vector<uint8_t>> attrmap;
|
||||
uint16_t nbTilesInBank[2] = {0, 0}; // Only used if there is an attrmap.
|
||||
uint16_t nbTilesMappedInBank[2] = {0, 0}; // Only used if there is an attrmap.
|
||||
if (!options.attrmap.empty()) {
|
||||
attrmap = readInto(options.attrmap);
|
||||
if (attrmap->size() != mapSize) {
|
||||
@@ -327,8 +338,8 @@ void reverse() {
|
||||
// The unsigned underflow for `tileOfs` is intentional, since a nonzero
|
||||
// base tile ID may overflow and continue with IDs from 0.
|
||||
if (uint8_t tileOfs = (*tilemap)[index] - options.baseTileIDs[bank];
|
||||
tileOfs >= nbTilesInBank[bank]) {
|
||||
nbTilesInBank[bank] = tileOfs + 1;
|
||||
tileOfs >= nbTilesMappedInBank[bank]) {
|
||||
nbTilesMappedInBank[bank] = tileOfs + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,29 +347,43 @@ void reverse() {
|
||||
verbosePrint(
|
||||
VERB_INFO,
|
||||
"Number of tiles in bank {0: %" PRIu16 ", 1: %" PRIu16 "}\n",
|
||||
nbTilesInBank[0],
|
||||
nbTilesInBank[1]
|
||||
nbTilesMappedInBank[0],
|
||||
nbTilesMappedInBank[1]
|
||||
);
|
||||
|
||||
for (int bank = 0; bank < 2; ++bank) {
|
||||
if (nbTilesInBank[bank] > options.maxNbTiles[bank]) {
|
||||
if (nbTilesMappedInBank[bank] > options.maxNbTiles[bank]) {
|
||||
error(
|
||||
"Bank %d contains %" PRIu16 " tiles, but the specified limit is %" PRIu16,
|
||||
bank,
|
||||
nbTilesInBank[bank],
|
||||
nbTilesMappedInBank[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
|
||||
);
|
||||
if (uint16_t const maxTotalNbTiles =
|
||||
nbTilesMappedInBank[1] > 0
|
||||
? std::max<uint16_t>(
|
||||
nbTilesMappedInBank[0], options.maxNbTiles[0] + nbTilesMappedInBank[1]
|
||||
)
|
||||
: nbTilesMappedInBank[0];
|
||||
maxTotalNbTiles > nbTiles + options.trim) {
|
||||
std::string message =
|
||||
"The tilemap references " + std::to_string(nbTilesMappedInBank[0]) + " tiles";
|
||||
if (nbTilesMappedInBank[1] > 0) {
|
||||
if (nbTilesMappedInBank[0] != options.maxNbTiles[0]) {
|
||||
message += " out of a maximum " + std::to_string(options.maxNbTiles[0]);
|
||||
}
|
||||
message += " in bank 0, and " + std::to_string(nbTilesMappedInBank[1])
|
||||
+ " in bank 1 (total: " + std::to_string(maxTotalNbTiles) + ")";
|
||||
}
|
||||
message += ", but only " + std::to_string(nbTiles) + " have been read";
|
||||
if (options.trim > 0) {
|
||||
message += " plus " + std::to_string(options.trim)
|
||||
+ " trimmed (total: " + std::to_string(nbTiles + options.trim) + ")";
|
||||
}
|
||||
fatal("%s", message.c_str());
|
||||
}
|
||||
|
||||
requireZeroErrors();
|
||||
@@ -388,7 +413,9 @@ void reverse() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
size_t const limit = std::min<size_t>(nbTiles, options.maxNbTiles[0]);
|
||||
// Tiles trimmed with `-x` were never written to the tile data file, but are
|
||||
// still referenced by the tilemap.
|
||||
size_t const limit = std::min<size_t>(nbTiles + options.trim, options.maxNbTiles[0]);
|
||||
for (size_t index = 0; index < mapSize; ++index) {
|
||||
size_t tx = index % width, ty = index / width;
|
||||
uint8_t tileID = (*tilemap)[index];
|
||||
@@ -548,7 +575,7 @@ void reverse() {
|
||||
// Get the tile ID at this location
|
||||
size_t tileOfs =
|
||||
tilemap ? static_cast<uint8_t>((*tilemap)[index] - options.baseTileIDs[bank])
|
||||
+ (bank ? nbTilesInBank[0] : 0)
|
||||
+ (bank ? options.maxNbTiles[0] : 0)
|
||||
: index;
|
||||
// This should have been enforced by the earlier checking.
|
||||
assume(tileOfs < nbTiles + options.trim);
|
||||
@@ -567,7 +594,7 @@ void reverse() {
|
||||
// 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 = options.bitDepth == 2 ? tileData[realY + 1] : bitplane0;
|
||||
uint8_t bitplane1 = options.bitDepth == 2 ? tileData[realY + 1] : 0;
|
||||
if (attribute & 0x20) { // Handle horizontal flip
|
||||
bitplane0 = flipTable[bitplane0];
|
||||
bitplane1 = flipTable[bitplane1];
|
||||
@@ -599,7 +626,7 @@ void reverse() {
|
||||
if (Rgba const &pixel = *color; pngColorType == PNG_COLOR_TYPE_GRAY) {
|
||||
gray = gray << pngDepth | (pixel.red & ((1 << pngDepth) - 1));
|
||||
} else if (pngColorType == PNG_COLOR_TYPE_PALETTE) {
|
||||
*ptr++ = palOfs * 4 + colorID;
|
||||
*ptr++ = palOfs * options.nbColorsPerPal + colorID;
|
||||
} else {
|
||||
*ptr++ = pixel.red;
|
||||
*ptr++ = pixel.green;
|
||||
|
||||
+33
-3
@@ -24,9 +24,17 @@ std::string toCGB(uint16_t color) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Copied from the "Modern - Accurate" (`GB_COLOR_CORRECTION_MODERN_ACCURATE`)
|
||||
// formula used by SameBoy in its `scale_channel_with_curve` function since
|
||||
// commit b5a611c5db46d6a0649d04d24d8d6339200f9ca1 (Dec 2020).
|
||||
// clang-format off: vertically align columns of values
|
||||
static std::array<uint8_t, 32> color_curve{
|
||||
0, 6, 12, 20, 28, 36, 45, 56, 66, 76, 88, 100, 113, 125, 137, 149,
|
||||
161, 172, 182, 192, 202, 210, 218, 225, 232, 238, 243, 247, 250, 252, 254, 255,
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
// Inverted `color_curve`, with gaps filled by polynomial interpolation.
|
||||
// clang-format off: vertically align columns of values
|
||||
static std::array<uint8_t, 256> reverse_curve{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1,
|
||||
@@ -48,6 +56,28 @@ static std::array<uint8_t, 256> reverse_curve{
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
Rgba Rgba::fromCGBColor(uint16_t color, bool useColorCurve) {
|
||||
uint8_t r = color & 0b11111, g = (color >> 5) & 0b11111, b = (color >> 10) & 0b11111;
|
||||
if (useColorCurve) {
|
||||
r = color_curve[r];
|
||||
g = color_curve[g];
|
||||
b = color_curve[b];
|
||||
if (g != b) {
|
||||
g = round(pow((pow(g / 255.0, 2.2) * 3 + pow(b / 255.0, 2.2)) / 4, 1 / 2.2) * 255);
|
||||
}
|
||||
} else {
|
||||
r = r << 3 | r >> 2;
|
||||
g = g << 3 | g >> 2;
|
||||
b = b << 3 | b >> 2;
|
||||
}
|
||||
return {
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
static_cast<uint8_t>(color & transparent ? 0x00 : 0xFF),
|
||||
};
|
||||
}
|
||||
|
||||
uint16_t Rgba::cgbColor() const {
|
||||
if (isTransparent()) {
|
||||
return transparent;
|
||||
|
||||
+299
-234
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "link/assign.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
#include <inttypes.h>
|
||||
#include <optional>
|
||||
@@ -15,6 +16,7 @@
|
||||
#include "helpers.hpp"
|
||||
#include "itertools.hpp"
|
||||
#include "linkdefs.hpp"
|
||||
#include "platform.hpp"
|
||||
#include "verbosity.hpp"
|
||||
|
||||
#include "link/main.hpp"
|
||||
@@ -23,21 +25,255 @@
|
||||
#include "link/symbol.hpp"
|
||||
#include "link/warning.hpp"
|
||||
|
||||
struct MemoryLocation {
|
||||
uint16_t address;
|
||||
uint32_t bank;
|
||||
};
|
||||
|
||||
struct FreeSpace {
|
||||
uint16_t address;
|
||||
uint16_t size;
|
||||
uint16_t size; // Never zero.
|
||||
|
||||
uint16_t addrOnePast() const { return address + size; }
|
||||
};
|
||||
|
||||
// Table of free space for each bank
|
||||
static std::vector<std::deque<FreeSpace>> memory[SECTTYPE_INVALID];
|
||||
using FreeSpaceIter = std::deque<FreeSpace>::iterator;
|
||||
|
||||
static std::deque<FreeSpace> &freeSpaceOfBank(Section const §ion, uint32_t bank) {
|
||||
assume(bank >= section.typeInfo().firstBank);
|
||||
assume(bank <= section.typeInfo().lastBank);
|
||||
return memory[section.type][bank - section.typeInfo().firstBank];
|
||||
}
|
||||
|
||||
struct Scrambling {
|
||||
uint16_t romxOfs = 0;
|
||||
uint16_t sramOfs = 0;
|
||||
uint16_t wramxOfs = 0;
|
||||
|
||||
// Helper for the next function, to give names to its two returned values.
|
||||
struct ScramblingInfo {
|
||||
uint16_t &curOfs;
|
||||
uint16_t maxOfs;
|
||||
};
|
||||
std::optional<ScramblingInfo> getInfoFor(SectionType type) {
|
||||
switch (type) {
|
||||
case SECTTYPE_ROMX:
|
||||
return {
|
||||
{romxOfs, options.scrambleROMX}
|
||||
};
|
||||
case SECTTYPE_SRAM:
|
||||
return {
|
||||
{sramOfs, options.scrambleSRAM}
|
||||
};
|
||||
case SECTTYPE_WRAMX:
|
||||
return {
|
||||
{wramxOfs, options.scrambleWRAMX}
|
||||
};
|
||||
|
||||
// Non-banked sections don't need scrambling support...
|
||||
case SECTTYPE_ROM0:
|
||||
case SECTTYPE_WRAM0:
|
||||
case SECTTYPE_OAM:
|
||||
case SECTTYPE_HRAM:
|
||||
assume(!sectionTypeInfo[type].isBanked());
|
||||
return std::nullopt;
|
||||
// ...but VRAM doesn't either, regardless of whether it's banked or not.
|
||||
case SECTTYPE_VRAM:
|
||||
return std::nullopt;
|
||||
|
||||
case SECTTYPE_INVALID:
|
||||
unreachable_();
|
||||
}
|
||||
return std::nullopt; // Dead code, but some compilers don't recognize that.
|
||||
}
|
||||
};
|
||||
static Scrambling scrambling;
|
||||
|
||||
struct MemoryLocation {
|
||||
uint16_t address;
|
||||
uint32_t bank;
|
||||
|
||||
static MemoryLocation initFor(Section const §ion) {
|
||||
MemoryLocation location;
|
||||
|
||||
if (section.isAddressFixed) { // This will never change.
|
||||
location.address = section.org;
|
||||
}
|
||||
|
||||
if (section.isBankFixed) {
|
||||
location.bank = section.bank;
|
||||
} else {
|
||||
location.bank = section.typeInfo().firstBank;
|
||||
|
||||
if (auto info = scrambling.getInfoFor(section.type);
|
||||
info.has_value() && info->maxOfs != 0) { // If scrambling is enabled...
|
||||
// ...then we will begin the search from a different offset for each section.
|
||||
// Go to the next offset (backwards), wrapping around. (Thus, no underflow!)
|
||||
info->curOfs = (info->curOfs != 0 ? info->curOfs : info->maxOfs) - 1;
|
||||
location.bank += info->curOfs;
|
||||
}
|
||||
}
|
||||
|
||||
return location;
|
||||
}
|
||||
|
||||
// Try again in the next bank, if one is available.
|
||||
[[nodiscard("This returns whether iteration can be continued")]]
|
||||
bool goToNextApplicableBankFor(Section const §ion) {
|
||||
assume(bank >= section.typeInfo().firstBank);
|
||||
assume(bank <= section.typeInfo().lastBank);
|
||||
|
||||
assume(!section.isBankFixed);
|
||||
|
||||
// Try scrambled banks in descending order until no bank in the scrambled range is
|
||||
// available.
|
||||
if (auto info = scrambling.getInfoFor(section.type);
|
||||
info.has_value() && info->maxOfs != 0) {
|
||||
// All floating sections within a scrambled region should be
|
||||
// within the scrambled bank pool.
|
||||
assume(bank < info->maxOfs + section.typeInfo().firstBank);
|
||||
|
||||
uint16_t ofsWithinPool = bank - section.typeInfo().firstBank;
|
||||
// Go to the next bank (backwards), wrapping around. (Thus, no overflow!)
|
||||
ofsWithinPool = (ofsWithinPool != 0 ? ofsWithinPool : info->maxOfs) - 1;
|
||||
|
||||
bank = ofsWithinPool + section.typeInfo().firstBank;
|
||||
// Keep iterating unless we have wrapped back around to the start offset.
|
||||
return ofsWithinPool != info->curOfs;
|
||||
}
|
||||
|
||||
// Otherwise, try in ascending order.
|
||||
if (bank == section.typeInfo().lastBank) {
|
||||
return false;
|
||||
}
|
||||
++bank;
|
||||
return true;
|
||||
}
|
||||
|
||||
void makeAddressAligned(uint16_t alignMask, uint16_t alignOfs) {
|
||||
// By how much the address is past the target offset within the current alignment "page".
|
||||
uint16_t offset = (address - alignOfs) & alignMask;
|
||||
// Move by one page *minus* that "overshoot" offset.
|
||||
// If it's 0, then this would move by a whole page, but `& alignMask` resets it back to 0.
|
||||
address += ((alignMask + 1) - offset) & alignMask;
|
||||
}
|
||||
};
|
||||
|
||||
static FreeSpaceIter tryPlacingInBank(Section const §ion, MemoryLocation &location) {
|
||||
std::deque<FreeSpace> &bankMem = freeSpaceOfBank(section, location.bank);
|
||||
|
||||
if (section.isAddressFixed) {
|
||||
// There is only one candidate location in this bank: the address at which the section is
|
||||
// fixed.
|
||||
assume(location.address == section.org);
|
||||
|
||||
FreeSpaceIter iter = std::find_if(RANGE(bankMem), [&location](FreeSpace const &freeSpace) {
|
||||
// If they both exactly match, that means the next block will begin past the requested
|
||||
// addr, so the function would fail anyway.
|
||||
return freeSpace.addrOnePast() >= location.address;
|
||||
});
|
||||
if (iter != bankMem.end()) {
|
||||
// We have the first block ending after the section's address, so all that's left is
|
||||
// checking that the address does fall into the block, and then that the section fits.
|
||||
if (location.address < iter->address
|
||||
|| location.address + section.size > iter->addrOnePast()) {
|
||||
return bankMem.end(); // Failed! Better luck next bank?
|
||||
}
|
||||
}
|
||||
return iter;
|
||||
|
||||
} else {
|
||||
// There are many possible locations within the bank, so we are going to iterate on free
|
||||
// blocks. If it is impossible to fit at the earliest (constraint-satisfying) address, then
|
||||
// no other address in the block will do; thus, we make only one attempt per block.
|
||||
return std::find_if(RANGE(bankMem), [&location, §ion](FreeSpace const &freeSpace) {
|
||||
location.address = freeSpace.address;
|
||||
if (section.isAlignFixed) {
|
||||
location.makeAddressAligned(section.alignMask, section.alignOfs);
|
||||
// Did it advance past the block? Or, rarely, overflowed?
|
||||
if (location.address >= freeSpace.addrOnePast()
|
||||
|| location.address < freeSpace.address) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Since `location.address` lies within the block,
|
||||
// we only need to check that its end address also does.
|
||||
return location.address + section.size <= freeSpace.addrOnePast();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Place section using first-fit decreasing algorithm
|
||||
// <https://en.wikipedia.org/wiki/Bin_packing_problem#First-fit_algorithm>
|
||||
// Returns an iterator to within `freeSpaceOfBank(section, location.bank)`
|
||||
// (guaranteeing that `location.bank` remains valid) pointing at the free block that
|
||||
// the section can go into (at `location.address`).
|
||||
// The iterator is an end iterator if and only if no suitable location was found.
|
||||
// `location` is updated accordingly.
|
||||
static FreeSpaceIter tryPlacing(Section const §ion, MemoryLocation &location) {
|
||||
if (section.isBankFixed) {
|
||||
assume(location.bank == section.bank);
|
||||
return tryPlacingInBank(section, location);
|
||||
}
|
||||
|
||||
do {
|
||||
if (FreeSpaceIter iter = tryPlacingInBank(section, location);
|
||||
iter != freeSpaceOfBank(section, location.bank).end()) {
|
||||
return iter; // Found one!
|
||||
}
|
||||
} while (location.goToNextApplicableBankFor(section));
|
||||
// Return a deque's end iterator to signal failure.
|
||||
// The exact deque doesn't matter, but the caller will use `freeSpaceOfBank` also.
|
||||
return freeSpaceOfBank(section, location.bank).end();
|
||||
}
|
||||
|
||||
static void
|
||||
updateFreeSpace(FreeSpaceIter iter, std::deque<FreeSpace> &bankMem, Section const §ion) {
|
||||
assume(section.org + section.size <= UINT16_MAX);
|
||||
uint16_t sectionEnd = section.org + section.size;
|
||||
assume(section.org >= iter->address);
|
||||
assume(sectionEnd <= iter->addrOnePast());
|
||||
|
||||
bool noLeftSpace = iter->address == section.org;
|
||||
bool noRightSpace = iter->address + iter->size == sectionEnd;
|
||||
if (noLeftSpace && noRightSpace) {
|
||||
// The free space is entirely deleted
|
||||
bankMem.erase(iter);
|
||||
} else if (!noLeftSpace && !noRightSpace) {
|
||||
// The free space is split in two
|
||||
uint16_t size = static_cast<uint16_t>(iter->address + iter->size - sectionEnd);
|
||||
// Resize the original space (address is unmodified)
|
||||
iter->size = section.org - iter->address;
|
||||
// Append the new space after the original one
|
||||
bankMem.insert(iter + 1, {.address = sectionEnd, .size = size});
|
||||
// `iter` cannot be reused from this point on, because `bankMem.insert`
|
||||
// invalidates iterators to itself!
|
||||
} else {
|
||||
// The amount of free spaces doesn't change: resize!
|
||||
iter->size -= section.size;
|
||||
if (noLeftSpace) {
|
||||
// The free space is moved *and* resized
|
||||
iter->address += section.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assigns a section to a given memory location
|
||||
static void assignSection(Section §ion, MemoryLocation const &location) {
|
||||
assume(location.address >= section.typeInfo().startAddr);
|
||||
// Zero-sized sections can start one past the end of their region.
|
||||
assume(location.address <= section.typeInfo().endAddr() + 1);
|
||||
// This one is not redundant, it guards against overflow!
|
||||
assume(location.address + section.size >= section.typeInfo().startAddr);
|
||||
assume(location.address + section.size <= section.typeInfo().endAddr() + 1);
|
||||
|
||||
if (section.isAddressFixed) {
|
||||
assume(location.address == section.org);
|
||||
} else if (section.isAlignFixed) {
|
||||
assume((location.address & section.alignMask) == section.alignOfs);
|
||||
}
|
||||
if (section.isBankFixed) {
|
||||
assume(location.bank == section.bank);
|
||||
}
|
||||
|
||||
// Propagate the assigned location to all UNIONs/FRAGMENTs
|
||||
// so `jr` patches in them will have the correct offset
|
||||
for (Section &piece : section.pieces()) {
|
||||
@@ -47,267 +283,93 @@ static void assignSection(Section §ion, MemoryLocation const &location) {
|
||||
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 uint16_t curScrambleWRAM = 0;
|
||||
static uint16_t curScrambleSRAM = 0;
|
||||
|
||||
MemoryLocation location;
|
||||
|
||||
// Determine which bank we should start searching in
|
||||
if (section.isBankFixed) {
|
||||
location.bank = section.bank;
|
||||
} else {
|
||||
location.bank = sectionTypeInfo[section.type].firstBank;
|
||||
|
||||
// Scramble the bank if applicable
|
||||
if (options.scrambleROMX && section.type == SECTTYPE_ROMX) {
|
||||
if (curScrambleROM == 0) {
|
||||
curScrambleROM = options.scrambleROMX;
|
||||
}
|
||||
location.bank += --curScrambleROM;
|
||||
} else if (options.scrambleWRAMX && section.type == SECTTYPE_WRAMX) {
|
||||
if (curScrambleWRAM == 0) {
|
||||
curScrambleWRAM = options.scrambleWRAMX;
|
||||
}
|
||||
location.bank += --curScrambleWRAM;
|
||||
} else if (options.scrambleSRAM && section.type == SECTTYPE_SRAM) {
|
||||
if (curScrambleSRAM == 0) {
|
||||
curScrambleSRAM = options.scrambleSRAM;
|
||||
}
|
||||
location.bank += --curScrambleSRAM;
|
||||
}
|
||||
}
|
||||
|
||||
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<size_t> 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<FreeSpace> &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
|
||||
static std::string describeConstraintsOf(Section const §ion) {
|
||||
std::string description = "\"" + section.name + "\" (" + section.typeInfo().name + " section) ";
|
||||
if (section.isBankFixed && section.typeInfo().isBanked()) {
|
||||
char bank[9];
|
||||
sprintf_to_array(bank, "%02" PRIx32, section.bank);
|
||||
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
|
||||
// We have previously ensured alignment to 15 or fewer bits, so this will progress
|
||||
assume(section.alignMask < (1 << 16) - 1);
|
||||
// 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);
|
||||
char addr[5];
|
||||
sprintf_to_array(addr, "%04" PRIx16, section.org);
|
||||
description = description + "at $" + bank + ":" + addr;
|
||||
} else if (section.isAlignFixed) {
|
||||
char mask[8];
|
||||
snprintf(mask, sizeof(mask), "%" PRIx16, static_cast<uint16_t>(~section.alignMask));
|
||||
char mask[5];
|
||||
sprintf_to_array(mask, "%" PRIx16, static_cast<uint16_t>(~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);
|
||||
char addr[5];
|
||||
sprintf_to_array(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<uint16_t>(~section.alignMask));
|
||||
snprintf(offset, sizeof(offset), "%" PRIx16, section.alignOfs);
|
||||
char mask[5], offset[5];
|
||||
sprintf_to_array(mask, "%" PRIx16, static_cast<uint16_t>(~section.alignMask));
|
||||
sprintf_to_array(offset, "%" PRIx16, section.alignOfs);
|
||||
description = description + "with align mask $" + mask + " and offset $" + offset;
|
||||
} else {
|
||||
description = description + "anywhere";
|
||||
}
|
||||
|
||||
if (auto info = scrambling.getInfoFor(section.type);
|
||||
info.has_value() && info->maxOfs != 0) { // Only mention scrambling if it is enabled.
|
||||
char size[6];
|
||||
sprintf_to_array(size, "%" PRIu16, info->maxOfs);
|
||||
description = description + " within the " + size + " scrambled banks";
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
MemoryLocation location = MemoryLocation::initFor(section);
|
||||
|
||||
// Specially handle 0-byte SECTIONs, as they ignore free space entirely.
|
||||
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,
|
||||
};
|
||||
if (!section.isAddressFixed) {
|
||||
location.address = section.typeInfo().startAddr;
|
||||
if (section.isAlignFixed) {
|
||||
location.makeAddressAligned(section.alignMask, section.alignOfs);
|
||||
}
|
||||
}
|
||||
|
||||
// This check safely handles sections with impossible alignment and no fixed address.
|
||||
// The above `location.address = section.typeInfo().startAddr` would be valid on its own,
|
||||
// but `location.makeAddressAligned(...)` can increase `location.address` above the valid
|
||||
// range for its `section`, which would violate an assumption in `assignSection`.
|
||||
// Note that sections with fixed addresses are handled earlier by `sect_DoSanityChecks`,
|
||||
// but this check would safely handle them too if they ever reached it.
|
||||
if (location.address <= section.typeInfo().endAddr() + 1) {
|
||||
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<size_t> spaceIdx = getPlacement(section, location); spaceIdx) {
|
||||
std::deque<FreeSpace> &bankMem =
|
||||
memory[section.type][location.bank - sectionTypeInfo[section.type].firstBank];
|
||||
FreeSpace &freeSpace = bankMem[*spaceIdx];
|
||||
|
||||
FreeSpaceIter iter = tryPlacing(section, location);
|
||||
if (std::deque<FreeSpace> &bankMem = freeSpaceOfBank(section, location.bank);
|
||||
iter != bankMem.end()) {
|
||||
assignSection(section, location);
|
||||
|
||||
// Update the free space
|
||||
assume(section.org + section.size <= UINT16_MAX);
|
||||
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<uint16_t>(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;
|
||||
}
|
||||
}
|
||||
updateFreeSpace(iter, bankMem, section);
|
||||
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) {
|
||||
fatal("Unable to place %s", describeConstraintsOf(section).c_str());
|
||||
} else if (uint16_t onePastEnd = section.typeInfo().endAddr() + 1;
|
||||
section.org + section.size > onePastEnd) {
|
||||
// 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(),
|
||||
describeConstraintsOf(section).c_str(),
|
||||
section.org + section.size,
|
||||
sectTypeEndAddr(section.type) + 1
|
||||
onePastEnd
|
||||
);
|
||||
} else {
|
||||
// Otherwise there is overlap with another section
|
||||
@@ -315,7 +377,7 @@ static void placeSection(Section §ion) {
|
||||
assume(overlap != nullptr);
|
||||
fatal(
|
||||
"Unable to place %s: section overlaps with \"%s\"",
|
||||
getSectionDescription(section).c_str(),
|
||||
describeConstraintsOf(section).c_str(),
|
||||
overlap->name.c_str()
|
||||
);
|
||||
}
|
||||
@@ -392,8 +454,9 @@ static void checkOverlayCompat() {
|
||||
unfixedList += "\n- and ";
|
||||
unfixedList += std::to_string(nbUnfixedSections - nbListed);
|
||||
unfixedList += " more";
|
||||
break;
|
||||
goto finish;
|
||||
}
|
||||
|
||||
unfixedList += "\n- \"";
|
||||
unfixedList += section->name;
|
||||
unfixedList += "\" (";
|
||||
@@ -410,6 +473,7 @@ static void checkOverlayCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
finish: // Can't `break` out of a nested loop
|
||||
fatal(
|
||||
"All sections must be fixed when using an overlay file; %zu %s not:%s",
|
||||
nbUnfixedSections,
|
||||
@@ -423,11 +487,12 @@ void assign_AssignSections() {
|
||||
|
||||
// Initialize the free space-modelling structs
|
||||
for (SectionType type : EnumSeq(SECTTYPE_INVALID)) {
|
||||
memory[type].resize(sectTypeBanks(type));
|
||||
SectionTypeInfo const &typeInfo = sectionTypeInfo[type];
|
||||
memory[type].resize(typeInfo.nbBanks());
|
||||
for (std::deque<FreeSpace> &bankMem : memory[type]) {
|
||||
bankMem.push_back({
|
||||
.address = sectionTypeInfo[type].startAddr,
|
||||
.size = sectionTypeInfo[type].size,
|
||||
.address = typeInfo.startAddr,
|
||||
.size = typeInfo.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+21
-20
@@ -31,7 +31,7 @@ static void setActiveTypeAndIdx(SectionType type, uint32_t idx) {
|
||||
}
|
||||
|
||||
void layout_SetFloatingSectionType(SectionType type) {
|
||||
if (sectTypeBanks(type) == 1) {
|
||||
if (!sectionTypeInfo[type].isBanked()) {
|
||||
// There is only a single bank anyway, so just set the index to 0.
|
||||
setActiveTypeAndIdx(type, 0);
|
||||
} else {
|
||||
@@ -46,8 +46,8 @@ void layout_SetFloatingSectionType(SectionType type) {
|
||||
}
|
||||
|
||||
void layout_SetSectionType(SectionType type) {
|
||||
if (sectTypeBanks(type) != 1) {
|
||||
scriptError("A bank number must be specified for %s", sectionTypeInfo[type].name.c_str());
|
||||
if (SectionTypeInfo const &typeInfo = sectionTypeInfo[type]; typeInfo.isBanked()) {
|
||||
scriptError("A bank number must be specified for %s", typeInfo.name);
|
||||
// Keep going with a default value for the bank index.
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ void layout_SetSectionType(SectionType type, uint32_t bank) {
|
||||
if (bank < typeInfo.firstBank) {
|
||||
scriptError(
|
||||
"%s bank %" PRIu32 " does not exist (the minimum is %" PRIu32 ")",
|
||||
typeInfo.name.c_str(),
|
||||
typeInfo.name,
|
||||
bank,
|
||||
typeInfo.firstBank
|
||||
);
|
||||
@@ -68,10 +68,11 @@ void layout_SetSectionType(SectionType type, uint32_t bank) {
|
||||
} else if (bank > typeInfo.lastBank) {
|
||||
scriptError(
|
||||
"%s bank %" PRIu32 " does not exist (the maximum is %" PRIu32 ")",
|
||||
typeInfo.name.c_str(),
|
||||
typeInfo.name,
|
||||
bank,
|
||||
typeInfo.lastBank
|
||||
);
|
||||
bank = typeInfo.lastBank;
|
||||
}
|
||||
|
||||
setActiveTypeAndIdx(type, bank - typeInfo.firstBank);
|
||||
@@ -87,22 +88,22 @@ void layout_SetAddr(uint32_t addr) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t &pc = curAddr[activeType][activeBankIdx];
|
||||
SectionTypeInfo const &typeInfo = sectionTypeInfo[activeType];
|
||||
|
||||
if (addr < pc) {
|
||||
if (uint16_t &pc = curAddr[activeType][activeBankIdx]; 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.
|
||||
} else if (addr > typeInfo.endAddr() + 1u) { // Allow "one past the end" zero-sized sections.
|
||||
scriptError(
|
||||
"Cannot set the current address to $%04" PRIx32 ": %s ends at $%04" PRIx16,
|
||||
addr,
|
||||
typeInfo.name.c_str(),
|
||||
sectTypeEndAddr(activeType)
|
||||
typeInfo.name,
|
||||
typeInfo.endAddr()
|
||||
);
|
||||
pc = sectTypeEndAddr(activeType);
|
||||
pc = typeInfo.endAddr();
|
||||
} else {
|
||||
pc = addr;
|
||||
}
|
||||
|
||||
isPcFloating = false;
|
||||
}
|
||||
|
||||
@@ -125,7 +126,7 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
|
||||
|
||||
if (isPcFloating) {
|
||||
if (alignment >= 16) {
|
||||
layout_SetAddr(floatingAlignOffset);
|
||||
layout_SetAddr(alignOfs);
|
||||
} else {
|
||||
uint32_t alignSize = 1u << alignment;
|
||||
uint32_t alignMask = alignSize - 1;
|
||||
@@ -181,7 +182,7 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
|
||||
", past $%04" PRIx16,
|
||||
pc,
|
||||
static_cast<uint16_t>(pc + length),
|
||||
static_cast<uint16_t>(sectTypeEndAddr(activeType) + 1)
|
||||
static_cast<uint16_t>(typeInfo.endAddr() + 1)
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -211,7 +212,7 @@ void layout_Pad(uint32_t length) {
|
||||
"Cannot increase the current address by %u bytes: only %u bytes to $%04" PRIx16,
|
||||
length,
|
||||
typeInfo.size - offset,
|
||||
static_cast<uint16_t>(sectTypeEndAddr(activeType) + 1)
|
||||
static_cast<uint16_t>(typeInfo.endAddr() + 1)
|
||||
);
|
||||
} else {
|
||||
pc += length;
|
||||
@@ -241,7 +242,7 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
|
||||
scriptError(
|
||||
"\"%s\" is specified to be a %s section, but it contains data",
|
||||
name.c_str(),
|
||||
typeInfo.name.c_str()
|
||||
typeInfo.name
|
||||
);
|
||||
} 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
|
||||
@@ -249,7 +250,7 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
|
||||
scriptError(
|
||||
"\"%s\" is specified to be a %s section, but it does not contain data",
|
||||
name.c_str(),
|
||||
typeInfo.name.c_str()
|
||||
typeInfo.name
|
||||
);
|
||||
} else {
|
||||
// SDCC areas don't have a type assigned yet, so the linker script gives them one.
|
||||
@@ -261,8 +262,8 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
|
||||
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()
|
||||
typeInfo.name,
|
||||
section->typeInfo().name
|
||||
);
|
||||
}
|
||||
|
||||
@@ -275,7 +276,7 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
|
||||
"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(),
|
||||
section->typeInfo().name,
|
||||
bank,
|
||||
section->bank
|
||||
);
|
||||
@@ -320,7 +321,7 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
|
||||
", but then it would overflow %s by %" PRIu16 " byte%s",
|
||||
name.c_str(),
|
||||
org,
|
||||
typeInfo.name.c_str(),
|
||||
typeInfo.name,
|
||||
overflowSize,
|
||||
overflowSize == 1 ? "" : "s"
|
||||
);
|
||||
|
||||
+15
-6
@@ -101,7 +101,7 @@ static void parseScrambleSpec(char *spec) {
|
||||
// clang-format off: vertically align nested initializers
|
||||
static UpperMap<std::pair<uint16_t *, uint16_t>> scrambleSpecs{
|
||||
{"ROMX", std::pair{&options.scrambleROMX, 65535}},
|
||||
{"SRAM", std::pair{&options.scrambleSRAM, 255 }},
|
||||
{"SRAM", std::pair{&options.scrambleSRAM, 256 }},
|
||||
{"WRAMX", std::pair{&options.scrambleWRAMX, 7 }},
|
||||
};
|
||||
// clang-format on
|
||||
@@ -433,14 +433,14 @@ int main(int argc, char *argv[]) {
|
||||
sectionTypeInfo[SECTTYPE_VRAM].lastBank = 0;
|
||||
}
|
||||
|
||||
// Read all object files first,
|
||||
// Read all object files...
|
||||
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,
|
||||
// Apply the linker script's modifications...
|
||||
if (localOptions.linkerScriptName) {
|
||||
verbosePrint(VERB_NOTICE, "Reading linker script...\n");
|
||||
|
||||
@@ -453,16 +453,25 @@ int main(int argc, char *argv[]) {
|
||||
requireZeroErrors();
|
||||
}
|
||||
|
||||
// then process them,
|
||||
// Attempt to generate a layout...
|
||||
sect_DoSanityChecks();
|
||||
requireZeroErrors();
|
||||
assign_AssignSections();
|
||||
|
||||
// Emit the symbol and/or map files now (if requested on the CLI),
|
||||
// so that more link-time info is available to debug any failing assertions.
|
||||
// This is safe to do, since we have a valid layout at this point;
|
||||
// assertions only check user-level assumptions made about it.
|
||||
out_WriteSymIfAny();
|
||||
out_WriteMapIfAny();
|
||||
|
||||
// Make any last-minute verifications...
|
||||
patch_CheckAssertions();
|
||||
|
||||
// and finally output the result.
|
||||
// And, finally, make the ROM and emit it!
|
||||
patch_ApplyPatches();
|
||||
requireZeroErrors();
|
||||
out_WriteFiles();
|
||||
out_WriteROMIfAny();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
+27
-39
@@ -71,7 +71,7 @@ void out_AddSection(Section const §ion) {
|
||||
1, // SECTTYPE_OAM
|
||||
};
|
||||
|
||||
uint32_t targetBank = section.bank - sectionTypeInfo[section.type].firstBank;
|
||||
uint32_t targetBank = section.bank - section.typeInfo().firstBank;
|
||||
if (targetBank >= maxNbBanks[section.type]) {
|
||||
fatal(
|
||||
"Section \"%s\" has an invalid bank range (%" PRIu32 " > %" PRIu32 ")",
|
||||
@@ -96,7 +96,7 @@ void out_AddSection(Section const §ion) {
|
||||
}
|
||||
|
||||
Section const *out_OverlappingSection(Section const §ion) {
|
||||
uint32_t bank = section.bank - sectionTypeInfo[section.type].firstBank;
|
||||
uint32_t bank = section.bank - section.typeInfo().firstBank;
|
||||
|
||||
for (Section const *ptr : sections[section.type][bank].sections) {
|
||||
if (ptr->org < section.org + section.size && section.org < ptr->org + ptr->size) {
|
||||
@@ -204,7 +204,7 @@ static void
|
||||
}
|
||||
}
|
||||
|
||||
static void writeROM() {
|
||||
void out_WriteROMIfAny() {
|
||||
if (options.outputFileName) {
|
||||
char const *outputFileName = options.outputFileName->c_str();
|
||||
if (*options.outputFileName != "-") {
|
||||
@@ -267,31 +267,27 @@ static void writeROM() {
|
||||
}
|
||||
|
||||
static void writeSymName(std::string const &name, FILE *file) {
|
||||
for (char const *ptr = name.c_str(); *ptr != '\0';) {
|
||||
for (size_t i = 0; i < name.length();) {
|
||||
// Output legal ASCII characters as-is
|
||||
if (char c = *ptr; continuesIdentifier(c)) {
|
||||
if (char c = name[i]; continuesIdentifier(c)) {
|
||||
putc(c, file);
|
||||
++ptr;
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Output illegal characters using Unicode escapes ('\u' or '\U')
|
||||
// Decode the UTF-8 codepoint; or at least attempt to
|
||||
Utf8Decoder decoder;
|
||||
do {
|
||||
if (decoder.update(*ptr) != UTF8_REJECT) {
|
||||
++ptr;
|
||||
continue;
|
||||
}
|
||||
// This sequence was invalid; emit a U+FFFD, and recover
|
||||
decoder.codepoint = 0xFFFD;
|
||||
// Skip continuation bytes
|
||||
// A NUL byte does not qualify, so we're good
|
||||
while ((*ptr & 0xC0) == 0x80) {
|
||||
++ptr;
|
||||
}
|
||||
while (i < name.length()) {
|
||||
decoder.update(static_cast<uint8_t>(name[i++]));
|
||||
if (decoder.state == UTF8_ACCEPT || decoder.state == UTF8_REJECT) {
|
||||
break;
|
||||
} while (decoder.state != UTF8_ACCEPT);
|
||||
}
|
||||
}
|
||||
if (decoder.state != UTF8_ACCEPT) {
|
||||
// This sequence was invalid or incomplete; emit a U+FFFD instead
|
||||
decoder.codepoint = 0xFFFD;
|
||||
}
|
||||
fprintf(
|
||||
file, decoder.codepoint <= 0xFFFF ? "\\u%04" PRIx32 : "\\U%08" PRIx32, decoder.codepoint
|
||||
);
|
||||
@@ -465,18 +461,19 @@ static void writeMapBank(SortedSections const §List, SectionType type, uint3
|
||||
fprintf(
|
||||
mapFile,
|
||||
"\n%s bank #%" PRIu32 ":\n",
|
||||
sectionTypeInfo[type].name.c_str(),
|
||||
sectionTypeInfo[type].name,
|
||||
bank + sectionTypeInfo[type].firstBank
|
||||
);
|
||||
|
||||
uint16_t prevEndAddr = sectionTypeInfo[type].startAddr;
|
||||
uint16_t used = forEachSection(sectList, [&](Section const §) {
|
||||
assume(sect.offset == 0);
|
||||
|
||||
writeEmptySpace(prevEndAddr, sect.org);
|
||||
|
||||
assume(sect.org + sect.size <= UINT16_MAX);
|
||||
prevEndAddr = sect.org + sect.size;
|
||||
|
||||
if (uint16_t endAddr = sect.org + sect.size; endAddr > prevEndAddr) {
|
||||
writeEmptySpace(prevEndAddr, sect.org);
|
||||
prevEndAddr = endAddr;
|
||||
}
|
||||
|
||||
fprintf(mapFile, "\tSECTION: $%04" PRIx16, sect.org);
|
||||
if (sect.size != 0) {
|
||||
@@ -495,12 +492,10 @@ static void writeMapBank(SortedSections const §List, SectionType type, uint3
|
||||
if (used == 0) {
|
||||
fputs("\tEMPTY\n", mapFile);
|
||||
} else {
|
||||
uint16_t bankEndAddr = sectionTypeInfo[type].startAddr + sectionTypeInfo[type].size;
|
||||
|
||||
uint16_t bankEndAddr = sectionTypeInfo[type].endAddr() + 1;
|
||||
writeEmptySpace(prevEndAddr, bankEndAddr);
|
||||
|
||||
uint16_t slack = sectionTypeInfo[type].size - used;
|
||||
|
||||
fprintf(mapFile, "\tTOTAL EMPTY: $%04" PRIx16 " byte%s\n", slack, slack == 1 ? "" : "s");
|
||||
}
|
||||
}
|
||||
@@ -517,13 +512,12 @@ static void writeMapSummary() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not output unused section types
|
||||
// Skip types which haven't been used at all.
|
||||
if (nbBanks == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t usedTotal = 0;
|
||||
|
||||
for (uint32_t bank = 0; bank < nbBanks; ++bank) {
|
||||
usedTotal += forEachSection(sections[type][bank], [](Section const &) {});
|
||||
}
|
||||
@@ -531,19 +525,19 @@ static void writeMapSummary() {
|
||||
fprintf(
|
||||
mapFile,
|
||||
"\t%s: %" PRIu32 " byte%s used / %zu free",
|
||||
sectionTypeInfo[type].name.c_str(),
|
||||
sectionTypeInfo[type].name,
|
||||
usedTotal,
|
||||
usedTotal == 1 ? "" : "s",
|
||||
static_cast<size_t>(nbBanks) * sectionTypeInfo[type].size - usedTotal
|
||||
);
|
||||
if (sectionTypeInfo[type].firstBank != sectionTypeInfo[type].lastBank || nbBanks > 1) {
|
||||
if (sectionTypeInfo[type].isBanked()) {
|
||||
fprintf(mapFile, " in %u bank%s", nbBanks, nbBanks == 1 ? "" : "s");
|
||||
}
|
||||
putc('\n', mapFile);
|
||||
}
|
||||
}
|
||||
|
||||
static void writeSym() {
|
||||
void out_WriteSymIfAny() {
|
||||
if (!options.symFileName) {
|
||||
return;
|
||||
}
|
||||
@@ -594,7 +588,7 @@ static void writeSym() {
|
||||
}
|
||||
}
|
||||
|
||||
static void writeMap() {
|
||||
void out_WriteMapIfAny() {
|
||||
if (!options.mapFileName) {
|
||||
return;
|
||||
}
|
||||
@@ -622,9 +616,3 @@ static void writeMap() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void out_WriteFiles() {
|
||||
writeROM();
|
||||
writeSym();
|
||||
writeMap();
|
||||
}
|
||||
|
||||
+3
-3
@@ -114,14 +114,14 @@ static int32_t computeRPNExpr(Patch const &patch, std::vector<Symbol> const &fil
|
||||
int32_t value;
|
||||
switch (command) {
|
||||
case RPN_ADD:
|
||||
value = popRPN(patch) + popRPN(patch);
|
||||
value = op_add(popRPN(patch), popRPN(patch));
|
||||
break;
|
||||
case RPN_SUB:
|
||||
value = popRPN(patch);
|
||||
value = popRPN(patch) - value;
|
||||
value = op_sub(popRPN(patch), value);
|
||||
break;
|
||||
case RPN_MUL:
|
||||
value = popRPN(patch) * popRPN(patch);
|
||||
value = op_mul(popRPN(patch), popRPN(patch));
|
||||
break;
|
||||
case RPN_DIV:
|
||||
value = popRPN(patch);
|
||||
|
||||
@@ -855,7 +855,6 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
|
||||
|
||||
// RAM sections can have a size, but don't get any data (they shouldn't have any)
|
||||
if (section->type != SECTTYPE_INVALID) {
|
||||
SectionTypeInfo const &typeInfo = sectionTypeInfo[section->type];
|
||||
// Otherwise, how would the type already be known at this point?
|
||||
assume(section->isAddressFixed);
|
||||
|
||||
@@ -866,7 +865,7 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
|
||||
"\"%s\" is implicitly defined as a %s section (being at address $%04" PRIx16
|
||||
"), but it has data! (Was a bad `__at()` value used?)",
|
||||
section->name.c_str(),
|
||||
typeInfo.name.c_str(),
|
||||
section->typeInfo().name,
|
||||
section->org
|
||||
);
|
||||
}
|
||||
@@ -876,7 +875,7 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
|
||||
"\"%s\" is implicitly defined as a %s section (being at address $%04" PRIx16
|
||||
"), but it doesn't have any data! (Was a bad `__at()` value used?)",
|
||||
section->name.c_str(),
|
||||
typeInfo.name.c_str(),
|
||||
section->typeInfo().name,
|
||||
section->org
|
||||
);
|
||||
}
|
||||
|
||||
+39
-17
@@ -56,6 +56,18 @@ static void checkPieceCompat(Section &target, Section const &other, size_t delta
|
||||
other.org
|
||||
);
|
||||
}
|
||||
} else if (other.org < delta) {
|
||||
// Check that `org` did not underflow. Note that it's safe for the above checks to
|
||||
// use an underflowed value, since their reported errors will still be accurate.
|
||||
fatalTwoAt(
|
||||
target,
|
||||
other,
|
||||
"Section \"%s\" contains %" PRIu16
|
||||
" bytes, but is also defined with a lower fixed address $%04" PRIx16,
|
||||
target.name.c_str(),
|
||||
target.size,
|
||||
other.org
|
||||
);
|
||||
}
|
||||
|
||||
target.isAddressFixed = true;
|
||||
@@ -115,8 +127,8 @@ static void mergeSections(Section &target, std::unique_ptr<Section> &&other) {
|
||||
*other,
|
||||
"Section \"%s\" is defined with type `%s`, but also with type `%s`",
|
||||
target.name.c_str(),
|
||||
sectionTypeInfo[target.type].name.c_str(),
|
||||
sectionTypeInfo[other->type].name.c_str()
|
||||
target.typeInfo().name,
|
||||
other->typeInfo().name
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,6 +158,15 @@ static void mergeSections(Section &target, std::unique_ptr<Section> &&other) {
|
||||
|
||||
case SECTION_FRAGMENT:
|
||||
checkPieceCompat(target, *other, target.size);
|
||||
// Check that `target.size += other->size` below will not overflow
|
||||
if (target.size + other->size > UINT16_MAX) {
|
||||
fatalTwoAt(
|
||||
target,
|
||||
*other,
|
||||
"Section \"%s\" fragments combined are larger than the GB address space",
|
||||
target.name.c_str()
|
||||
);
|
||||
}
|
||||
// Append `other` to `target`
|
||||
other->offset = target.size;
|
||||
target.size += other->size;
|
||||
@@ -181,7 +202,7 @@ void sect_AddSection(std::unique_ptr<Section> &§ion) {
|
||||
fatal(
|
||||
"Section \"%s\" is of type `%s`, which cannot be `UNION`ized",
|
||||
section->name.c_str(),
|
||||
sectionTypeInfo[section->type].name.c_str()
|
||||
section->typeInfo().name
|
||||
);
|
||||
} else {
|
||||
sections.add(section->name, std::move(section));
|
||||
@@ -214,6 +235,7 @@ static void doSanityChecks(Section §ion) {
|
||||
bankModeError = true;
|
||||
} else {
|
||||
section.type = SECTTYPE_ROM0;
|
||||
section.isBankFixed = false;
|
||||
}
|
||||
}
|
||||
if (options.isWRAM0Mode && section.type == SECTTYPE_WRAMX) {
|
||||
@@ -226,6 +248,7 @@ static void doSanityChecks(Section §ion) {
|
||||
bankModeError = true;
|
||||
} else {
|
||||
section.type = SECTTYPE_WRAM0;
|
||||
section.isBankFixed = false;
|
||||
}
|
||||
}
|
||||
if (options.isDmgMode && section.type == SECTTYPE_VRAM && section.isBankFixed
|
||||
@@ -243,18 +266,20 @@ static void doSanityChecks(Section §ion) {
|
||||
section.isAlignFixed = false;
|
||||
}
|
||||
|
||||
// The section's type is determined now, so we can get its type info
|
||||
SectionTypeInfo const &typeInfo = section.typeInfo();
|
||||
|
||||
// Too large an alignment may not be satisfiable
|
||||
if (section.isAlignFixed && (section.alignMask & sectionTypeInfo[section.type].startAddr)) {
|
||||
if (section.isAlignFixed && (section.alignMask & typeInfo.startAddr) > section.alignOfs) {
|
||||
error(
|
||||
"Section \"%s\" has type `%s`, which cannot be aligned to $%04x bytes",
|
||||
section.name.c_str(),
|
||||
sectionTypeInfo[section.type].name.c_str(),
|
||||
typeInfo.name,
|
||||
section.alignMask + 1
|
||||
);
|
||||
}
|
||||
|
||||
uint32_t minbank = sectionTypeInfo[section.type].firstBank,
|
||||
maxbank = sectionTypeInfo[section.type].lastBank;
|
||||
uint32_t minbank = typeInfo.firstBank, maxbank = typeInfo.lastBank;
|
||||
|
||||
if (!bankModeError && section.isBankFixed
|
||||
&& (section.bank < minbank || section.bank > maxbank)) {
|
||||
@@ -271,12 +296,12 @@ static void doSanityChecks(Section §ion) {
|
||||
}
|
||||
|
||||
// Check if section has a chance to be placed
|
||||
if (section.size > sectionTypeInfo[section.type].size) {
|
||||
if (section.size > typeInfo.size) {
|
||||
error(
|
||||
"Section \"%s\" is bigger than the max size for that type: $%" PRIx16 " > $%" PRIx16,
|
||||
section.name.c_str(),
|
||||
section.size,
|
||||
sectionTypeInfo[section.type].size
|
||||
typeInfo.size
|
||||
);
|
||||
}
|
||||
|
||||
@@ -300,24 +325,21 @@ static void doSanityChecks(Section §ion) {
|
||||
}
|
||||
|
||||
// Ensure the target address is valid
|
||||
if (section.org < sectionTypeInfo[section.type].startAddr
|
||||
|| section.org > sectTypeEndAddr(section.type)) {
|
||||
if (section.org < typeInfo.startAddr || section.org > typeInfo.endAddr()) {
|
||||
error(
|
||||
"Section \"%s\"'s fixed address $%04" PRIx16 " is outside of range [$%04" PRIx16
|
||||
"; $%04" PRIx16 "]",
|
||||
section.name.c_str(),
|
||||
section.org,
|
||||
sectionTypeInfo[section.type].startAddr,
|
||||
sectTypeEndAddr(section.type)
|
||||
typeInfo.startAddr,
|
||||
typeInfo.endAddr()
|
||||
);
|
||||
}
|
||||
|
||||
if (section.org + section.size > sectTypeEndAddr(section.type) + 1) {
|
||||
} else if (section.org + section.size > typeInfo.endAddr() + 1) {
|
||||
error(
|
||||
"Section \"%s\"'s end address $%04x is greater than last address $%04x",
|
||||
section.name.c_str(),
|
||||
section.org + section.size,
|
||||
sectTypeEndAddr(section.type) + 1
|
||||
typeInfo.endAddr() + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-12
@@ -2,65 +2,61 @@
|
||||
|
||||
#include "linkdefs.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
// The default values are the most lax, as they are used as-is by RGBASM; only RGBLINK has the full
|
||||
// info, so RGBASM's job is only to catch unconditional errors earlier.
|
||||
// clang-format off: nested initializers
|
||||
SectionTypeInfo sectionTypeInfo[SECTTYPE_INVALID] = {
|
||||
{
|
||||
.name = "WRAM0"s,
|
||||
.name = "WRAM0",
|
||||
.startAddr = 0xC000,
|
||||
.size = 0x2000, // Patched to 0x1000 if !isWRAM0Mode
|
||||
.firstBank = 0,
|
||||
.lastBank = 0,
|
||||
},
|
||||
{
|
||||
.name = "VRAM"s,
|
||||
.name = "VRAM",
|
||||
.startAddr = 0x8000,
|
||||
.size = 0x2000,
|
||||
.firstBank = 0,
|
||||
.lastBank = 1, // Patched to 0 if isDmgMode
|
||||
},
|
||||
{
|
||||
.name = "ROMX"s,
|
||||
.name = "ROMX",
|
||||
.startAddr = 0x4000,
|
||||
.size = 0x4000,
|
||||
.firstBank = 1,
|
||||
.lastBank = 65535,
|
||||
},
|
||||
{
|
||||
.name = "ROM0"s,
|
||||
.name = "ROM0",
|
||||
.startAddr = 0x0000,
|
||||
.size = 0x8000, // Patched to 0x4000 if !is32kMode
|
||||
.firstBank = 0,
|
||||
.lastBank = 0,
|
||||
},
|
||||
{
|
||||
.name = "HRAM"s,
|
||||
.name = "HRAM",
|
||||
.startAddr = 0xFF80,
|
||||
.size = 0x007F,
|
||||
.firstBank = 0,
|
||||
.lastBank = 0,
|
||||
},
|
||||
{
|
||||
.name = "WRAMX"s,
|
||||
.name = "WRAMX",
|
||||
.startAddr = 0xD000,
|
||||
.size = 0x1000,
|
||||
.firstBank = 1,
|
||||
.lastBank = 7,
|
||||
},
|
||||
{
|
||||
.name = "SRAM"s,
|
||||
.name = "SRAM",
|
||||
.startAddr = 0xA000,
|
||||
.size = 0x2000,
|
||||
.firstBank = 0,
|
||||
.lastBank = 255,
|
||||
},
|
||||
{
|
||||
.name = "OAM"s,
|
||||
.name = "OAM",
|
||||
.startAddr = 0xFE00,
|
||||
.size = 0x00A0,
|
||||
.firstBank = 0,
|
||||
|
||||
@@ -8,6 +8,23 @@
|
||||
|
||||
#include "helpers.hpp" // assume, clz, ctz
|
||||
|
||||
// Signed overflow is UB, so these compute with unsigned arithmetic, which wraps around.
|
||||
// Casting back is OK because the types implement two's complement behavior.
|
||||
|
||||
int32_t op_add(int32_t augend, int32_t addend) {
|
||||
return static_cast<int32_t>(static_cast<uint32_t>(augend) + static_cast<uint32_t>(addend));
|
||||
}
|
||||
|
||||
int32_t op_sub(int32_t minuend, int32_t subtrahend) {
|
||||
return static_cast<int32_t>(static_cast<uint32_t>(minuend) - static_cast<uint32_t>(subtrahend));
|
||||
}
|
||||
|
||||
int32_t op_mul(int32_t multiplicand, int32_t multiplier) {
|
||||
return static_cast<int32_t>(
|
||||
static_cast<uint32_t>(multiplicand) * static_cast<uint32_t>(multiplier)
|
||||
);
|
||||
}
|
||||
|
||||
int32_t op_divide(int32_t dividend, int32_t divisor) {
|
||||
assume(divisor != 0); // Division by 0 is UB
|
||||
assume(dividend != INT32_MIN || divisor != -1); // INT32_MIN / -1 is UB
|
||||
|
||||
+22
-8
@@ -4,23 +4,37 @@
|
||||
|
||||
#include "helpers.hpp"
|
||||
|
||||
// We do not build `make develop` with `-fsanitize=leak` because macOS clang++ does not support it.
|
||||
// Instead, we enable ASan (`-fsanitize=address`) to check for memory leaks in all four programs.
|
||||
#ifdef __clang__
|
||||
#if __has_feature(address_sanitizer) && !defined(__SANITIZE_ADDRESS__)
|
||||
#define __SANITIZE_ADDRESS__
|
||||
#endif
|
||||
#if __has_feature(address_sanitizer) && !defined(__SANITIZE_UNDEFINED__)
|
||||
#define __SANITIZE_UNDEFINED__
|
||||
#endif
|
||||
#if !defined(NDEBUG) && defined(__SANITIZE_ADDRESS__) && !defined(__APPLE__)
|
||||
#endif
|
||||
|
||||
#if !defined(NDEBUG) && defined(__SANITIZE_ADDRESS__)
|
||||
extern "C" {
|
||||
char const *__asan_default_options(void) {
|
||||
return "detect_leaks=1"
|
||||
":detect_stack_use_after_return=1"
|
||||
return ":check_initialization_order=1"
|
||||
":detect_invalid_pointer_pairs=2"
|
||||
":check_initialization_order=1"
|
||||
// `detect_leaks` is not supported on macOS.
|
||||
#ifndef __APPLE__
|
||||
":detect_leaks=1"
|
||||
#endif
|
||||
":detect_stack_use_after_return=1"
|
||||
// ":fast_unwind_on_malloc=0" // Enable this if ASan outputs bad backtraces
|
||||
":print_legend=0"
|
||||
":strict_init_order=1"
|
||||
":strict_string_checks=1"
|
||||
":print_legend=0";
|
||||
":strict_string_checks=1";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !defined(NDEBUG) && defined(__SANITIZE_UNDEFINED__)
|
||||
extern "C" {
|
||||
char const *__ubsan_default_options(void) {
|
||||
return "print_stacktrace=1";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
SECTION "test", ROM0
|
||||
db 1, 2, 3
|
||||
align 16, 2
|
||||
@@ -0,0 +1,3 @@
|
||||
error: Section already contains 3 bytes, higher than this aligned address $0002
|
||||
at align-beyond-size.asm(3)
|
||||
Assembly aborted with 1 error
|
||||
@@ -0,0 +1 @@
|
||||
assert x, "oops \0 null"
|
||||
@@ -0,0 +1,2 @@
|
||||
FATAL: Assertion messages cannot contain '\0' characters
|
||||
at assert-nul.asm(1)
|
||||
@@ -0,0 +1,20 @@
|
||||
opt Q8
|
||||
println strfmt("%f", 1.999)
|
||||
println strfmt("%f", 2.999)
|
||||
println strfmt("%f", 3.999)
|
||||
println strfmt("%f", 16777215.999)
|
||||
println strfmt("%f", 4294967295.999)
|
||||
|
||||
opt Q16
|
||||
println strfmt("%f", 1.999999)
|
||||
println strfmt("%f", 2.999999)
|
||||
println strfmt("%f", 3.999999)
|
||||
println strfmt("%f", 65535.999999)
|
||||
println strfmt("%f", 4294967295.999999)
|
||||
|
||||
opt Q24
|
||||
println strfmt("%f", 1.999999999)
|
||||
println strfmt("%f", 2.999999999)
|
||||
println strfmt("%f", 3.999999999)
|
||||
println strfmt("%f", 255.999999999)
|
||||
println strfmt("%f", 4294967295.999999999)
|
||||
@@ -0,0 +1,12 @@
|
||||
warning: Magnitude of fixed-point constant is too large [-Wlarge-constant]
|
||||
at fixed-point-carry.asm(5)
|
||||
warning: Magnitude of fixed-point constant is too large [-Wlarge-constant]
|
||||
at fixed-point-carry.asm(6)
|
||||
warning: Magnitude of fixed-point constant is too large [-Wlarge-constant]
|
||||
at fixed-point-carry.asm(12)
|
||||
warning: Magnitude of fixed-point constant is too large [-Wlarge-constant]
|
||||
at fixed-point-carry.asm(13)
|
||||
warning: Magnitude of fixed-point constant is too large [-Wlarge-constant]
|
||||
at fixed-point-carry.asm(19)
|
||||
warning: Magnitude of fixed-point constant is too large [-Wlarge-constant]
|
||||
at fixed-point-carry.asm(20)
|
||||
@@ -0,0 +1,15 @@
|
||||
2.00000
|
||||
3.00000
|
||||
4.00000
|
||||
0.00000
|
||||
0.00000
|
||||
2.00000
|
||||
3.00000
|
||||
4.00000
|
||||
0.00000
|
||||
0.00000
|
||||
2.00000
|
||||
3.00000
|
||||
4.00000
|
||||
0.00000
|
||||
0.00000
|
||||
@@ -5,9 +5,9 @@ $18B
|
||||
$62B85
|
||||
$4
|
||||
$10000
|
||||
$20000
|
||||
$10000
|
||||
$10000
|
||||
$10000
|
||||
$20000
|
||||
$C570A
|
||||
$13333
|
||||
$13333
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
SECTION FRAGMENT "test", ROM0
|
||||
ds $11, 42
|
||||
|
||||
SECTION FRAGMENT "test", ROM0[$10]
|
||||
@@ -0,0 +1,2 @@
|
||||
FATAL: Section "test" already contains 17 bytes, higher than this fragment's fixed address $0010
|
||||
at fragment-org-beyond-size.asm(4)
|
||||
@@ -0,0 +1,3 @@
|
||||
SECTION "test", ROM0
|
||||
LOAD "test", WRAM0
|
||||
ENDL
|
||||
@@ -0,0 +1,4 @@
|
||||
FATAL: Section "test" already defined
|
||||
at load-already-defined.asm(2)
|
||||
and also:
|
||||
at load-already-defined.asm(1)
|
||||
@@ -12,9 +12,8 @@ error: Macro argument `\<2147483647>` not defined
|
||||
at negative-macro-args.asm::mac(11) <- negative-macro-args.asm(21)
|
||||
error: Macro argument `\<-2147483648>` not defined
|
||||
at negative-macro-args.asm::mac(14) <- negative-macro-args.asm(21)
|
||||
error: Number in bracketed macro argument is too large
|
||||
error: Macro argument `\<-2147483648>` not defined
|
||||
at negative-macro-args.asm::mac(14) <- negative-macro-args.asm(21)
|
||||
while expanding symbol `i`
|
||||
error: Macro argument `\<-2147483648>` not defined
|
||||
at negative-macro-args.asm::mac(15) <- negative-macro-args.asm(21)
|
||||
error: Number in bracketed macro argument is too large
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
0: ==
|
||||
-8: ==
|
||||
2147483647: ==
|
||||
-2147483648: == >
|
||||
-2147483648: ==
|
||||
2147483648: == >
|
||||
-1: G == G
|
||||
4294967295: G == >
|
||||
|
||||
@@ -12,8 +12,6 @@ error: Macro argument `\1` not defined
|
||||
at negative-shifted-macro-args.asm::test(15) <- negative-shifted-macro-args.asm(22)
|
||||
error: Macro argument `\<-1>` not defined
|
||||
at negative-shifted-macro-args.asm::test(16) <- negative-shifted-macro-args.asm(22)
|
||||
error: Number in bracketed macro argument is too large
|
||||
error: Macro argument `\<-2147483648>` not defined
|
||||
at negative-shifted-macro-args.asm::test(19) <- negative-shifted-macro-args.asm(22)
|
||||
error: syntax error, unexpected >
|
||||
at negative-shifted-macro-args.asm::test(19) <- negative-shifted-macro-args.asm(22)
|
||||
Assembly aborted with 9 errors
|
||||
Assembly aborted with 8 errors
|
||||
|
||||
@@ -11,3 +11,4 @@ $0
|
||||
|
||||
|
||||
$A
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
SECTION "test", ROM0
|
||||
SECTION "test", WRAM0
|
||||
@@ -0,0 +1,4 @@
|
||||
FATAL: Section "test" already defined
|
||||
at section-already-defined.asm(2)
|
||||
and also:
|
||||
at section-already-defined.asm(1)
|
||||
@@ -0,0 +1,2 @@
|
||||
SECTION "test\0 foo", ROM0
|
||||
SECTION "test\0 bar", ROM0
|
||||
@@ -0,0 +1,2 @@
|
||||
FATAL: Section names cannot contain '\0' characters
|
||||
at section-name-nul.asm(1)
|
||||
@@ -0,0 +1,6 @@
|
||||
MACRO m
|
||||
println \1
|
||||
shift $8000_0000 ; INT32_MIN
|
||||
println \1
|
||||
ENDM
|
||||
m 1, 2, 3
|
||||
@@ -0,0 +1,2 @@
|
||||
warning: Cannot shift macro arguments past their beginning [-Wmacro-shift]
|
||||
at shift-int-min.asm::m(3) <- shift-int-min.asm(6)
|
||||
@@ -0,0 +1,2 @@
|
||||
$1
|
||||
$1
|
||||
@@ -0,0 +1,22 @@
|
||||
section "test", rom0
|
||||
|
||||
opt Wtruncation=1
|
||||
|
||||
; good
|
||||
ld hl, sp + 0
|
||||
ld hl, sp + 127
|
||||
ld hl, sp - 128
|
||||
ld hl, sp + -128
|
||||
add sp, 0
|
||||
add sp, 127
|
||||
add sp, -128
|
||||
|
||||
; bad
|
||||
ld hl, sp + 128
|
||||
ld hl, sp - 129
|
||||
ld hl, sp + 255
|
||||
ld hl, sp - 256
|
||||
add sp, 128
|
||||
add sp, -129
|
||||
add sp, 255
|
||||
add sp, -256
|
||||
@@ -0,0 +1,16 @@
|
||||
warning: Expression must be signed 8-bit [-Wtruncation]
|
||||
at sp-signed-truncation.asm(15)
|
||||
warning: Expression must be signed 8-bit [-Wtruncation]
|
||||
at sp-signed-truncation.asm(16)
|
||||
warning: Expression must be signed 8-bit [-Wtruncation]
|
||||
at sp-signed-truncation.asm(17)
|
||||
warning: Expression must be signed 8-bit [-Wtruncation]
|
||||
at sp-signed-truncation.asm(18)
|
||||
warning: Expression must be signed 8-bit [-Wtruncation]
|
||||
at sp-signed-truncation.asm(19)
|
||||
warning: Expression must be signed 8-bit [-Wtruncation]
|
||||
at sp-signed-truncation.asm(20)
|
||||
warning: Expression must be signed 8-bit [-Wtruncation]
|
||||
at sp-signed-truncation.asm(21)
|
||||
warning: Expression must be signed 8-bit [-Wtruncation]
|
||||
at sp-signed-truncation.asm(22)
|
||||
@@ -26,3 +26,7 @@ PRINTLN STRFMT("%d eol %", 1)
|
||||
PRINTLN STRFMT("invalid %w spec", 42)
|
||||
|
||||
PRINTLN STRFMT("one=%d two=%d three=%d", 1)
|
||||
|
||||
DEF NUL EQUS STRFMT("%s \0 %s", "goodbye", "world")
|
||||
ASSERT #NUL === "goodbye \0 world"
|
||||
PRINTLN #NUL
|
||||
|
||||
Binary file not shown.
+7
-3
@@ -2,6 +2,10 @@
|
||||
|
||||
export LC_ALL=C
|
||||
|
||||
# Screen width for help/usage text (for reproducible test results)
|
||||
export COLUMNS=79
|
||||
shopt -u checkwinsize # Prevent subsequent commands from resetting `COLUMNS`
|
||||
|
||||
# Game Boy release date, 1989-04-21T12:34:56Z (for reproducible test results)
|
||||
export SOURCE_DATE_EPOCH=609165296
|
||||
|
||||
@@ -86,9 +90,9 @@ for i in *.asm notexist.asm; do
|
||||
# "make-deps.asm" refers to its output filename in its desired output.
|
||||
# "notexist.asm" doesn't exist, so there's no point in trying to `cat` it.
|
||||
# Skip testing with stdin input for those files.
|
||||
if [[ "$i" = include-recursion.asm || "$i" = make-deps.asm || "$i" = notexist.asm ]]; then
|
||||
continue
|
||||
fi
|
||||
case "$i" in
|
||||
include-recursion.asm | make-deps.asm | notexist.asm) continue;;
|
||||
esac
|
||||
|
||||
# shellcheck disable=SC2002 # (This use of `cat` intentionally makes the input an unseekable pipe.)
|
||||
cat "$i" | "$RGBASM" "${RGBASMFLAGS[@]}" -o "$o" - >"$output" 2>"$errput"
|
||||
|
||||
Vendored
+7
-3
@@ -69,12 +69,16 @@ case "$actionname" in
|
||||
|
||||
action() {
|
||||
if [ ! -d "$EXT_TEST_REPO" ]; then
|
||||
git clone "https://$EXT_TEST_DOMAIN/$EXT_TEST_OWNER/$EXT_TEST_REPO.git" \
|
||||
git clone "https://$EXT_TEST_DOMAIN/$EXT_TEST_OWNER/$EXT_TEST_REPO" \
|
||||
--revision="$EXT_TEST_COMMIT" --depth=1 --recursive --shallow-submodules \
|
||||
--config advice.detachedHead=false
|
||||
fi
|
||||
--jobs="$(getconf _NPROCESSORS_ONLN)" --config advice.detachedHead=false
|
||||
pushd "$EXT_TEST_REPO"
|
||||
else
|
||||
pushd "$EXT_TEST_REPO"
|
||||
git remote set-url origin "https://$EXT_TEST_DOMAIN/$EXT_TEST_OWNER/$EXT_TEST_REPO"
|
||||
git fetch --depth=1 --recurse-submodules --jobs="$(getconf _NPROCESSORS_ONLN)" origin -- "$EXT_TEST_COMMIT"
|
||||
git checkout --force --detach "$EXT_TEST_COMMIT" --
|
||||
fi
|
||||
if [ -f "../patches/$EXT_TEST_REPO.patch" ]; then
|
||||
git apply --ignore-whitespace "../patches/$EXT_TEST_REPO.patch"
|
||||
fi
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
EXT_TEST_OWNER=ISSOtm
|
||||
EXT_TEST_REPO=gb-starter-kit
|
||||
EXT_TEST_DOMAIN=codeberg.org
|
||||
EXT_TEST_COMMIT=83162620eece547d9fb95c3e0b89d7a8812bcb7c
|
||||
EXT_TEST_COMMIT=e3e395fdd6cdbfa168cf1b814d73c93ab167c7d8
|
||||
EXT_TEST_IS_NONFREE=false
|
||||
|
||||
EXT_TEST_TARGET=all
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
EXT_TEST_OWNER=zladx
|
||||
EXT_TEST_REPO=LADX-Disassembly
|
||||
EXT_TEST_DOMAIN=github.com
|
||||
EXT_TEST_COMMIT=76e9128437b90708ee27dce2d6c9752ad9f22f2d
|
||||
EXT_TEST_COMMIT=968956783bd7ca4487a5712c46e98ae894d8dc0f
|
||||
EXT_TEST_IS_NONFREE=true
|
||||
|
||||
EXT_TEST_TARGET=default
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
EXT_TEST_OWNER=pret
|
||||
EXT_TEST_REPO=pokecrystal
|
||||
EXT_TEST_DOMAIN=github.com
|
||||
EXT_TEST_COMMIT=3438c7003a57fa2987fcb223d14b660761b33c64
|
||||
EXT_TEST_COMMIT=7a7881d0d62e0ddbd82dcf10e7116807487ac651
|
||||
EXT_TEST_IS_NONFREE=true
|
||||
|
||||
EXT_TEST_TARGET=compare
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
EXT_TEST_OWNER=pret
|
||||
EXT_TEST_REPO=pokered
|
||||
EXT_TEST_DOMAIN=github.com
|
||||
EXT_TEST_COMMIT=405b6246372d7e5a2cb029cbb65219b13286b8c9
|
||||
EXT_TEST_COMMIT=a1a22aaf84d1675bcdbaeb194592379d586d838e
|
||||
EXT_TEST_IS_NONFREE=true
|
||||
|
||||
EXT_TEST_TARGET=compare
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
export LC_ALL=C
|
||||
|
||||
# Screen width for help/usage text (for reproducible test results)
|
||||
export COLUMNS=79
|
||||
shopt -u checkwinsize # Prevent subsequent commands from resetting `COLUMNS`
|
||||
|
||||
tmpdir="$(mktemp -d)"
|
||||
# shellcheck disable=SC2064 # (Immediate expansion is the desired behavior.)
|
||||
trap "cd; rm -rf ${tmpdir@Q}" EXIT
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
error: Color #ff800080 is neither transparent (alpha < 16) nor opaque (alpha >= 240) (first seen at (0, 8))
|
||||
Conversion aborted after 1 error
|
||||
FATAL: Image contains 1 ambiguous color (neither transparent nor opaque)
|
||||
Conversion aborted after 2 errors
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user