Compare commits

...
110 Commits
Author SHA1 Message Date
ISSOtm f7b8207322 Release v1.0.4 2026-09-22 12:47:35 -04:00
Rangi 33ef57ea49 Mention a sed command for updating the man page dates 2026-09-22 12:35:31 -04:00
Rangi 6e5d7a9823 Correct the ROM hash for gb-starter-kit
I hadn't set the right `SOURCE_DATE_EPOCH`...
2026-09-22 12:33:10 -04:00
Rangi 4d5b38ebfe Update external test commits 2026-09-22 12:29:08 -04:00
Rangi eca8c1f3e7 Do not deprecate implicit transparent color 0 2026-09-22 12:03:33 -04:00
Rangi bc1ed831ec Remove leftover .asm file 2026-09-21 21:41:00 -04:00
ISSOtm 4eba73d7b2 Enable -Werror in all release build processes
Since we build in a different configuration, it's plausible that some
warnings would only surface in Release (optimised) builds.
That should be an immediate blocker, so let's make it fail loudly.
2026-09-21 20:36:33 -04:00
ISSOtm 970342f564 Avoid double-zipping all release artifacts
Follow-up to 306a83a4, actually fixing the release script (oops!)
but also propagating the change to other release artifacts,
since unlike the regression testing workflows, we make the archives ourselves.
2026-09-21 20:36:32 -04:00
ISSOtm c43b123183 Add a reminder to read the release process doc 2026-09-21 19:36:02 -04:00
Eldred Habert 306a83a4b4 Avoid double-zipping the binaries
https://github.blog/changelog/2026-02-26-github-actions-now-supports-uploading-and-downloading-non-zipped-artifacts/
2026-09-21 18:34:50 -04:00
Rangi 6da3c3c38d Only include binaries in CPack zip, not man pages 2026-09-21 18:34:50 -04:00
Eldred Habert 629074bc28 Omit build commands from auto-generated install.sh (#2138)
Fixes #2045
2026-09-21 16:04:08 -04:00
Eldred Habert 63a06f02c3 Fix constrained empty sections escaping their regions (#2137) 2026-09-21 15:54:10 -04:00
Rangi42 c8b05a6a46 Use += in Bash shell script 2026-09-21 13:27:03 -04:00
Rangi 2882424407 Cancel in-progress CI jobs when a newer commit supersedes them (#2136) 2026-09-21 19:21:28 +02:00
RangiandEldred Habert 04bbe80ae0 Some shell script style improvements (#2087)
* Escape special characters in filenames when comparing gfx .err output
* Use `if` instead of `&&`
* Use `case` instead of `if` disjunction

---------

Co-authored-by: Eldred Habert <[email protected]>
2026-09-21 10:15:40 -04:00
Rangi 84c1671cb3 Correct documentation about when SIZEOF and STARTOF are constant 2026-09-21 15:47:25 +02:00
Rangi a2dd318f1d Remove executable status from non-script files (git update-index --chmod=-x) 2026-09-20 19:46:40 -04:00
Rangi ace8db1b73 Fix RGBGFX -t/--trim-end combined with -B/--background-color for non-deduplicated output (#2130) 2026-09-20 15:05:06 -04:00
Rangi 408e861ecc Correct warning message about different RGBGFX -L and -r widths 2026-09-20 14:32:04 -04:00
ISSOtm e1ed304e6b Apply the same enabling logic for UBSan as for ASan 2026-09-19 23:30:35 +02:00
ISSOtm bb0fc08980 Enable our default ASan flags on macOS
Except for the leak detection (Clang complains it's not supported there),
for which I have corrected the comment and its location.
2026-09-19 23:30:35 +02:00
Rangi b7f6c641f1 Define more debug options for make develop (#2124) 2026-09-19 22:07:10 +02:00
Rangi b80bbe0fe6 Correct documentation for rgbgfx -x re: interaction with -N (#2126) 2026-09-19 14:40:02 -04:00
Rangi c151450d9f Fix underflowing SECTION FRAGMENT fixed address in RGBLINK (#2122) 2026-09-18 22:00:27 -04:00
Rangi 76bba2809b Fix incorrect EMPTY space in .map files (#2119) 2026-09-19 03:17:15 +02:00
Eldred Habert 44328d522b Avoid using snprintf where GCC can check sizes (#2121)
Interestingly, Clang's `-Wformat-overflow` seems to only warn
about buffers so small they will *always* underflow,
whereas GCC tries to infer whether they can overflow *at all*.
(I'm guessing they lean towards false-negatives and false-positives resp.)

Anyway, calling `sprintf` here remains safe, since GCC (notably, via CI)
checks our work, and it simplifies the code ever so slightly while
also providing an (admittedly negligible) performance improvement.

Note that there may be more locations where we could use `sprintf`,
but a cursory glance at our other uses of `snprintf` didn't seem fruitful.
(One way to test is to set the target buffer size to 1, and see if a
warning pops up for such a trivially-wrong size. If not, you can be certain
the compiler won't be able to help you with a more realistic size.)
2026-09-19 03:15:07 +02:00
Rangi 142eae4558 Fix repeated "And N more" in overlay error message (#2118) 2026-09-18 17:35:30 -04:00
Rangi 89403ca23b Fix RGBGFX to support -C/--color-curve in -r/--reverse mode (#2110)
Clarifies error message when RGBGFX reverse mode has `-c` mismatching `-p`.
2026-09-18 17:35:22 -04:00
Eldred Habert 0286d4119e Check out less of the repo for contrib verification
Shorter checkout time = good, I think.
2026-09-18 21:45:35 +02:00
Eldred Habert c803a20259 Only check contrib scripts when one of them changes
Less unnecessary CI!
2026-09-18 21:39:30 +02:00
ISSOtm aab22852d3 Factor out free space manipulation 2026-09-18 21:19:37 +02:00
ISSOtm 0e41ace1e3 Refactor section placement algorithm
Massively simplified, by not trying to shoehorn differently-behaving
cases into the same one loop!

This may have introduced bugs (the test suite has caught three different
oversights, two of which via external projects, in fact!), but this also
makes the logic clearer and more streamlined, so that we are also less
likely to have any latent or future bugs.

In particular, previously, the first iteration of the loop could attempt
placement at an address not matching the section's constraints,
which made advancing to the next target address unnecessarily complicated
(https://github.com/gbdev/rgbds/pull/2064#discussion_r3985466596),
among other weirdness. The code ended up being defensive, and thus the
overall logic was murky.

I'm also expecting that this should provide a performance improvement due
to being essentially a form of loop-invariant code motion (and very likely
one a compiler couldn't have performed automatically), though I haven't
measured.
2026-09-18 21:19:37 +02:00
ISSOtm ce5c8ce8ac Respect zero-size sections' align offsets
Fixes #2095

This code pre-dates the introduction of alignment offsets,
so it was never updated to take it into account.

That said, some of the code that I have factored out is a little awkward because
it will be used in an upcoming refactoring of the whole assignment loop,
since that turns out to be more byzantine than it ought to be due to
trying to handle a too disparate set of cases in a single loop.
2026-09-18 21:19:37 +02:00
ISSOtm c49828f7c9 Fix memory region alignment checks
Turns out the checks could fail spuriously if the starting alignment is non-zero,
e.g. `align 8,$C0` in HRAM. (Fixes #2113.)
This was tripped up by a test added to exercise the linker's section placement algorithm
on a similar aligment-related issue. :D

Note that this may be caused by the code in question pre-dating alignment offsets,
and we never checked. That said, these checks only work due to the regions' starting
addresses being no more aligned than their size (..if that makes sense?), so allowing
custom memory regions (#524) could violate that assumption and make those checks incorrect.
2026-09-18 21:19:37 +02:00
ISSOtm 5b9903f358 Mark a link test as failed if assembling fails
Assembling is never *supposed* to fail, but in this case I ended up adding
a test case that triggered a bug in the assembler, and that caused the rest
of the test to become garbled. This is thus more robust.
2026-09-18 21:19:37 +02:00
ISSOtm d43ef53745 Remove unreachable bank range check
The only case where it is not generated by our own code
(which we ought to be `assume`ing is correct) is when the bank
is used-specified, but that is caught by an earlier check.
2026-09-18 21:19:37 +02:00
ISSOtm ef709fade6 Refactor and fix sect packing's bank iteration
Moving both pieces of related code together, as it were.
This also ends up changing the behaviour of scrambling,
where sections could “leak” out of the scrambling's bank pool
and resume being placed normally.

We have discussed it offline, and decided that this was a bug.
The test suite has been updated accordingly, which even gives
the occasion to move this test from a special case to the main
generic loop!
2026-09-18 21:19:37 +02:00
ISSOtm 8685bcbfbf Refactor all scrambling-related dispatch into a single func
Yay for (poor man's) pattern matching! 🦀
2026-09-18 21:19:37 +02:00
ISSOtm 1ca4282e31 Move a function closer to its usage point
Trying to keep code grouped together well.
2026-09-18 21:19:37 +02:00
Eldred Habert afe1f01d00 Fix linker script align 16 in floating address context (#2115)
Turns out we were just using the wrong var lmao
2026-09-17 20:46:55 -04:00
Rangi 8bf5957c83 Fix RGBLINK assertion failure when -s and -c invalidly match (#2109) 2026-09-18 01:34:14 +02:00
ISSOtm f234796428 Avoid allocating for our linkdef names (#2111)
This reverts a change introduced in fd78a9ae8, though it wasn't that commit's main point
so I'm feeling okay with undoing that.

This feels like an overkill change, using a static string is good enough for this
since we never modify this. I have considered using `string_view` instead, to have
the best of both worlds, but that's not NUL-terminated so our print functions
get a little grumpy.
2026-09-17 13:58:30 -04:00
ISSOtm 0b9f8ab523 Use Section::typeInfo() more 2026-09-17 13:19:26 -04:00
Eldred Habert 3b250d5386 Emit auxiliary files even if asserts fail (#2104)
Closes #2100
2026-09-17 17:47:53 +02:00
Rangi b4988afc8a Fix RGBLINK ignoring alignment constraints when placing empty sections
Fixes #2095
2026-09-17 11:16:18 -04:00
ISSOtm 6f69867d04 Make more RGBLINK tests use at-files
Less special handling = good

Possibly this can be done to more tests, but this is what I found.
2026-09-17 10:25:47 -04:00
ISSOtm 960f82126f Fix to actually allow one-past-the-end zero-sized sections in linker scripts 2026-09-17 10:19:47 -04:00
ISSOtm b9b5c9a2b4 Avoid reporting redundant errors on too-large fixed sect addr
This would happen for any such section of non-zero size.
2026-09-17 10:17:26 -04:00
Rangi aa657cd481 Make array buffers for snprintf just large enough for their uint32_t and uint16_t types
Note that `bank` can store 9 bytes (8 digits plus NUL terminator)
even though `section.bank` should never go above 16-bit (4 digits).
2026-09-17 10:12:05 -04:00
ISSOtm e39dbe1752 Remove no-op in link test script
No, `printf` does not do that.
This works because making it read from stdin suppresses
any words after the first one.
2026-09-17 09:35:32 -04:00
ISSOtm 9f175f7923 Use SectionTypeInfo struct member functions for section type info 2026-09-17 09:33:21 -04:00
Rangi b2967ff509 Correct rgblink(5) linkerscript doc to match behavior of unbanked ROMX/WRAMX (#2103) 2026-09-17 05:32:15 +02:00
RangiandEldred Habert e8d9eb4511 Document how multiple rgblink -S get merged (#2098)
Co-authored-by: Eldred Habert <[email protected]>
2026-09-17 01:52:10 +02:00
Rangi 589616338f Remove redundant conditional check (#2099)
If `nbBanks > 1` is true, then `sectionTypeInfo[type].firstBank !=
sectionTypeInfo[type].lastBank` must also be true, so checking
whether either is true is unnecessary.

(On the other hand, if `nbBanks == 1`, then we might still have
`sectionTypeInfo[type].firstBank != sectionTypeInfo[type].lastBank`,
since only one of many valid banks for `type` could be in use.)
2026-09-17 00:27:22 +02:00
ISSOtm 6587941512 Allow scrambling SRAM banks up to the full 8-bit range 2026-09-15 23:06:19 -04:00
Rangi 004749ea75 Halt processing when any ambiguous-transparency colors exist (#2093) 2026-09-16 04:03:56 +02:00
Rangi f4c6221eb1 Fix underflow of ALIGN[16, offset] (#2090) 2026-09-15 20:22:33 -04:00
Rangi d81ab2fb23 Fix underflowing SECTION FRAGMENT fixed address (#2091) 2026-09-15 20:22:19 -04:00
Rangi febdf63896 Fix invalid UTF-8 byte $E2 at the end of a symbol name reading past the string's end (#2092)
This is only possible in an invalid object file, but we do have other safety checks for those.

Assisted-by: opencode:big-pickle
2026-09-15 20:22:06 -04:00
Rangi b0f49a36b7 Fix -t/--tiny for ROMX in bank 1, and -w/--wramx for WRAMX in bank 1 (#2089) 2026-09-14 15:53:45 -04:00
Rangi ed1ac7309b Fix overflow with high fixed-point magnitudes 2026-09-14 14:27:16 -04:00
Rangi d082f24a2d Fix rounding of fixed-point constants where fractional digits should carry into integer part (#2088) 2026-09-14 13:52:39 -04:00
Rangi 7746ddbf2b Fix reverse-graphics check for whether the tilemap references too-high tile IDs in bank 1 (#2086) 2026-09-14 10:26:57 -04:00
Eldred Habert 0fe43bf0cb Use CMake-native methods to do our static-build macOS trickery (#2083)
This should remain equivalent for our current setups,
but play nicer with anyone wanting to experiment further.

It *may* break for someone building on an Intel Mac,
but this is a libpng limitation and thus out of our control.
2026-09-14 09:55:34 -04:00
Eldred Habert c8e8a2f522 Avoid duplicating warning flags when linking with the Makefile (#2085)
The warning flag list with `make develop` is quite long,
and this duplication is making those command lines particularly indigestible.
2026-09-13 20:35:54 -04:00
Eldred Habert de1e1ff688 Actually silence -Wformat et al. on MinGW (#2084)
Thus far, we have just ensured they wouldn't fail the build,
but they still pollute the build log, so we should silence them entirely.
2026-09-13 20:25:13 -04:00
Rangi f8a1da71c1 Only sort generated palette colors by PLTE order for indexed PNG images
Non-indexed images can still have a PLTE chunk. We should use
`sortRgb` instead of `sortIndexed` for them, since their pixels' colors
may not all be present in the PLTE chunk and would be unsortable.
2026-09-13 18:53:31 -04:00
Rangi 49b3f566b1 Fix handling of PNG files with RGB or RGBA color type that have a PLTE chunk 2026-09-13 16:09:49 -04:00
Rangi 82f58b95ac Fix reversing some images with an input tileset (#2081) 2026-09-13 21:00:58 +02:00
RangiandEldred Habert e9fc7b3135 Warn when tile or palette IDs cannot fit in the tilemap or attrmap respectively (#2077)
Co-authored-by: Eldred Habert <[email protected]>
2026-09-13 19:18:24 +02:00
Rangi 47bbd30f2c Fix a failing assertion when two sections have the same name (#2079) 2026-09-12 17:58:37 -04:00
Rangi 520cc2a80d Fix reversing a tilemapped image with -x trimmed tiles 2026-09-12 15:38:34 -04:00
Rangi a703b8c704 Ignore .pdb files (generated by CMake on Windows) 2026-09-12 14:49:17 -04:00
Rangi e9c800b3be Fix reversing 1bpp images
Fixes #2075
2026-09-12 14:17:33 -04:00
Rangi 8a7019e1f8 Only register colors inside the -L/--slice region, including transparent color
Fixes #2074
2026-09-12 12:52:35 -04:00
Rangi 439a7a1828 Fix reversing multiple palettes with fewer than 4 colors per palette
Fixes #2073
2026-09-12 11:50:47 -04:00
Rangi 438b943a1a Make background tile IDs consistent whether or not deduplication is enabled
Fixes #2072
2026-09-11 20:45:52 -04:00
Erickson Munoz 5a2927778f Remove extra parentheses in man page comment (#2066) 2026-09-11 20:40:06 -04:00
RangiandISSOtm 9e785495ff Fix crash when a background tile first occurs after both tile banks are full (#2070)
Co-authored-by: ISSOtm <[email protected]>
2026-09-12 01:04:31 +02:00
ISSOtm 9d69a08909 Determine the number of fetch/clone jobs ourselves
Apparently our CI has some versions of Git too old to support zero to mean that.
2026-09-11 16:31:19 -04:00
ISSOtm 970bb84450 Fix updating ext-test repos from existing copies
The current setup fails with a "cannot read tree" error.

Also added flags to parallelise each fetch, which is relevant when submodules
are involved. (So, pretty much just starter-kit, haha.)
It would be nice to also parallelise the various fetches, but that would hit
the same remote multiple times, and could lead to rate-limiting...
2026-09-11 16:17:26 -04:00
Rangi a662cad7ce Fix infinite loop when an aligned HRAM section cannot be placed (#2064) 2026-09-11 05:21:26 +02:00
Rangi e85375f3e1 Avoid truncation warning from sp instruction test 2026-09-10 19:09:15 -04:00
Rangi 429a2325fb Add test case for reversing image with transparent color 2026-09-10 18:18:37 -04:00
Rangi 9c5e872f9c First color of -c palettes with transparent pixels is transparent $8000, not unspecified $ffff 2026-09-10 16:41:32 -04:00
Rangi 25240e9fc0 Fix but deprecate an implicit transparent color #0 2026-09-10 19:32:00 +02:00
Rangi 3112239174 Fix contrib/view_palettes.sh to allow output to stdout with - 2026-09-10 19:32:00 +02:00
Rangi 8a6b1946e3 Parse inline palette specs after getting CLI palette size limit (#2054) 2026-09-10 18:54:24 +02:00
Max Freedom Pollard 631ef003e7 Avoid signed overflow in RGBLINK's +, -, and * (#2060)
RGBASM computes these with unsigned arithmetic and casts back, since
signed overflow is UB in C++, but RGBLINK's RPN evaluator used `int32_t`
directly, so `src/link/patch.cpp` tripped UBSan on lines 117, 121, and
124. Share the three operators through `opmath.cpp` so both evaluators
stay in step.
2026-09-07 16:15:07 -04:00
Rangi fdd6cece30 Warn about signed 8-bit truncation for add sp, e8 and ld hl, sp + e8 2026-09-07 19:28:45 +02:00
Rangi db9509fba0 Describe rlc/rrc/rlca/rrca" as capitalized "Rotate Left/Right Circularly" 2026-09-07 19:19:29 +02:00
Rangi 7d0cb000e8 Specify COLUMNS in test scripts for consistent output 2026-09-06 00:07:16 +02:00
ISSOtm a0ab1802ff Avoid fetching external repos on 32-bit Cygwin
The comment says we aren't doing this, but we are
2026-09-03 13:39:24 +02:00
ISSOtm bf3ab7cb18 Explicitly specify the Python version to be installed
Turns out `python3-imaging` only redirects to Python 3.**6**'s version,
not the latest like the metapackage does.
2026-09-03 13:39:24 +02:00
Rangi 8988cdde46 Fix reversing images with fewer than 4 colors per palette 2026-09-03 13:39:24 +02:00
Rangi b36028d1d1 Avoid unsigned overflow in INCBIN slice size check 2026-08-25 15:47:27 -04:00
Rangi 760cc4d464 Use PRIu8 for uint8_t 2026-08-25 15:06:31 -04:00
Rangi cebf57630a Prevent creation of a dangling reference to a temporary rvalue 2026-08-25 15:04:28 -04:00
Rangi eb2b7c1842 A 1GB ROM does not have "more than 65536 banks" 2026-08-25 15:02:44 -04:00
Rangi a2c52caca2 Disallow NUL characters in section names and assertion messages
Allowing these in object files would lose anything after the '\0'
when RGBLINK reads the object.
2026-08-22 19:51:03 -04:00
Rangi 21a4b85a4f Fix STRFMT stopping at NUL characters in format spec strings 2026-08-22 19:23:49 -04:00
Rangi 9fa5058add Correct error message for macro arg \<-INT_MIN> 2026-08-22 19:02:23 -04:00
Rangi e287ee2724 Fix C++ UB from negating INT_MIN with macro shift INT_MIN 2026-08-22 18:48:36 -04:00
Rangi 0888600cb7 Avoid OOM allocation error from invalid too-high bank numbers 2026-08-22 18:31:26 -04:00
Rangi 8bc7de35f9 Prevent SECTION FRAGMENT combined sizes from overflowing their uint16_t size 2026-08-22 18:24:17 -04:00
Rangi ed0a2d1075 Fix infinite loop when a symbol name in an invalid object file starts with an invalid UTF-8 byte like 0xC0 2026-08-22 18:06:32 -04:00
Rangi 71d9e236bc Document multiple -v options in the rgbasm and rgblink man pages, same as rgbgfx 2026-08-19 09:21:05 -04:00
ISSOtm d9003f633a Allow the CMake user to provide their own version string
This will be useful for downstream packagers, as well as rgbds-live
2026-08-15 20:25:35 +02:00
ISSOtm cda77721d4 Avoid assuming that RGBDS is the root CMakeLists
This breaks rgbds-live building us as a subproject, for example
2026-08-15 20:05:17 +02:00
267 changed files with 1735 additions and 887 deletions
+4 -4
View File
@@ -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
)
+4
View File
@@ -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
+13 -2
View File
@@ -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
+4
View File
@@ -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
+4
View File
@@ -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
+4
View File
@@ -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
+16 -6
View File
@@ -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/}"
+13 -7
View File
@@ -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
+4
View File
@@ -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
+1
View File
@@ -8,6 +8,7 @@
*.o
*.exe
*.dll
*.pdb
*.gcno
*.gcda
*.gcov
+39 -18
View File
@@ -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
View File
@@ -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 . .
+25 -19
View File
@@ -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"
+9 -6
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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>&lt;MAJOR&gt;</i>.<i>&lt;MINOR&gt;</i>.<i>&lt;PATCH&gt;</i></code>,
or <code>v<i>&lt;MAJOR&gt;</i>.<i>&lt;MINOR&gt;</i>.<i>&lt;PATCH&gt;</i>-rc<i>&lt;RC&gt;</i></code>
+2
View File
@@ -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
+2
View File
@@ -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;
};
+4 -1
View File
@@ -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);
-2
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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; }
+5 -1
View File
@@ -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};
}
+3 -2
View File
@@ -11,7 +11,8 @@ void out_AddSection(Section const &section);
// Finds an assigned section overlapping another one.
Section const *out_OverlappingSection(Section const &section);
// Writes all output (bin, sym, map) files.
void out_WriteFiles();
void out_WriteROMIfAny();
void out_WriteSymIfAny();
void out_WriteMapIfAny();
#endif // RGBDS_LINK_OUTPUT_HPP
+2
View File
@@ -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
View File
@@ -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 };
+3
View File
@@ -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);
+22
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd August 1, 2026
.Dd September 22, 2026
.Dt RGBDS 5
.Os
.Sh NAME
+1 -1
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd August 1, 2026
.Dd September 22, 2026
.Dt RGBDS 7
.Os
.Sh NAME
+1 -1
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd August 1, 2026
.Dd September 22, 2026
.Dt RGBFIX 1
.Os
.Sh NAME
+14 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -100,7 +100,7 @@ void sect_ForEach(void (*callback)(Section &)) {
void sect_CheckSizes() {
for (Section const &sect : 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 &sect, 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 &sect, 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([&sect]() {
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([&sect]() {
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 &sect;
@@ -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 &sect;
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 &section, 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 &section) {
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 &section) {
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 &section, 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, &section](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 &section, 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 &section) {
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 &section, 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 &section, 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 &section, 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 &section) {
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 &section, 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 &section) {
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 &section) {
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 &section) {
// 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 &section) {
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
View File
@@ -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
View File
@@ -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
View File
@@ -71,7 +71,7 @@ void out_AddSection(Section const &section) {
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 &section) {
}
Section const *out_OverlappingSection(Section const &section) {
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 &sectList, 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 &sect) {
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 &sectList, 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
View File
@@ -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);
+2 -3
View File
@@ -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
View File
@@ -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> &&section) {
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 &section) {
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 &section) {
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 &section) {
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 &section) {
}
// 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 &section) {
}
// 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
View File
@@ -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,
+17
View File
@@ -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
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
SECTION "test", ROM0
db 1, 2, 3
align 16, 2
+3
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
assert x, "oops \0 null"
+2
View File
@@ -0,0 +1,2 @@
FATAL: Assertion messages cannot contain '\0' characters
at assert-nul.asm(1)
+20
View File
@@ -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)
+12
View File
@@ -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)
+15
View File
@@ -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
+2 -2
View File
@@ -5,9 +5,9 @@ $18B
$62B85
$4
$10000
$20000
$10000
$10000
$10000
$20000
$C570A
$13333
$13333
+4
View File
@@ -0,0 +1,4 @@
SECTION FRAGMENT "test", ROM0
ds $11, 42
SECTION FRAGMENT "test", ROM0[$10]
+2
View File
@@ -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)
+3
View File
@@ -0,0 +1,3 @@
SECTION "test", ROM0
LOAD "test", WRAM0
ENDL
+4
View File
@@ -0,0 +1,4 @@
FATAL: Section "test" already defined
at load-already-defined.asm(2)
and also:
at load-already-defined.asm(1)
+1 -2
View File
@@ -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
+1 -1
View File
@@ -8,7 +8,7 @@
0: ==
-8: ==
2147483647: ==
-2147483648: == >
-2147483648: ==
2147483648: == >
-1: G == G
4294967295: G == >
+2 -4
View File
@@ -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
+1
View File
@@ -11,3 +11,4 @@ $0
$A
+2
View File
@@ -0,0 +1,2 @@
SECTION "test", ROM0
SECTION "test", WRAM0
+4
View File
@@ -0,0 +1,4 @@
FATAL: Section "test" already defined
at section-already-defined.asm(2)
and also:
at section-already-defined.asm(1)
+2
View File
@@ -0,0 +1,2 @@
SECTION "test\0 foo", ROM0
SECTION "test\0 bar", ROM0
+2
View File
@@ -0,0 +1,2 @@
FATAL: Section names cannot contain '\0' characters
at section-name-nul.asm(1)
+6
View File
@@ -0,0 +1,6 @@
MACRO m
println \1
shift $8000_0000 ; INT32_MIN
println \1
ENDM
m 1, 2, 3
+2
View File
@@ -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)
+2
View File
@@ -0,0 +1,2 @@
$1
$1
+22
View File
@@ -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
+16
View File
@@ -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)
+4
View File
@@ -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
View File
@@ -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"
+7 -3
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+4
View File
@@ -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
+2 -1
View File
@@ -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