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 fi
files=( files=(
win64/rgbds-win64.zip rgbds-win64.zip
win32/rgbds-win32.zip rgbds-win32.zip
macos/rgbds-macos.zip rgbds-macos.zip
linux/rgbds-linux-x86_64.tar.xz rgbds-linux-x86_64.tar.xz
rgbds-source.tar.gz rgbds-source.tar.gz
) )
+4
View File
@@ -6,6 +6,10 @@ on:
tags: tags:
- '*' - '*'
concurrency:
group: ${{github.workflow}}-${{github.ref}}
cancel-in-progress: true
env: env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/) # Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
TERM: xterm-256color TERM: xterm-256color
+13 -2
View File
@@ -1,7 +1,14 @@
name: Contrib script syntax checking name: Contrib script syntax checking
on: on:
- push push:
- pull_request 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: env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/) # Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
@@ -15,6 +22,10 @@ jobs:
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v7 uses: actions/checkout@v7
with:
sparse-checkout: | # Avoid checking out other files, we don't use them here.
contrib
.github/scripts
- name: Install zsh - name: Install zsh
run: | run: |
sudo apt install -Uyq zsh sudo apt install -Uyq zsh
+4
View File
@@ -1,6 +1,10 @@
name: Diff completeness check name: Diff completeness check
on: pull_request on: pull_request
concurrency:
group: ${{github.workflow}}-${{github.event.pull_request.number || github.ref}}
cancel-in-progress: true
env: env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/) # Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
TERM: xterm-256color TERM: xterm-256color
+4
View File
@@ -1,6 +1,10 @@
name: Code format checking name: Code format checking
on: pull_request on: pull_request
concurrency:
group: ${{github.workflow}}-${{github.event.pull_request.number || github.ref}}
cancel-in-progress: true
env: env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/) # Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
TERM: xterm-256color TERM: xterm-256color
+4
View File
@@ -3,6 +3,10 @@ on:
- push - push
- pull_request - pull_request
concurrency:
group: ${{github.workflow}}-${{github.event.pull_request.number || github.ref}}
cancel-in-progress: true
env: env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/) # Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
TERM: xterm-256color TERM: xterm-256color
+16 -6
View File
@@ -4,6 +4,10 @@ on:
tags: tags:
- v[0-9]* - v[0-9]*
concurrency:
group: ${{github.workflow}}-${{github.ref}}
cancel-in-progress: true
env: env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/) # Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
TERM: xterm-256color TERM: xterm-256color
@@ -38,7 +42,9 @@ jobs:
shell: cmd shell: cmd
run: | run: |
call .github\scripts\msvc-env.bat ${{ matrix.bits }} 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 cmake --build build -- -k 0
- name: Package binaries - name: Package binaries
working-directory: build working-directory: build
@@ -47,8 +53,8 @@ jobs:
- name: Upload Windows binaries - name: Upload Windows binaries
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: win${{ matrix.bits }}
path: build/rgbds-win${{ matrix.bits }}.zip path: build/rgbds-win${{ matrix.bits }}.zip
archive: false
if-no-files-found: error if-no-files-found: error
macos: macos:
@@ -61,7 +67,8 @@ jobs:
./.github/scripts/install-deps.sh macos lld ./.github/scripts/install-deps.sh macos lld
- name: Build binaries - name: Build binaries
run: | 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 cmake --build build -- -k 0
strip rgb{asm,link,fix,gfx} strip rgb{asm,link,fix,gfx}
env: env:
@@ -75,8 +82,8 @@ jobs:
- name: Upload macOS binaries - name: Upload macOS binaries
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: macos
path: rgbds-macos.zip path: rgbds-macos.zip
archive: false
if-no-files-found: error if-no-files-found: error
linux: linux:
@@ -89,7 +96,7 @@ jobs:
./.github/scripts/install-deps.sh ubuntu-22.04 ./.github/scripts/install-deps.sh ubuntu-22.04
- name: Build binaries - name: Build binaries
run: | 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} strip rgb{asm,link,fix,gfx}
- name: Create install script - name: Create install script
run: | run: |
@@ -100,8 +107,8 @@ jobs:
- name: Upload Linux binaries - name: Upload Linux binaries
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: linux
path: rgbds-linux-x86_64.tar.xz path: rgbds-linux-x86_64.tar.xz
archive: false
if-no-files-found: error if-no-files-found: error
release: release:
@@ -117,6 +124,9 @@ jobs:
make dist Q= make dist Q=
- name: Download all binary packages - name: Download all binary packages
uses: actions/download-artifact@v8 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 - name: Draft the release
run: | run: |
.github/scripts/draft-release.sh "${ref#refs/tags/}" .github/scripts/draft-release.sh "${ref#refs/tags/}"
+13 -7
View File
@@ -3,6 +3,10 @@ on:
- push - push
- pull_request - pull_request
concurrency:
group: ${{github.workflow}}-${{github.event.pull_request.number || github.ref}}
cancel-in-progress: true
env: env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/) # Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
TERM: xterm-256color TERM: xterm-256color
@@ -197,7 +201,7 @@ jobs:
run: | # ASan seems to be broken on Windows, so we disable it. run: | # ASan seems to be broken on Windows, so we disable it.
call .github\scripts\msvc-env.bat ${{ matrix.bits }} call .github\scripts\msvc-env.bat ${{ matrix.bits }}
cmake -B build -G Ninja --preset develop -DFETCHCONTENT_BASE_DIR="${{ env.DEPS_ROOT_DIR }}" ^ 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 cmake --build build -- -k 0
- name: Install - name: Install
shell: cmd shell: cmd
@@ -207,12 +211,12 @@ jobs:
- name: Package binaries - name: Package binaries
working-directory: build working-directory: build
run: | 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 - name: Upload Windows binaries
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: rgbds-canary-w${{ matrix.bits }}-${{ matrix.os }} path: build/rgbds-canary-w${{ matrix.bits }}-${{ matrix.os }}.zip
path: build/rgbds-win${{ matrix.bits }}.zip archive: false
if-no-files-found: error if-no-files-found: error
- name: Compute test dependency cache params - name: Compute test dependency cache params
id: test-deps-cache-params id: test-deps-cache-params
@@ -363,8 +367,8 @@ jobs:
make make
ninja ninja
pkg-config pkg-config
python3 python312
python-imaging python312-imaging
- name: Build using Make - name: Build using Make
if: matrix.arch == 'x86' if: matrix.arch == 'x86'
run: | # Cygwin doesn't support `make develop` sanitizers ASan or UBSan, so we don't use it. 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 type rgbasm rgblink rgbfix rgbgfx
man -w 1 rgbasm rgblink rgbfix rgbgfx man -w 1 rgbasm rgblink rgbfix rgbgfx
- name: Compute test dependency cache params - name: Compute test dependency cache params
if: matrix.arch == 'x86_64'
id: test-deps-cache-params id: test-deps-cache-params
run: | run: |
paths=$(test/external/fetch-repos.sh --get-paths) paths=$(test/external/fetch-repos.sh --get-paths)
@@ -394,13 +399,14 @@ jobs:
tee -a <<<"paths=\"${paths//,/\\n}\"" $GITHUB_OUTPUT tee -a <<<"paths=\"${paths//,/\\n}\"" $GITHUB_OUTPUT
tee -a <<<"hash=${hash%-}" $GITHUB_OUTPUT tee -a <<<"hash=${hash%-}" $GITHUB_OUTPUT
- name: Check test dependency repositories cache - name: Check test dependency repositories cache
if: matrix.arch == 'x86_64'
id: test-deps-cache id: test-deps-cache
uses: actions/cache@v5 uses: actions/cache@v5
with: with:
path: ${{ fromJSON(steps.test-deps-cache-params.outputs.paths) }} path: ${{ fromJSON(steps.test-deps-cache-params.outputs.paths) }}
key: cygwin-${{ matrix.bits }}-${{ steps.test-deps-cache-params.outputs.hash }} key: cygwin-${{ matrix.bits }}-${{ steps.test-deps-cache-params.outputs.hash }}
- name: Fetch test dependency repositories - 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 continue-on-error: true
run: | run: |
test/external/fetch-repos.sh test/external/fetch-repos.sh
+4
View File
@@ -7,6 +7,10 @@ on:
- man/* - man/*
workflow_dispatch: workflow_dispatch:
concurrency:
group: ${{github.workflow}}-${{github.ref}}
cancel-in-progress: true
env: env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/) # Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
TERM: xterm-256color TERM: xterm-256color
+1
View File
@@ -8,6 +8,7 @@
*.o *.o
*.exe *.exe
*.dll *.dll
*.pdb
*.gcno *.gcno
*.gcda *.gcda
*.gcov *.gcov
+59 -38
View File
@@ -11,21 +11,21 @@ file(STRINGS "include/version.hpp" version_defines REGEX "^[ \t]*#define[ \t]+PA
foreach(line IN LISTS version_defines) foreach(line IN LISTS version_defines)
# We want the `CMAKE_MATCH_n` variables, so we just need to run *some* regex op. # 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}") 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() endforeach()
project(rgbds project(rgbds
VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}" VERSION "${RGBDS_VERSION_MAJOR}.${RGBDS_VERSION_MINOR}.${RGBDS_VERSION_PATCH}"
LANGUAGES CXX LANGUAGES CXX
DESCRIPTION "Game Boy assembly toolchain" DESCRIPTION "Game Boy assembly toolchain"
HOMEPAGE_URL "https://rgbds.gbdev.io") HOMEPAGE_URL "https://rgbds.gbdev.io")
if(DEFINED VERSION_RC) if(DEFINED VERSION_RC)
string(APPEND CMAKE_PROJECT_VERSION "-rc${VERSION_RC}") string(APPEND PROJECT_VERSION "-rc${RGBDS_VERSION_RC}")
endif() endif()
# Reject in-source builds, as they may conflict with the Makefile. # Reject in-source builds, as they may conflict with the Makefile.
get_filename_component(srcdir "${CMAKE_SOURCE_DIR}" REALPATH) get_filename_component(srcdir "${CMAKE_CURRENT_SOURCE_DIR}" REALPATH)
get_filename_component(bindir "${CMAKE_BINARY_DIR}" REALPATH) get_filename_component(bindir "${CMAKE_CURRENT_BINARY_DIR}" REALPATH)
if(srcdir STREQUAL bindir) if(srcdir STREQUAL bindir)
message(FATAL_ERROR "RGBDS should not be built in the source directory. 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.") Instead, create a separate build directory and specify to CMake the path to the source directory.")
@@ -52,14 +52,22 @@ endif()
if(SANITIZERS) if(SANITIZERS)
if(MSVC) if(MSVC)
message(STATUS "ASan enabled") 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. else() # We assume a GNU-like compiler.
message(STATUS "ASan and UBSan enabled") message(STATUS "ASan and UBSan enabled")
set(SAN_FLAGS -fsanitize=address -fsanitize=undefined set(SAN_FLAGS -fsanitize=address -fsanitize=undefined
-fsanitize=float-divide-by-zero) -fsanitize=float-divide-by-zero)
add_compile_options(${SAN_FLAGS}) add_compile_options(${SAN_FLAGS})
add_link_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), # 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 # 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. # be able to override these easily so we put them first.
@@ -82,35 +90,46 @@ endif()
# Use versioning consistent with Makefile: # Use versioning consistent with Makefile:
# the git revision is used but uses the fallback in an archive. # the git revision is used but uses the fallback in an archive.
message(CHECK_START "Determining RGBDS version from Git history") if(DEFINED RGBDS_VERSION_STRING) # Possibly stored in the cache, e.g. if specified on the CLI.
list(APPEND CMAKE_MESSAGE_INDENT " ") if(RGBDS_VERSION_STRING STREQUAL "")
set(GIT_REV "") # This fallback is important! message(STATUS "Will generate version string from `version.hpp`")
find_package(Git)
list(POP_BACK CMAKE_MESSAGE_INDENT)
if(NOT Git_FOUND)
message(CHECK_FAIL "Git not 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
ERROR_VARIABLE git_err ERROR_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE result)
if(NOT result EQUAL 0)
# Note that this happens e.g. when building from a tarball, so it shouldn't fail the build!
message(CHECK_FAIL "error ${result} from Git:")
list(APPEND CMAKE_MESSAGE_INDENT " ")
message("${git_err}")
list(POP_BACK CMAKE_MESSAGE_INDENT)
else() else()
message(CHECK_PASS "${GIT_REV}") message(STATUS "Using provided version string - ${RGBDS_VERSION_STRING}")
if(NOT "${GIT_REV}" MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+(-rc[0-9]+)?") endif()
# Can't find an ancestor tag! (That passes `--match`, anyway.) else()
message(WARNING "No `v*` Git tag reachable; falling back") message(CHECK_START "Determining RGBDS version from Git history")
elseif(NOT CMAKE_MATCH_0 STREQUAL "v${CMAKE_PROJECT_VERSION}") # Note that we do NOT store this in the cache, since the Git revision is fairly volatile.
message(SEND_ERROR "\ set(RGBDS_VERSION_STRING "") # An empty value causes `version.cpp` to generate a version string.
list(APPEND CMAKE_MESSAGE_INDENT " ")
find_package(Git)
list(POP_BACK CMAKE_MESSAGE_INDENT)
if(NOT Git_FOUND)
message(CHECK_FAIL "Git not found")
else()
execute_process(COMMAND "${GIT_EXECUTABLE}" --git-dir=.git -c safe.directory='*'
describe --tags --dirty --always --match "v[0-9]*"
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)
# Note that this happens e.g. when building from a tarball, so it shouldn't fail the build!
message(CHECK_FAIL "error ${result} from Git:")
list(APPEND CMAKE_MESSAGE_INDENT " ")
message("${git_err}")
list(POP_BACK CMAKE_MESSAGE_INDENT)
else()
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${PROJECT_VERSION}")
message(SEND_ERROR "\
Version mismatch! Git says ${CMAKE_MATCH_0}, 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() endif()
endif() endif()
@@ -185,7 +204,7 @@ else()
# We rely on runtimes more modern than that, so we can ignore those warnings. # 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`. # 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/ # 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()
endif() endif()
@@ -215,17 +234,19 @@ endforeach()
# the rest is rather convention from our side, and thus more appropriate for presets or CLI flags. # the rest is rather convention from our side, and thus more appropriate for presets or CLI flags.
## CPACK_PACKAGE_NAME: copied from `project()` ## CPACK_PACKAGE_NAME: copied from `project()`
set(CPACK_PACKAGE_VENDOR "GBDev") 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. 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()` ## CPACK_PACKAGE_DESCRIPTION_SUMMARY: copied from `project()`
set(CPACK_PACKAGE_HOMEPAGE_URL "https://rgbds.gbdev.io") set(CPACK_PACKAGE_HOMEPAGE_URL "https://rgbds.gbdev.io")
## CPACK_PACKAGE_FILE_NAME: should be provided at runtime (`cpack -P`) ## CPACK_PACKAGE_FILE_NAME: should be provided at runtime (`cpack -P`)
set(CPACK_PACKAGE_CHECKSUM SHA256) set(CPACK_PACKAGE_CHECKSUM SHA256)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE") set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE")
set(CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/README.md") 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_STRIP_FILES ON) # Only applies to binary packages, not sources.
set(CPACK_VERBATIM_VARIABLES ON) set(CPACK_VERBATIM_VARIABLES ON)
set(CPACK_THREADS 0) # Use all available CPU cores. set(CPACK_THREADS 0) # Use all available CPU cores.
set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF) set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF)
set(CPACK_COMPONENTS_GROUPING ALL_COMPONENTS_IN_ONE)
set(CPACK_ARCHIVE_COMPONENT_INSTALL ON)
include(CPack) include(CPack)
+1 -1
View File
@@ -1,6 +1,6 @@
FROM debian:13-slim FROM debian:13-slim
LABEL org.opencontainers.image.source=https://github.com/gbdev/rgbds LABEL org.opencontainers.image.source=https://github.com/gbdev/rgbds
ARG version=1.0.3 ARG version=1.0.4
WORKDIR /rgbds WORKDIR /rgbds
COPY . . COPY . .
+25 -19
View File
@@ -34,12 +34,12 @@ WARNFLAGS := -Wall -pedantic -Wno-unknown-warning-option \
CXXFLAGS ?= -O3 -flto -DNDEBUG CXXFLAGS ?= -O3 -flto -DNDEBUG
# Non-overridable CXXFLAGS # Non-overridable CXXFLAGS
REQUIREDCXXFLAGS := -std=c++20 -I include -fno-exceptions -fno-rtti REQUIREDCXXFLAGS := -std=c++20 -I include -fno-exceptions -fno-rtti
REALCXXFLAGS := ${CXXFLAGS} ${WARNFLAGS} ${REQUIREDCXXFLAGS} REALCXXFLAGS := ${CXXFLAGS} ${REQUIREDCXXFLAGS}
# Overridable LDFLAGS # Overridable LDFLAGS
LDFLAGS ?= LDFLAGS ?=
# Non-overridable LDFLAGS # Non-overridable LDFLAGS
REQUIREDLDFLAGS := -DBUILD_VERSION_STRING=\"${VERSION_STRING}\" 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 # Wrapper around bison that passes flags depending on what the version supports
BISON := src/bison.sh BISON := src/bison.sh
@@ -130,22 +130,22 @@ rgbgfx_obj := \
src/verbosity.o src/verbosity.o
rgbasm: ${rgbasm_obj} 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} 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} 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} 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 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 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 # 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) # 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 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 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 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 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 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 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 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 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 src/gfx/rgba.o: src/gfx/rgba.cpp
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $< $Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
.cpp.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: install.sh:
$Qecho '#!/usr/bin/env bash' > $@ $Qecho '#!/usr/bin/env bash' > $@
$Q${MAKE} -t all
$Q${MAKE} -s -n install MANSRC= >> $@ $Q${MAKE} -s -n install MANSRC= >> $@
$Qchmod +x $@ $Qchmod +x $@
@@ -225,7 +229,9 @@ develop:
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1 \ -Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1 \
-Wno-format-nonliteral -Wno-strict-overflow -Wno-unused-but-set-variable \ -Wno-format-nonliteral -Wno-strict-overflow -Wno-unused-but-set-variable \
-Wno-type-limits -Wno-tautological-constant-out-of-range-compare -Wvla \ -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" \ -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero" \
CXXFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" 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. # 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. # 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. set(CMAKE_OSX_DEPLOYMENT_TARGET 10.4 CACHE STRING "Minimum Mac OS X version to target for deployment (at runtime)")
# The `-arch` flags build a "fat binary" that works on both Apple architectures: # This builds a "fat binary" that works on both Apple architectures:
# older Intel x64 Macs and newer ARM "Apple Silicon" ones. # 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. # Due to a libpng build script limitation/bug (as of 1.6.58), the native architecture has to be first...
add_compile_options(${secret_sauce}) # and since our CI builds this executable on an ARM machine, that's what we're putting first.
add_link_options(${secret_sauce}) set(CMAKE_OSX_ARCHITECTURES arm64 x86_64 CACHE STRING "Build architectures for Mac OS X")
set(PNG_HARDWARE_OPTIMIZATIONS OFF) # These do not play well with a dual-arch build. # 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. # 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. # 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" printf $(printf '\\x%x' $i{,,,}) >> "$TMP/tmp.palmap"
done 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`. - [Dockerfile](/Dockerfile): update `ARG version`.
- [test/external/\*.cfg](/test/external): update test dependency commits - [test/external/\*.cfg](/test/external): update test dependency commits
(preferably, use the latest available). (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>, 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> 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 addCheckBitIndex(uint8_t mask);
void checkNBit(uint8_t n) const; void checkNBit(uint8_t n) const;
void checkSignedNBit(uint8_t n) const;
void encode(std::vector<uint8_t> &buffer) const; void encode(std::vector<uint8_t> &buffer) const;
}; };
bool checkNBit(int32_t v, uint8_t n, char const *name); 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 #endif // RGBDS_ASM_RPN_HPP
+2
View File
@@ -42,6 +42,8 @@ struct Section {
std::deque<Patch> patches; std::deque<Patch> patches;
std::vector<uint8_t> data; 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) uint32_t getID() const; // ID of the section in the object file (`UINT32_MAX` if none)
bool isSizeKnown() const; bool isSizeKnown() const;
}; };
+4 -1
View File
@@ -25,7 +25,8 @@ struct Options {
std::array<uint8_t, 2> baseTileIDs{0, 0}; // -b std::array<uint8_t, 2> baseTileIDs{0, 0}; // -b
enum { enum {
NO_SPEC, NO_SPEC,
EXPLICIT, INLINE,
EXTERNAL,
EMBEDDED, EMBEDDED,
DMG, DMG,
} palSpecType = NO_SPEC; // -c } palSpecType = NO_SPEC; // -c
@@ -59,6 +60,8 @@ struct Options {
uint16_t maxNbColors() const { return nbColorsPerPal * nbPalettes; } uint16_t maxNbColors() const { return nbColorsPerPal * nbPalettes; }
bool hasExplicitPalSpec() const { return palSpecType == INLINE || palSpecType == EXTERNAL; }
uint8_t dmgColors[4] = {}; uint8_t dmgColors[4] = {};
uint8_t dmgValue(uint8_t i) const { uint8_t dmgValue(uint8_t i) const {
assume(i < 4); assume(i < 4);
-2
View File
@@ -8,8 +8,6 @@
void parseInlinePalSpec(char const * const rawArg); void parseInlinePalSpec(char const * const rawArg);
void parseExternalPalSpec(char const *arg); void parseExternalPalSpec(char const *arg);
void parseDmgPalSpec(char const * const rawArg); void parseDmgPalSpec(char const * const rawArg);
void parseDmgPalSpec(uint8_t palSpecDmg);
void parseBackgroundPalSpec(char const *arg); void parseBackgroundPalSpec(char const *arg);
#endif // RGBDS_GFX_PAL_SPEC_HPP #endif // RGBDS_GFX_PAL_SPEC_HPP
+1
View File
@@ -14,6 +14,7 @@ struct Png {
uint32_t height = 0; uint32_t height = 0;
std::vector<Rgba> pixels{}; std::vector<Rgba> pixels{};
std::vector<Rgba> palette{}; std::vector<Rgba> palette{};
bool isIndexed = false;
Png() {} Png() {}
Png(char const *filename, std::streambuf &file); Png(char const *filename, std::streambuf &file);
+10 -19
View File
@@ -20,23 +20,6 @@ struct Rgba {
explicit constexpr Rgba(uint32_t rgba = 0) explicit constexpr Rgba(uint32_t rgba = 0)
: red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {} : 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`) // Returns this RGBA as a 32-bit number that can be printed in hex (`#%08x`)
// to yield its CSS representation (`#rrggbbaa`). // to yield its CSS representation (`#rrggbbaa`).
uint32_t toCSS() const { uint32_t toCSS() const {
@@ -45,14 +28,22 @@ struct Rgba {
}; };
return shl(red, 24) | shl(green, 16) | shl(blue, 8) | shl(alpha, 0); return shl(red, 24) | shl(green, 16) | shl(blue, 8) | shl(alpha, 0);
} }
bool operator==(Rgba const &rhs) const { return toCSS() == rhs.toCSS(); } 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; static constexpr uint8_t transparency_threshold = 0x10;
bool isTransparent() const { return alpha < transparency_threshold; } bool isTransparent() const { return alpha < transparency_threshold; }
static constexpr uint8_t opacity_threshold = 0xF0; static constexpr uint8_t opacity_threshold = 0xF0;
bool isOpaque() const { return alpha >= opacity_threshold; } 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; uint16_t cgbColor() const;
bool isGray() const { return red == green && green == blue; } 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); return std::rend(r._iterable);
} }
// Prevent creation of a dangling reference to a temporary rvalue
template<typename IterableT> template<typename IterableT>
ReversedIterable<IterableT> reversed(IterableT &&_iterable) { ReversedIterable<IterableT> reversed(IterableT &&_iterable) = delete;
template<typename IterableT>
ReversedIterable<IterableT> reversed(IterableT &_iterable) {
return {_iterable}; return {_iterable};
} }
+3 -2
View File
@@ -11,7 +11,8 @@ void out_AddSection(Section const &section);
// Finds an assigned section overlapping another one. // Finds an assigned section overlapping another one.
Section const *out_OverlappingSection(Section const &section); Section const *out_OverlappingSection(Section const &section);
// Writes all output (bin, sym, map) files. void out_WriteROMIfAny();
void out_WriteFiles(); void out_WriteSymIfAny();
void out_WriteMapIfAny();
#endif // RGBDS_LINK_OUTPUT_HPP #endif // RGBDS_LINK_OUTPUT_HPP
+2
View File
@@ -51,6 +51,8 @@ struct Section {
std::vector<Symbol *> symbols; std::vector<Symbol *> symbols;
std::unique_ptr<Section> nextPiece; // The next fragment or union "piece" of this section std::unique_ptr<Section> nextPiece; // The next fragment or union "piece" of this section
SectionTypeInfo const &typeInfo() const { return sectionTypeInfo[type]; }
private: private:
// Template class for both const and non-const iterators over the "pieces" of this section // Template class for both const and non-const iterators over the "pieces" of this section
template<QualifiedEquivalent<Section> SectionT> template<QualifiedEquivalent<Section> SectionT>
+18 -19
View File
@@ -4,7 +4,6 @@
#define RGBDS_LINKDEFS_HPP #define RGBDS_LINKDEFS_HPP
#include <stdint.h> #include <stdint.h>
#include <string>
#include "helpers.hpp" // assume #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_UNION_BIT = 7;
static constexpr uint8_t SECTTYPE_FRAGMENT_BIT = 6; 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 { enum FileStackNodeType {
NODE_REPT, NODE_REPT,
NODE_FILE, NODE_FILE,
@@ -90,31 +96,24 @@ enum FileStackNodeType {
static constexpr uint8_t FSTACKNODE_QUIET_BIT = 7; static constexpr uint8_t FSTACKNODE_QUIET_BIT = 7;
// Nont-`const` members may be patched in RGBLINK depending on CLI flags // Non-`const` members may be patched in RGBLINK depending on CLI flags
extern struct SectionTypeInfo { struct SectionTypeInfo {
std::string const name; char const *name;
uint16_t const startAddr; uint16_t const startAddr;
uint16_t size; uint16_t size;
uint32_t const firstBank; uint32_t const firstBank;
uint32_t lastBank; uint32_t lastBank;
} sectionTypeInfo[SECTTYPE_INVALID];
// Tells whether a section has data in its object file definition, // Returns a memory region's end address (last byte), e.g. 0x7FFF
// depending on type. uint16_t endAddr() const { return startAddr + size - 1; }
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 // Returns a memory region's number of banks, or 1 for regions without banking
static inline uint16_t sectTypeEndAddr(SectionType type) { uint32_t nbBanks() const { return lastBank - firstBank + 1; }
return sectionTypeInfo[type].startAddr + sectionTypeInfo[type].size - 1;
}
// Returns a memory region's number of banks, or 1 for regions without banking bool isBanked() const { return nbBanks() != 1; }
static inline uint32_t sectTypeBanks(SectionType type) { };
return sectionTypeInfo[type].lastBank - sectionTypeInfo[type].firstBank + 1;
} extern SectionTypeInfo sectionTypeInfo[SECTTYPE_INVALID];
enum SectionModifier { SECTION_NORMAL, SECTION_UNION, SECTION_FRAGMENT }; enum SectionModifier { SECTION_NORMAL, SECTION_UNION, SECTION_FRAGMENT };
+3
View File
@@ -5,6 +5,9 @@
#include <stdint.h> #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_divide(int32_t dividend, int32_t divisor);
int32_t op_modulo(int32_t dividend, int32_t divisor); int32_t op_modulo(int32_t dividend, int32_t divisor);
int32_t op_exponent(int32_t base, uint32_t power); int32_t op_exponent(int32_t base, uint32_t power);
+22
View File
@@ -66,4 +66,26 @@
#define _POSIX_C_SOURCE 200809L #define _POSIX_C_SOURCE 200809L
#endif #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 #endif // RGBDS_PLATFORM_HPP
+1 -1
View File
@@ -5,7 +5,7 @@
#define PACKAGE_VERSION_MAJOR 1 #define PACKAGE_VERSION_MAJOR 1
#define PACKAGE_VERSION_MINOR 0 #define PACKAGE_VERSION_MINOR 0
#define PACKAGE_VERSION_PATCH 3 #define PACKAGE_VERSION_PATCH 4
// #define PACKAGE_VERSION_RC 1 // #define PACKAGE_VERSION_RC 1
char const *get_package_version_string(); char const *get_package_version_string();
+7 -7
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT .\" SPDX-License-Identifier: MIT
.\" .\"
.Dd August 1, 2026 .Dd September 22, 2026
.Dt GBZ80 7 .Dt GBZ80 7
.Os .Os
.Sh NAME .Sh NAME
@@ -1525,7 +1525,7 @@ Set according to result.
.Ss RLC r8 .Ss RLC r8
Rotate register Rotate register
.Ar r8 .Ar r8
left. Left Circularly.
.Bd -literal .Bd -literal
┏━ Flags ━┓ ┏━━━━━━━ r8 ━━━━━━┓ ┏━ Flags ━┓ ┏━━━━━━━ r8 ━━━━━━┓
┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐ ┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐
@@ -1551,7 +1551,7 @@ Set according to result.
.Ss RLC [HL] .Ss RLC [HL]
Rotate the byte pointed to by Rotate the byte pointed to by
.Sy HL .Sy HL
left. Left Circularly.
.Bd -literal .Bd -literal
┏━ Flags ━┓ ┏━━━━━━ [HL] ━━━━━┓ ┏━ Flags ━┓ ┏━━━━━━ [HL] ━━━━━┓
┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐ ┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐
@@ -1568,7 +1568,7 @@ Flags: See
.Ss RLCA .Ss RLCA
Rotate register Rotate register
.Sy A .Sy A
left. Left Circularly.
.Bd -literal .Bd -literal
┏━ Flags ━┓ ┏━━━━━━━ A ━━━━━━━┓ ┏━ Flags ━┓ ┏━━━━━━━ A ━━━━━━━┓
┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐ ┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐
@@ -1663,7 +1663,7 @@ Set according to result.
.Ss RRC r8 .Ss RRC r8
Rotate register Rotate register
.Ar r8 .Ar r8
right. Right Circularly.
.Bd -literal .Bd -literal
┏━━━━━━━ r8 ━━━━━━┓ ┏━ Flags ━┓ ┏━━━━━━━ r8 ━━━━━━┓ ┏━ Flags ━┓
┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃ ┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃
@@ -1689,7 +1689,7 @@ Set according to result.
.Ss RRC [HL] .Ss RRC [HL]
Rotate the byte pointed to by Rotate the byte pointed to by
.Sy HL .Sy HL
right. Right Circularly.
.Bd -literal .Bd -literal
┏━━━━━━ [HL] ━━━━━┓ ┏━ Flags ━┓ ┏━━━━━━ [HL] ━━━━━┓ ┏━ Flags ━┓
┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃ ┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃
@@ -1706,7 +1706,7 @@ Flags: See
.Ss RRCA .Ss RRCA
Rotate register Rotate register
.Sy A .Sy A
right. Right Circularly.
.Bd -literal .Bd -literal
┏━━━━━━━ A ━━━━━━━┓ ┏━ Flags ━┓ ┏━━━━━━━ A ━━━━━━━┓ ┏━ Flags ━┓
┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃ ┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃
+1 -1
View File
@@ -2,7 +2,7 @@
.\" .\"
.\" SPDX-License-Identifier: MIT .\" SPDX-License-Identifier: MIT
.\" .\"
.Dd August 1, 2026 .Dd September 22, 2026
.Dt RGBASM-OLD 5 .Dt RGBASM-OLD 5
.Os .Os
.Sh NAME .Sh NAME
+3 -2
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT .\" SPDX-License-Identifier: MIT
.\" .\"
.Dd August 1, 2026 .Dd September 22, 2026
.Dt RGBASM 1 .Dt RGBASM 1
.Os .Os
.Sh NAME .Sh NAME
@@ -8,7 +8,8 @@
.Nd Game Boy assembler .Nd Game Boy assembler
.Sh SYNOPSIS .Sh SYNOPSIS
.Nm .Nm
.Op Fl EhVvw .Op Fl EhVw
.Op Fl v Op Fl v No ...
.Op Fl B Ar param .Op Fl B Ar param
.Op Fl b Ar chars .Op Fl b Ar chars
.Op Fl \-color Ar when .Op Fl \-color Ar when
+16 -3
View File
@@ -2,7 +2,7 @@
.\" .\"
.\" SPDX-License-Identifier: MIT .\" SPDX-License-Identifier: MIT
.\" .\"
.Dd August 1, 2026 .Dd September 22, 2026
.Dt RGBASM 5 .Dt RGBASM 5
.Os .Os
.Sh NAME .Sh NAME
@@ -729,17 +729,21 @@ String constants are not expanded within the parentheses.
.Ic IF ) , .Ic IF ) ,
or 0 if only RGBLINK can compute its value. or 0 if only RGBLINK can compute its value.
.It Fn BANK arg Ta Returns a bank number. .It Fn BANK arg Ta Returns a bank number.
.Pp
If If
.Ar arg .Ar arg
is the symbol is the symbol
.Ic @ , .Ic @ ,
this function returns the bank of the current section. this function returns the bank of the current section.
.Pp
If If
.Ar arg .Ar arg
is a string, it returns the bank of the section that has that name. is a string, it returns the bank of the section that has that name.
.Pp
If If
.Ar arg .Ar arg
is a label, it returns the bank number the label is in. is a label, it returns the bank number the label is in.
.Pp
The result may be constant if The result may be constant if
.Nm .Nm
is able to compute it. is able to compute it.
@@ -757,10 +761,17 @@ this function returns the name of the current section.
.Ar arg .Ar arg
is a string, this function returns the size of the section named is a string, this function returns the size of the section named
.Ar arg . .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 If
.Ar arg .Ar arg
is a section type keyword, it returns the size of that section type. 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. The result is not constant, since only RGBLINK can compute its value.
.Pp
If If
.Ar arg .Ar arg
is an 8-bit or 16-bit register, it returns the size of that register. 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 .Ar arg
is a string, this function returns the starting address of the section named is a string, this function returns the starting address of the section named
.Ar arg . .Ar arg .
The result is a known constant if the section has a fixed address.
.Pp
If If
.Ar arg .Ar arg
is a section type keyword, it returns the starting address of that section type. 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 MACRO lb
ld \e1, (\e2) << 8 | (\e3) ld \e1, (\e2) << 8 | (\e3)
ENDM ENDM
lb hl, 20, 18 ; Expands to "ld hl, ((20) << 8) | (18)" 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 de, 3 + 1, NUM**2 ; Expands to "ld de, (3 + 1) << 8 | (NUM**2)"
.Ed .Ed
.Pp .Pp
You expand the arguments inside the macro body by using the escape sequences 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 .\" SPDX-License-Identifier: MIT
.\" .\"
.Dd August 1, 2026 .Dd September 22, 2026
.Dt RGBDS 5 .Dt RGBDS 5
.Os .Os
.Sh NAME .Sh NAME
+1 -1
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT .\" SPDX-License-Identifier: MIT
.\" .\"
.Dd August 1, 2026 .Dd September 22, 2026
.Dt RGBDS 7 .Dt RGBDS 7
.Os .Os
.Sh NAME .Sh NAME
+1 -1
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT .\" SPDX-License-Identifier: MIT
.\" .\"
.Dd August 1, 2026 .Dd September 22, 2026
.Dt RGBFIX 1 .Dt RGBFIX 1
.Os .Os
.Sh NAME .Sh NAME
+14 -10
View File
@@ -2,7 +2,7 @@
.\" .\"
.\" SPDX-License-Identifier: MIT .\" SPDX-License-Identifier: MIT
.\" .\"
.Dd August 1, 2026 .Dd September 22, 2026
.Dt RGBGFX 1 .Dt RGBGFX 1
.Os .Os
.Sh NAME .Sh NAME
@@ -491,9 +491,17 @@ deduplication if
.Fl u .Fl u
was enabled, so you probably don't want to use this option in combination with was enabled, so you probably don't want to use this option in combination with
.Fl u . .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 .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 .It Fl Y , Fl \-mirror-y
Deduplicate tiles that are vertically symmetrical mirror images of each other across the Y axis. Deduplicate tiles that are vertically symmetrical mirror images of each other across the Y axis.
Implies Implies
@@ -585,15 +593,11 @@ for example because you want to use palette swaps, please use
.Fl c .Fl c
to specify the palette explicitly. to specify the palette explicitly.
.Pp .Pp
First, if the image contains Note that if the image contains
.Em any .Em any
transparent pixel, color #0 of transparent pixels, color #0 of
.Em all .Em all
palettes will be allocated to it. palettes will be transparent.
This is done
.Sy even if palettes were explicitly specified using Fl c ;
then the specification only covers color #1 onwards.
.Pq If you do not want this, ask your image editor to remove the alpha channel.
.Pp .Pp
After generating palettes, After generating palettes,
.Nm .Nm
+11 -3
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT .\" SPDX-License-Identifier: MIT
.\" .\"
.Dd August 1, 2026 .Dd September 22, 2026
.Dt RGBLINK 1 .Dt RGBLINK 1
.Os .Os
.Sh NAME .Sh NAME
@@ -8,7 +8,8 @@
.Nd Game Boy linker .Nd Game Boy linker
.Sh SYNOPSIS .Sh SYNOPSIS
.Nm .Nm
.Op Fl dhMtVvwx .Op Fl dhMtVwx
.Op Fl v Op Fl v No ...
.Op Fl B Ar param .Op Fl B Ar param
.Op Fl \-color Ar when .Op Fl \-color Ar when
.Op Fl l Ar linker_script .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" .Bl -column "Region name" "Max value" "Size optional"
Region name Ta Max size Ta Size optional Region name Ta Max size Ta Size optional
.Cm romx Ta 65535 Ta \&No .Cm romx Ta 65535 Ta \&No
.Cm sram Ta 255 Ta \&No .Cm sram Ta 256 Ta \&No
.Cm wramx Ta 7 Ta Yes .Cm wramx Ta 7 Ta Yes
.El .El
.Pp .Pp
@@ -307,6 +308,13 @@ Later region specs override earlier ones; for example,
disables scrambling for disables scrambling for
.Cm romx . .Cm romx .
.Pp .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 .Cm wramx
scrambling is silently ignored if scrambling is silently ignored if
.Fl w .Fl w
+1 -4
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT .\" SPDX-License-Identifier: MIT
.\" .\"
.Dd August 1, 2026 .Dd September 22, 2026
.Dt RGBLINK 5 .Dt RGBLINK 5
.Os .Os
.Sh NAME .Sh NAME
@@ -73,14 +73,11 @@ and
.Ic HRAM . .Ic HRAM .
The bank number can be omitted from the types that only contain a single bank, which are: The bank number can be omitted from the types that only contain a single bank, which are:
.Ic ROM0 , .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 VRAM No if Fl d No is passed to Xr rgblink 1 ,
.Ic WRAM0 , .Ic WRAM0 ,
.Ic WRAMX No if Fl w No is passed to Xr rgblink 1 ,
.Ic OAM , .Ic OAM ,
and and
.Ic HRAM . .Ic HRAM .
.Pq Ic SRAM No is the only type that can never have its bank number omitted.
.Pp .Pp
After a bank specification, the After a bank specification, the
.Dq current address .Dq current address
+1 -1
View File
@@ -9,7 +9,7 @@ add_library(common OBJECT
"util.cpp" "util.cpp"
"version.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) find_package(BISON 3.0.0 REQUIRED)
# BISON 4.0 deprecates passing this BISON_FLAGS string to `bison_target`'s `COMPILE_FLAGS`, # 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; std::string str;
size_t argIndex = 0; 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 != '%') { if (int c = spec[i]; c != '%') {
str += c; str += c;
++i; ++i;
+14 -6
View File
@@ -542,7 +542,7 @@ static uint32_t readBracketedMacroArgNum() {
} }
} }
uint32_t n = readNumber<10>(bumpChar(), nullptr); 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"); error("Number in bracketed macro argument is too large");
return 0; return 0;
} }
@@ -1005,14 +1005,22 @@ static uint32_t finishReadingFixedPoint(uint32_t integer) {
precision = options.fixPrecision; 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 // Cast to unsigned avoids undefined overflow behavior
uint32_t fractional = uint32_t fractional =
static_cast<uint32_t>(round(static_cast<double>(dividend) / divisor * (1ULL << precision))); 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; 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)) { count > 0 && (static_cast<uint32_t>(count) > nbArgs || shift > nbArgs - count)) {
warning(WARNING_MACRO_SHIFT, "Cannot shift macro arguments past their end"); warning(WARNING_MACRO_SHIFT, "Cannot shift macro arguments past their end");
shift = nbArgs; 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"); warning(WARNING_MACRO_SHIFT, "Cannot shift macro arguments past their beginning");
shift = 0; shift = 0;
} else { } else {
+4
View File
@@ -162,6 +162,10 @@ void out_CreatePatch(uint32_t type, Expression const &expr, uint32_t ofs, uint32
void out_CreateAssert( void out_CreateAssert(
AssertionType type, Expression const &expr, std::string const &message, uint32_t ofs 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(); Assertion &assertion = assertions.emplace_front();
initPatch(assertion.patch, type, expr, ofs); initPatch(assertion.patch, type, expr, ofs);
+11 -3
View File
@@ -340,6 +340,7 @@
%type <Expression> reloc_3bit %type <Expression> reloc_3bit
%type <Expression> reloc_8bit %type <Expression> reloc_8bit
%type <Expression> reloc_16bit %type <Expression> reloc_16bit
%type <Expression> reloc_8bit_signed
// Constant numbers // Constant numbers
%type <int32_t> iconst %type <int32_t> iconst
@@ -1241,6 +1242,13 @@ reloc_8bit:
} }
; ;
reloc_8bit_signed:
relocexpr {
$$ = std::move($1);
$$.checkSignedNBit(8);
}
;
reloc_16bit: reloc_16bit:
relocexpr { relocexpr {
$$ = std::move($1); $$ = std::move($1);
@@ -1886,7 +1894,7 @@ sm83_add:
| SM83_ADD MODE_HL COMMA reg_ss { | SM83_ADD MODE_HL COMMA reg_ss {
sect_ConstByte(0x09 | ($4 << 4)); sect_ConstByte(0x09 | ($4 << 4));
} }
| SM83_ADD MODE_SP COMMA reloc_8bit { | SM83_ADD MODE_SP COMMA reloc_8bit_signed {
sect_ConstByte(0xE8); sect_ConstByte(0xE8);
sect_RelByte($4, 1); sect_RelByte($4, 1);
} }
@@ -2434,11 +2442,11 @@ op_a_n:
op_sp_offset: op_sp_offset:
OP_ADD relocexpr { OP_ADD relocexpr {
$$ = std::move($2); $$ = std::move($2);
$$.checkNBit(8); $$.checkSignedNBit(8);
} }
| OP_SUB relocexpr { | OP_SUB relocexpr {
$$.makeUnaryOp(RPN_NEG, std::move($2)); $$.makeUnaryOp(RPN_NEG, std::move($2));
$$.checkNBit(8); $$.checkSignedNBit(8);
} }
| %empty { | %empty {
::error("\"LD HL, SP\" is not a valid instruction; use \"LD HL, SP + 0\""); ::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 (src1.isKnown() && src2.isKnown()) {
// If both expressions are known, just compute the value // If both expressions are known, just compute the value
int32_t lval = src1.value(), rval = src2.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) { switch (op) {
case RPN_LOGOR: case RPN_LOGOR:
@@ -325,10 +324,10 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
data = lval != rval; data = lval != rval;
break; break;
case RPN_ADD: case RPN_ADD:
data = static_cast<int32_t>(ulval + urval); data = op_add(lval, rval);
break; break;
case RPN_SUB: case RPN_SUB:
data = static_cast<int32_t>(ulval - urval); data = op_sub(lval, rval);
break; break;
case RPN_XOR: case RPN_XOR:
data = lval ^ rval; data = lval ^ rval;
@@ -370,7 +369,7 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
data = op_shift_right_unsigned(lval, rval); data = op_shift_right_unsigned(lval, rval);
break; break;
case RPN_MUL: case RPN_MUL:
data = static_cast<int32_t>(ulval * urval); data = op_mul(lval, rval);
break; break;
case RPN_DIV: case RPN_DIV:
if (rval == 0) { if (rval == 0) {
@@ -512,6 +511,25 @@ bool checkNBit(int32_t v, uint8_t n, char const *name) {
return true; 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 { void Expression::encode(std::vector<uint8_t> &buffer) const {
assume(buffer.empty()); assume(buffer.empty());
+66 -49
View File
@@ -100,7 +100,7 @@ void sect_ForEach(void (*callback)(Section &)) {
void sect_CheckSizes() { void sect_CheckSizes() {
for (Section const &sect : sections) { 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( error(
"Section \"%s\" grew too big (max size = 0x%" PRIX32 " bytes, reached 0x%" PRIX32 "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. // combination of both.
// The merging is however performed at the *end* of the original section! // The merging is however performed at the *end* of the original section!
if (org != UINT32_MAX) { if (org != UINT32_MAX) {
uint16_t curOrg = org - sect.size; // If both are fixed, they must be compatible
if (uint16_t curOrg = org - sect.size; sect.org != UINT32_MAX && sect.org != curOrg) {
// If both are fixed, they must be the same
if (sect.org != UINT32_MAX && sect.org != curOrg) {
sectError( sectError(
"Section \"%s\" already declared as fixed at incompatible address $%04" PRIx32, "Section \"%s\" already declared as fixed at incompatible address $%04" PRIx32,
sect.name.c_str(), sect.name.c_str(),
@@ -231,6 +229,16 @@ static void mergeFragments(Section &sect, uint32_t org, uint8_t alignment, uint1
sectAlignSize, sectAlignSize,
sect.alignOfs 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 { } else {
// Otherwise, just override // Otherwise, just override
sect.org = curOrg; sect.org = curOrg;
@@ -274,14 +282,6 @@ static void mergeSections(
) { ) {
sectErrors.clear(); 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) { if (sect.modifier != mod) {
sectError( sectError(
"Section \"%s\" already declared as `SECTION %s`", "Section \"%s\" already declared as `SECTION %s`",
@@ -290,8 +290,29 @@ static void mergeSections(
); );
} else { } else {
switch (mod) { 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_UNION:
case SECTION_FRAGMENT: { 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) = void (*merge)(Section &, uint32_t, uint8_t, uint16_t) =
mod == SECTION_UNION ? mergeSectUnion : mergeFragments; mod == SECTION_UNION ? mergeSectUnion : mergeFragments;
merge(sect, org, alignment, alignOffset); merge(sect, org, alignment, alignOffset);
@@ -310,20 +331,6 @@ static void mergeSections(
} }
break; 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); out_RegisterNode(sect.src);
// It is only needed to allocate memory for ROM sections. // It is only needed to allocate memory for ROM sections.
if (sectTypeHasData(type)) { if (sectTypeHasData(sect.type)) {
sect.data.resize(sectionTypeInfo[type].size); sect.data.resize(sect.typeInfo().size);
} }
return &sect; return &sect;
@@ -396,7 +403,7 @@ static Section *createSectionFragmentLiteral(Section const &parent) {
// Section fragment literals must be ROM sections. // Section fragment literals must be ROM sections.
assume(sectTypeHasData(sect.type)); assume(sectTypeHasData(sect.type));
sect.data.resize(sectionTypeInfo[sect.type].size); sect.data.resize(sect.typeInfo().size);
return &sect; return &sect;
} }
@@ -417,24 +424,24 @@ static Section *getSection(
uint32_t alignMask = alignSize - 1; uint32_t alignMask = alignSize - 1;
// First, validate parameters, and normalize them if applicable // First, validate parameters, and normalize them if applicable
SectionTypeInfo const &typeInfo = sectionTypeInfo[type];
if (bank != UINT32_MAX) { if (bank != UINT32_MAX) {
if (type != SECTTYPE_ROMX && type != SECTTYPE_VRAM && type != SECTTYPE_SRAM if (type != SECTTYPE_ROMX && type != SECTTYPE_VRAM && type != SECTTYPE_SRAM
&& type != SECTTYPE_WRAMX) { && type != SECTTYPE_WRAMX) {
error("`BANK` only allowed for `ROMX`, `WRAMX`, `SRAM`, or `VRAM` sections"); error("`BANK` only allowed for `ROMX`, `WRAMX`, `SRAM`, or `VRAM` sections");
} else if (bank < sectionTypeInfo[type].firstBank } else if (bank < typeInfo.firstBank || bank > typeInfo.lastBank) {
|| bank > sectionTypeInfo[type].lastBank) {
error( error(
"%s bank value $%04" PRIx32 " out of range ($%04" PRIx32 " to $%04" PRIx32 ")", "%s bank value $%04" PRIx32 " out of range ($%04" PRIx32 " to $%04" PRIx32 ")",
sectionTypeInfo[type].name.c_str(), typeInfo.name,
bank, bank,
sectionTypeInfo[type].firstBank, typeInfo.firstBank,
sectionTypeInfo[type].lastBank typeInfo.lastBank
); );
} }
} else if (sectTypeBanks(type) == 1) { } else if (!typeInfo.isBanked()) {
// If the section type only has a single bank, implicitly force it // 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. // 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 != UINT32_MAX) {
if (org < sectionTypeInfo[type].startAddr || org > sectTypeEndAddr(type)) { if (org < typeInfo.startAddr || org > typeInfo.endAddr()) {
error( error(
"Section \"%s\"'s fixed address $%04" PRIx32 " is outside of range [$%04" PRIx16 "Section \"%s\"'s fixed address $%04" PRIx32 " is outside of range [$%04" PRIx16
"; $%04" PRIx16 "]", "; $%04" PRIx16 "]",
name.c_str(), name.c_str(),
org, org,
sectionTypeInfo[type].startAddr, typeInfo.startAddr,
sectTypeEndAddr(type) typeInfo.endAddr()
); );
} }
} }
@@ -465,18 +472,15 @@ static Section *getSection(
if (alignment != 0) { if (alignment != 0) {
// It doesn't make sense to have both alignment and org set // It doesn't make sense to have both alignment and org set
if (org != UINT32_MAX) { 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()); error("Section \"%s\"'s fixed address does not match its alignment", name.c_str());
} }
alignment = 0; // Ignore it if it's satisfied alignment = 0; // Ignore it if it's satisfied
} else if (sectionTypeInfo[type].startAddr & alignMask) { } else if ((typeInfo.startAddr & alignMask) > alignOffset) {
error( error(
"Section \"%s\"'s alignment cannot be attained in %s", "Section \"%s\"'s alignment cannot be attained in %s", name.c_str(), typeInfo.name
name.c_str(),
sectionTypeInfo[type].name.c_str()
); );
alignment = 0; // Ignore it if it's unattainable alignment = 0; // Ignore it if it's unattainable
org = 0;
} else if (alignment == 16) { } else if (alignment == 16) {
// Treat an alignment of 16 as fixing the address. // Treat an alignment of 16 as fixing the address.
alignment = 0; alignment = 0;
@@ -544,6 +548,10 @@ void sect_NewSection(
SectionSpec const &attrs, SectionSpec const &attrs,
SectionModifier mod SectionModifier mod
) { ) {
if (name.find('\0') != std::string::npos) {
fatal("Section names cannot contain '\\0' characters");
}
for (SectionStackEntry &entry : sectionStack) { for (SectionStackEntry &entry : sectionStack) {
if (entry.section && entry.section->name == name) { if (entry.section && entry.section->name == name) {
fatal("Section \"%s\" is already on the stack", name.c_str()); 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) { } else if (alignment == 16) {
// Treat an alignment large enough as fixing the address. // Treat an alignment large enough as fixing the address.
// Note that this also ensures that a section's alignment never becomes 16 or greater. // Note that this also ensures that a section's alignment never becomes 16 or greater.
sect->align = 0; // Reset the alignment, since we're fixing the address. if (offset < curOffset) {
sect->org = 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) { } else if (alignment > sect->align) {
sect->align = alignment; sect->align = alignment;
// We need `(sect->alignOfs + curOffset) & alignMask == offset` // 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 *fileSize
); );
return false; return false;
} else if (startPos + length > *fileSize) { } else if (length > *fileSize - startPos) {
error( error(
"Specified range in `INCBIN` file \"%s\" is out of bounds (%" PRIu32 " + %" PRIu32 "Specified range in `INCBIN` file \"%s\" is out of bounds (%" PRIu32 " + %" PRIu32
" > %" PRIu64 ")", " > %" PRIu64 ")",
+2 -2
View File
@@ -251,9 +251,9 @@ static void
error("\"%s\" has more than 65536 banks", name); // LCOV_EXCL_LINE error("\"%s\" has more than 65536 banks", name); // LCOV_EXCL_LINE
}; };
static constexpr off_t NB_BANKS_LIMIT = 0x10000; 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 (input == output) {
if (fileSize >= NB_BANKS_LIMIT * BANK_SIZE) { if (fileSize > NB_BANKS_LIMIT * BANK_SIZE) {
return errorTooLarge(); // LCOV_EXCL_LINE return errorTooLarge(); // LCOV_EXCL_LINE
} }
// Compute number of banks and ROMX len from file size // 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; len = maxLen;
warning( warning(
WARNING_TRUNCATION, WARNING_TRUNCATION,
"Truncating title \"%s\" to %u chars", "Truncating title \"%s\" to %" PRIu8 " chars",
options.title->c_str(), options.title->c_str(),
maxLen maxLen
); );
+54 -37
View File
@@ -36,13 +36,13 @@ Options options;
// Flags which must be processed after the option parsing finishes // Flags which must be processed after the option parsing finishes
static struct LocalOptions { static struct LocalOptions {
std::optional<std::string> externalPalSpec; // -c std::optional<std::string> palSpec; // -c
bool autoAttrmap; // -A bool autoAttrmap; // -A
bool autoTilemap; // -T bool autoTilemap; // -T
bool autoPalettes; // -P bool autoPalettes; // -P
bool autoPalmap; // -Q bool autoPalmap; // -Q
bool groupOutputs; // -O bool groupOutputs; // -O
bool reverse; // -r bool reverse; // -r
bool autoAny() const { return autoAttrmap || autoTilemap || autoPalettes || autoPalmap; } bool autoAny() const { return autoAttrmap || autoTilemap || autoPalettes || autoPalmap; }
} localOptions; } localOptions;
@@ -193,10 +193,10 @@ static void parseArg(int ch, char *arg) {
break; break;
case 'c': 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] == '#') { if (arg[0] == '#') {
options.palSpecType = Options::EXPLICIT; options.palSpecType = Options::INLINE;
parseInlinePalSpec(arg); localOptions.palSpec = arg;
} else if (strcasecmp(arg, "embedded") == 0) { } else if (strcasecmp(arg, "embedded") == 0) {
// Use PLTE, error out if missing // Use PLTE, error out if missing
options.palSpecType = Options::EMBEDDED; options.palSpecType = Options::EMBEDDED;
@@ -204,13 +204,13 @@ static void parseArg(int ch, char *arg) {
options.palSpecType = Options::NO_SPEC; options.palSpecType = Options::NO_SPEC;
} else if (strcasecmp(arg, "dmg") == 0) { } else if (strcasecmp(arg, "dmg") == 0) {
options.palSpecType = Options::DMG; 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) { } else if (strncasecmp(arg, "dmg=", literal_strlen("dmg=")) == 0) {
options.palSpecType = Options::DMG; options.palSpecType = Options::DMG;
parseDmgPalSpec(&arg[literal_strlen("dmg=")]); localOptions.palSpec = &arg[literal_strlen("dmg=")];
} else { } else {
options.palSpecType = Options::EXPLICIT; options.palSpecType = Options::EXTERNAL;
localOptions.externalPalSpec = arg; localOptions.palSpec = arg;
} }
break; break;
@@ -299,6 +299,10 @@ static void parseArg(int ch, char *arg) {
break; break;
case 'N': 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); options.maxNbTiles[0] = readNumber(argPtr, "Number of tiles in bank 0", 256);
if (options.maxNbTiles[0] > 256) { if (options.maxNbTiles[0] > 256) {
error("Bank 0 cannot contain more than 256 tiles"); error("Bank 0 cannot contain more than 256 tiles");
@@ -514,28 +518,17 @@ static void verboseOutputConfig() {
// -s/--palette-size // -s/--palette-size
fprintf(stderr, "\tPalettes contain %" PRIu8 " colors\n", options.nbColorsPerPal); fprintf(stderr, "\tPalettes contain %" PRIu8 " colors\n", options.nbColorsPerPal);
// -c/--colors // -c/--colors
if (options.palSpecType == Options::NO_SPEC) { switch (options.palSpecType) {
case Options::NO_SPEC:
fputs("\tAutomatic palette generation\n", stderr); fputs("\tAutomatic palette generation\n", stderr);
} else { break;
fprintf(stderr, "\t%s palette spec\n", [] { case Options::INLINE:
switch (options.palSpecType) { case Options::EXTERNAL:
case Options::EXPLICIT: fputs("\tExplicit palette spec\n\t[\n", stderr);
return "Explicit";
case Options::EMBEDDED:
return "Embedded";
case Options::DMG:
return "DMG";
default:
return "???";
}
}());
}
if (options.palSpecType == Options::EXPLICIT) {
fputs("\t[\n", stderr);
for (auto const &pal : options.palSpec) { for (auto const &pal : options.palSpec) {
fputs("\t\t", stderr); fputs("\t\t", stderr);
for (auto const &color : pal) { for (auto const &color : pal) {
if (color) { if (color.has_value()) {
fprintf(stderr, "#%06x, ", color->toCSS() >> 8); fprintf(stderr, "#%06x, ", color->toCSS() >> 8);
} else { } else {
fputs("#none, ", stderr); fputs("#none, ", stderr);
@@ -544,6 +537,13 @@ static void verboseOutputConfig() {
putc('\n', stderr); putc('\n', stderr);
} }
fputs("\t]\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 // -L/--slice
if (options.inputSlice.width || options.inputSlice.height || options.inputSlice.left 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.autoPalettes, options.palettes, ".pal");
autoOutPath(localOptions.autoPalmap, options.palmap, ".palmap"); autoOutPath(localOptions.autoPalmap, options.palmap, ".palmap");
// Execute deferred external pal spec parsing, now that all other params are known // Execute deferred pal spec parsing, now that all other params are known.
if (localOptions.externalPalSpec) { // Do not parse pal specs if `options.nbColorsPerPal` is invalid.
parseExternalPalSpec(localOptions.externalPalSpec->c_str()); 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); verboseDo(VERB_CONFIG, verboseOutputConfig);
@@ -687,8 +705,7 @@ int main(int argc, char *argv[]) {
} else { } else {
process(); process();
} }
} else if (!options.palettes.empty() && options.palSpecType == Options::EXPLICIT } else if (!options.palettes.empty() && options.hasExplicitPalSpec() && !localOptions.reverse) {
&& !localOptions.reverse) {
processPalettes(); processPalettes();
} else { } else {
usage.printAndExit("No input file specified (pass \"-\" to read from standard input)"); 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/rgba.hpp"
#include "gfx/warning.hpp" #include "gfx/warning.hpp"
using namespace std::literals;
using namespace std::string_view_literals; using namespace std::string_view_literals;
static char const *hexDigits = "0123456789ABCDEFabcdef"; static char const *hexDigits = "0123456789ABCDEFabcdef";
@@ -131,8 +132,14 @@ void parseInlinePalSpec(char const * const rawArg) {
if (n == arg.length()) { if (n == arg.length()) {
break; break;
} else if (arg[n] != ';' && arg[n] != ':') { } else if (arg[n] != ';' && arg[n] != ':') {
if (nbColors == 4) { if (nbColors == options.nbColorsPerPal) {
parseError(n, 1, "Each palette can only contain up to 4 colors"); // `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; return;
} }
break; break;
@@ -535,10 +542,10 @@ static void parseGBCFile(char const *filename, std::filebuf &file) {
} }
options.palSpec.push_back({ options.palSpec.push_back({
Rgba::fromCGBColor(toWord(buf[0], buf[1])), Rgba::fromCGBColor(toWord(buf[0], buf[1]), false),
Rgba::fromCGBColor(toWord(buf[2], buf[3])), Rgba::fromCGBColor(toWord(buf[2], buf[3]), false),
Rgba::fromCGBColor(toWord(buf[4], buf[5])), Rgba::fromCGBColor(toWord(buf[4], buf[5]), false),
Rgba::fromCGBColor(toWord(buf[6], buf[7])), Rgba::fromCGBColor(toWord(buf[6], buf[7]), false),
}); });
} }
} }
@@ -664,11 +671,7 @@ void parseDmgPalSpec(char const * const rawArg) {
return; return;
} }
parseDmgPalSpec(toHex(arg[0], arg[1])); options.palSpecDmg = toHex(arg[0], arg[1]);
}
void parseDmgPalSpec(uint8_t palSpecDmg) {
options.palSpecDmg = palSpecDmg;
// Map gray shades to their DMG color indexes for fast lookup by `Rgba::grayIndex` // Map gray shades to their DMG color indexes for fast lookup by `Rgba::grayIndex`
for (uint8_t i = 0; i < 4; ++i) { for (uint8_t i = 0; i < 4; ++i) {
+1
View File
@@ -177,6 +177,7 @@ Png::Png(char const *filename, std::streambuf &file) {
break; break;
case PNG_COLOR_TYPE_PALETTE: case PNG_COLOR_TYPE_PALETTE:
png_set_palette_to_rgb(png); png_set_palette_to_rgb(png);
isIndexed = true; // This enables sorting generated palette colors by the PLTE chunk
break; break;
} }
+139 -73
View File
@@ -179,9 +179,14 @@ struct Image {
std::unordered_set<std::pair<uint32_t, uint32_t>, decltype(hashPair)> fusions; std::unordered_set<std::pair<uint32_t, uint32_t>, decltype(hashPair)> fusions;
// Register colors from `png` into `colors` // Register colors from `png` into `colors`
for (uint32_t y = 0; y < png.height; ++y) { uint32_t const pxLeft = options.inputSlice.width ? options.inputSlice.left : 0;
for (uint32_t x = 0; x < png.width; ++x) { uint32_t const pxTop = options.inputSlice.height ? options.inputSlice.top : 0;
if (Rgba const &color = pixel(x, y); color.isTransparent() == color.isOpaque()) { 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 // Report ambiguously transparent or opaque colors
if (uint32_t css = color.toCSS(); ambiguous.find(css) == ambiguous.end()) { if (uint32_t css = color.toCSS(); ambiguous.find(css) == ambiguous.end()) {
error( 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 { class TilesVisitor {
@@ -365,8 +377,9 @@ static std::pair<std::vector<size_t>, std::vector<Palette>>
// LCOV_EXCL_STOP // LCOV_EXCL_STOP
std::vector<Palette> palettes(nbPalettes); std::vector<Palette> palettes(nbPalettes);
// If the image contains at least one transparent pixel, force transparency in the first slot of // If the image contains at least one transparent pixel, force transparency in the first slot of
// all palettes // all palettes.
if (options.hasTransparentPixels) { if (options.hasTransparentPixels) {
for (Palette &pal : palettes) { for (Palette &pal : palettes) {
pal.colors[0] = Rgba::transparent; 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 // "Sort" colors in the generated palettes, see the man page for the flowchart
if (options.palSpecType == Options::DMG) { if (options.palSpecType == Options::DMG) {
sortGrayscale(palettes, image.colors.raw()); 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(
WARNING_EMBEDDED, WARNING_EMBEDDED,
"Sorting palette colors by PNG's embedded PLTE chunk without '-c/--colors 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>> static std::pair<std::vector<size_t>, std::vector<Palette>>
makePalsAsSpecified(std::vector<ColorSet> const &colorSets) { makePalsAsSpecified(std::vector<ColorSet> const &colorSets) {
// Convert the palette spec to actual palettes
std::vector<Palette> palettes(options.palSpec.size()); 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)) { 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) { for (size_t i = 0; i < options.nbColorsPerPal; ++i) {
// If the spec has a gap, there's no need to copy anything. // If the spec has a gap, there's no need to copy anything.
if (spec[i].has_value() && spec[i]->isOpaque()) { if (!spec[i].has_value() || !spec[i]->isOpaque()) {
pal[i] = spec[i]->cgbColor(); 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 // 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 // If the palette generation is wrong, other (dependee) operations are likely to be
// nonsensical, so fatal-error outright // nonsensical, so fatal-error outright
fatal( fatal("Generated %zu palettes, over the maximum of %" PRIu16, nbPals, options.nbPalettes);
"Generated %zu palettes, over the maximum of %" PRIu16, } else if (nbPals > 8 && !options.attrmap.empty() && options.palmap.empty()) {
palettes.size(), // With `-n/--nb-palettes` greater than 8, palette IDs may be truncated in the attrmap
options.nbPalettes // (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()) { if (!options.palettes.empty()) {
@@ -631,17 +680,15 @@ static void outputUnoptimizedTileData(
// LCOV_EXCL_STOP // LCOV_EXCL_STOP
} }
uint64_t widthTiles = options.inputSlice.width ? options.inputSlice.width : image.png.width / 8; size_t nbTiles = std::count_if(RANGE(attrmap), [](AttrmapEntry const &attr) {
uint64_t heightTiles = return !attr.isBackgroundTile();
options.inputSlice.height ? options.inputSlice.height : image.png.height / 8; });
uint64_t nbTiles = widthTiles * heightTiles; size_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0;
uint64_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0; size_t tileIdx = 0;
uint64_t tileIdx = 0;
for (auto const &[tile, attr] : zip(image.visitAsTiles(), attrmap)) { for (auto const &[tile, attr] : zip(image.visitAsTiles(), attrmap)) {
// Do not emit fully-background tiles. // Do not emit fully-background tiles.
if (attr.isBackgroundTile()) { if (attr.isBackgroundTile()) {
++tileIdx;
continue; continue;
} }
@@ -691,38 +738,49 @@ static void outputUnoptimizedMaps(
autoOpenPath(options.attrmap, attrmapOutput); autoOpenPath(options.attrmap, attrmapOutput);
autoOpenPath(options.palmap, palmapOutput); 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; uint16_t tileIdx = 0;
uint8_t bank = 0; uint8_t bank = 0;
for (AttrmapEntry const &attr : attrmap) { for (AttrmapEntry const &attr : attrmap) {
// The update-increment logic at the end of this loop may increment `bank` from 1 to 2, // A non-zero base ID may make this addition overflow, wrapping around the available
// if both banks 0 and 1 are full, but by then all the `attrmap` entries should have been // palette IDs. Since the operands are unsigned, this won't cause undefined behavior.
// processed, since there cannot be more tiles than could fit in both banks. // With `-n/--nb-palettes` greater than 8, palette IDs may be truncated in the attrmap
assume(bank < 2); // (though not in the palmap), which was already warned about.
// 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; uint8_t palID = attr.getPalID(mappings) + options.basePalID;
if (attrmapOutput.has_value()) {
(*attrmapOutput)->sputc((palID & 0b111) | bank << 3); // The other flags are all 0
}
if (palmapOutput.has_value()) {
(*palmapOutput)->sputc(palID);
}
// Background tiles were not emitted in the tile data, so their ID and bank do not update.
if (attr.isBackgroundTile()) { if (attr.isBackgroundTile()) {
continue; // 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);
if (tileIdx + 1 < options.maxNbTiles[bank]) { emit(tilemapOutput, options.baseTileIDs[0]);
++tileIdx; 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 { } else {
++bank; // The only valid tile banks are 0 and 1.
tileIdx = 0; assume(bank < 2);
// 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);
++tileIdx;
// 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.xFlip = false;
attr.yFlip = false; attr.yFlip = false;
attr.bank = 0; attr.bank = 0;
attr.tileID = 0; attr.tileID = options.baseTileIDs[attr.bank];
} else { } 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) { if (inputWithoutOutput && matchType == TileData::NOPE) {
error( error(
@@ -831,8 +889,8 @@ static UniqueTiles dedupTiles(
attr.xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP; attr.xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP;
attr.yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP; attr.yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP;
attr.bank = tileID >= options.maxNbTiles[0]; attr.bank = tileIdx >= options.maxNbTiles[0];
attr.tileID = (attr.bank ? tileID - options.maxNbTiles[0] : tileID) attr.tileID = (attr.bank ? tileIdx - options.maxNbTiles[0] : tileIdx)
+ options.baseTileIDs[attr.bank]; + options.baseTileIDs[attr.bank];
} }
} }
@@ -849,9 +907,9 @@ static void outputTileData(UniqueTiles const &tiles) {
// LCOV_EXCL_STOP // LCOV_EXCL_STOP
} }
uint64_t nbTiles = tiles.size(); size_t nbTiles = tiles.size();
uint64_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0; size_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0;
uint64_t tileIdx = 0; size_t tileIdx = 0;
for (TileData const *tile : tiles) { for (TileData const *tile : tiles) {
assume(tile->tileID == tileIdx); assume(tile->tileID == tileIdx);
@@ -889,6 +947,8 @@ static void outputTilemap(std::vector<AttrmapEntry> const &attrmap) {
// LCOV_EXCL_STOP // 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) { for (AttrmapEntry const &entry : attrmap) {
output->sputc(entry.tileID); // The tile ID has already been converted output->sputc(entry.tileID); // The tile ID has already been converted
} }
@@ -908,6 +968,8 @@ static void
attr |= entry.bank << 3; attr |= entry.bank << 3;
// The unsigned underflow for the palette ID is intentional, since a // The unsigned underflow for the palette ID is intentional, since a
// nonzero base palette ID may overflow and continue with IDs from 0. // 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; attr |= (entry.getPalID(mappings) + options.basePalID) & 0b111;
output->sputc(attr); output->sputc(attr);
} }
@@ -990,12 +1052,14 @@ void process() {
for (auto tile : image.visitAsTiles()) { for (auto tile : image.visitAsTiles()) {
AttrmapEntry &attrs = attrmap.emplace_back(); 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; std::unordered_set<uint16_t> tileColors;
for (uint32_t y = 0; y < 8; ++y) { for (uint32_t y = 0; y < 8; ++y) {
for (uint32_t x = 0; x < 8; ++x) { for (uint32_t x = 0; x < 8; ++x) {
if (Rgba color = tile.pixel(x, y); Rgba color = tile.pixel(x, y);
color.isOpaque() || !options.hasTransparentPixels) { // Ambiguous colors should not be in `tileColors`
assume(color.isOpaque() != color.isTransparent());
if (color.isOpaque() || !options.hasTransparentPixels) {
tileColors.insert(color.cgbColor()); tileColors.insert(color.cgbColor());
} }
} }
@@ -1140,20 +1204,31 @@ continue_visiting_tiles:;
: makePalsAsSpecified(colorSets); : makePalsAsSpecified(colorSets);
outputPalettes(palettes); outputPalettes(palettes);
// If deduplication is not happening, we just need to output the tile data and/or maps as-is auto checkTileCountLimit = [](size_t nbTiles) {
if (!options.allowDedup) { if (nbTiles > options.maxNbTiles[0] + options.maxNbTiles[1]) {
// 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]) {
fatal( fatal(
"Image contains %zu tiles, exceeding the limit of %" PRIu16 " + %" PRIu16, "Image contains %zu tiles, exceeding the limit of %" PRIu16 " + %" PRIu16,
nbTiles, nbTiles,
options.maxNbTiles[0], options.maxNbTiles[0],
options.maxNbTiles[1] 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. // I currently cannot figure out useful semantics for this combination of flags.
if (!options.inputTileset.empty()) { if (!options.inputTileset.empty()) {
@@ -1176,16 +1251,7 @@ continue_visiting_tiles:;
verbosePrint(VERB_NOTICE, "Deduplicating tiles...\n"); verbosePrint(VERB_NOTICE, "Deduplicating tiles...\n");
UniqueTiles tiles = dedupTiles(image, attrmap, palettes, mappings); UniqueTiles tiles = dedupTiles(image, attrmap, palettes, mappings);
// Check the tile count checkTileCountLimit(tiles.size());
if (size_t nbTiles = tiles.size();
nbTiles > options.maxNbTiles[0] + options.maxNbTiles[1]) {
fatal(
"Image contains %zu tiles, exceeding the limit of %" PRIu16 " + %" PRIu16,
nbTiles,
options.maxNbTiles[0],
options.maxNbTiles[1]
);
}
if (!options.output.empty()) { if (!options.output.empty()) {
verbosePrint(VERB_NOTICE, "Generating optimized tile data...\n"); 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"); 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 if (options.inputSlice.left != 0 || options.inputSlice.top != 0
|| options.inputSlice.height != 0) { || options.inputSlice.height != 0) {
warnx("\"Sliced-off\" pixels are ignored in reverse mode"); 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( warnx(
"Specified input slice width (%" PRIu16 "Specified input slice width (%" PRIu16
") does not match provided reversing width (%" PRIu16 " * 8)", ") does not match provided reversing width (%" PRIu16 ")",
options.inputSlice.width, options.inputSlice.width,
options.reversedWidth options.reversedWidth
); );
@@ -216,26 +212,28 @@ void reverse() {
} }
palettes.clear(); 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 (;;) { 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) { nbRead == 0) {
break; break;
} else if (nbRead != buf.size()) { } else if (nbRead != palSize) {
fatal( fatal(
"Palette data size (%zu) is not a multiple of %zu bytes\n", "Palette data size (%zu) is not a multiple of %zu bytes",
palettes.size() * buf.size() + nbRead, palettes.size() * palSize + nbRead,
buf.size() palSize
); );
} }
// Expand the colors // Expand the little-endian RGB555 colors to RGB888
auto &palette = palettes.emplace_back(); auto &palette = palettes.emplace_back();
std::generate( std::generate(
palette.begin(), palette.begin(),
palette.begin() + options.nbColorsPerPal, palette.begin() + options.nbColorsPerPal,
[&buf, i = 0]() mutable { [&buf, i = 0]() mutable {
i += 2; 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) { if (options.hasExplicitPalSpec() && palettes != options.palSpec) {
warnx("Colors in the palette file do not match those specified with '-c'"); // 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 // This spacing aligns "...versus with `-c`" above the column of `-c` palettes
fputs("Colors specified in the palette file: ...versus with '-c':\n", stderr); 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) { 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) { } else if (options.palSpecType == Options::EMBEDDED) {
warnx("An embedded palette was requested, but no palette file was specified; ignoring " warnx("An embedded palette was requested, but no palette file was specified; ignoring "
"request"); "request");
} else if (options.palSpecType == Options::EXPLICIT) { } else if (options.hasExplicitPalSpec()) {
palettes = std::move(options.palSpec); // We won't be using it again. palettes = std::move(options.palSpec); // We won't be using it again.
} }
std::optional<std::vector<uint8_t>> attrmap; 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()) { if (!options.attrmap.empty()) {
attrmap = readInto(options.attrmap); attrmap = readInto(options.attrmap);
if (attrmap->size() != mapSize) { if (attrmap->size() != mapSize) {
@@ -327,8 +338,8 @@ void reverse() {
// The unsigned underflow for `tileOfs` is intentional, since a nonzero // The unsigned underflow for `tileOfs` is intentional, since a nonzero
// base tile ID may overflow and continue with IDs from 0. // base tile ID may overflow and continue with IDs from 0.
if (uint8_t tileOfs = (*tilemap)[index] - options.baseTileIDs[bank]; if (uint8_t tileOfs = (*tilemap)[index] - options.baseTileIDs[bank];
tileOfs >= nbTilesInBank[bank]) { tileOfs >= nbTilesMappedInBank[bank]) {
nbTilesInBank[bank] = tileOfs + 1; nbTilesMappedInBank[bank] = tileOfs + 1;
} }
} }
} }
@@ -336,29 +347,43 @@ void reverse() {
verbosePrint( verbosePrint(
VERB_INFO, VERB_INFO,
"Number of tiles in bank {0: %" PRIu16 ", 1: %" PRIu16 "}\n", "Number of tiles in bank {0: %" PRIu16 ", 1: %" PRIu16 "}\n",
nbTilesInBank[0], nbTilesMappedInBank[0],
nbTilesInBank[1] nbTilesMappedInBank[1]
); );
for (int bank = 0; bank < 2; ++bank) { for (int bank = 0; bank < 2; ++bank) {
if (nbTilesInBank[bank] > options.maxNbTiles[bank]) { if (nbTilesMappedInBank[bank] > options.maxNbTiles[bank]) {
error( error(
"Bank %d contains %" PRIu16 " tiles, but the specified limit is %" PRIu16, "Bank %d contains %" PRIu16 " tiles, but the specified limit is %" PRIu16,
bank, bank,
nbTilesInBank[bank], nbTilesMappedInBank[bank],
options.maxNbTiles[bank] options.maxNbTiles[bank]
); );
} }
} }
if (nbTilesInBank[0] + nbTilesInBank[1] > nbTiles) { if (uint16_t const maxTotalNbTiles =
fatal( nbTilesMappedInBank[1] > 0
"The tilemap references %" PRIu16 " tiles in bank 0 and %" PRIu16 ? std::max<uint16_t>(
" in bank 1, but only %zu have been read in total", nbTilesMappedInBank[0], options.maxNbTiles[0] + nbTilesMappedInBank[1]
nbTilesInBank[0], )
nbTilesInBank[1], : nbTilesMappedInBank[0];
nbTiles 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(); requireZeroErrors();
@@ -388,7 +413,9 @@ void reverse() {
} }
} }
} else { } 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) { for (size_t index = 0; index < mapSize; ++index) {
size_t tx = index % width, ty = index / width; size_t tx = index % width, ty = index / width;
uint8_t tileID = (*tilemap)[index]; uint8_t tileID = (*tilemap)[index];
@@ -548,7 +575,7 @@ void reverse() {
// Get the tile ID at this location // Get the tile ID at this location
size_t tileOfs = size_t tileOfs =
tilemap ? static_cast<uint8_t>((*tilemap)[index] - options.baseTileIDs[bank]) tilemap ? static_cast<uint8_t>((*tilemap)[index] - options.baseTileIDs[bank])
+ (bank ? nbTilesInBank[0] : 0) + (bank ? options.maxNbTiles[0] : 0)
: index; : index;
// This should have been enforced by the earlier checking. // This should have been enforced by the earlier checking.
assume(tileOfs < nbTiles + options.trim); assume(tileOfs < nbTiles + options.trim);
@@ -567,7 +594,7 @@ void reverse() {
// If vertically mirrored, fetch the bytes from the other end // If vertically mirrored, fetch the bytes from the other end
uint8_t realY = (attribute & 0x40 ? 7 - y : y) * options.bitDepth; uint8_t realY = (attribute & 0x40 ? 7 - y : y) * options.bitDepth;
uint8_t bitplane0 = tileData[realY]; 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 if (attribute & 0x20) { // Handle horizontal flip
bitplane0 = flipTable[bitplane0]; bitplane0 = flipTable[bitplane0];
bitplane1 = flipTable[bitplane1]; bitplane1 = flipTable[bitplane1];
@@ -599,7 +626,7 @@ void reverse() {
if (Rgba const &pixel = *color; pngColorType == PNG_COLOR_TYPE_GRAY) { if (Rgba const &pixel = *color; pngColorType == PNG_COLOR_TYPE_GRAY) {
gray = gray << pngDepth | (pixel.red & ((1 << pngDepth) - 1)); gray = gray << pngDepth | (pixel.red & ((1 << pngDepth) - 1));
} else if (pngColorType == PNG_COLOR_TYPE_PALETTE) { } else if (pngColorType == PNG_COLOR_TYPE_PALETTE) {
*ptr++ = palOfs * 4 + colorID; *ptr++ = palOfs * options.nbColorsPerPal + colorID;
} else { } else {
*ptr++ = pixel.red; *ptr++ = pixel.red;
*ptr++ = pixel.green; *ptr++ = pixel.green;
+38 -8
View File
@@ -24,16 +24,24 @@ std::string toCGB(uint16_t color) {
return buf; return buf;
} }
// Based on inverting the "Modern - Accurate" formula used by SameBoy // Copied from the "Modern - Accurate" (`GB_COLOR_CORRECTION_MODERN_ACCURATE`)
// since commit b5a611c5db46d6a0649d04d24d8d6339200f9ca1 (Dec 2020), // formula used by SameBoy in its `scale_channel_with_curve` function since
// with gaps in the scale curve filled by polynomial interpolation. // 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 // clang-format off: vertically align columns of values
static std::array<uint8_t, 256> reverse_curve{ static std::array<uint8_t, 256> reverse_curve{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1,
1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3,
3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5,
5, 5, 5, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 5, 5, 5, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7,
7, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10, 7, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10,
10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13,
13, 13, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 16, 16, 16, 13, 13, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 16, 16, 16,
16, 16, 16, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 19, 19, 16, 16, 16, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 19, 19,
@@ -48,6 +56,28 @@ static std::array<uint8_t, 256> reverse_curve{
}; };
// clang-format on // 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 { uint16_t Rgba::cgbColor() const {
if (isTransparent()) { if (isTransparent()) {
return transparent; return transparent;
+303 -238
View File
@@ -2,6 +2,7 @@
#include "link/assign.hpp" #include "link/assign.hpp"
#include <algorithm>
#include <deque> #include <deque>
#include <inttypes.h> #include <inttypes.h>
#include <optional> #include <optional>
@@ -15,6 +16,7 @@
#include "helpers.hpp" #include "helpers.hpp"
#include "itertools.hpp" #include "itertools.hpp"
#include "linkdefs.hpp" #include "linkdefs.hpp"
#include "platform.hpp"
#include "verbosity.hpp" #include "verbosity.hpp"
#include "link/main.hpp" #include "link/main.hpp"
@@ -23,21 +25,255 @@
#include "link/symbol.hpp" #include "link/symbol.hpp"
#include "link/warning.hpp" #include "link/warning.hpp"
struct MemoryLocation {
uint16_t address;
uint32_t bank;
};
struct FreeSpace { struct FreeSpace {
uint16_t address; 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 // Table of free space for each bank
static std::vector<std::deque<FreeSpace>> memory[SECTTYPE_INVALID]; 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 // Assigns a section to a given memory location
static void assignSection(Section &section, MemoryLocation const &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 // Propagate the assigned location to all UNIONs/FRAGMENTs
// so `jr` patches in them will have the correct offset // so `jr` patches in them will have the correct offset
for (Section &piece : section.pieces()) { for (Section &piece : section.pieces()) {
@@ -47,267 +283,93 @@ static void assignSection(Section &section, MemoryLocation const &location) {
out_AddSection(section); out_AddSection(section);
} }
// Checks whether a given location is suitable for placing a given section static std::string describeConstraintsOf(Section const &section) {
// This checks not only that the location has enough room for the section, but std::string description = "\"" + section.name + "\" (" + section.typeInfo().name + " section) ";
// also that the constraints (alignment...) are respected. if (section.isBankFixed && section.typeInfo().isBanked()) {
static bool isLocationSuitable( char bank[9];
Section const &section, FreeSpace const &freeSpace, MemoryLocation const &location sprintf_to_array(bank, "%02" PRIx32, section.bank);
) {
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
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) { if (section.isAddressFixed) {
char addr[8]; char addr[5];
snprintf(addr, sizeof(addr), "%04" PRIx16, section.org); sprintf_to_array(addr, "%04" PRIx16, section.org);
description = description + "at $" + bank + ":" + addr; description = description + "at $" + bank + ":" + addr;
} else if (section.isAlignFixed) { } else if (section.isAlignFixed) {
char mask[8]; char mask[5];
snprintf(mask, sizeof(mask), "%" PRIx16, static_cast<uint16_t>(~section.alignMask)); sprintf_to_array(mask, "%" PRIx16, static_cast<uint16_t>(~section.alignMask));
description = description + "in bank $" + bank + " with align mask $" + mask; description = description + "in bank $" + bank + " with align mask $" + mask;
} else { } else {
description = description + "in bank $" + bank; description = description + "in bank $" + bank;
} }
} else { } else {
if (section.isAddressFixed) { if (section.isAddressFixed) {
char addr[8]; char addr[5];
snprintf(addr, sizeof(addr), "%04" PRIx16, section.org); sprintf_to_array(addr, "%04" PRIx16, section.org);
description = description + "at address $" + addr; description = description + "at address $" + addr;
} else if (section.isAlignFixed) { } else if (section.isAlignFixed) {
char mask[8], offset[8]; char mask[5], offset[5];
snprintf(mask, sizeof(mask), "%" PRIx16, static_cast<uint16_t>(~section.alignMask)); sprintf_to_array(mask, "%" PRIx16, static_cast<uint16_t>(~section.alignMask));
snprintf(offset, sizeof(offset), "%" PRIx16, section.alignOfs); sprintf_to_array(offset, "%" PRIx16, section.alignOfs);
description = description + "with align mask $" + mask + " and offset $" + offset; description = description + "with align mask $" + mask + " and offset $" + offset;
} else { } else {
description = description + "anywhere"; 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; return description;
} }
// Places a section in a suitable location, or error out if it fails to. // 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! // Due to the implemented algorithm, this should be called with sections of decreasing size!
static void placeSection(Section &section) { 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) { if (section.size == 0) {
// Unless the SECTION's address was fixed, the starting address if (!section.isAddressFixed) {
// is fine for any alignment, as checked in sect_DoSanityChecks. location.address = section.typeInfo().startAddr;
MemoryLocation location = { if (section.isAlignFixed) {
.address = location.makeAddressAligned(section.alignMask, section.alignOfs);
section.isAddressFixed ? section.org : sectionTypeInfo[section.type].startAddr,
.bank = section.isBankFixed ? section.bank : sectionTypeInfo[section.type].firstBank,
};
assignSection(section, location);
return;
}
// Place section using first-fit decreasing algorithm
// https://en.wikipedia.org/wiki/Bin_packing_problem#First-fit_algorithm
MemoryLocation location = getStartLocation(section);
if (std::optional<size_t> spaceIdx = getPlacement(section, location); spaceIdx) {
std::deque<FreeSpace> &bankMem =
memory[section.type][location.bank - sectionTypeInfo[section.type].firstBank];
FreeSpace &freeSpace = bankMem[*spaceIdx];
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;
} }
} }
// 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;
}
}
FreeSpaceIter iter = tryPlacing(section, location);
if (std::deque<FreeSpace> &bankMem = freeSpaceOfBank(section, location.bank);
iter != bankMem.end()) {
assignSection(section, location);
updateFreeSpace(iter, bankMem, section);
return; return;
} }
if (!section.isBankFixed || !section.isAddressFixed) { if (!section.isBankFixed || !section.isAddressFixed) {
// If a section failed to go to several places, nothing we can report // If a section failed to go to several places, nothing we can report
fatal("Unable to place %s", getSectionDescription(section).c_str()); fatal("Unable to place %s", describeConstraintsOf(section).c_str());
} else if (section.org + section.size > sectTypeEndAddr(section.type) + 1) { } 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 // If the section just can't fit the bank, report that
fatal( fatal(
"Unable to place %s: section runs past end of region ($%04x > $%04x)", "Unable to place %s: section runs past end of region ($%04x > $%04x)",
getSectionDescription(section).c_str(), describeConstraintsOf(section).c_str(),
section.org + section.size, section.org + section.size,
sectTypeEndAddr(section.type) + 1 onePastEnd
); );
} else { } else {
// Otherwise there is overlap with another section // Otherwise there is overlap with another section
@@ -315,7 +377,7 @@ static void placeSection(Section &section) {
assume(overlap != nullptr); assume(overlap != nullptr);
fatal( fatal(
"Unable to place %s: section overlaps with \"%s\"", "Unable to place %s: section overlaps with \"%s\"",
getSectionDescription(section).c_str(), describeConstraintsOf(section).c_str(),
overlap->name.c_str() overlap->name.c_str()
); );
} }
@@ -392,8 +454,9 @@ static void checkOverlayCompat() {
unfixedList += "\n- and "; unfixedList += "\n- and ";
unfixedList += std::to_string(nbUnfixedSections - nbListed); unfixedList += std::to_string(nbUnfixedSections - nbListed);
unfixedList += " more"; unfixedList += " more";
break; goto finish;
} }
unfixedList += "\n- \""; unfixedList += "\n- \"";
unfixedList += section->name; unfixedList += section->name;
unfixedList += "\" ("; unfixedList += "\" (";
@@ -410,6 +473,7 @@ static void checkOverlayCompat() {
} }
} }
finish: // Can't `break` out of a nested loop
fatal( fatal(
"All sections must be fixed when using an overlay file; %zu %s not:%s", "All sections must be fixed when using an overlay file; %zu %s not:%s",
nbUnfixedSections, nbUnfixedSections,
@@ -423,11 +487,12 @@ void assign_AssignSections() {
// Initialize the free space-modelling structs // Initialize the free space-modelling structs
for (SectionType type : EnumSeq(SECTTYPE_INVALID)) { 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]) { for (std::deque<FreeSpace> &bankMem : memory[type]) {
bankMem.push_back({ bankMem.push_back({
.address = sectionTypeInfo[type].startAddr, .address = typeInfo.startAddr,
.size = sectionTypeInfo[type].size, .size = typeInfo.size,
}); });
} }
} }
+21 -20
View File
@@ -31,7 +31,7 @@ static void setActiveTypeAndIdx(SectionType type, uint32_t idx) {
} }
void layout_SetFloatingSectionType(SectionType type) { 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. // There is only a single bank anyway, so just set the index to 0.
setActiveTypeAndIdx(type, 0); setActiveTypeAndIdx(type, 0);
} else { } else {
@@ -46,8 +46,8 @@ void layout_SetFloatingSectionType(SectionType type) {
} }
void layout_SetSectionType(SectionType type) { void layout_SetSectionType(SectionType type) {
if (sectTypeBanks(type) != 1) { if (SectionTypeInfo const &typeInfo = sectionTypeInfo[type]; typeInfo.isBanked()) {
scriptError("A bank number must be specified for %s", sectionTypeInfo[type].name.c_str()); scriptError("A bank number must be specified for %s", typeInfo.name);
// Keep going with a default value for the bank index. // 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) { if (bank < typeInfo.firstBank) {
scriptError( scriptError(
"%s bank %" PRIu32 " does not exist (the minimum is %" PRIu32 ")", "%s bank %" PRIu32 " does not exist (the minimum is %" PRIu32 ")",
typeInfo.name.c_str(), typeInfo.name,
bank, bank,
typeInfo.firstBank typeInfo.firstBank
); );
@@ -68,10 +68,11 @@ void layout_SetSectionType(SectionType type, uint32_t bank) {
} else if (bank > typeInfo.lastBank) { } else if (bank > typeInfo.lastBank) {
scriptError( scriptError(
"%s bank %" PRIu32 " does not exist (the maximum is %" PRIu32 ")", "%s bank %" PRIu32 " does not exist (the maximum is %" PRIu32 ")",
typeInfo.name.c_str(), typeInfo.name,
bank, bank,
typeInfo.lastBank typeInfo.lastBank
); );
bank = typeInfo.lastBank;
} }
setActiveTypeAndIdx(type, bank - typeInfo.firstBank); setActiveTypeAndIdx(type, bank - typeInfo.firstBank);
@@ -87,22 +88,22 @@ void layout_SetAddr(uint32_t addr) {
return; return;
} }
uint16_t &pc = curAddr[activeType][activeBankIdx];
SectionTypeInfo const &typeInfo = sectionTypeInfo[activeType]; 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); 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( scriptError(
"Cannot set the current address to $%04" PRIx32 ": %s ends at $%04" PRIx16, "Cannot set the current address to $%04" PRIx32 ": %s ends at $%04" PRIx16,
addr, addr,
typeInfo.name.c_str(), typeInfo.name,
sectTypeEndAddr(activeType) typeInfo.endAddr()
); );
pc = sectTypeEndAddr(activeType); pc = typeInfo.endAddr();
} else { } else {
pc = addr; pc = addr;
} }
isPcFloating = false; isPcFloating = false;
} }
@@ -125,7 +126,7 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
if (isPcFloating) { if (isPcFloating) {
if (alignment >= 16) { if (alignment >= 16) {
layout_SetAddr(floatingAlignOffset); layout_SetAddr(alignOfs);
} else { } else {
uint32_t alignSize = 1u << alignment; uint32_t alignSize = 1u << alignment;
uint32_t alignMask = alignSize - 1; uint32_t alignMask = alignSize - 1;
@@ -181,7 +182,7 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
", past $%04" PRIx16, ", past $%04" PRIx16,
pc, pc,
static_cast<uint16_t>(pc + length), static_cast<uint16_t>(pc + length),
static_cast<uint16_t>(sectTypeEndAddr(activeType) + 1) static_cast<uint16_t>(typeInfo.endAddr() + 1)
); );
return; 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, "Cannot increase the current address by %u bytes: only %u bytes to $%04" PRIx16,
length, length,
typeInfo.size - offset, typeInfo.size - offset,
static_cast<uint16_t>(sectTypeEndAddr(activeType) + 1) static_cast<uint16_t>(typeInfo.endAddr() + 1)
); );
} else { } else {
pc += length; pc += length;
@@ -241,7 +242,7 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
scriptError( scriptError(
"\"%s\" is specified to be a %s section, but it contains data", "\"%s\" is specified to be a %s section, but it contains data",
name.c_str(), name.c_str(),
typeInfo.name.c_str() typeInfo.name
); );
} else if (sectTypeHasData(activeType) && section->data.empty() && section->size != 0) { } 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 // 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( scriptError(
"\"%s\" is specified to be a %s section, but it does not contain data", "\"%s\" is specified to be a %s section, but it does not contain data",
name.c_str(), name.c_str(),
typeInfo.name.c_str() typeInfo.name
); );
} else { } else {
// SDCC areas don't have a type assigned yet, so the linker script gives them one. // 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( scriptError(
"\"%s\" is specified to be a %s section, but it is already a %s section", "\"%s\" is specified to be a %s section, but it is already a %s section",
name.c_str(), name.c_str(),
typeInfo.name.c_str(), typeInfo.name,
sectionTypeInfo[section->type].name.c_str() 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 "The linker script places section \"%s\" in %s bank %" PRIu32
", but it was already defined in bank %" PRIu32, ", but it was already defined in bank %" PRIu32,
name.c_str(), name.c_str(),
sectionTypeInfo[section->type].name.c_str(), section->typeInfo().name,
bank, bank,
section->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", ", but then it would overflow %s by %" PRIu16 " byte%s",
name.c_str(), name.c_str(),
org, org,
typeInfo.name.c_str(), typeInfo.name,
overflowSize, overflowSize,
overflowSize == 1 ? "" : "s" overflowSize == 1 ? "" : "s"
); );
+15 -6
View File
@@ -101,7 +101,7 @@ static void parseScrambleSpec(char *spec) {
// clang-format off: vertically align nested initializers // clang-format off: vertically align nested initializers
static UpperMap<std::pair<uint16_t *, uint16_t>> scrambleSpecs{ static UpperMap<std::pair<uint16_t *, uint16_t>> scrambleSpecs{
{"ROMX", std::pair{&options.scrambleROMX, 65535}}, {"ROMX", std::pair{&options.scrambleROMX, 65535}},
{"SRAM", std::pair{&options.scrambleSRAM, 255 }}, {"SRAM", std::pair{&options.scrambleSRAM, 256 }},
{"WRAMX", std::pair{&options.scrambleWRAMX, 7 }}, {"WRAMX", std::pair{&options.scrambleWRAMX, 7 }},
}; };
// clang-format on // clang-format on
@@ -433,14 +433,14 @@ int main(int argc, char *argv[]) {
sectionTypeInfo[SECTTYPE_VRAM].lastBank = 0; sectionTypeInfo[SECTTYPE_VRAM].lastBank = 0;
} }
// Read all object files first, // Read all object files...
size_t nbFiles = localOptions.inputFileNames.size(); size_t nbFiles = localOptions.inputFileNames.size();
obj_Setup(nbFiles); obj_Setup(nbFiles);
for (size_t i = 0; i < nbFiles; ++i) { for (size_t i = 0; i < nbFiles; ++i) {
obj_ReadFile(localOptions.inputFileNames[i], nbFiles - i - 1); obj_ReadFile(localOptions.inputFileNames[i], nbFiles - i - 1);
} }
// apply the linker script's modifications, // Apply the linker script's modifications...
if (localOptions.linkerScriptName) { if (localOptions.linkerScriptName) {
verbosePrint(VERB_NOTICE, "Reading linker script...\n"); verbosePrint(VERB_NOTICE, "Reading linker script...\n");
@@ -453,16 +453,25 @@ int main(int argc, char *argv[]) {
requireZeroErrors(); requireZeroErrors();
} }
// then process them, // Attempt to generate a layout...
sect_DoSanityChecks(); sect_DoSanityChecks();
requireZeroErrors(); requireZeroErrors();
assign_AssignSections(); 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(); patch_CheckAssertions();
// and finally output the result. // And, finally, make the ROM and emit it!
patch_ApplyPatches(); patch_ApplyPatches();
requireZeroErrors(); requireZeroErrors();
out_WriteFiles(); out_WriteROMIfAny();
return 0; return 0;
} }
+26 -38
View File
@@ -71,7 +71,7 @@ void out_AddSection(Section const &section) {
1, // SECTTYPE_OAM 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]) { if (targetBank >= maxNbBanks[section.type]) {
fatal( fatal(
"Section \"%s\" has an invalid bank range (%" PRIu32 " > %" PRIu32 ")", "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) { 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) { for (Section const *ptr : sections[section.type][bank].sections) {
if (ptr->org < section.org + section.size && section.org < ptr->org + ptr->size) { 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) { if (options.outputFileName) {
char const *outputFileName = options.outputFileName->c_str(); char const *outputFileName = options.outputFileName->c_str();
if (*options.outputFileName != "-") { if (*options.outputFileName != "-") {
@@ -267,31 +267,27 @@ static void writeROM() {
} }
static void writeSymName(std::string const &name, FILE *file) { 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 // Output legal ASCII characters as-is
if (char c = *ptr; continuesIdentifier(c)) { if (char c = name[i]; continuesIdentifier(c)) {
putc(c, file); putc(c, file);
++ptr; ++i;
continue; continue;
} }
// Output illegal characters using Unicode escapes ('\u' or '\U') // Output illegal characters using Unicode escapes ('\u' or '\U')
// Decode the UTF-8 codepoint; or at least attempt to // Decode the UTF-8 codepoint; or at least attempt to
Utf8Decoder decoder; Utf8Decoder decoder;
do { while (i < name.length()) {
if (decoder.update(*ptr) != UTF8_REJECT) { decoder.update(static_cast<uint8_t>(name[i++]));
++ptr; if (decoder.state == UTF8_ACCEPT || decoder.state == UTF8_REJECT) {
continue; break;
} }
// This sequence was invalid; emit a U+FFFD, and recover }
if (decoder.state != UTF8_ACCEPT) {
// This sequence was invalid or incomplete; emit a U+FFFD instead
decoder.codepoint = 0xFFFD; decoder.codepoint = 0xFFFD;
// Skip continuation bytes }
// A NUL byte does not qualify, so we're good
while ((*ptr & 0xC0) == 0x80) {
++ptr;
}
break;
} while (decoder.state != UTF8_ACCEPT);
fprintf( fprintf(
file, decoder.codepoint <= 0xFFFF ? "\\u%04" PRIx32 : "\\U%08" PRIx32, decoder.codepoint 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( fprintf(
mapFile, mapFile,
"\n%s bank #%" PRIu32 ":\n", "\n%s bank #%" PRIu32 ":\n",
sectionTypeInfo[type].name.c_str(), sectionTypeInfo[type].name,
bank + sectionTypeInfo[type].firstBank bank + sectionTypeInfo[type].firstBank
); );
uint16_t prevEndAddr = sectionTypeInfo[type].startAddr; uint16_t prevEndAddr = sectionTypeInfo[type].startAddr;
uint16_t used = forEachSection(sectList, [&](Section const &sect) { uint16_t used = forEachSection(sectList, [&](Section const &sect) {
assume(sect.offset == 0); assume(sect.offset == 0);
writeEmptySpace(prevEndAddr, sect.org);
assume(sect.org + sect.size <= UINT16_MAX); 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); fprintf(mapFile, "\tSECTION: $%04" PRIx16, sect.org);
if (sect.size != 0) { if (sect.size != 0) {
@@ -495,12 +492,10 @@ static void writeMapBank(SortedSections const &sectList, SectionType type, uint3
if (used == 0) { if (used == 0) {
fputs("\tEMPTY\n", mapFile); fputs("\tEMPTY\n", mapFile);
} else { } else {
uint16_t bankEndAddr = sectionTypeInfo[type].startAddr + sectionTypeInfo[type].size; uint16_t bankEndAddr = sectionTypeInfo[type].endAddr() + 1;
writeEmptySpace(prevEndAddr, bankEndAddr); writeEmptySpace(prevEndAddr, bankEndAddr);
uint16_t slack = sectionTypeInfo[type].size - used; uint16_t slack = sectionTypeInfo[type].size - used;
fprintf(mapFile, "\tTOTAL EMPTY: $%04" PRIx16 " byte%s\n", slack, slack == 1 ? "" : "s"); fprintf(mapFile, "\tTOTAL EMPTY: $%04" PRIx16 " byte%s\n", slack, slack == 1 ? "" : "s");
} }
} }
@@ -517,13 +512,12 @@ static void writeMapSummary() {
continue; continue;
} }
// Do not output unused section types // Skip types which haven't been used at all.
if (nbBanks == 0) { if (nbBanks == 0) {
continue; continue;
} }
uint32_t usedTotal = 0; uint32_t usedTotal = 0;
for (uint32_t bank = 0; bank < nbBanks; ++bank) { for (uint32_t bank = 0; bank < nbBanks; ++bank) {
usedTotal += forEachSection(sections[type][bank], [](Section const &) {}); usedTotal += forEachSection(sections[type][bank], [](Section const &) {});
} }
@@ -531,19 +525,19 @@ static void writeMapSummary() {
fprintf( fprintf(
mapFile, mapFile,
"\t%s: %" PRIu32 " byte%s used / %zu free", "\t%s: %" PRIu32 " byte%s used / %zu free",
sectionTypeInfo[type].name.c_str(), sectionTypeInfo[type].name,
usedTotal, usedTotal,
usedTotal == 1 ? "" : "s", usedTotal == 1 ? "" : "s",
static_cast<size_t>(nbBanks) * sectionTypeInfo[type].size - usedTotal 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"); fprintf(mapFile, " in %u bank%s", nbBanks, nbBanks == 1 ? "" : "s");
} }
putc('\n', mapFile); putc('\n', mapFile);
} }
} }
static void writeSym() { void out_WriteSymIfAny() {
if (!options.symFileName) { if (!options.symFileName) {
return; return;
} }
@@ -594,7 +588,7 @@ static void writeSym() {
} }
} }
static void writeMap() { void out_WriteMapIfAny() {
if (!options.mapFileName) { if (!options.mapFileName) {
return; 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; int32_t value;
switch (command) { switch (command) {
case RPN_ADD: case RPN_ADD:
value = popRPN(patch) + popRPN(patch); value = op_add(popRPN(patch), popRPN(patch));
break; break;
case RPN_SUB: case RPN_SUB:
value = popRPN(patch); value = popRPN(patch);
value = popRPN(patch) - value; value = op_sub(popRPN(patch), value);
break; break;
case RPN_MUL: case RPN_MUL:
value = popRPN(patch) * popRPN(patch); value = op_mul(popRPN(patch), popRPN(patch));
break; break;
case RPN_DIV: case RPN_DIV:
value = popRPN(patch); 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) // RAM sections can have a size, but don't get any data (they shouldn't have any)
if (section->type != SECTTYPE_INVALID) { if (section->type != SECTTYPE_INVALID) {
SectionTypeInfo const &typeInfo = sectionTypeInfo[section->type];
// Otherwise, how would the type already be known at this point? // Otherwise, how would the type already be known at this point?
assume(section->isAddressFixed); 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 "\"%s\" is implicitly defined as a %s section (being at address $%04" PRIx16
"), but it has data! (Was a bad `__at()` value used?)", "), but it has data! (Was a bad `__at()` value used?)",
section->name.c_str(), section->name.c_str(),
typeInfo.name.c_str(), section->typeInfo().name,
section->org 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 "\"%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?)", "), but it doesn't have any data! (Was a bad `__at()` value used?)",
section->name.c_str(), section->name.c_str(),
typeInfo.name.c_str(), section->typeInfo().name,
section->org section->org
); );
} }
+39 -17
View File
@@ -56,6 +56,18 @@ static void checkPieceCompat(Section &target, Section const &other, size_t delta
other.org 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; target.isAddressFixed = true;
@@ -115,8 +127,8 @@ static void mergeSections(Section &target, std::unique_ptr<Section> &&other) {
*other, *other,
"Section \"%s\" is defined with type `%s`, but also with type `%s`", "Section \"%s\" is defined with type `%s`, but also with type `%s`",
target.name.c_str(), target.name.c_str(),
sectionTypeInfo[target.type].name.c_str(), target.typeInfo().name,
sectionTypeInfo[other->type].name.c_str() other->typeInfo().name
); );
} }
@@ -146,6 +158,15 @@ static void mergeSections(Section &target, std::unique_ptr<Section> &&other) {
case SECTION_FRAGMENT: case SECTION_FRAGMENT:
checkPieceCompat(target, *other, target.size); 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` // Append `other` to `target`
other->offset = target.size; other->offset = target.size;
target.size += other->size; target.size += other->size;
@@ -181,7 +202,7 @@ void sect_AddSection(std::unique_ptr<Section> &&section) {
fatal( fatal(
"Section \"%s\" is of type `%s`, which cannot be `UNION`ized", "Section \"%s\" is of type `%s`, which cannot be `UNION`ized",
section->name.c_str(), section->name.c_str(),
sectionTypeInfo[section->type].name.c_str() section->typeInfo().name
); );
} else { } else {
sections.add(section->name, std::move(section)); sections.add(section->name, std::move(section));
@@ -214,6 +235,7 @@ static void doSanityChecks(Section &section) {
bankModeError = true; bankModeError = true;
} else { } else {
section.type = SECTTYPE_ROM0; section.type = SECTTYPE_ROM0;
section.isBankFixed = false;
} }
} }
if (options.isWRAM0Mode && section.type == SECTTYPE_WRAMX) { if (options.isWRAM0Mode && section.type == SECTTYPE_WRAMX) {
@@ -226,6 +248,7 @@ static void doSanityChecks(Section &section) {
bankModeError = true; bankModeError = true;
} else { } else {
section.type = SECTTYPE_WRAM0; section.type = SECTTYPE_WRAM0;
section.isBankFixed = false;
} }
} }
if (options.isDmgMode && section.type == SECTTYPE_VRAM && section.isBankFixed if (options.isDmgMode && section.type == SECTTYPE_VRAM && section.isBankFixed
@@ -243,18 +266,20 @@ static void doSanityChecks(Section &section) {
section.isAlignFixed = false; 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 // 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( error(
"Section \"%s\" has type `%s`, which cannot be aligned to $%04x bytes", "Section \"%s\" has type `%s`, which cannot be aligned to $%04x bytes",
section.name.c_str(), section.name.c_str(),
sectionTypeInfo[section.type].name.c_str(), typeInfo.name,
section.alignMask + 1 section.alignMask + 1
); );
} }
uint32_t minbank = sectionTypeInfo[section.type].firstBank, uint32_t minbank = typeInfo.firstBank, maxbank = typeInfo.lastBank;
maxbank = sectionTypeInfo[section.type].lastBank;
if (!bankModeError && section.isBankFixed if (!bankModeError && section.isBankFixed
&& (section.bank < minbank || section.bank > maxbank)) { && (section.bank < minbank || section.bank > maxbank)) {
@@ -271,12 +296,12 @@ static void doSanityChecks(Section &section) {
} }
// Check if section has a chance to be placed // Check if section has a chance to be placed
if (section.size > sectionTypeInfo[section.type].size) { if (section.size > typeInfo.size) {
error( error(
"Section \"%s\" is bigger than the max size for that type: $%" PRIx16 " > $%" PRIx16, "Section \"%s\" is bigger than the max size for that type: $%" PRIx16 " > $%" PRIx16,
section.name.c_str(), section.name.c_str(),
section.size, section.size,
sectionTypeInfo[section.type].size typeInfo.size
); );
} }
@@ -300,24 +325,21 @@ static void doSanityChecks(Section &section) {
} }
// Ensure the target address is valid // Ensure the target address is valid
if (section.org < sectionTypeInfo[section.type].startAddr if (section.org < typeInfo.startAddr || section.org > typeInfo.endAddr()) {
|| section.org > sectTypeEndAddr(section.type)) {
error( error(
"Section \"%s\"'s fixed address $%04" PRIx16 " is outside of range [$%04" PRIx16 "Section \"%s\"'s fixed address $%04" PRIx16 " is outside of range [$%04" PRIx16
"; $%04" PRIx16 "]", "; $%04" PRIx16 "]",
section.name.c_str(), section.name.c_str(),
section.org, section.org,
sectionTypeInfo[section.type].startAddr, typeInfo.startAddr,
sectTypeEndAddr(section.type) typeInfo.endAddr()
); );
} } else if (section.org + section.size > typeInfo.endAddr() + 1) {
if (section.org + section.size > sectTypeEndAddr(section.type) + 1) {
error( error(
"Section \"%s\"'s end address $%04x is greater than last address $%04x", "Section \"%s\"'s end address $%04x is greater than last address $%04x",
section.name.c_str(), section.name.c_str(),
section.org + section.size, section.org + section.size,
sectTypeEndAddr(section.type) + 1 typeInfo.endAddr() + 1
); );
} }
} }
+8 -12
View File
@@ -2,65 +2,61 @@
#include "linkdefs.hpp" #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 // 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. // info, so RGBASM's job is only to catch unconditional errors earlier.
// clang-format off: nested initializers // clang-format off: nested initializers
SectionTypeInfo sectionTypeInfo[SECTTYPE_INVALID] = { SectionTypeInfo sectionTypeInfo[SECTTYPE_INVALID] = {
{ {
.name = "WRAM0"s, .name = "WRAM0",
.startAddr = 0xC000, .startAddr = 0xC000,
.size = 0x2000, // Patched to 0x1000 if !isWRAM0Mode .size = 0x2000, // Patched to 0x1000 if !isWRAM0Mode
.firstBank = 0, .firstBank = 0,
.lastBank = 0, .lastBank = 0,
}, },
{ {
.name = "VRAM"s, .name = "VRAM",
.startAddr = 0x8000, .startAddr = 0x8000,
.size = 0x2000, .size = 0x2000,
.firstBank = 0, .firstBank = 0,
.lastBank = 1, // Patched to 0 if isDmgMode .lastBank = 1, // Patched to 0 if isDmgMode
}, },
{ {
.name = "ROMX"s, .name = "ROMX",
.startAddr = 0x4000, .startAddr = 0x4000,
.size = 0x4000, .size = 0x4000,
.firstBank = 1, .firstBank = 1,
.lastBank = 65535, .lastBank = 65535,
}, },
{ {
.name = "ROM0"s, .name = "ROM0",
.startAddr = 0x0000, .startAddr = 0x0000,
.size = 0x8000, // Patched to 0x4000 if !is32kMode .size = 0x8000, // Patched to 0x4000 if !is32kMode
.firstBank = 0, .firstBank = 0,
.lastBank = 0, .lastBank = 0,
}, },
{ {
.name = "HRAM"s, .name = "HRAM",
.startAddr = 0xFF80, .startAddr = 0xFF80,
.size = 0x007F, .size = 0x007F,
.firstBank = 0, .firstBank = 0,
.lastBank = 0, .lastBank = 0,
}, },
{ {
.name = "WRAMX"s, .name = "WRAMX",
.startAddr = 0xD000, .startAddr = 0xD000,
.size = 0x1000, .size = 0x1000,
.firstBank = 1, .firstBank = 1,
.lastBank = 7, .lastBank = 7,
}, },
{ {
.name = "SRAM"s, .name = "SRAM",
.startAddr = 0xA000, .startAddr = 0xA000,
.size = 0x2000, .size = 0x2000,
.firstBank = 0, .firstBank = 0,
.lastBank = 255, .lastBank = 255,
}, },
{ {
.name = "OAM"s, .name = "OAM",
.startAddr = 0xFE00, .startAddr = 0xFE00,
.size = 0x00A0, .size = 0x00A0,
.firstBank = 0, .firstBank = 0,
+17
View File
@@ -8,6 +8,23 @@
#include "helpers.hpp" // assume, clz, ctz #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) { int32_t op_divide(int32_t dividend, int32_t divisor) {
assume(divisor != 0); // Division by 0 is UB assume(divisor != 0); // Division by 0 is UB
assume(dividend != INT32_MIN || divisor != -1); // INT32_MIN / -1 is UB assume(dividend != INT32_MIN || divisor != -1); // INT32_MIN / -1 is UB
+22 -8
View File
@@ -4,23 +4,37 @@
#include "helpers.hpp" #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__ #ifdef __clang__
#if __has_feature(address_sanitizer) && !defined(__SANITIZE_ADDRESS__) #if __has_feature(address_sanitizer) && !defined(__SANITIZE_ADDRESS__)
#define __SANITIZE_ADDRESS__ #define __SANITIZE_ADDRESS__
#endif #endif
#if __has_feature(address_sanitizer) && !defined(__SANITIZE_UNDEFINED__)
#define __SANITIZE_UNDEFINED__
#endif
#endif #endif
#if !defined(NDEBUG) && defined(__SANITIZE_ADDRESS__) && !defined(__APPLE__)
#if !defined(NDEBUG) && defined(__SANITIZE_ADDRESS__)
extern "C" { extern "C" {
char const *__asan_default_options(void) { char const *__asan_default_options(void) {
return "detect_leaks=1" return ":check_initialization_order=1"
":detect_stack_use_after_return=1"
":detect_invalid_pointer_pairs=2" ":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_init_order=1"
":strict_string_checks=1" ":strict_string_checks=1";
":print_legend=0"; }
}
#endif
#if !defined(NDEBUG) && defined(__SANITIZE_UNDEFINED__)
extern "C" {
char const *__ubsan_default_options(void) {
return "print_stacktrace=1";
} }
} }
#endif #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 $62B85
$4 $4
$10000 $10000
$20000
$10000 $10000
$10000 $20000
$10000
$C570A $C570A
$13333 $13333
$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) at negative-macro-args.asm::mac(11) <- negative-macro-args.asm(21)
error: Macro argument `\<-2147483648>` not defined error: Macro argument `\<-2147483648>` not defined
at negative-macro-args.asm::mac(14) <- negative-macro-args.asm(21) 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) at negative-macro-args.asm::mac(14) <- negative-macro-args.asm(21)
while expanding symbol `i`
error: Macro argument `\<-2147483648>` not defined error: Macro argument `\<-2147483648>` not defined
at negative-macro-args.asm::mac(15) <- negative-macro-args.asm(21) at negative-macro-args.asm::mac(15) <- negative-macro-args.asm(21)
error: Number in bracketed macro argument is too large error: Number in bracketed macro argument is too large
+1 -1
View File
@@ -8,7 +8,7 @@
0: == 0: ==
-8: == -8: ==
2147483647: == 2147483647: ==
-2147483648: == > -2147483648: ==
2147483648: == > 2147483648: == >
-1: G == G -1: G == G
4294967295: 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) at negative-shifted-macro-args.asm::test(15) <- negative-shifted-macro-args.asm(22)
error: Macro argument `\<-1>` not defined error: Macro argument `\<-1>` not defined
at negative-shifted-macro-args.asm::test(16) <- negative-shifted-macro-args.asm(22) 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) at negative-shifted-macro-args.asm::test(19) <- negative-shifted-macro-args.asm(22)
error: syntax error, unexpected > Assembly aborted with 8 errors
at negative-shifted-macro-args.asm::test(19) <- negative-shifted-macro-args.asm(22)
Assembly aborted with 9 errors
+1
View File
@@ -11,3 +11,4 @@ $0
$A $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("invalid %w spec", 42)
PRINTLN STRFMT("one=%d two=%d three=%d", 1) 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 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) # Game Boy release date, 1989-04-21T12:34:56Z (for reproducible test results)
export SOURCE_DATE_EPOCH=609165296 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. # "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. # "notexist.asm" doesn't exist, so there's no point in trying to `cat` it.
# Skip testing with stdin input for those files. # Skip testing with stdin input for those files.
if [[ "$i" = include-recursion.asm || "$i" = make-deps.asm || "$i" = notexist.asm ]]; then case "$i" in
continue include-recursion.asm | make-deps.asm | notexist.asm) continue;;
fi esac
# shellcheck disable=SC2002 # (This use of `cat` intentionally makes the input an unseekable pipe.) # shellcheck disable=SC2002 # (This use of `cat` intentionally makes the input an unseekable pipe.)
cat "$i" | "$RGBASM" "${RGBASMFLAGS[@]}" -o "$o" - >"$output" 2>"$errput" cat "$i" | "$RGBASM" "${RGBASMFLAGS[@]}" -o "$o" - >"$output" 2>"$errput"
+8 -4
View File
@@ -69,12 +69,16 @@ case "$actionname" in
action() { action() {
if [ ! -d "$EXT_TEST_REPO" ]; then 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 \ --revision="$EXT_TEST_COMMIT" --depth=1 --recursive --shallow-submodules \
--config advice.detachedHead=false --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 fi
pushd "$EXT_TEST_REPO"
git checkout --force --detach "$EXT_TEST_COMMIT" --
if [ -f "../patches/$EXT_TEST_REPO.patch" ]; then if [ -f "../patches/$EXT_TEST_REPO.patch" ]; then
git apply --ignore-whitespace "../patches/$EXT_TEST_REPO.patch" git apply --ignore-whitespace "../patches/$EXT_TEST_REPO.patch"
fi fi
+1 -1
View File
@@ -1,7 +1,7 @@
EXT_TEST_OWNER=ISSOtm EXT_TEST_OWNER=ISSOtm
EXT_TEST_REPO=gb-starter-kit EXT_TEST_REPO=gb-starter-kit
EXT_TEST_DOMAIN=codeberg.org EXT_TEST_DOMAIN=codeberg.org
EXT_TEST_COMMIT=83162620eece547d9fb95c3e0b89d7a8812bcb7c EXT_TEST_COMMIT=e3e395fdd6cdbfa168cf1b814d73c93ab167c7d8
EXT_TEST_IS_NONFREE=false EXT_TEST_IS_NONFREE=false
EXT_TEST_TARGET=all EXT_TEST_TARGET=all
+1 -1
View File
@@ -1,7 +1,7 @@
EXT_TEST_OWNER=zladx EXT_TEST_OWNER=zladx
EXT_TEST_REPO=LADX-Disassembly EXT_TEST_REPO=LADX-Disassembly
EXT_TEST_DOMAIN=github.com EXT_TEST_DOMAIN=github.com
EXT_TEST_COMMIT=76e9128437b90708ee27dce2d6c9752ad9f22f2d EXT_TEST_COMMIT=968956783bd7ca4487a5712c46e98ae894d8dc0f
EXT_TEST_IS_NONFREE=true EXT_TEST_IS_NONFREE=true
EXT_TEST_TARGET=default EXT_TEST_TARGET=default
+1 -1
View File
@@ -1,7 +1,7 @@
EXT_TEST_OWNER=pret EXT_TEST_OWNER=pret
EXT_TEST_REPO=pokecrystal EXT_TEST_REPO=pokecrystal
EXT_TEST_DOMAIN=github.com EXT_TEST_DOMAIN=github.com
EXT_TEST_COMMIT=3438c7003a57fa2987fcb223d14b660761b33c64 EXT_TEST_COMMIT=7a7881d0d62e0ddbd82dcf10e7116807487ac651
EXT_TEST_IS_NONFREE=true EXT_TEST_IS_NONFREE=true
EXT_TEST_TARGET=compare EXT_TEST_TARGET=compare
+1 -1
View File
@@ -1,7 +1,7 @@
EXT_TEST_OWNER=pret EXT_TEST_OWNER=pret
EXT_TEST_REPO=pokered EXT_TEST_REPO=pokered
EXT_TEST_DOMAIN=github.com EXT_TEST_DOMAIN=github.com
EXT_TEST_COMMIT=405b6246372d7e5a2cb029cbb65219b13286b8c9 EXT_TEST_COMMIT=a1a22aaf84d1675bcdbaeb194592379d586d838e
EXT_TEST_IS_NONFREE=true EXT_TEST_IS_NONFREE=true
EXT_TEST_TARGET=compare EXT_TEST_TARGET=compare
+4
View File
@@ -2,6 +2,10 @@
export LC_ALL=C 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)" tmpdir="$(mktemp -d)"
# shellcheck disable=SC2064 # (Immediate expansion is the desired behavior.) # shellcheck disable=SC2064 # (Immediate expansion is the desired behavior.)
trap "cd; rm -rf ${tmpdir@Q}" EXIT 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)) 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