Compare commits

..
133 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
Rangi 307846b03e Release v1.0.3 2026-08-01 11:27:43 -04:00
Rangi e7e671b249 Update external test commits 2026-08-01 11:24:42 -04:00
Rangi 0c772ea049 Update external testing CI with libbet's new way of running Python
libbet now uses `py -3` if the `py` binary exists, instead of if
`COMSPEC` is defined. So we don't need to `unset COMSPEC`, we just
need to avoid Windows' `py` launcher from being in Cygwin's `PATH`.

It turns out that `C:\cygwin\bin\env.exe CYGWIN_NOWINPATH=1 ...`
was not effectively preventing Windows' `PATH` from being inherited
by Cygwin's `PATH`, because to take effect it needs to run Cygwin's
`/etc/profile` script. The simple way to do that is using a login
shell. So we don't need the `PREFIX`/`--prefix` from the previous
commit after all.
2026-08-01 04:49:43 +02:00
Rangi 6f64fd00f6 Fix CI testing of installing RGBDS on Cygwin (#2038)
We need to pass `-e` to Cygwin `bash` explicitly, since specifying
it as the non-default `shell:` no longer automatically adds `-e`
behavior to `run` blocks.

We also need to install to `/usr/bin` (which `cygwin-install-action`
automatically adds to the `PATH`), not `/usr/local/bin` (which is not
in the default `PATH`). This tests our `PREFIX`/`--prefix` handling.
2026-07-31 10:25:59 -04:00
Rangi 4384223951 CI testing in Cygwin 64-bit now uses CMake and runs external tests (#2037) 2026-07-30 12:59:53 -04:00
Rangi 4e87f61656 Edit some comments and formatting in testing.yml for consistency 2026-07-29 22:08:21 -04:00
Rangi 9d854114ea Use Ninja as the generator for CMake on FreeBSD 2026-07-30 02:33:47 +02:00
Rangi 70d8d67ffc Run external tests for FreeBSD (#2035)
This requires aliasing `make` to `gmake` and `python` to `python3`
2026-07-29 19:45:07 -04:00
Rangi ca2b8e4cde Support the COLUMNS environment variable for help/usage output 2026-07-24 14:46:05 +02:00
Rangi 80c8e77b19 Fixed-point FMOD and LOG explicitly return 0 for some invalid inputs
This is consistent with `fix_Div`'s explicit error handling,
and does not rely on subtle C/C++ `fmod` or `log` IEEE 754 behavior.
2026-07-23 00:38:44 -04:00
Rangi 604c69e048 Fix palette-packing decant logic to avoid UB from invalid iterator (#2031)
The "Iterate through the component's color sets, and transfer them"
logic shifted `from.begin()`, which would make subsequent `std::advance`
calls go past `_assigned.end()` and cause UB. This rewrite uses absolute
numeric indexes into `_assigned` to avoid that potential problem.

I haven't been able to craft a test case that actually *reaches*
that piece of logic, and I suspect that it's unreachable given how
we sort and process color sets before decanting, but cannot prove
that; so it stays in. (Maybe if we added 4bpp 16-colors-per-palette
support, it would become reachable?)
2026-07-22 23:48:02 -04:00
Rangi ec5e6cbabb Handle -Wtruncation for link-time jr values
Fixes #2028
2026-07-21 14:51:58 -04:00
Rangi 50678d0942 Use _fseeki64/_ftelli64 on Windows since fseek/fell are limited by 32-bit long 2026-07-21 14:38:14 -04:00
Rangi c691ec6607 Use std::optional<uint64_t> instead of long with a special -1 sentinel for seekSize 2026-07-21 14:31:05 -04:00
Rangi c3073f4318 Assume that InsertionOrderedMap never adds duplicate keys 2026-07-21 14:07:45 -04:00
Rangi e5b03e3cb1 Zero-initialize Png struct width and height just in case 2026-07-21 13:58:31 -04:00
Rangi 0d379e9016 Assume clz/ctz argument is nonzero in our fallback implementations 2026-07-21 13:56:57 -04:00
Rangi 9cbfec30c7 Avoid redundant "Unexpected text" error message after "Unknown" error 2026-07-21 13:54:11 -04:00
Rangi 9def9c9aa8 Fix an SDAS object error message to use the valid section name variable 2026-07-21 13:41:53 -04:00
Rangi b5681c0d34 Set binary or text mode on stdout as well as stdin 2026-07-21 13:39:28 -04:00
Rangi ec43f06bd9 Fix handling of at-files containing -- followed by positional arguments 2026-07-21 13:35:39 -04:00
Rangi 106ec399d5 Fix reading value of relocations symbol/section index from SDAS objects 2026-07-21 13:14:35 -04:00
Rangi d9c0d37fd5 Explicitly allow JR offsets to wrap around in RGBLINK
This reverts commit f27a8132d9.
2026-07-20 19:23:11 -04:00
287 changed files with 1989 additions and 994 deletions
+4 -4
View File
@@ -10,10 +10,10 @@ if [[ "$tag" = v*-rc* ]]; then
fi
files=(
win64/rgbds-win64.zip
win32/rgbds-win32.zip
macos/rgbds-macos.zip
linux/rgbds-linux-x86_64.tar.xz
rgbds-win64.zip
rgbds-win32.zip
rgbds-macos.zip
rgbds-linux-x86_64.tar.xz
rgbds-source.tar.gz
)
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env sh
local_bin_dir="$PWD/.local/bin"
mkdir -p "$local_bin_dir"
# Alias `make` to `gmake` and `python` to `python3` for use by external tests.
ln -s "$(which gmake)" "$local_bin_dir/make"
ln -s "$(which python3)" "$local_bin_dir/python"
export PATH="$local_bin_dir:$PATH"
+2 -1
View File
@@ -58,7 +58,8 @@ case "${OS%%-*}" in
printf 'PATH=%s\n' "$PATH" >>"$GITHUB_ENV" # Make it available to later CI steps too.
;;
freebsd)
pkg install -y bash bison cmake git png
# GNU `gmake` and `gcc` are dependencies for most repos built by our external tests.
pkg install -y bash bison cmake ninja git png gmake lang/gcc python3 py312-pillow
;;
windows)
# GitHub Actions' hosted runners ship CMake 3.x, but versions prior to 4.0.0 ignore `CPACK_PACKAGE_FILE_NAME`.
+4
View File
@@ -6,6 +6,10 @@ on:
tags:
- '*'
concurrency:
group: ${{github.workflow}}-${{github.ref}}
cancel-in-progress: true
env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
TERM: xterm-256color
+13 -2
View File
@@ -1,7 +1,14 @@
name: Contrib script syntax checking
on:
- push
- pull_request
push:
paths: [ 'contrib/**', .github/scripts/check-contrib.sh ]
pull_request:
paths: [ 'contrib/**', .github/scripts/check-contrib.sh ]
workflow_dispatch:
concurrency:
group: ${{github.workflow}}-${{github.event.pull_request.number || github.ref}}
cancel-in-progress: true
env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
@@ -15,6 +22,10 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v7
with:
sparse-checkout: | # Avoid checking out other files, we don't use them here.
contrib
.github/scripts
- name: Install zsh
run: |
sudo apt install -Uyq zsh
+4
View File
@@ -1,6 +1,10 @@
name: Diff completeness check
on: pull_request
concurrency:
group: ${{github.workflow}}-${{github.event.pull_request.number || github.ref}}
cancel-in-progress: true
env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
TERM: xterm-256color
+4
View File
@@ -1,6 +1,10 @@
name: Code format checking
on: pull_request
concurrency:
group: ${{github.workflow}}-${{github.event.pull_request.number || github.ref}}
cancel-in-progress: true
env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
TERM: xterm-256color
+4
View File
@@ -3,6 +3,10 @@ on:
- push
- pull_request
concurrency:
group: ${{github.workflow}}-${{github.event.pull_request.number || github.ref}}
cancel-in-progress: true
env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
TERM: xterm-256color
+16 -6
View File
@@ -4,6 +4,10 @@ on:
tags:
- v[0-9]*
concurrency:
group: ${{github.workflow}}-${{github.ref}}
cancel-in-progress: true
env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
TERM: xterm-256color
@@ -38,7 +42,9 @@ jobs:
shell: cmd
run: |
call .github\scripts\msvc-env.bat ${{ matrix.bits }}
cmake -B build -G Ninja -DFETCHCONTENT_BASE_DIR="${{ env.DEPS_ROOT_DIR }}" -DCMAKE_C_FLAGS="/nologo"
cmake -B build -G Ninja -DFETCHCONTENT_BASE_DIR="${{ env.DEPS_ROOT_DIR }}" ^
-DCMAKE_COMPILE_WARNING_AS_ERROR=YES -DCMAKE_LINK_WARNING_AS_ERROR=YES ^
-DCMAKE_C_FLAGS="/nologo" -DCMAKE_INSTALL_BINDIR=. -DCPACK_COMPONENTS_ALL=binaries
cmake --build build -- -k 0
- name: Package binaries
working-directory: build
@@ -47,8 +53,8 @@ jobs:
- name: Upload Windows binaries
uses: actions/upload-artifact@v7
with:
name: win${{ matrix.bits }}
path: build/rgbds-win${{ matrix.bits }}.zip
archive: false
if-no-files-found: error
macos:
@@ -61,7 +67,8 @@ jobs:
./.github/scripts/install-deps.sh macos lld
- name: Build binaries
run: |
cmake -B build -G Ninja --preset macos-static -DFETCHCONTENT_BASE_DIR="${{ env.DEPS_ROOT_DIR }}"
cmake -B build -G Ninja --preset macos-static -DFETCHCONTENT_BASE_DIR="${{ env.DEPS_ROOT_DIR }}" \
-DCMAKE_COMPILE_WARNING_AS_ERROR=YES -DCMAKE_LINK_WARNING_AS_ERROR=YES
cmake --build build -- -k 0
strip rgb{asm,link,fix,gfx}
env:
@@ -75,8 +82,8 @@ jobs:
- name: Upload macOS binaries
uses: actions/upload-artifact@v7
with:
name: macos
path: rgbds-macos.zip
archive: false
if-no-files-found: error
linux:
@@ -89,7 +96,7 @@ jobs:
./.github/scripts/install-deps.sh ubuntu-22.04
- name: Build binaries
run: |
make -k -j "$(getconf _NPROCESSORS_ONLN)" WARNFLAGS="-Wall -Wextra -pedantic -static" PKG_CONFIG="pkg-config --static" Q=
make -k -j "$(getconf _NPROCESSORS_ONLN)" WARNFLAGS="-Werror -Wall -Wextra -pedantic -static" PKG_CONFIG="pkg-config --static" Q=
strip rgb{asm,link,fix,gfx}
- name: Create install script
run: |
@@ -100,8 +107,8 @@ jobs:
- name: Upload Linux binaries
uses: actions/upload-artifact@v7
with:
name: linux
path: rgbds-linux-x86_64.tar.xz
archive: false
if-no-files-found: error
release:
@@ -117,6 +124,9 @@ jobs:
make dist Q=
- name: Download all binary packages
uses: actions/download-artifact@v8
with:
skip-decompress: true # Do not decompress `.zip`s, we want them as-is.
merge-multiple: true # Do not download non-`.zip`s into separate directories.
- name: Draft the release
run: |
.github/scripts/draft-release.sh "${ref#refs/tags/}"
+74 -23
View File
@@ -3,6 +3,10 @@ on:
- push
- pull_request
concurrency:
group: ${{github.workflow}}-${{github.event.pull_request.number || github.ref}}
cancel-in-progress: true
env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
TERM: xterm-256color
@@ -196,8 +200,8 @@ jobs:
shell: cmd
run: | # ASan seems to be broken on Windows, so we disable it.
call .github\scripts\msvc-env.bat ${{ matrix.bits }}
cmake -B build -G Ninja --preset develop -DSANITIZERS=OFF ^
-DFETCHCONTENT_BASE_DIR="${{ env.DEPS_ROOT_DIR }}" -DCMAKE_C_FLAGS="/nologo"
cmake -B build -G Ninja --preset develop -DFETCHCONTENT_BASE_DIR="${{ env.DEPS_ROOT_DIR }}" ^
-DSANITIZERS=OFF -DCMAKE_C_FLAGS="/nologo" -DCMAKE_INSTALL_BINDIR=. -DCPACK_COMPONENTS_ALL=binaries
cmake --build build -- -k 0
- name: Install
shell: cmd
@@ -207,12 +211,12 @@ jobs:
- name: Package binaries
working-directory: build
run: |
cpack -DCPACK_PACKAGE_FILE_NAME=rgbds-win${{ matrix.bits }} -G ZIP --verbose
cpack -DCPACK_PACKAGE_FILE_NAME=rgbds-canary-w${{ matrix.bits }}-${{ matrix.os }} -G ZIP --verbose
- name: Upload Windows binaries
uses: actions/upload-artifact@v7
with:
name: rgbds-canary-w${{ matrix.bits }}-${{ matrix.os }}
path: build/rgbds-win${{ matrix.bits }}.zip
path: build/rgbds-canary-w${{ matrix.bits }}-${{ matrix.os }}.zip
archive: false
if-no-files-found: error
- name: Compute test dependency cache params
id: test-deps-cache-params
@@ -255,10 +259,11 @@ jobs:
key: dep-srcs-${{ hashFiles('cmake/deps.cmake') }}
enableCrossOsArchive: true
- name: Cross-build Windows binaries
run: | # MinGW does not support `--preset develop` sanitizers ASan or UBSan.
run: | # MinGW doesn't support `--preset develop` sanitizers ASan or UBSan, so we disable them.
dll_search_dir=$(printf '%s\n' /usr/lib/gcc/*-w64-mingw32/*-win32 | tee -a /dev/stderr)
cmake -B build -G Ninja --preset develop -DSANITIZERS=OFF --toolchain cmake/toolchain-mingw${{ matrix.bits }}.cmake \
-DFETCHCONTENT_BASE_DIR="${{ env.DEPS_ROOT_DIR }}" -DDLL_SEARCH_DIRS="$dll_search_dir"
cmake -B build -G Ninja --preset develop -DFETCHCONTENT_BASE_DIR="${{ env.DEPS_ROOT_DIR }}" \
-DSANITIZERS=OFF -DDLL_SEARCH_DIRS="$dll_search_dir" \
--toolchain cmake/toolchain-mingw${{ matrix.bits }}.cmake
cmake --build build -- -k 0
- name: Package Windows binaries
run: |
@@ -332,18 +337,16 @@ jobs:
cygwin:
strategy:
matrix:
bits: [32, 64]
include:
- bits: 32
arch: x86
- bits: 64
arch: x86_64
arch: [x86, x86_64]
fail-fast: false
runs-on: windows-2022
timeout-minutes: 30
env:
CYGWIN_NOWINPATH: 1
CHERE_INVOKING: 1
defaults:
run:
shell: C:\cygwin\bin\env.exe CYGWIN_NOWINPATH=1 CHERE_INVOKING=1 C:\cygwin\bin\bash.exe -o igncr '{0}'
shell: C:\cygwin\bin\bash.exe -leo igncr '{0}'
steps:
- name: Save Windows git location for the PATH
shell: pwsh
@@ -357,22 +360,64 @@ jobs:
platform: ${{ matrix.arch }}
packages: >-
bison
cmake
gcc-g++
git
libpng-devel
make
ninja
pkg-config
- name: Build
run: | # Cygwin does not support `make develop` sanitizers ASan or UBSan
python312
python312-imaging
- name: Build using Make
if: matrix.arch == 'x86'
run: | # Cygwin doesn't support `make develop` sanitizers ASan or UBSan, so we don't use it.
make -k -j "$(getconf _NPROCESSORS_ONLN)" Q=
- name: Install
- name: Install using Make
if: matrix.arch == 'x86'
run: |
make install Q=
type rgbasm rgblink rgbfix rgbgfx
man -w 1 rgbasm rgblink rgbfix rgbgfx
- name: Run tests
- name: Build using CMake
if: matrix.arch == 'x86_64'
run: | # Cygwin doesn't support `--preset develop` sanitizers ASan or UBSan, so we disable them.
cmake -B build -G Ninja --preset develop -DSANITIZERS=OFF
cmake --build build -- -k 0
- name: Install using CMake
if: matrix.arch == 'x86_64'
run: |
cmake --install build --verbose
type rgbasm rgblink rgbfix rgbgfx
man -w 1 rgbasm rgblink rgbfix rgbgfx
- name: Compute test dependency cache params
if: matrix.arch == 'x86_64'
id: test-deps-cache-params
run: |
paths=$(test/external/fetch-repos.sh --get-paths)
hash=$(test/external/fetch-repos.sh --get-hash)
tee -a <<<"paths=\"${paths//,/\\n}\"" $GITHUB_OUTPUT
tee -a <<<"hash=${hash%-}" $GITHUB_OUTPUT
- name: Check test dependency repositories cache
if: matrix.arch == 'x86_64'
id: test-deps-cache
uses: actions/cache@v5
with:
path: ${{ fromJSON(steps.test-deps-cache-params.outputs.paths) }}
key: cygwin-${{ matrix.bits }}-${{ steps.test-deps-cache-params.outputs.hash }}
- name: Fetch test dependency repositories
if: matrix.arch == 'x86_64' && steps.test-deps-cache.outputs.cache-hit != 'true'
continue-on-error: true
run: |
test/external/fetch-repos.sh
- name: Run tests using our script
if: matrix.arch == 'x86'
run: | # 32-bit Cygwin's `git` 2.38 doesn't support `git clone --revision`, so we don't fetch/run external tests.
test/run-tests.sh --os cygwin --only-internal --jobs "$(getconf _NPROCESSORS_ONLN)"
- name: Run tests using CTest
if: matrix.arch == 'x86_64'
run: |
ctest --test-dir build --schedule-random
- name: Use Windows git location in the PATH
shell: pwsh
run: | # Prevents the `actions/checkout` post-job cleanup from using Cygwin's git binary
@@ -395,12 +440,18 @@ jobs:
GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0
release: "15.1"
usesh: true
copyback: false
prepare: |
.github/scripts/install-deps.sh freebsd
run: | # Leak detection is not supported on FreeBSD, so disable it.
cmake -B build --preset develop
cmake --build build --verbose -- -k -j "$(getconf _NPROCESSORS_ONLN)"
ASAN_OPTIONS=detect_leaks=0 ctest --test-dir build --schedule-random --label-exclude external
run: |
: # Build
cmake -B build -G Ninja --preset develop
cmake --build build -- -k 0
: # Install
cmake --install build --verbose
type rgbasm rgblink rgbfix rgbgfx
man -w 1 rgbasm rgblink rgbfix rgbgfx
: # Run tests
: # Leak detection is not supported on FreeBSD, so we disable it.
. .github/scripts/freebsd-env.sh
ASAN_OPTIONS=detect_leaks=0 ctest --test-dir build --schedule-random
+4
View File
@@ -7,6 +7,10 @@ on:
- man/*
workflow_dispatch:
concurrency:
group: ${{github.workflow}}-${{github.ref}}
cancel-in-progress: true
env:
# Force colored output (see https://bixense.com/clicolors/ and https://force-color.org/)
TERM: xterm-256color
+1
View File
@@ -8,6 +8,7 @@
*.o
*.exe
*.dll
*.pdb
*.gcno
*.gcda
*.gcov
+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)
# We want the `CMAKE_MATCH_n` variables, so we just need to run *some* regex op.
string(REGEX MATCH "PACKAGE_(VERSION_[^ \t]+)[ \t]+([0-9]+)" dummy "${line}")
set("${CMAKE_MATCH_1}" "${CMAKE_MATCH_2}")
set("RGBDS_${CMAKE_MATCH_1}" "${CMAKE_MATCH_2}")
endforeach()
project(rgbds
VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}"
VERSION "${RGBDS_VERSION_MAJOR}.${RGBDS_VERSION_MINOR}.${RGBDS_VERSION_PATCH}"
LANGUAGES CXX
DESCRIPTION "Game Boy assembly toolchain"
HOMEPAGE_URL "https://rgbds.gbdev.io")
if(DEFINED VERSION_RC)
string(APPEND CMAKE_PROJECT_VERSION "-rc${VERSION_RC}")
string(APPEND PROJECT_VERSION "-rc${RGBDS_VERSION_RC}")
endif()
# Reject in-source builds, as they may conflict with the Makefile.
get_filename_component(srcdir "${CMAKE_SOURCE_DIR}" REALPATH)
get_filename_component(bindir "${CMAKE_BINARY_DIR}" REALPATH)
get_filename_component(srcdir "${CMAKE_CURRENT_SOURCE_DIR}" REALPATH)
get_filename_component(bindir "${CMAKE_CURRENT_BINARY_DIR}" REALPATH)
if(srcdir STREQUAL bindir)
message(FATAL_ERROR "RGBDS should not be built in the source directory.
Instead, create a separate build directory and specify to CMake the path to the source directory.")
@@ -52,14 +52,22 @@ endif()
if(SANITIZERS)
if(MSVC)
message(STATUS "ASan enabled")
add_compile_options(/fsanitize=address) # Note that this shouldn't be passed to the linker.
add_compile_options(/fsanitize=address /sdl) # Note that this shouldn't be passed to the linker.
add_definitions(/_MSVC_STL_HARDENING=1 /_MSVC_STL_DESTRUCTOR_TOMBSTONES=1 /D_ITERATOR_DEBUG_LEVEL=2)
else() # We assume a GNU-like compiler.
message(STATUS "ASan and UBSan enabled")
set(SAN_FLAGS -fsanitize=address -fsanitize=undefined
-fsanitize=float-divide-by-zero)
add_compile_options(${SAN_FLAGS})
add_link_options(${SAN_FLAGS})
add_definitions(-D_GLIBCXX_ASSERTIONS -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG)
# Consider using `_GLIBCXX_DEBUG_BACKTRACE` (and `-lstdc++exp`) if unable to debug locally.
add_definitions(-D_GLIBCXX_DEBUG -D_GLIBCXX_DEBUG_PEDANTIC -D_GLIBCXX_SANITIZE_VECTOR
-D_GLIBCXX_VERBOSE_ASSERT
# This enables debug-mode checks for `std::string`s, but stdlibc++ says
# "this is unsupported and not guaranteed to work", so removing it may be fine.
# https://gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode_semantics.html
-D_GLIBCXX_EXTERN_TEMPLATE=0
-D_LIBCPP_DEBUG -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG)
# A non-zero optimization level is desired even in debug mode (especially for Clang),
# and the two codegen flags improve the sanitizers' backtraces, but we want the user to
# be able to override these easily so we put them first.
@@ -82,35 +90,46 @@ endif()
# Use versioning consistent with Makefile:
# the git revision is used but uses the fallback in an archive.
message(CHECK_START "Determining RGBDS version from Git history")
list(APPEND CMAKE_MESSAGE_INDENT " ")
set(GIT_REV "") # This fallback is important!
find_package(Git)
list(POP_BACK CMAKE_MESSAGE_INDENT)
if(NOT Git_FOUND)
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)
if(DEFINED RGBDS_VERSION_STRING) # Possibly stored in the cache, e.g. if specified on the CLI.
if(RGBDS_VERSION_STRING STREQUAL "")
message(STATUS "Will generate version string from `version.hpp`")
else()
message(CHECK_PASS "${GIT_REV}")
if(NOT "${GIT_REV}" MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+(-rc[0-9]+)?")
# Can't find an ancestor tag! (That passes `--match`, anyway.)
message(WARNING "No `v*` Git tag reachable; falling back")
elseif(NOT CMAKE_MATCH_0 STREQUAL "v${CMAKE_PROJECT_VERSION}")
message(SEND_ERROR "\
message(STATUS "Using provided version string - ${RGBDS_VERSION_STRING}")
endif()
else()
message(CHECK_START "Determining RGBDS version from Git history")
# Note that we do NOT store this in the cache, since the Git revision is fairly volatile.
set(RGBDS_VERSION_STRING "") # An empty value causes `version.cpp` to generate a version string.
list(APPEND CMAKE_MESSAGE_INDENT " ")
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.hpp says v${CMAKE_PROJECT_VERSION}!")
version.hpp says v${PROJECT_VERSION}
Did you forget to consult `RELEASE.md`?")
endif()
endif()
endif()
endif()
@@ -185,7 +204,7 @@ else()
# We rely on runtimes more modern than that, so we can ignore those warnings.
# If this is a problem for you: use Microsoft's equivalents, or define `__USE_MINGW_ANSI_STDIO=1`.
# https://sourceforge.net/p/mingw-w64/wiki2/gnu%20printf/
add_compile_options(-Wno-error=format -Wno-error=format-extra-args)
add_compile_options(-Wno-format -Wno-format-extra-args)
endif()
endif()
@@ -215,17 +234,19 @@ endforeach()
# the rest is rather convention from our side, and thus more appropriate for presets or CLI flags.
## CPACK_PACKAGE_NAME: copied from `project()`
set(CPACK_PACKAGE_VENDOR "GBDev")
set(CPACK_PACKAGE_VERSION "${CMAKE_PROJECT_VERSION}") # The individual components are defined implicitly.
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") # The individual components are defined implicitly.
set(CPACK_PACKAGE_DESCRIPTION "An assembly toolchain for the Nintendo Game Boy and Game Boy Color") # Same as our repo's description.
## CPACK_PACKAGE_DESCRIPTION_SUMMARY: copied from `project()`
set(CPACK_PACKAGE_HOMEPAGE_URL "https://rgbds.gbdev.io")
## CPACK_PACKAGE_FILE_NAME: should be provided at runtime (`cpack -P`)
set(CPACK_PACKAGE_CHECKSUM SHA256)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
set(CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/README.md")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE")
set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
set(CPACK_STRIP_FILES ON) # Only applies to binary packages, not sources.
set(CPACK_VERBATIM_VARIABLES ON)
set(CPACK_THREADS 0) # Use all available CPU cores.
set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF)
set(CPACK_COMPONENTS_GROUPING ALL_COMPONENTS_IN_ONE)
set(CPACK_ARCHIVE_COMPONENT_INSTALL ON)
include(CPack)
+1 -1
View File
@@ -1,6 +1,6 @@
FROM debian:13-slim
LABEL org.opencontainers.image.source=https://github.com/gbdev/rgbds
ARG version=1.0.2
ARG version=1.0.4
WORKDIR /rgbds
COPY . .
+25 -19
View File
@@ -34,12 +34,12 @@ WARNFLAGS := -Wall -pedantic -Wno-unknown-warning-option \
CXXFLAGS ?= -O3 -flto -DNDEBUG
# Non-overridable CXXFLAGS
REQUIREDCXXFLAGS := -std=c++20 -I include -fno-exceptions -fno-rtti
REALCXXFLAGS := ${CXXFLAGS} ${WARNFLAGS} ${REQUIREDCXXFLAGS}
REALCXXFLAGS := ${CXXFLAGS} ${REQUIREDCXXFLAGS}
# Overridable LDFLAGS
LDFLAGS ?=
# Non-overridable LDFLAGS
REQUIREDLDFLAGS := -DBUILD_VERSION_STRING=\"${VERSION_STRING}\"
REALLDFLAGS := ${LDFLAGS} ${WARNFLAGS} ${REQUIREDLDFLAGS}
REALLDFLAGS := ${LDFLAGS} ${REQUIREDLDFLAGS}
# Wrapper around bison that passes flags depending on what the version supports
BISON := src/bison.sh
@@ -130,22 +130,22 @@ rgbgfx_obj := \
src/verbosity.o
rgbasm: ${rgbasm_obj}
$Q${CXX} ${REALLDFLAGS} -o $@ ${rgbasm_obj} ${REALCXXFLAGS} src/version.cpp
$Q${CXX} ${WARNFLAGS} ${REALLDFLAGS} -o $@ ${rgbasm_obj} ${REALCXXFLAGS} src/version.cpp
rgblink: ${rgblink_obj}
$Q${CXX} ${REALLDFLAGS} -o $@ ${rgblink_obj} ${REALCXXFLAGS} src/version.cpp
$Q${CXX} ${WARNFLAGS} ${REALLDFLAGS} -o $@ ${rgblink_obj} ${REALCXXFLAGS} src/version.cpp
rgbfix: ${rgbfix_obj}
$Q${CXX} ${REALLDFLAGS} -o $@ ${rgbfix_obj} ${REALCXXFLAGS} src/version.cpp
$Q${CXX} ${WARNFLAGS} ${REALLDFLAGS} -o $@ ${rgbfix_obj} ${REALCXXFLAGS} src/version.cpp
rgbgfx: ${rgbgfx_obj}
$Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ ${rgbgfx_obj} ${REALCXXFLAGS} ${PNGLDLIBS} src/version.cpp
$Q${CXX} ${WARNFLAGS} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ ${rgbgfx_obj} ${REALCXXFLAGS} ${PNGLDLIBS} src/version.cpp
test/gfx/randtilegen: test/gfx/randtilegen.cpp
$Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCXXFLAGS} ${PNGCFLAGS} ${PNGLDLIBS}
$Q${CXX} ${WARNFLAGS} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCXXFLAGS} ${PNGCFLAGS} ${PNGLDLIBS}
test/gfx/rgbgfx_test: test/gfx/rgbgfx_test.cpp
$Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCXXFLAGS} ${PNGCFLAGS} ${PNGLDLIBS}
$Q${CXX} ${WARNFLAGS} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCXXFLAGS} ${PNGCFLAGS} ${PNGLDLIBS}
# Rules to process files
@@ -163,29 +163,33 @@ src/link/script.hpp: src/link/script.cpp
# Only RGBGFX uses libpng (POSIX make doesn't support pattern rules to cover all these)
src/gfx/color_set.o: src/gfx/color_set.cpp
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
src/gfx/main.o: src/gfx/main.cpp
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
src/gfx/pal_packing.o: src/gfx/pal_packing.cpp
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
src/gfx/pal_sorting.o: src/gfx/pal_sorting.cpp
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
src/gfx/pal_spec.o: src/gfx/pal_spec.cpp
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
src/gfx/png.o: src/gfx/png.cpp
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
src/gfx/process.o: src/gfx/process.cpp
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
src/gfx/reverse.o: src/gfx/reverse.cpp
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
src/gfx/rgba.o: src/gfx/rgba.cpp
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
.cpp.o:
$Q${CXX} ${REALCXXFLAGS} -c -o $@ $<
$Q${CXX} ${WARNFLAGS} ${REALCXXFLAGS} -c -o $@ $<
# Relying on the Makefile itself to introspect the installation commands.
# First pretending to build the executables to exclude their build commands,
# and then emitting the install commands without actually executing them.
install.sh:
$Qecho '#!/usr/bin/env bash' > $@
$Q${MAKE} -t all
$Q${MAKE} -s -n install MANSRC= >> $@
$Qchmod +x $@
@@ -225,7 +229,9 @@ develop:
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1 \
-Wno-format-nonliteral -Wno-strict-overflow -Wno-unused-but-set-variable \
-Wno-type-limits -Wno-tautological-constant-out-of-range-compare -Wvla \
-D_GLIBCXX_ASSERTIONS -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG \
-D_GLIBCXX_DEBUG -D_GLIBCXX_DEBUG_PEDANTIC -D_GLIBCXX_SANITIZE_VECTOR \
-D_GLIBCXX_VERBOSE_ASSERT -D_GLIBCXX_EXTERN_TEMPLATE=0 \
-D_LIBCPP_DEBUG -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG \
-fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero" \
CXXFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls"
+9 -6
View File
@@ -6,13 +6,16 @@
# triggers some poorly-tested code paths within Apple's linker, which then crashes.
# This can be worked around by using LLVM's LLD linker and passing `-fuse-ld=lld` when linking.
# The `-mmacosx-version-min=10.4` flag ensures that the binary only uses APIs available on Mac OS X 10.4 Tiger.
# The `-arch` flags build a "fat binary" that works on both Apple architectures:
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.4 CACHE STRING "Minimum Mac OS X version to target for deployment (at runtime)")
# This builds a "fat binary" that works on both Apple architectures:
# older Intel x64 Macs and newer ARM "Apple Silicon" ones.
set(secret_sauce -mmacosx-version-min=10.4 "SHELL:-arch x86_64" "SHELL:-arch arm64") # Avoid `-arch` being dedup'd.
add_compile_options(${secret_sauce})
add_link_options(${secret_sauce})
set(PNG_HARDWARE_OPTIMIZATIONS OFF) # These do not play well with a dual-arch build.
# Due to a libpng build script limitation/bug (as of 1.6.58), the native architecture has to be first...
# and since our CI builds this executable on an ARM machine, that's what we're putting first.
set(CMAKE_OSX_ARCHITECTURES arm64 x86_64 CACHE STRING "Build architectures for Mac OS X")
# This controls the SIMD optimizations, which include architecture-specific headers that get rejected
# in a dual-arch build, thus we have to disable them.
# This shouldn't be a big deal for RGBGFX, anyway?
set(PNG_HARDWARE_OPTIMIZATIONS OFF)
# Mac OS X has always provided zlib, so we can safely link dynamically against it.
# However, libpng is *not* provided by default, so we link it statically, which requires downloading and building it from source.
+1 -1
View File
@@ -22,4 +22,4 @@ for (( i = 0; i < NB_PALS; i++ )); do
printf $(printf '\\x%x' $i{,,,}) >> "$TMP/tmp.palmap"
done
"${RGBGFX:-${RGBDS+$RGBDS/}rgbgfx}" -r 4 "$2" -o "$TMP/tmp.2bpp" -OTQ -p "$1" -n "$NB_PALS"
"${RGBGFX:-${RGBDS+$RGBDS/}rgbgfx}" -o "$TMP/tmp.2bpp" -OTQ -p "$1" -n "$NB_PALS" -r 4 -- "$2"
+2 -1
View File
@@ -12,7 +12,8 @@ GitHub.
- [Dockerfile](/Dockerfile): update `ARG version`.
- [test/external/\*.cfg](/test/external): update test dependency commits
(preferably, use the latest available).
- [man/\*](/man/): update dates and authors.
- [man/\*](/man/): update dates and authors. You can use
`sed -Ei '/^\.Dd/ s/ .*/ '"$(LC_ALL=C date +'%B %d, %Y')"/ man/*`.
2. Create a Git tag formatted as <code>v<i>&lt;MAJOR&gt;</i>.<i>&lt;MINOR&gt;</i>.<i>&lt;PATCH&gt;</i></code>,
or <code>v<i>&lt;MAJOR&gt;</i>.<i>&lt;MINOR&gt;</i>.<i>&lt;PATCH&gt;</i>-rc<i>&lt;RC&gt;</i></code>
+2
View File
@@ -57,10 +57,12 @@ struct Expression {
void addCheckBitIndex(uint8_t mask);
void checkNBit(uint8_t n) const;
void checkSignedNBit(uint8_t n) const;
void encode(std::vector<uint8_t> &buffer) const;
};
bool checkNBit(int32_t v, uint8_t n, char const *name);
bool checkSignedNBit(int32_t v, uint8_t n, char const *name);
#endif // RGBDS_ASM_RPN_HPP
+2
View File
@@ -42,6 +42,8 @@ struct Section {
std::deque<Patch> patches;
std::vector<uint8_t> data;
SectionTypeInfo const &typeInfo() const { return sectionTypeInfo[type]; }
uint32_t getID() const; // ID of the section in the object file (`UINT32_MAX` if none)
bool isSizeKnown() const;
};
+3
View File
@@ -34,6 +34,9 @@ public:
} else {
assume(mode & std::ios_base::out);
_file.emplace<std::streambuf *>(std::cout.rdbuf());
if (setmode(STDOUT_FILENO, (mode & std::ios_base::binary) ? O_BINARY : O_TEXT) == -1) {
return nullptr;
}
}
return this;
}
+4 -1
View File
@@ -25,7 +25,8 @@ struct Options {
std::array<uint8_t, 2> baseTileIDs{0, 0}; // -b
enum {
NO_SPEC,
EXPLICIT,
INLINE,
EXTERNAL,
EMBEDDED,
DMG,
} palSpecType = NO_SPEC; // -c
@@ -59,6 +60,8 @@ struct Options {
uint16_t maxNbColors() const { return nbColorsPerPal * nbPalettes; }
bool hasExplicitPalSpec() const { return palSpecType == INLINE || palSpecType == EXTERNAL; }
uint8_t dmgColors[4] = {};
uint8_t dmgValue(uint8_t i) const {
assume(i < 4);
-2
View File
@@ -8,8 +8,6 @@
void parseInlinePalSpec(char const * const rawArg);
void parseExternalPalSpec(char const *arg);
void parseDmgPalSpec(char const * const rawArg);
void parseDmgPalSpec(uint8_t palSpecDmg);
void parseBackgroundPalSpec(char const *arg);
#endif // RGBDS_GFX_PAL_SPEC_HPP
+3 -1
View File
@@ -10,9 +10,11 @@
#include "gfx/rgba.hpp"
struct Png {
uint32_t width, height;
uint32_t width = 0;
uint32_t height = 0;
std::vector<Rgba> pixels{};
std::vector<Rgba> palette{};
bool isIndexed = false;
Png() {}
Png(char const *filename, std::streambuf &file);
+10 -19
View File
@@ -20,23 +20,6 @@ struct Rgba {
explicit constexpr Rgba(uint32_t rgba = 0)
: red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {}
// CGB colors are RGB555, so we use bit 15 to signify that the color is transparent instead
// Since the rest of the bits don't matter then, we return 0x8000 (1 << 15) exactly.
static constexpr uint16_t transparent = 0b1'00000'00000'00000;
static constexpr Rgba fromCGBColor(uint16_t color) {
constexpr auto _5to8 = [](uint8_t channel) -> uint8_t {
channel &= 0b11111; // For caller's convenience
return channel << 3 | channel >> 2;
};
return {
_5to8(color),
_5to8(color >> 5),
_5to8(color >> 10),
static_cast<uint8_t>(color & transparent ? 0x00 : 0xFF),
};
}
// Returns this RGBA as a 32-bit number that can be printed in hex (`#%08x`)
// to yield its CSS representation (`#rrggbbaa`).
uint32_t toCSS() const {
@@ -45,14 +28,22 @@ struct Rgba {
};
return shl(red, 24) | shl(green, 16) | shl(blue, 8) | shl(alpha, 0);
}
bool operator==(Rgba const &rhs) const { return toCSS() == rhs.toCSS(); }
// We allow some leeway to consider colors as transparent or opaque,
// but intermediate alpha values are still ambiguous.
static constexpr uint8_t transparency_threshold = 0x10;
bool isTransparent() const { return alpha < transparency_threshold; }
static constexpr uint8_t opacity_threshold = 0xF0;
bool isOpaque() const { return alpha >= opacity_threshold; }
// Computes the equivalent CGB color, respects the color curve depending on options
bool isAmbiguous() const { return isTransparent() == isOpaque(); }
// CGB colors are RGB555, so we use bit 15 to signify that the color is transparent instead
// Since the rest of the bits don't matter then, we return 0x8000 (1 << 15) exactly.
static constexpr uint16_t transparent = 0b1'00000'00000'00000;
// Computes the equivalent RGB888 color; respects the color curve depending on argument
static Rgba fromCGBColor(uint16_t color, bool useColorCurve);
// Computes the equivalent RGB555 color; respects the color curve depending on options
uint16_t cgbColor() const;
bool isGray() const { return red == green && green == blue; }
+14 -16
View File
@@ -50,42 +50,40 @@ static inline void unreachable_() {
#pragma intrinsic(_BitScanReverse, _BitScanForward)
static inline int ctz(unsigned int x) {
unsigned long cnt;
assume(x != 0);
_BitScanForward(&cnt, x);
return cnt;
unsigned long count;
_BitScanForward(&count, x);
return count;
}
static inline int clz(unsigned int x) {
unsigned long cnt;
assume(x != 0);
_BitScanReverse(&cnt, x);
return 31 - cnt;
unsigned long count;
_BitScanReverse(&count, x);
return 31 - count;
}
#else
#include <limits.h>
static inline int ctz(unsigned int x) {
int cnt = 0;
assume(x != 0);
int count = 0;
while (!(x & 1)) {
x >>= 1;
++cnt;
++count;
}
return cnt;
return count;
}
static inline int clz(unsigned int x) {
int cnt = 0;
assume(x != 0);
int count = 0;
while (x <= UINT_MAX / 2) {
x <<= 1;
++cnt;
++count;
}
return cnt;
return count;
}
#endif
+12 -2
View File
@@ -12,7 +12,7 @@
#include <unordered_map>
#include <utility>
#include "helpers.hpp" // Enum
#include "helpers.hpp" // Enum, assume
// A wrapper around iterables to reverse their iteration order; used in `for`-each loops.
template<typename IterableT>
@@ -30,8 +30,12 @@ auto end(ReversedIterable<IterableT> r) {
return std::rend(r._iterable);
}
// Prevent creation of a dangling reference to a temporary rvalue
template<typename IterableT>
ReversedIterable<IterableT> reversed(IterableT &&_iterable) {
ReversedIterable<IterableT> reversed(IterableT &&_iterable) = delete;
template<typename IterableT>
ReversedIterable<IterableT> reversed(IterableT &_iterable) {
return {_iterable};
}
@@ -55,12 +59,18 @@ public:
typename decltype(list)::const_iterator begin() const { return list.begin(); }
typename decltype(list)::const_iterator end() const { return list.end(); }
// Adding a key that already exists would make the previous value unreachable.
// `InsertionOrderedMap`s are only used for charmaps and sections, which each
// avoid `add`ing if already present, so we can `assume` this is not a concern.
ItemT &add(KeyT const &key) {
assume(!contains(key));
map[key] = list.size();
return list.emplace_back();
}
ItemT &add(KeyT const &key, ItemT &&value) {
assume(!contains(key));
map[key] = list.size();
list.emplace_back(std::move(value));
return list.back();
+3 -2
View File
@@ -11,7 +11,8 @@ void out_AddSection(Section const &section);
// Finds an assigned section overlapping another one.
Section const *out_OverlappingSection(Section const &section);
// Writes all output (bin, sym, map) files.
void out_WriteFiles();
void out_WriteROMIfAny();
void out_WriteSymIfAny();
void out_WriteMapIfAny();
#endif // RGBDS_LINK_OUTPUT_HPP
+2
View File
@@ -51,6 +51,8 @@ struct Section {
std::vector<Symbol *> symbols;
std::unique_ptr<Section> nextPiece; // The next fragment or union "piece" of this section
SectionTypeInfo const &typeInfo() const { return sectionTypeInfo[type]; }
private:
// Template class for both const and non-const iterators over the "pieces" of this section
template<QualifiedEquivalent<Section> SectionT>
+18 -19
View File
@@ -4,7 +4,6 @@
#define RGBDS_LINKDEFS_HPP
#include <stdint.h>
#include <string>
#include "helpers.hpp" // assume
@@ -82,6 +81,13 @@ static constexpr uint8_t SECTTYPE_TYPE_MASK = 0b111;
static constexpr uint8_t SECTTYPE_UNION_BIT = 7;
static constexpr uint8_t SECTTYPE_FRAGMENT_BIT = 6;
// Tells whether a section has data in its object file definition,
// depending on type.
static inline bool sectTypeHasData(SectionType type) {
assume(type != SECTTYPE_INVALID);
return type == SECTTYPE_ROM0 || type == SECTTYPE_ROMX;
}
enum FileStackNodeType {
NODE_REPT,
NODE_FILE,
@@ -90,31 +96,24 @@ enum FileStackNodeType {
static constexpr uint8_t FSTACKNODE_QUIET_BIT = 7;
// Nont-`const` members may be patched in RGBLINK depending on CLI flags
extern struct SectionTypeInfo {
std::string const name;
// Non-`const` members may be patched in RGBLINK depending on CLI flags
struct SectionTypeInfo {
char const *name;
uint16_t const startAddr;
uint16_t size;
uint32_t const firstBank;
uint32_t lastBank;
} sectionTypeInfo[SECTTYPE_INVALID];
// Tells whether a section has data in its object file definition,
// depending on type.
static inline bool sectTypeHasData(SectionType type) {
assume(type != SECTTYPE_INVALID);
return type == SECTTYPE_ROM0 || type == SECTTYPE_ROMX;
}
// Returns a memory region's end address (last byte), e.g. 0x7FFF
uint16_t endAddr() const { return startAddr + size - 1; }
// Returns a memory region's end address (last byte), e.g. 0x7FFF
static inline uint16_t sectTypeEndAddr(SectionType type) {
return sectionTypeInfo[type].startAddr + sectionTypeInfo[type].size - 1;
}
// Returns a memory region's number of banks, or 1 for regions without banking
uint32_t nbBanks() const { return lastBank - firstBank + 1; }
// Returns a memory region's number of banks, or 1 for regions without banking
static inline uint32_t sectTypeBanks(SectionType type) {
return sectionTypeInfo[type].lastBank - sectionTypeInfo[type].firstBank + 1;
}
bool isBanked() const { return nbBanks() != 1; }
};
extern SectionTypeInfo sectionTypeInfo[SECTTYPE_INVALID];
enum SectionModifier { SECTION_NORMAL, SECTION_UNION, SECTION_FRAGMENT };
+3
View File
@@ -5,6 +5,9 @@
#include <stdint.h>
int32_t op_add(int32_t augend, int32_t addend);
int32_t op_sub(int32_t minuend, int32_t subtrahend);
int32_t op_mul(int32_t multiplicand, int32_t multiplier);
int32_t op_divide(int32_t dividend, int32_t divisor);
int32_t op_modulo(int32_t dividend, int32_t divisor);
int32_t op_exponent(int32_t base, uint32_t power);
+30 -2
View File
@@ -55,9 +55,37 @@
#define setmode(fd, mode) (0)
#endif
// MingGW and Cygwin need POSIX functions which are not standard C explicitly enabled,
#if defined(__MINGW32__) || defined(__CYGWIN__)
// Windows has 32-bit `long`, which limits `fseek` and `ftell` to 2 GiB
#if defined(_MSC_VER) || defined(__MINGW32__)
#define fseek _fseeki64
#define ftell _ftelli64
#endif
// MingGW and Cygwin may need POSIX functions which are not standard C explicitly enabled
#if (defined(__MINGW32__) || defined(__CYGWIN__)) && !defined(_POSIX_C_SOURCE)
#define _POSIX_C_SOURCE 200809L
#endif
// Apple has deprecated `sprintf` since Xcode 14 (for macOS 13), but we use it solely in
// contexts where both the size of the buffer *and* max size of the printed string are
// known statically, which GCC thus checks for.
#ifdef __APPLE__
#define sprintf_to_array(array, ...) \
do { \
static_assert( \
std::is_array_v<decltype(array)>, "Only use this macro to print to an array!" \
); \
snprintf(array, sizeof(array), __VA_ARGS__); \
} while (0)
#else
#define sprintf_to_array(array, ...) \
do { \
static_assert( \
std::is_array_v<decltype(array)>, "Only use this macro to print to an array!" \
); \
sprintf(array, __VA_ARGS__); \
} while (0)
#endif
#endif // RGBDS_PLATFORM_HPP
+1 -1
View File
@@ -27,7 +27,7 @@ int xfclose(FILE *file);
int xclose(int fd);
// Measure file size with `fseek` and `ftell` idiom
long seekSize(FILE *file);
std::optional<uint64_t> seekSize(FILE *file);
// Locale-independent character class functions
bool isNewline(int c);
+1 -1
View File
@@ -5,7 +5,7 @@
#define PACKAGE_VERSION_MAJOR 1
#define PACKAGE_VERSION_MINOR 0
#define PACKAGE_VERSION_PATCH 2
#define PACKAGE_VERSION_PATCH 4
// #define PACKAGE_VERSION_RC 1
char const *get_package_version_string();
+7 -7
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 20, 2026
.Dd September 22, 2026
.Dt GBZ80 7
.Os
.Sh NAME
@@ -1525,7 +1525,7 @@ Set according to result.
.Ss RLC r8
Rotate register
.Ar r8
left.
Left Circularly.
.Bd -literal
┏━ Flags ━┓ ┏━━━━━━━ r8 ━━━━━━┓
┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐
@@ -1551,7 +1551,7 @@ Set according to result.
.Ss RLC [HL]
Rotate the byte pointed to by
.Sy HL
left.
Left Circularly.
.Bd -literal
┏━ Flags ━┓ ┏━━━━━━ [HL] ━━━━━┓
┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐
@@ -1568,7 +1568,7 @@ Flags: See
.Ss RLCA
Rotate register
.Sy A
left.
Left Circularly.
.Bd -literal
┏━ Flags ━┓ ┏━━━━━━━ A ━━━━━━━┓
┃ C ←╂─┬─╂─ b7 ← ... ← b0 ←╂─┐
@@ -1663,7 +1663,7 @@ Set according to result.
.Ss RRC r8
Rotate register
.Ar r8
right.
Right Circularly.
.Bd -literal
┏━━━━━━━ r8 ━━━━━━┓ ┏━ Flags ━┓
┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃
@@ -1689,7 +1689,7 @@ Set according to result.
.Ss RRC [HL]
Rotate the byte pointed to by
.Sy HL
right.
Right Circularly.
.Bd -literal
┏━━━━━━ [HL] ━━━━━┓ ┏━ Flags ━┓
┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃
@@ -1706,7 +1706,7 @@ Flags: See
.Ss RRCA
Rotate register
.Sy A
right.
Right Circularly.
.Bd -literal
┏━━━━━━━ A ━━━━━━━┓ ┏━ Flags ━┓
┌─╂→ b7 → ... → b0 ─╂─┬─╂→ C ┃
+1 -1
View File
@@ -2,7 +2,7 @@
.\"
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 20, 2026
.Dd September 22, 2026
.Dt RGBASM-OLD 5
.Os
.Sh NAME
+6 -2
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 20, 2026
.Dd September 22, 2026
.Dt RGBASM 1
.Os
.Sh NAME
@@ -8,7 +8,8 @@
.Nd Game Boy assembler
.Sh SYNOPSIS
.Nm
.Op Fl EhVvw
.Op Fl EhVw
.Op Fl v Op Fl v No ...
.Op Fl B Ar param
.Op Fl b Ar chars
.Op Fl \-color Ar when
@@ -177,6 +178,9 @@ or
The defaults are 0123.
.It Fl h , Fl \-help
Print help text for the program and exit.
(Help text wraps to the value of the
.Dv COLUMNS
environment variable if that is defined as nonzero; or else to the console window width if output is to a TTY.)
.It Fl I Ar path , Fl \-include Ar path
Add a new
.Dq include path ;
+16 -3
View File
@@ -2,7 +2,7 @@
.\"
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 20, 2026
.Dd September 22, 2026
.Dt RGBASM 5
.Os
.Sh NAME
@@ -729,17 +729,21 @@ String constants are not expanded within the parentheses.
.Ic IF ) ,
or 0 if only RGBLINK can compute its value.
.It Fn BANK arg Ta Returns a bank number.
.Pp
If
.Ar arg
is the symbol
.Ic @ ,
this function returns the bank of the current section.
.Pp
If
.Ar arg
is a string, it returns the bank of the section that has that name.
.Pp
If
.Ar arg
is a label, it returns the bank number the label is in.
.Pp
The result may be constant if
.Nm
is able to compute it.
@@ -757,10 +761,17 @@ this function returns the name of the current section.
.Ar arg
is a string, this function returns the size of the section named
.Ar arg .
The result is a known constant if the section is already "completed" (i.e. not a
.Ic UNION
or
.Ic FRAGMENT
and no longer the current section).
.Pp
If
.Ar arg
is a section type keyword, it returns the size of that section type.
The result is not constant, since only RGBLINK can compute its value.
.Pp
If
.Ar arg
is an 8-bit or 16-bit register, it returns the size of that register.
@@ -768,6 +779,8 @@ is an 8-bit or 16-bit register, it returns the size of that register.
.Ar arg
is a string, this function returns the starting address of the section named
.Ar arg .
The result is a known constant if the section has a fixed address.
.Pp
If
.Ar arg
is a section type keyword, it returns the starting address of that section type.
@@ -2052,8 +2065,8 @@ It's possible to pass arguments to macros as well!
MACRO lb
ld \e1, (\e2) << 8 | (\e3)
ENDM
lb hl, 20, 18 ; Expands to "ld hl, ((20) << 8) | (18)"
lb de, 3 + 1, NUM**2 ; Expands to "ld de, ((3 + 1) << 8) | (NUM**2)"
lb hl, 20, 18 ; Expands to "ld hl, (20) << 8 | (18)"
lb de, 3 + 1, NUM**2 ; Expands to "ld de, (3 + 1) << 8 | (NUM**2)"
.Ed
.Pp
You expand the arguments inside the macro body by using the escape sequences
+1 -1
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 20, 2026
.Dd September 22, 2026
.Dt RGBDS 5
.Os
.Sh NAME
+1 -1
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 20, 2026
.Dd September 22, 2026
.Dt RGBDS 7
.Os
.Sh NAME
+4 -1
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 20, 2026
.Dd September 22, 2026
.Dt RGBFIX 1
.Os
.Sh NAME
@@ -152,6 +152,9 @@ Trash the global checksum.
.El
.It Fl h , Fl \-help
Print help text for the program and exit.
(Help text wraps to the value of the
.Dv COLUMNS
environment variable if that is defined as nonzero; or else to the console window width if output is to a TTY.)
.It Fl i Ar game_id , Fl \-game-id Ar game_id
Set the game ID string
.Pq Ad 0x13F Ns \(en Ns Ad 0x142
+17 -10
View File
@@ -2,7 +2,7 @@
.\"
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 20, 2026
.Dd September 22, 2026
.Dt RGBGFX 1
.Os
.Sh NAME
@@ -257,6 +257,9 @@ Set the bit depth of the output tile data, in bits per pixel (bpp), either 1 or
This changes how tile data is output, and the maximum number of colors per palette (2 and 4 respectively).
.It Fl h , Fl \-help
Print help text for the program and exit.
(Help text wraps to the value of the
.Dv COLUMNS
environment variable if that is defined as nonzero; or else to the console window width if output is to a TTY.)
.It Fl i Ar input_tiles , Fl \-input-tileset Ar input_tiles
Use the specified input tiles in addition to having
.Nm
@@ -488,9 +491,17 @@ deduplication if
.Fl u
was enabled, so you probably don't want to use this option in combination with
.Fl u .
Note also that the tiles that don't get output will not count towards
.Pp
Also note that the tiles that don't get output in the tile data
.Em do
still count towards
.Fl N Ap s
limit.
limit, since they may get output in the tilemap or attribute map.
If you need to omit input tiles from all output in a way that does not count towards
.Fl N ,
use
.Fl B
background tiles.
.It Fl Y , Fl \-mirror-y
Deduplicate tiles that are vertically symmetrical mirror images of each other across the Y axis.
Implies
@@ -582,15 +593,11 @@ for example because you want to use palette swaps, please use
.Fl c
to specify the palette explicitly.
.Pp
First, if the image contains
Note that if the image contains
.Em any
transparent pixel, color #0 of
transparent pixels, color #0 of
.Em all
palettes will be allocated to it.
This is done
.Sy even if palettes were explicitly specified using Fl c ;
then the specification only covers color #1 onwards.
.Pq If you do not want this, ask your image editor to remove the alpha channel.
palettes will be transparent.
.Pp
After generating palettes,
.Nm
+15 -4
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 20, 2026
.Dd September 22, 2026
.Dt RGBLINK 1
.Os
.Sh NAME
@@ -8,7 +8,8 @@
.Nd Game Boy linker
.Sh SYNOPSIS
.Nm
.Op Fl dhMtVvwx
.Op Fl dhMtVwx
.Op Fl v Op Fl v No ...
.Op Fl B Ar param
.Op Fl \-color Ar when
.Op Fl l Ar linker_script
@@ -52,7 +53,7 @@ but also prohibits the use of banked VRAM.
.Nm
accepts the usual short and long options, such as
.Fl V
and
and
.Fl -version .
Options later in the command line override those set earlier, except for when duplicate options are considered an error.
Options can be abbreviated as long as the abbreviation is unambiguous:
@@ -152,6 +153,9 @@ This option automatically enables
.Fl w .
.It Fl h , Fl \-help
Print help text for the program and exit.
(Help text wraps to the value of the
.Dv COLUMNS
environment variable if that is defined as nonzero; or else to the console window width if output is to a TTY.)
.It Fl l Ar linker_script , Fl \-linkerscript Ar linker_script
Specify a linker script file that tells the linker how sections must be placed in the ROM.
The attributes assigned in the linker script must be consistent with any assigned in the code.
@@ -281,7 +285,7 @@ Certain regions allow omitting the size, in which case it defaults to its maximu
.Bl -column "Region name" "Max value" "Size optional"
Region name Ta Max size Ta Size optional
.Cm romx Ta 65535 Ta \&No
.Cm sram Ta 255 Ta \&No
.Cm sram Ta 256 Ta \&No
.Cm wramx Ta 7 Ta Yes
.El
.Pp
@@ -304,6 +308,13 @@ Later region specs override earlier ones; for example,
disables scrambling for
.Cm romx .
.Pp
The arguments to multiple
.Fl S
options are processed as if concatenated; for example,
.Fl S Cm romx=16 Fl S Cm wramx=8 Fl S Cm sram=4
will work the same as
.Fl S Cm romx=16,wramx=8,sram=4 .
.Pp
.Cm wramx
scrambling is silently ignored if
.Fl w
+1 -4
View File
@@ -1,6 +1,6 @@
.\" SPDX-License-Identifier: MIT
.\"
.Dd July 20, 2026
.Dd September 22, 2026
.Dt RGBLINK 5
.Os
.Sh NAME
@@ -73,14 +73,11 @@ and
.Ic HRAM .
The bank number can be omitted from the types that only contain a single bank, which are:
.Ic ROM0 ,
.Ic ROMX No if Fl t No is passed to Xr rgblink 1 ,
.Ic VRAM No if Fl d No is passed to Xr rgblink 1 ,
.Ic WRAM0 ,
.Ic WRAMX No if Fl w No is passed to Xr rgblink 1 ,
.Ic OAM ,
and
.Ic HRAM .
.Pq Ic SRAM No is the only type that can never have its bank number omitted.
.Pp
After a bank specification, the
.Dq current address
+1 -1
View File
@@ -9,7 +9,7 @@ add_library(common OBJECT
"util.cpp"
"version.cpp"
)
target_compile_definitions(common PRIVATE "BUILD_VERSION_STRING=\"${GIT_REV}\"")
target_compile_definitions(common PRIVATE "BUILD_VERSION_STRING=\"${RGBDS_VERSION_STRING}\"")
find_package(BISON 3.0.0 REQUIRED)
# BISON 4.0 deprecates passing this BISON_FLAGS string to `bison_target`'s `COMPILE_FLAGS`,
+4 -4
View File
@@ -151,11 +151,11 @@ std::optional<std::string> act_ReadFile(std::string const &name, uint32_t maxLen
Defer closeFile{[&] { xfclose(file); }};
size_t readSize = maxLen;
if (long fileSize = seekSize(file); fileSize != -1) {
if (std::optional<uint64_t> fileSize = seekSize(file); fileSize.has_value()) {
// If the file is seekable and shorter than the max length,
// just read as many bytes as there are
if (static_cast<size_t>(fileSize) < readSize) {
readSize = fileSize;
if (*fileSize < readSize) {
readSize = *fileSize;
}
// LCOV_EXCL_START
} else if (errno != ESPIPE) {
@@ -555,7 +555,7 @@ std::string act_StringFormat(
std::string str;
size_t argIndex = 0;
for (size_t i = 0; spec[i] != '\0';) {
for (size_t i = 0; i < spec.length();) {
if (int c = spec[i]; c != '%') {
str += c;
++i;
+8 -1
View File
@@ -77,7 +77,11 @@ int32_t fix_Div(int32_t i, int32_t j, int32_t q) {
}
int32_t fix_Mod(int32_t i, int32_t j, int32_t q) {
return double2fix(fmod(fix2double(i, q), fix2double(j, q)), q);
double divisor = fix2double(j, q);
if (fpclassify(divisor) == FP_ZERO) {
return 0;
}
return double2fix(fmod(fix2double(i, q), divisor), q);
}
int32_t fix_Pow(int32_t i, int32_t j, int32_t q) {
@@ -86,6 +90,9 @@ int32_t fix_Pow(int32_t i, int32_t j, int32_t q) {
int32_t fix_Log(int32_t i, int32_t j, int32_t q) {
double divisor = log(fix2double(j, q));
if (isnan(divisor) || isinf(divisor)) {
return 0;
}
if (fpclassify(divisor) == FP_ZERO) {
return INT32_MAX;
}
+14 -6
View File
@@ -542,7 +542,7 @@ static uint32_t readBracketedMacroArgNum() {
}
}
uint32_t n = readNumber<10>(bumpChar(), nullptr);
if (n > INT32_MAX) {
if (n > INT32_MAX && !(negative && n == static_cast<uint32_t>(INT32_MAX) + 1)) {
error("Number in bracketed macro argument is too large");
return 0;
}
@@ -1005,14 +1005,22 @@ static uint32_t finishReadingFixedPoint(uint32_t integer) {
precision = options.fixPrecision;
}
if (integer >= (1ULL << (32 - precision))) {
warning(WARNING_LARGE_CONSTANT, "Magnitude of fixed-point constant is too large");
return 0;
}
// Cast to unsigned avoids undefined overflow behavior
uint32_t fractional =
static_cast<uint32_t>(round(static_cast<double>(dividend) / divisor * (1ULL << precision)));
// Carry from `fractional` to `integer` if `round` rounded up to the next integer
assume(fractional <= 1ULL << precision);
bool overflowed = false;
if (fractional == 1ULL << precision) {
overflowed = integer == UINT32_MAX;
++integer; // This may overflow from UINT32_MAX to 0
fractional = 0;
}
if (overflowed || integer >= 1ULL << (32 - precision)) {
warning(WARNING_LARGE_CONSTANT, "Magnitude of fixed-point constant is too large");
return 0;
}
return (integer << precision) | fractional;
}
+1 -1
View File
@@ -72,7 +72,7 @@ void MacroArgs::shiftArgs(int32_t count) {
count > 0 && (static_cast<uint32_t>(count) > nbArgs || shift > nbArgs - count)) {
warning(WARNING_MACRO_SHIFT, "Cannot shift macro arguments past their end");
shift = nbArgs;
} else if (count < 0 && shift < static_cast<uint32_t>(-count)) {
} else if (count < 0 && (count == INT32_MIN || shift < static_cast<uint32_t>(-count))) {
warning(WARNING_MACRO_SHIFT, "Cannot shift macro arguments past their beginning");
shift = 0;
} else {
+4
View File
@@ -162,6 +162,10 @@ void out_CreatePatch(uint32_t type, Expression const &expr, uint32_t ofs, uint32
void out_CreateAssert(
AssertionType type, Expression const &expr, std::string const &message, uint32_t ofs
) {
if (message.find('\0') != std::string::npos) {
fatal("Assertion messages cannot contain '\\0' characters");
}
Assertion &assertion = assertions.emplace_front();
initPatch(assertion.patch, type, expr, ofs);
+11 -3
View File
@@ -340,6 +340,7 @@
%type <Expression> reloc_3bit
%type <Expression> reloc_8bit
%type <Expression> reloc_16bit
%type <Expression> reloc_8bit_signed
// Constant numbers
%type <int32_t> iconst
@@ -1241,6 +1242,13 @@ reloc_8bit:
}
;
reloc_8bit_signed:
relocexpr {
$$ = std::move($1);
$$.checkSignedNBit(8);
}
;
reloc_16bit:
relocexpr {
$$ = std::move($1);
@@ -1886,7 +1894,7 @@ sm83_add:
| SM83_ADD MODE_HL COMMA reg_ss {
sect_ConstByte(0x09 | ($4 << 4));
}
| SM83_ADD MODE_SP COMMA reloc_8bit {
| SM83_ADD MODE_SP COMMA reloc_8bit_signed {
sect_ConstByte(0xE8);
sect_RelByte($4, 1);
}
@@ -2434,11 +2442,11 @@ op_a_n:
op_sp_offset:
OP_ADD relocexpr {
$$ = std::move($2);
$$.checkNBit(8);
$$.checkSignedNBit(8);
}
| OP_SUB relocexpr {
$$.makeUnaryOp(RPN_NEG, std::move($2));
$$.checkNBit(8);
$$.checkSignedNBit(8);
}
| %empty {
::error("\"LD HL, SP\" is not a valid instruction; use \"LD HL, SP + 0\"");
+22 -4
View File
@@ -297,7 +297,6 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
if (src1.isKnown() && src2.isKnown()) {
// If both expressions are known, just compute the value
int32_t lval = src1.value(), rval = src2.value();
uint32_t ulval = static_cast<uint32_t>(lval), urval = static_cast<uint32_t>(rval);
switch (op) {
case RPN_LOGOR:
@@ -325,10 +324,10 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
data = lval != rval;
break;
case RPN_ADD:
data = static_cast<int32_t>(ulval + urval);
data = op_add(lval, rval);
break;
case RPN_SUB:
data = static_cast<int32_t>(ulval - urval);
data = op_sub(lval, rval);
break;
case RPN_XOR:
data = lval ^ rval;
@@ -370,7 +369,7 @@ void Expression::makeBinaryOp(RPNCommand op, Expression &&src1, Expression const
data = op_shift_right_unsigned(lval, rval);
break;
case RPN_MUL:
data = static_cast<int32_t>(ulval * urval);
data = op_mul(lval, rval);
break;
case RPN_DIV:
if (rval == 0) {
@@ -512,6 +511,25 @@ bool checkNBit(int32_t v, uint8_t n, char const *name) {
return true;
}
// Checks that an RPN expression's value fits within N bits (must be signed)
void Expression::checkSignedNBit(uint8_t n) const {
if (isKnown()) {
::checkSignedNBit(value(), n, nullptr);
}
}
bool checkSignedNBit(int32_t v, uint8_t n, char const *name) {
assume(n != 0); // That doesn't make sense
assume(n < CHAR_BIT * sizeof(int) - 1); // Otherwise `1 << n` is UB
if (v < -(1 << (n - 1)) || v >= 1 << (n - 1)) {
warning(WARNING_TRUNCATION_1, "%s must be signed %u-bit", name ? name : "Expression", n);
return false;
}
return true;
}
void Expression::encode(std::vector<uint8_t> &buffer) const {
assume(buffer.empty());
+80 -60
View File
@@ -21,7 +21,8 @@
#include "helpers.hpp"
#include "itertools.hpp" // InsertionOrderedMap
#include "linkdefs.hpp"
#include "util.hpp" // xfclose, seekSize
#include "platform.hpp" // fseek
#include "util.hpp" // xfclose, seekSize
#include "asm/fstack.hpp"
#include "asm/lexer.hpp"
@@ -99,7 +100,7 @@ void sect_ForEach(void (*callback)(Section &)) {
void sect_CheckSizes() {
for (Section const &sect : sections) {
if (uint32_t maxSize = sectionTypeInfo[sect.type].size; sect.size > maxSize) {
if (uint32_t maxSize = sect.typeInfo().size; sect.size > maxSize) {
error(
"Section \"%s\" grew too big (max size = 0x%" PRIX32 " bytes, reached 0x%" PRIX32
")",
@@ -213,10 +214,8 @@ static void mergeFragments(Section &sect, uint32_t org, uint8_t alignment, uint1
// combination of both.
// The merging is however performed at the *end* of the original section!
if (org != UINT32_MAX) {
uint16_t curOrg = org - sect.size;
// If both are fixed, they must be the same
if (sect.org != UINT32_MAX && sect.org != curOrg) {
// If both are fixed, they must be compatible
if (uint16_t curOrg = org - sect.size; sect.org != UINT32_MAX && sect.org != curOrg) {
sectError(
"Section \"%s\" already declared as fixed at incompatible address $%04" PRIx32,
sect.name.c_str(),
@@ -230,6 +229,16 @@ static void mergeFragments(Section &sect, uint32_t org, uint8_t alignment, uint1
sectAlignSize,
sect.alignOfs
);
} else if (org < sect.size) {
// Check that `curOrg` did not underflow. Note that it's safe for the above checks to
// use an underflowed value, since their reported errors will still be accurate.
sectError(
"Section \"%s\" already contains %" PRIu32
" bytes, higher than this fragment's fixed address $%04" PRIx32,
sect.name.c_str(),
sect.size,
org
);
} else {
// Otherwise, just override
sect.org = curOrg;
@@ -273,14 +282,6 @@ static void mergeSections(
) {
sectErrors.clear();
if (type != sect.type) {
sectError(
"Section \"%s\" already exists but with type `%s`",
sect.name.c_str(),
sectionTypeInfo[sect.type].name.c_str()
);
}
if (sect.modifier != mod) {
sectError(
"Section \"%s\" already declared as `SECTION %s`",
@@ -289,8 +290,29 @@ static void mergeSections(
);
} else {
switch (mod) {
case SECTION_NORMAL:
// Only union/fragment sections can end up with multiple errors queued in `sectErrors`,
// and they cannot encounter this error, so it's okay for this one to skip the queue.
// Queueing it in `sectErrors` would require a sentinel value anyway (e.g. an empty
// string) to handle the "no trace" callback.
assume(sectErrors.empty());
fatalNoTrace([&sect]() {
fprintf(stderr, "Section \"%s\" already defined\n", sect.name.c_str());
fstk_TraceCurrent();
fputs(" and also:\n", stderr);
sect.src->printBacktrace(sect.fileLine);
});
case SECTION_UNION:
case SECTION_FRAGMENT: {
if (type != sect.type) {
sectError(
"Section \"%s\" already exists but with type `%s`",
sect.name.c_str(),
sect.typeInfo().name
);
}
void (*merge)(Section &, uint32_t, uint8_t, uint16_t) =
mod == SECTION_UNION ? mergeSectUnion : mergeFragments;
merge(sect, org, alignment, alignOffset);
@@ -309,20 +331,6 @@ static void mergeSections(
}
break;
}
case SECTION_NORMAL:
// Only union/fragment sections can end up with multiple errors queued in `sectErrors`,
// and they cannot encounter this error, so it's okay for this one to skip the queue.
// Queueing it in `sectErrors` would require a sentinel value anyway (e.g. an empty
// string) to handle the "no trace" callback.
assume(sectErrors.empty());
fatalNoTrace([&sect]() {
fprintf(stderr, "Section \"%s\" already defined\n", sect.name.c_str());
fstk_TraceCurrent();
fputs(" and also:\n", stderr);
sect.src->printBacktrace(sect.fileLine);
});
break;
}
}
@@ -369,8 +377,8 @@ static Section *createSection(
out_RegisterNode(sect.src);
// It is only needed to allocate memory for ROM sections.
if (sectTypeHasData(type)) {
sect.data.resize(sectionTypeInfo[type].size);
if (sectTypeHasData(sect.type)) {
sect.data.resize(sect.typeInfo().size);
}
return &sect;
@@ -395,7 +403,7 @@ static Section *createSectionFragmentLiteral(Section const &parent) {
// Section fragment literals must be ROM sections.
assume(sectTypeHasData(sect.type));
sect.data.resize(sectionTypeInfo[sect.type].size);
sect.data.resize(sect.typeInfo().size);
return &sect;
}
@@ -416,24 +424,24 @@ static Section *getSection(
uint32_t alignMask = alignSize - 1;
// First, validate parameters, and normalize them if applicable
SectionTypeInfo const &typeInfo = sectionTypeInfo[type];
if (bank != UINT32_MAX) {
if (type != SECTTYPE_ROMX && type != SECTTYPE_VRAM && type != SECTTYPE_SRAM
&& type != SECTTYPE_WRAMX) {
error("`BANK` only allowed for `ROMX`, `WRAMX`, `SRAM`, or `VRAM` sections");
} else if (bank < sectionTypeInfo[type].firstBank
|| bank > sectionTypeInfo[type].lastBank) {
} else if (bank < typeInfo.firstBank || bank > typeInfo.lastBank) {
error(
"%s bank value $%04" PRIx32 " out of range ($%04" PRIx32 " to $%04" PRIx32 ")",
sectionTypeInfo[type].name.c_str(),
typeInfo.name,
bank,
sectionTypeInfo[type].firstBank,
sectionTypeInfo[type].lastBank
typeInfo.firstBank,
typeInfo.lastBank
);
}
} else if (sectTypeBanks(type) == 1) {
} else if (!typeInfo.isBanked()) {
// If the section type only has a single bank, implicitly force it
bank = sectionTypeInfo[type].firstBank;
bank = typeInfo.firstBank;
}
// This should be redundant, as the parser guarantees that `AlignmentSpec` will be valid.
@@ -449,14 +457,14 @@ static Section *getSection(
}
if (org != UINT32_MAX) {
if (org < sectionTypeInfo[type].startAddr || org > sectTypeEndAddr(type)) {
if (org < typeInfo.startAddr || org > typeInfo.endAddr()) {
error(
"Section \"%s\"'s fixed address $%04" PRIx32 " is outside of range [$%04" PRIx16
"; $%04" PRIx16 "]",
name.c_str(),
org,
sectionTypeInfo[type].startAddr,
sectTypeEndAddr(type)
typeInfo.startAddr,
typeInfo.endAddr()
);
}
}
@@ -464,18 +472,15 @@ static Section *getSection(
if (alignment != 0) {
// It doesn't make sense to have both alignment and org set
if (org != UINT32_MAX) {
if ((org - alignOffset) & alignMask) {
if ((org & alignMask) != alignOffset) {
error("Section \"%s\"'s fixed address does not match its alignment", name.c_str());
}
alignment = 0; // Ignore it if it's satisfied
} else if (sectionTypeInfo[type].startAddr & alignMask) {
} else if ((typeInfo.startAddr & alignMask) > alignOffset) {
error(
"Section \"%s\"'s alignment cannot be attained in %s",
name.c_str(),
sectionTypeInfo[type].name.c_str()
"Section \"%s\"'s alignment cannot be attained in %s", name.c_str(), typeInfo.name
);
alignment = 0; // Ignore it if it's unattainable
org = 0;
} else if (alignment == 16) {
// Treat an alignment of 16 as fixing the address.
alignment = 0;
@@ -543,6 +548,10 @@ void sect_NewSection(
SectionSpec const &attrs,
SectionModifier mod
) {
if (name.find('\0') != std::string::npos) {
fatal("Section names cannot contain '\\0' characters");
}
for (SectionStackEntry &entry : sectionStack) {
if (entry.section && entry.section->name == name) {
fatal("Section \"%s\" is already on the stack", name.c_str());
@@ -705,8 +714,17 @@ void sect_AlignPC(uint8_t alignment, uint16_t offset) {
} else if (alignment == 16) {
// Treat an alignment large enough as fixing the address.
// Note that this also ensures that a section's alignment never becomes 16 or greater.
sect->align = 0; // Reset the alignment, since we're fixing the address.
sect->org = offset - curOffset;
if (offset < curOffset) {
error(
"Section already contains %" PRIu32
" bytes, higher than this aligned address $%04" PRIx32,
curOffset,
offset
);
} else {
sect->align = 0; // Reset the alignment, since we're fixing the address.
sect->org = offset - curOffset;
}
} else if (alignment > sect->align) {
sect->align = alignment;
// We need `(sect->alignOfs + curOffset) & alignMask == offset`
@@ -977,13 +995,14 @@ bool sect_BinaryFile(std::string const &name, uint32_t startPos) {
}
Defer closeFile{[&] { xfclose(file); }};
if (long fileSize = seekSize(file); fileSize != -1) {
if (startPos > static_cast<size_t>(fileSize)) {
if (std::optional<uint64_t> fileSize = seekSize(file); fileSize.has_value()) {
if (startPos > *fileSize) {
error(
"Specified start position (%" PRIu32 ") is greater than length of \"%s\" (%ld)",
"Specified start position (%" PRIu32 ") is greater than length of \"%s\" (%" PRIu64
")",
startPos,
name.c_str(),
fileSize
*fileSize
);
return false;
}
@@ -1037,23 +1056,24 @@ bool sect_BinaryFileSlice(std::string const &name, uint32_t startPos, uint32_t l
}
Defer closeFile{[&] { xfclose(file); }};
if (long fileSize = seekSize(file); fileSize != -1) {
if (startPos > static_cast<size_t>(fileSize)) {
if (std::optional<uint64_t> fileSize = seekSize(file); fileSize.has_value()) {
if (startPos > *fileSize) {
error(
"Specified start position (%" PRIu32 ") is greater than length of \"%s\" (%ld)",
"Specified start position (%" PRIu32 ") is greater than length of \"%s\" (%" PRIu64
")",
startPos,
name.c_str(),
fileSize
*fileSize
);
return false;
} else if (startPos + length > static_cast<size_t>(fileSize)) {
} else if (length > *fileSize - startPos) {
error(
"Specified range in `INCBIN` file \"%s\" is out of bounds (%" PRIu32 " + %" PRIu32
" > %ld)",
" > %" PRIu64 ")",
name.c_str(),
startPos,
length,
fileSize
*fileSize
);
return false;
}
+1 -1
View File
@@ -130,7 +130,7 @@ void cli_ParseArgs(
// This happens if `--` is passed, process the remaining arg(s) as positional
assume(musl_optind < curArgc);
for (int i = musl_optind; i < curArgc; ++i) {
parseArg(1, argv[i]); // Positional argument
parseArg(1, curArgv[i]); // Positional argument
}
}
+2 -2
View File
@@ -251,9 +251,9 @@ static void
error("\"%s\" has more than 65536 banks", name); // LCOV_EXCL_LINE
};
static constexpr off_t NB_BANKS_LIMIT = 0x10000;
static_assert(NB_BANKS_LIMIT * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS");
static_assert(NB_BANKS_LIMIT * BANK_SIZE < SSIZE_MAX, "Max input file size too large for OS");
if (input == output) {
if (fileSize >= NB_BANKS_LIMIT * BANK_SIZE) {
if (fileSize > NB_BANKS_LIMIT * BANK_SIZE) {
return errorTooLarge(); // LCOV_EXCL_LINE
}
// Compute number of banks and ROMX len from file size
+1 -1
View File
@@ -240,7 +240,7 @@ static void parseArg(int ch, char *arg) {
len = maxLen;
warning(
WARNING_TRUNCATION,
"Truncating title \"%s\" to %u chars",
"Truncating title \"%s\" to %" PRIu8 " chars",
options.title->c_str(),
maxLen
);
+54 -37
View File
@@ -36,13 +36,13 @@ Options options;
// Flags which must be processed after the option parsing finishes
static struct LocalOptions {
std::optional<std::string> externalPalSpec; // -c
bool autoAttrmap; // -A
bool autoTilemap; // -T
bool autoPalettes; // -P
bool autoPalmap; // -Q
bool groupOutputs; // -O
bool reverse; // -r
std::optional<std::string> palSpec; // -c
bool autoAttrmap; // -A
bool autoTilemap; // -T
bool autoPalettes; // -P
bool autoPalmap; // -Q
bool groupOutputs; // -O
bool reverse; // -r
bool autoAny() const { return autoAttrmap || autoTilemap || autoPalettes || autoPalmap; }
} localOptions;
@@ -193,10 +193,10 @@ static void parseArg(int ch, char *arg) {
break;
case 'c':
localOptions.externalPalSpec = std::nullopt; // Allow overriding a previous pal spec
localOptions.palSpec = std::nullopt; // Allow overriding a previous pal spec
if (arg[0] == '#') {
options.palSpecType = Options::EXPLICIT;
parseInlinePalSpec(arg);
options.palSpecType = Options::INLINE;
localOptions.palSpec = arg;
} else if (strcasecmp(arg, "embedded") == 0) {
// Use PLTE, error out if missing
options.palSpecType = Options::EMBEDDED;
@@ -204,13 +204,13 @@ static void parseArg(int ch, char *arg) {
options.palSpecType = Options::NO_SPEC;
} else if (strcasecmp(arg, "dmg") == 0) {
options.palSpecType = Options::DMG;
parseDmgPalSpec(0xE4); // Same darkest-first order as `sortGrayscale`
localOptions.palSpec = "e4"; // Same darkest-first order as `sortGrayscale`
} else if (strncasecmp(arg, "dmg=", literal_strlen("dmg=")) == 0) {
options.palSpecType = Options::DMG;
parseDmgPalSpec(&arg[literal_strlen("dmg=")]);
localOptions.palSpec = &arg[literal_strlen("dmg=")];
} else {
options.palSpecType = Options::EXPLICIT;
localOptions.externalPalSpec = arg;
options.palSpecType = Options::EXTERNAL;
localOptions.palSpec = arg;
}
break;
@@ -299,6 +299,10 @@ static void parseArg(int ch, char *arg) {
break;
case 'N':
// Explicit numbers for either tile bank cannot be greater than 256.
// If they were greater than 256, it would permit tile IDs to be truncated in the tilemap.
// We do warn that tile IDs may be truncated for the implicit/default unlimited number of
// tiles in bank 0.
options.maxNbTiles[0] = readNumber(argPtr, "Number of tiles in bank 0", 256);
if (options.maxNbTiles[0] > 256) {
error("Bank 0 cannot contain more than 256 tiles");
@@ -514,28 +518,17 @@ static void verboseOutputConfig() {
// -s/--palette-size
fprintf(stderr, "\tPalettes contain %" PRIu8 " colors\n", options.nbColorsPerPal);
// -c/--colors
if (options.palSpecType == Options::NO_SPEC) {
switch (options.palSpecType) {
case Options::NO_SPEC:
fputs("\tAutomatic palette generation\n", stderr);
} else {
fprintf(stderr, "\t%s palette spec\n", [] {
switch (options.palSpecType) {
case Options::EXPLICIT:
return "Explicit";
case Options::EMBEDDED:
return "Embedded";
case Options::DMG:
return "DMG";
default:
return "???";
}
}());
}
if (options.palSpecType == Options::EXPLICIT) {
fputs("\t[\n", stderr);
break;
case Options::INLINE:
case Options::EXTERNAL:
fputs("\tExplicit palette spec\n\t[\n", stderr);
for (auto const &pal : options.palSpec) {
fputs("\t\t", stderr);
for (auto const &color : pal) {
if (color) {
if (color.has_value()) {
fprintf(stderr, "#%06x, ", color->toCSS() >> 8);
} else {
fputs("#none, ", stderr);
@@ -544,6 +537,13 @@ static void verboseOutputConfig() {
putc('\n', stderr);
}
fputs("\t]\n", stderr);
break;
case Options::EMBEDDED:
fputs("\tEmbedded palette spec from PNG indexed PLTE chunk\n", stderr);
break;
case Options::DMG:
fprintf(stderr, "\tDMG palette spec $%02" PRIx8 "\n", options.palSpecDmg);
break;
}
// -L/--slice
if (options.inputSlice.width || options.inputSlice.height || options.inputSlice.left
@@ -671,9 +671,27 @@ int main(int argc, char *argv[]) {
autoOutPath(localOptions.autoPalettes, options.palettes, ".pal");
autoOutPath(localOptions.autoPalmap, options.palmap, ".palmap");
// Execute deferred external pal spec parsing, now that all other params are known
if (localOptions.externalPalSpec) {
parseExternalPalSpec(localOptions.externalPalSpec->c_str());
// Execute deferred pal spec parsing, now that all other params are known.
// Do not parse pal specs if `options.nbColorsPerPal` is invalid.
if (options.nbColorsPerPal > 0 && options.nbColorsPerPal <= 4) {
switch (options.palSpecType) {
case Options::NO_SPEC:
case Options::EMBEDDED:
assume(!localOptions.palSpec);
break;
case Options::INLINE:
assume(localOptions.palSpec);
parseInlinePalSpec(localOptions.palSpec->c_str());
break;
case Options::EXTERNAL:
assume(localOptions.palSpec);
parseExternalPalSpec(localOptions.palSpec->c_str());
break;
case Options::DMG:
assume(localOptions.palSpec);
parseDmgPalSpec(localOptions.palSpec->c_str());
break;
}
}
verboseDo(VERB_CONFIG, verboseOutputConfig);
@@ -687,8 +705,7 @@ int main(int argc, char *argv[]) {
} else {
process();
}
} else if (!options.palettes.empty() && options.palSpecType == Options::EXPLICIT
&& !localOptions.reverse) {
} else if (!options.palettes.empty() && options.hasExplicitPalSpec() && !localOptions.reverse) {
processPalettes();
} else {
usage.printAndExit("No input file specified (pass \"-\" to read from standard input)");
+25 -18
View File
@@ -136,6 +136,8 @@ public:
}
const_iterator end() const { return const_iterator{&_assigned, _assigned.end()}; }
iterator slotAt(size_t index) { return iterator{&_assigned, _assigned.begin() + index}; }
void assign(ColorSetAttrs const &&attrs) {
auto freeSlot =
std::find_if_not(RANGE(_assigned), [](std::optional<ColorSetAttrs> const &slot) {
@@ -160,7 +162,9 @@ public:
});
}
size_t nbColorSets() const { return std::distance(RANGE(*this)); }
size_t nbSlots() const { return _assigned.size(); }
bool isFree(size_t slotIndex) const { return !_assigned[slotIndex].has_value(); }
private:
template<typename IteratorT>
@@ -318,24 +322,30 @@ static void decant(std::vector<AssignedSets> &assignments, std::vector<ColorSet>
decantOn([&colorSets](AssignedSets &to, AssignedSets &from) {
// We need to iterate on all the "components", which are groups of color sets sharing at
// least one color with another color set in the group.
// We do this by adding the first available color set, and then looking for palettes with
// common colors. (As an optimization, we know we can skip palettes already scanned.)
std::vector<bool> processed(from.nbColorSets(), false);
for (std::vector<bool>::iterator wasProcessed;
(wasProcessed = std::find(RANGE(processed), false)) != processed.end();) {
auto attrs = from.begin();
std::advance(attrs, wasProcessed - processed.begin());
// We do this by adding the first free color set, and then looking for palettes with
// common colors. (As an optimization, we know we can skip palettes already processed.)
std::vector<bool> processed(from.nbSlots(), false);
for (size_t startIdx = 0; startIdx < processed.size(); ++startIdx) {
if (processed[startIdx] || from.isFree(startIdx)) {
continue;
}
size_t startColorSetIdx = from.slotAt(startIdx)->colorSetIndex;
// Build up the "component"; start by marking the first color set as processed
std::unordered_set<uint16_t> colors(RANGE(colorSets[attrs->colorSetIndex]));
std::vector<size_t> members = {static_cast<size_t>(wasProcessed - processed.begin())};
for (*wasProcessed = true; ++wasProcessed != processed.end(); ++attrs) {
std::unordered_set<uint16_t> colors(RANGE(colorSets[startColorSetIdx]));
std::vector<size_t> members = {startIdx};
processed[startIdx] = true;
for (size_t nextIdx = startIdx + 1; nextIdx < processed.size(); ++nextIdx) {
if (processed[nextIdx] || from.isFree(nextIdx)) {
continue;
}
size_t nextColorSetIdx = from.slotAt(nextIdx)->colorSetIndex;
// If at least one color matches, add it
if (ColorSet const &colorSet = colorSets[attrs->colorSetIndex];
if (ColorSet const &colorSet = colorSets[nextColorSetIdx];
std::find_first_of(RANGE(colors), RANGE(colorSet)) != colors.end()) {
colors.insert(RANGE(colorSet));
members.push_back(wasProcessed - processed.begin());
*wasProcessed = true; // Mark the added color set as processed
members.push_back(nextIdx);
processed[nextIdx] = true; // Mark the added color set as processed
}
}
@@ -344,11 +354,8 @@ static void decant(std::vector<AssignedSets> &assignments, std::vector<ColorSet>
}
// Iterate through the component's color sets, and transfer them
auto member = from.begin();
size_t curIndex = 0;
for (size_t index : members) {
std::advance(member, index - curIndex);
curIndex = index;
auto member = from.slotAt(index);
to.assign(std::move(*member));
from.remove(member); // Removing does not shift elements, so it's cheap
}
+15 -11
View File
@@ -29,6 +29,7 @@
#include "gfx/rgba.hpp"
#include "gfx/warning.hpp"
using namespace std::literals;
using namespace std::string_view_literals;
static char const *hexDigits = "0123456789ABCDEFabcdef";
@@ -131,8 +132,14 @@ void parseInlinePalSpec(char const * const rawArg) {
if (n == arg.length()) {
break;
} else if (arg[n] != ';' && arg[n] != ':') {
if (nbColors == 4) {
parseError(n, 1, "Each palette can only contain up to 4 colors");
if (nbColors == options.nbColorsPerPal) {
// `parseError` cannot take variadic arguments, since `format_` and
// `-Wformat-security` would complain about passing a template parameter pack
// to the C-style variadic `error` function, so we format the error message
// before passing it to `parseError`.
std::string msg = "Each palette can only contain up to "s
+ std::to_string(options.nbColorsPerPal) + " colors";
parseError(n, 1, msg.c_str());
return;
}
break;
@@ -535,10 +542,10 @@ static void parseGBCFile(char const *filename, std::filebuf &file) {
}
options.palSpec.push_back({
Rgba::fromCGBColor(toWord(buf[0], buf[1])),
Rgba::fromCGBColor(toWord(buf[2], buf[3])),
Rgba::fromCGBColor(toWord(buf[4], buf[5])),
Rgba::fromCGBColor(toWord(buf[6], buf[7])),
Rgba::fromCGBColor(toWord(buf[0], buf[1]), false),
Rgba::fromCGBColor(toWord(buf[2], buf[3]), false),
Rgba::fromCGBColor(toWord(buf[4], buf[5]), false),
Rgba::fromCGBColor(toWord(buf[6], buf[7]), false),
});
}
}
@@ -664,11 +671,7 @@ void parseDmgPalSpec(char const * const rawArg) {
return;
}
parseDmgPalSpec(toHex(arg[0], arg[1]));
}
void parseDmgPalSpec(uint8_t palSpecDmg) {
options.palSpecDmg = palSpecDmg;
options.palSpecDmg = toHex(arg[0], arg[1]);
// Map gray shades to their DMG color indexes for fast lookup by `Rgba::grayIndex`
for (uint8_t i = 0; i < 4; ++i) {
@@ -708,6 +711,7 @@ void parseBackgroundPalSpec(char const *arg) {
break;
default:
error("Unknown background color specification \"%s\"", arg);
return;
}
if (arg[size + 1] != '\0') {
+1
View File
@@ -177,6 +177,7 @@ Png::Png(char const *filename, std::streambuf &file) {
break;
case PNG_COLOR_TYPE_PALETTE:
png_set_palette_to_rgb(png);
isIndexed = true; // This enables sorting generated palette colors by the PLTE chunk
break;
}
+139 -73
View File
@@ -179,9 +179,14 @@ struct Image {
std::unordered_set<std::pair<uint32_t, uint32_t>, decltype(hashPair)> fusions;
// Register colors from `png` into `colors`
for (uint32_t y = 0; y < png.height; ++y) {
for (uint32_t x = 0; x < png.width; ++x) {
if (Rgba const &color = pixel(x, y); color.isTransparent() == color.isOpaque()) {
uint32_t const pxLeft = options.inputSlice.width ? options.inputSlice.left : 0;
uint32_t const pxTop = options.inputSlice.height ? options.inputSlice.top : 0;
uint32_t const pxRight = options.inputSlice.width ? options.inputSlice.right() : png.width;
uint32_t const pxBottom =
options.inputSlice.height ? options.inputSlice.bottom() : png.height;
for (uint32_t y = pxTop; y < pxBottom; ++y) {
for (uint32_t x = pxLeft; x < pxRight; ++x) {
if (Rgba const &color = pixel(x, y); color.isAmbiguous()) {
// Report ambiguously transparent or opaque colors
if (uint32_t css = color.toCSS(); ambiguous.find(css) == ambiguous.end()) {
error(
@@ -213,6 +218,13 @@ struct Image {
}
}
}
if (size_t nbAmbiguousColors = ambiguous.size(); nbAmbiguousColors > 0) {
fatal(
"Image contains %zu ambiguous color%s (neither transparent nor opaque)",
nbAmbiguousColors,
nbAmbiguousColors == 1 ? "" : "s"
);
}
}
class TilesVisitor {
@@ -365,8 +377,9 @@ static std::pair<std::vector<size_t>, std::vector<Palette>>
// LCOV_EXCL_STOP
std::vector<Palette> palettes(nbPalettes);
// If the image contains at least one transparent pixel, force transparency in the first slot of
// all palettes
// all palettes.
if (options.hasTransparentPixels) {
for (Palette &pal : palettes) {
pal.colors[0] = Rgba::transparent;
@@ -384,7 +397,14 @@ static std::pair<std::vector<size_t>, std::vector<Palette>>
// "Sort" colors in the generated palettes, see the man page for the flowchart
if (options.palSpecType == Options::DMG) {
sortGrayscale(palettes, image.colors.raw());
} else if (!image.png.palette.empty()) {
} else if (image.png.isIndexed) {
// A PNG image using PNG_COLOR_TYPE_RGB (2) or PNG_COLOR_TYPE_RGBA (6) can still
// contain a PLTE chunk. From the PNG spec: "If present, it provides a suggested set of
// from 1 to 256 colors to which the truecolor image can be quantized if the viewer
// cannot display truecolor directly." We only sort palette colors by the PLTE chunk's
// color order if the image uses PNG_COLOR_TYPE_PALETTE (3), since that guarantees every
// color used will also be in the embedded palette.
assume(!image.png.palette.empty());
warning(
WARNING_EMBEDDED,
"Sorting palette colors by PNG's embedded PLTE chunk without '-c/--colors embedded'"
@@ -400,14 +420,43 @@ static std::pair<std::vector<size_t>, std::vector<Palette>>
static std::pair<std::vector<size_t>, std::vector<Palette>>
makePalsAsSpecified(std::vector<ColorSet> const &colorSets) {
// Convert the palette spec to actual palettes
std::vector<Palette> palettes(options.palSpec.size());
// If the image contains at least one transparent pixel, force transparency in the first slot of
// all palettes
if (options.hasTransparentPixels) {
for (Palette &pal : palettes) {
pal.colors[0] = Rgba::transparent;
}
}
// Convert the palette spec to actual palettes
for (auto [spec, pal] : zip(options.palSpec, palettes)) {
bool skipFirst = false;
// If the image contains any transparent pixels, color #0 of all palettes is transparent.
// Thus, all explicit palette specs should leave color #0 as "#none" or transparent.
// If they specify an opaque color #0, we have legacy behavior of implicitly inserting
// a transparent color #0, and expecting the spec to only cover the subsequent colors.
if (options.hasTransparentPixels && spec.front().has_value() && spec.front()->isOpaque()) {
skipFirst = true;
}
for (size_t i = 0; i < options.nbColorsPerPal; ++i) {
// If the spec has a gap, there's no need to copy anything.
if (spec[i].has_value() && spec[i]->isOpaque()) {
pal[i] = spec[i]->cgbColor();
if (!spec[i].has_value() || !spec[i]->isOpaque()) {
continue;
}
// If we're skipping color #0 as implicitly transparent, a full spec
// plus the implicit transparent color will be too large for a palette.
if (i + skipFirst >= options.nbColorsPerPal) {
error(
"Each palette spec can only contain up to %" PRIu8
" color%s plus the implict transparent color",
options.nbColorsPerPal - 1,
options.nbColorsPerPal - 1 == 1 ? "" : "s"
);
giveUp();
}
pal[i + skipFirst] = spec[i]->cgbColor();
}
}
@@ -458,14 +507,14 @@ static void outputPalettes(std::vector<Palette> const &palettes) {
});
// LCOV_EXCL_STOP
if (palettes.size() > options.nbPalettes) {
if (size_t nbPals = palettes.size(); nbPals > options.nbPalettes) {
// If the palette generation is wrong, other (dependee) operations are likely to be
// nonsensical, so fatal-error outright
fatal(
"Generated %zu palettes, over the maximum of %" PRIu16,
palettes.size(),
options.nbPalettes
);
fatal("Generated %zu palettes, over the maximum of %" PRIu16, nbPals, options.nbPalettes);
} else if (nbPals > 8 && !options.attrmap.empty() && options.palmap.empty()) {
// With `-n/--nb-palettes` greater than 8, palette IDs may be truncated in the attrmap
// (though not in the palmap), so warn about that.
warnx("Generated %zu palettes, of which only 8 are representable in the attrmap", nbPals);
}
if (!options.palettes.empty()) {
@@ -631,17 +680,15 @@ static void outputUnoptimizedTileData(
// LCOV_EXCL_STOP
}
uint64_t widthTiles = options.inputSlice.width ? options.inputSlice.width : image.png.width / 8;
uint64_t heightTiles =
options.inputSlice.height ? options.inputSlice.height : image.png.height / 8;
uint64_t nbTiles = widthTiles * heightTiles;
uint64_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0;
uint64_t tileIdx = 0;
size_t nbTiles = std::count_if(RANGE(attrmap), [](AttrmapEntry const &attr) {
return !attr.isBackgroundTile();
});
size_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0;
size_t tileIdx = 0;
for (auto const &[tile, attr] : zip(image.visitAsTiles(), attrmap)) {
// Do not emit fully-background tiles.
if (attr.isBackgroundTile()) {
++tileIdx;
continue;
}
@@ -691,38 +738,49 @@ static void outputUnoptimizedMaps(
autoOpenPath(options.attrmap, attrmapOutput);
autoOpenPath(options.palmap, palmapOutput);
auto const emit = [](std::optional<File> &output, uint8_t byte) {
if (output.has_value()) {
output.value()->sputc(byte);
}
};
uint16_t tileIdx = 0;
uint8_t bank = 0;
for (AttrmapEntry const &attr : attrmap) {
// The update-increment logic at the end of this loop may increment `bank` from 1 to 2,
// if both banks 0 and 1 are full, but by then all the `attrmap` entries should have been
// processed, since there cannot be more tiles than could fit in both banks.
assume(bank < 2);
// The unsigned overflow for `tileID` and `palID` is intentional, since
// nonzero base IDs may overflow beyond 255 and continue with IDs from 0.
if (tilemapOutput.has_value()) {
uint8_t tileID = (attr.isBackgroundTile() ? 0 : tileIdx) + options.baseTileIDs[bank];
(*tilemapOutput)->sputc(tileID);
}
// A non-zero base ID may make this addition overflow, wrapping around the available
// palette IDs. Since the operands are unsigned, this won't cause undefined behavior.
// With `-n/--nb-palettes` greater than 8, palette IDs may be truncated in the attrmap
// (though not in the palmap), which was already warned about.
uint8_t palID = attr.getPalID(mappings) + options.basePalID;
if (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()) {
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]) {
++tileIdx;
emit(tilemapOutput, options.baseTileIDs[0]);
emit(attrmapOutput, palID & 0b111); // The other flags are all zeros.
emit(palmapOutput, palID);
// Since background tiles are not in tile data, they do not increment the tile index.
} else {
++bank;
tileIdx = 0;
// The only valid tile banks are 0 and 1.
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.yFlip = false;
attr.bank = 0;
attr.tileID = 0;
attr.tileID = options.baseTileIDs[attr.bank];
} else {
auto [tileID, matchType] = tiles.addTile({tile, palettes[attr.getPalID(mappings)]});
auto [tileIdx, matchType] = tiles.addTile({tile, palettes[attr.getPalID(mappings)]});
if (inputWithoutOutput && matchType == TileData::NOPE) {
error(
@@ -831,8 +889,8 @@ static UniqueTiles dedupTiles(
attr.xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP;
attr.yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP;
attr.bank = tileID >= options.maxNbTiles[0];
attr.tileID = (attr.bank ? tileID - options.maxNbTiles[0] : tileID)
attr.bank = tileIdx >= options.maxNbTiles[0];
attr.tileID = (attr.bank ? tileIdx - options.maxNbTiles[0] : tileIdx)
+ options.baseTileIDs[attr.bank];
}
}
@@ -849,9 +907,9 @@ static void outputTileData(UniqueTiles const &tiles) {
// LCOV_EXCL_STOP
}
uint64_t nbTiles = tiles.size();
uint64_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0;
uint64_t tileIdx = 0;
size_t nbTiles = tiles.size();
size_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0;
size_t tileIdx = 0;
for (TileData const *tile : tiles) {
assume(tile->tileID == tileIdx);
@@ -889,6 +947,8 @@ static void outputTilemap(std::vector<AttrmapEntry> const &attrmap) {
// LCOV_EXCL_STOP
}
// With `-N/--nb-tiles` unlimited (by default) for bank 0, tile IDs may be truncated in the
// tilemap, which was already warned about.
for (AttrmapEntry const &entry : attrmap) {
output->sputc(entry.tileID); // The tile ID has already been converted
}
@@ -908,6 +968,8 @@ static void
attr |= entry.bank << 3;
// The unsigned underflow for the palette ID is intentional, since a
// nonzero base palette ID may overflow and continue with IDs from 0.
// With `-n/--nb-palettes` greater than 8, palette IDs may be truncated in the attrmap
// (though not in the palmap), which was already warned about.
attr |= (entry.getPalID(mappings) + options.basePalID) & 0b111;
output->sputc(attr);
}
@@ -990,12 +1052,14 @@ void process() {
for (auto tile : image.visitAsTiles()) {
AttrmapEntry &attrs = attrmap.emplace_back();
// Count the unique non-transparent colors for packing
// Count the unique opaque colors for packing
std::unordered_set<uint16_t> tileColors;
for (uint32_t y = 0; y < 8; ++y) {
for (uint32_t x = 0; x < 8; ++x) {
if (Rgba color = tile.pixel(x, y);
color.isOpaque() || !options.hasTransparentPixels) {
Rgba color = tile.pixel(x, y);
// Ambiguous colors should not be in `tileColors`
assume(color.isOpaque() != color.isTransparent());
if (color.isOpaque() || !options.hasTransparentPixels) {
tileColors.insert(color.cgbColor());
}
}
@@ -1140,20 +1204,31 @@ continue_visiting_tiles:;
: makePalsAsSpecified(colorSets);
outputPalettes(palettes);
// If deduplication is not happening, we just need to output the tile data and/or maps as-is
if (!options.allowDedup) {
// Check the tile count
if (size_t nbTiles = std::count_if(
RANGE(attrmap), [](AttrmapEntry const &attr) { return !attr.isBackgroundTile(); }
);
nbTiles > options.maxNbTiles[0] + options.maxNbTiles[1]) {
auto checkTileCountLimit = [](size_t nbTiles) {
if (nbTiles > options.maxNbTiles[0] + options.maxNbTiles[1]) {
fatal(
"Image contains %zu tiles, exceeding the limit of %" PRIu16 " + %" PRIu16,
nbTiles,
options.maxNbTiles[0],
options.maxNbTiles[1]
);
} else if (((nbTiles > 256 && options.maxNbTiles[0] > 256)
|| (nbTiles > options.maxNbTiles[0] + 256u && options.maxNbTiles[1] > 256))
&& !options.tilemap.empty()) {
// With `-N/--nb-tiles` unlimited (by default) for bank 0, tile IDs may be truncated in
// the tilemap, so warn about that.
warnx(
"Image contains %zu tiles, of which only 256 are representable in the tilemap",
nbTiles
);
}
};
// If deduplication is not happening, we just need to output the tile data and/or maps as-is
if (!options.allowDedup) {
checkTileCountLimit(std::count_if(RANGE(attrmap), [](AttrmapEntry const &attr) {
return !attr.isBackgroundTile();
}));
// I currently cannot figure out useful semantics for this combination of flags.
if (!options.inputTileset.empty()) {
@@ -1176,16 +1251,7 @@ continue_visiting_tiles:;
verbosePrint(VERB_NOTICE, "Deduplicating tiles...\n");
UniqueTiles tiles = dedupTiles(image, attrmap, palettes, mappings);
// Check the tile count
if (size_t nbTiles = tiles.size();
nbTiles > options.maxNbTiles[0] + options.maxNbTiles[1]) {
fatal(
"Image contains %zu tiles, exceeding the limit of %" PRIu16 " + %" PRIu16,
nbTiles,
options.maxNbTiles[0],
options.maxNbTiles[1]
);
}
checkTileCountLimit(tiles.size());
if (!options.output.empty()) {
verbosePrint(VERB_NOTICE, "Generating optimized tile data...\n");
+63 -36
View File
@@ -119,18 +119,14 @@ void reverse() {
warnx("Tile deduplication is enabled, but no tilemap is provided");
}
if (options.useColorCurve) {
warnx("The color curve is not yet supported in reverse mode");
}
if (options.inputSlice.left != 0 || options.inputSlice.top != 0
|| options.inputSlice.height != 0) {
warnx("\"Sliced-off\" pixels are ignored in reverse mode");
}
if (options.inputSlice.width != 0 && options.inputSlice.width != options.reversedWidth * 8) {
if (options.inputSlice.width != 0 && options.inputSlice.width != options.reversedWidth) {
warnx(
"Specified input slice width (%" PRIu16
") does not match provided reversing width (%" PRIu16 " * 8)",
") does not match provided reversing width (%" PRIu16 ")",
options.inputSlice.width,
options.reversedWidth
);
@@ -216,26 +212,28 @@ void reverse() {
}
palettes.clear();
std::array<uint8_t, sizeof(uint16_t) * 4> buf; // 4 colors
std::array<uint8_t, sizeof(uint16_t) * 4> buf; // max 4 colors
size_t const palSize = sizeof(uint16_t) * options.nbColorsPerPal;
assume(buf.size() >= palSize);
for (;;) {
if (size_t nbRead = file->sgetn(reinterpret_cast<char *>(buf.data()), buf.size());
if (size_t nbRead = file->sgetn(reinterpret_cast<char *>(buf.data()), palSize);
nbRead == 0) {
break;
} else if (nbRead != buf.size()) {
} else if (nbRead != palSize) {
fatal(
"Palette data size (%zu) is not a multiple of %zu bytes\n",
palettes.size() * buf.size() + nbRead,
buf.size()
"Palette data size (%zu) is not a multiple of %zu bytes",
palettes.size() * palSize + nbRead,
palSize
);
}
// Expand the colors
// Expand the little-endian RGB555 colors to RGB888
auto &palette = palettes.emplace_back();
std::generate(
palette.begin(),
palette.begin() + options.nbColorsPerPal,
[&buf, i = 0]() mutable {
i += 2;
return Rgba::fromCGBColor(buf[i - 2] | buf[i - 1] << 8); // little-endian
return Rgba::fromCGBColor(buf[i - 2] | buf[i - 1] << 8, options.useColorCurve);
}
);
}
@@ -248,8 +246,21 @@ void reverse() {
);
}
if (options.palSpecType == Options::EXPLICIT && palettes != options.palSpec) {
warnx("Colors in the palette file do not match those specified with '-c'");
if (options.hasExplicitPalSpec() && palettes != options.palSpec) {
// The explicit `-c` pal spec does not match the input `-p` palette file.
// Check whether their 8-to-5-bit-reduced GB colors nevertheless match.
std::vector<std::array<std::optional<Rgba>, 4>> palSpecQuantized(options.palSpec);
for (auto &pal : palSpecQuantized) {
for (auto &color : pal) {
if (color.has_value()) {
color = Rgba::fromCGBColor(color->cgbColor(), options.useColorCurve);
}
}
}
warnx(
"Colors %s the palette file do not match those specified with '-c'",
palettes == palSpecQuantized ? "reversed from" : "in"
);
// This spacing aligns "...versus with `-c`" above the column of `-c` palettes
fputs("Colors specified in the palette file: ...versus with '-c':\n", stderr);
for (size_t i = 0; i < palettes.size() || i < options.palSpec.size(); ++i) {
@@ -272,12 +283,12 @@ void reverse() {
} else if (options.palSpecType == Options::EMBEDDED) {
warnx("An embedded palette was requested, but no palette file was specified; ignoring "
"request");
} else if (options.palSpecType == Options::EXPLICIT) {
} else if (options.hasExplicitPalSpec()) {
palettes = std::move(options.palSpec); // We won't be using it again.
}
std::optional<std::vector<uint8_t>> attrmap;
uint16_t nbTilesInBank[2] = {0, 0}; // Only used if there is an attrmap.
uint16_t nbTilesMappedInBank[2] = {0, 0}; // Only used if there is an attrmap.
if (!options.attrmap.empty()) {
attrmap = readInto(options.attrmap);
if (attrmap->size() != mapSize) {
@@ -327,8 +338,8 @@ void reverse() {
// The unsigned underflow for `tileOfs` is intentional, since a nonzero
// base tile ID may overflow and continue with IDs from 0.
if (uint8_t tileOfs = (*tilemap)[index] - options.baseTileIDs[bank];
tileOfs >= nbTilesInBank[bank]) {
nbTilesInBank[bank] = tileOfs + 1;
tileOfs >= nbTilesMappedInBank[bank]) {
nbTilesMappedInBank[bank] = tileOfs + 1;
}
}
}
@@ -336,29 +347,43 @@ void reverse() {
verbosePrint(
VERB_INFO,
"Number of tiles in bank {0: %" PRIu16 ", 1: %" PRIu16 "}\n",
nbTilesInBank[0],
nbTilesInBank[1]
nbTilesMappedInBank[0],
nbTilesMappedInBank[1]
);
for (int bank = 0; bank < 2; ++bank) {
if (nbTilesInBank[bank] > options.maxNbTiles[bank]) {
if (nbTilesMappedInBank[bank] > options.maxNbTiles[bank]) {
error(
"Bank %d contains %" PRIu16 " tiles, but the specified limit is %" PRIu16,
bank,
nbTilesInBank[bank],
nbTilesMappedInBank[bank],
options.maxNbTiles[bank]
);
}
}
if (nbTilesInBank[0] + nbTilesInBank[1] > nbTiles) {
fatal(
"The tilemap references %" PRIu16 " tiles in bank 0 and %" PRIu16
" in bank 1, but only %zu have been read in total",
nbTilesInBank[0],
nbTilesInBank[1],
nbTiles
);
if (uint16_t const maxTotalNbTiles =
nbTilesMappedInBank[1] > 0
? std::max<uint16_t>(
nbTilesMappedInBank[0], options.maxNbTiles[0] + nbTilesMappedInBank[1]
)
: nbTilesMappedInBank[0];
maxTotalNbTiles > nbTiles + options.trim) {
std::string message =
"The tilemap references " + std::to_string(nbTilesMappedInBank[0]) + " tiles";
if (nbTilesMappedInBank[1] > 0) {
if (nbTilesMappedInBank[0] != options.maxNbTiles[0]) {
message += " out of a maximum " + std::to_string(options.maxNbTiles[0]);
}
message += " in bank 0, and " + std::to_string(nbTilesMappedInBank[1])
+ " in bank 1 (total: " + std::to_string(maxTotalNbTiles) + ")";
}
message += ", but only " + std::to_string(nbTiles) + " have been read";
if (options.trim > 0) {
message += " plus " + std::to_string(options.trim)
+ " trimmed (total: " + std::to_string(nbTiles + options.trim) + ")";
}
fatal("%s", message.c_str());
}
requireZeroErrors();
@@ -388,7 +413,9 @@ void reverse() {
}
}
} else {
size_t const limit = std::min<size_t>(nbTiles, options.maxNbTiles[0]);
// Tiles trimmed with `-x` were never written to the tile data file, but are
// still referenced by the tilemap.
size_t const limit = std::min<size_t>(nbTiles + options.trim, options.maxNbTiles[0]);
for (size_t index = 0; index < mapSize; ++index) {
size_t tx = index % width, ty = index / width;
uint8_t tileID = (*tilemap)[index];
@@ -548,7 +575,7 @@ void reverse() {
// Get the tile ID at this location
size_t tileOfs =
tilemap ? static_cast<uint8_t>((*tilemap)[index] - options.baseTileIDs[bank])
+ (bank ? nbTilesInBank[0] : 0)
+ (bank ? options.maxNbTiles[0] : 0)
: index;
// This should have been enforced by the earlier checking.
assume(tileOfs < nbTiles + options.trim);
@@ -567,7 +594,7 @@ void reverse() {
// If vertically mirrored, fetch the bytes from the other end
uint8_t realY = (attribute & 0x40 ? 7 - y : y) * options.bitDepth;
uint8_t bitplane0 = tileData[realY];
uint8_t bitplane1 = options.bitDepth == 2 ? tileData[realY + 1] : bitplane0;
uint8_t bitplane1 = options.bitDepth == 2 ? tileData[realY + 1] : 0;
if (attribute & 0x20) { // Handle horizontal flip
bitplane0 = flipTable[bitplane0];
bitplane1 = flipTable[bitplane1];
@@ -599,7 +626,7 @@ void reverse() {
if (Rgba const &pixel = *color; pngColorType == PNG_COLOR_TYPE_GRAY) {
gray = gray << pngDepth | (pixel.red & ((1 << pngDepth) - 1));
} else if (pngColorType == PNG_COLOR_TYPE_PALETTE) {
*ptr++ = palOfs * 4 + colorID;
*ptr++ = palOfs * options.nbColorsPerPal + colorID;
} else {
*ptr++ = pixel.red;
*ptr++ = pixel.green;
+38 -8
View File
@@ -24,16 +24,24 @@ std::string toCGB(uint16_t color) {
return buf;
}
// Based on inverting the "Modern - Accurate" formula used by SameBoy
// since commit b5a611c5db46d6a0649d04d24d8d6339200f9ca1 (Dec 2020),
// with gaps in the scale curve filled by polynomial interpolation.
// Copied from the "Modern - Accurate" (`GB_COLOR_CORRECTION_MODERN_ACCURATE`)
// formula used by SameBoy in its `scale_channel_with_curve` function since
// commit b5a611c5db46d6a0649d04d24d8d6339200f9ca1 (Dec 2020).
// clang-format off: vertically align columns of values
static std::array<uint8_t, 32> color_curve{
0, 6, 12, 20, 28, 36, 45, 56, 66, 76, 88, 100, 113, 125, 137, 149,
161, 172, 182, 192, 202, 210, 218, 225, 232, 238, 243, 247, 250, 252, 254, 255,
};
// clang-format on
// Inverted `color_curve`, with gaps filled by polynomial interpolation.
// clang-format off: vertically align columns of values
static std::array<uint8_t, 256> reverse_curve{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1,
1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3,
3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5,
5, 5, 5, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7,
7, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1,
1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3,
3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5,
5, 5, 5, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7,
7, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10,
10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13,
13, 13, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 16, 16, 16,
16, 16, 16, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 19, 19,
@@ -48,6 +56,28 @@ static std::array<uint8_t, 256> reverse_curve{
};
// clang-format on
Rgba Rgba::fromCGBColor(uint16_t color, bool useColorCurve) {
uint8_t r = color & 0b11111, g = (color >> 5) & 0b11111, b = (color >> 10) & 0b11111;
if (useColorCurve) {
r = color_curve[r];
g = color_curve[g];
b = color_curve[b];
if (g != b) {
g = round(pow((pow(g / 255.0, 2.2) * 3 + pow(b / 255.0, 2.2)) / 4, 1 / 2.2) * 255);
}
} else {
r = r << 3 | r >> 2;
g = g << 3 | g >> 2;
b = b << 3 | b >> 2;
}
return {
r,
g,
b,
static_cast<uint8_t>(color & transparent ? 0x00 : 0xFF),
};
}
uint16_t Rgba::cgbColor() const {
if (isTransparent()) {
return transparent;
+303 -238
View File
@@ -2,6 +2,7 @@
#include "link/assign.hpp"
#include <algorithm>
#include <deque>
#include <inttypes.h>
#include <optional>
@@ -15,6 +16,7 @@
#include "helpers.hpp"
#include "itertools.hpp"
#include "linkdefs.hpp"
#include "platform.hpp"
#include "verbosity.hpp"
#include "link/main.hpp"
@@ -23,21 +25,255 @@
#include "link/symbol.hpp"
#include "link/warning.hpp"
struct MemoryLocation {
uint16_t address;
uint32_t bank;
};
struct FreeSpace {
uint16_t address;
uint16_t size;
uint16_t size; // Never zero.
uint16_t addrOnePast() const { return address + size; }
};
// Table of free space for each bank
static std::vector<std::deque<FreeSpace>> memory[SECTTYPE_INVALID];
using FreeSpaceIter = std::deque<FreeSpace>::iterator;
static std::deque<FreeSpace> &freeSpaceOfBank(Section const &section, uint32_t bank) {
assume(bank >= section.typeInfo().firstBank);
assume(bank <= section.typeInfo().lastBank);
return memory[section.type][bank - section.typeInfo().firstBank];
}
struct Scrambling {
uint16_t romxOfs = 0;
uint16_t sramOfs = 0;
uint16_t wramxOfs = 0;
// Helper for the next function, to give names to its two returned values.
struct ScramblingInfo {
uint16_t &curOfs;
uint16_t maxOfs;
};
std::optional<ScramblingInfo> getInfoFor(SectionType type) {
switch (type) {
case SECTTYPE_ROMX:
return {
{romxOfs, options.scrambleROMX}
};
case SECTTYPE_SRAM:
return {
{sramOfs, options.scrambleSRAM}
};
case SECTTYPE_WRAMX:
return {
{wramxOfs, options.scrambleWRAMX}
};
// Non-banked sections don't need scrambling support...
case SECTTYPE_ROM0:
case SECTTYPE_WRAM0:
case SECTTYPE_OAM:
case SECTTYPE_HRAM:
assume(!sectionTypeInfo[type].isBanked());
return std::nullopt;
// ...but VRAM doesn't either, regardless of whether it's banked or not.
case SECTTYPE_VRAM:
return std::nullopt;
case SECTTYPE_INVALID:
unreachable_();
}
return std::nullopt; // Dead code, but some compilers don't recognize that.
}
};
static Scrambling scrambling;
struct MemoryLocation {
uint16_t address;
uint32_t bank;
static MemoryLocation initFor(Section const &section) {
MemoryLocation location;
if (section.isAddressFixed) { // This will never change.
location.address = section.org;
}
if (section.isBankFixed) {
location.bank = section.bank;
} else {
location.bank = section.typeInfo().firstBank;
if (auto info = scrambling.getInfoFor(section.type);
info.has_value() && info->maxOfs != 0) { // If scrambling is enabled...
// ...then we will begin the search from a different offset for each section.
// Go to the next offset (backwards), wrapping around. (Thus, no underflow!)
info->curOfs = (info->curOfs != 0 ? info->curOfs : info->maxOfs) - 1;
location.bank += info->curOfs;
}
}
return location;
}
// Try again in the next bank, if one is available.
[[nodiscard("This returns whether iteration can be continued")]]
bool goToNextApplicableBankFor(Section const &section) {
assume(bank >= section.typeInfo().firstBank);
assume(bank <= section.typeInfo().lastBank);
assume(!section.isBankFixed);
// Try scrambled banks in descending order until no bank in the scrambled range is
// available.
if (auto info = scrambling.getInfoFor(section.type);
info.has_value() && info->maxOfs != 0) {
// All floating sections within a scrambled region should be
// within the scrambled bank pool.
assume(bank < info->maxOfs + section.typeInfo().firstBank);
uint16_t ofsWithinPool = bank - section.typeInfo().firstBank;
// Go to the next bank (backwards), wrapping around. (Thus, no overflow!)
ofsWithinPool = (ofsWithinPool != 0 ? ofsWithinPool : info->maxOfs) - 1;
bank = ofsWithinPool + section.typeInfo().firstBank;
// Keep iterating unless we have wrapped back around to the start offset.
return ofsWithinPool != info->curOfs;
}
// Otherwise, try in ascending order.
if (bank == section.typeInfo().lastBank) {
return false;
}
++bank;
return true;
}
void makeAddressAligned(uint16_t alignMask, uint16_t alignOfs) {
// By how much the address is past the target offset within the current alignment "page".
uint16_t offset = (address - alignOfs) & alignMask;
// Move by one page *minus* that "overshoot" offset.
// If it's 0, then this would move by a whole page, but `& alignMask` resets it back to 0.
address += ((alignMask + 1) - offset) & alignMask;
}
};
static FreeSpaceIter tryPlacingInBank(Section const &section, MemoryLocation &location) {
std::deque<FreeSpace> &bankMem = freeSpaceOfBank(section, location.bank);
if (section.isAddressFixed) {
// There is only one candidate location in this bank: the address at which the section is
// fixed.
assume(location.address == section.org);
FreeSpaceIter iter = std::find_if(RANGE(bankMem), [&location](FreeSpace const &freeSpace) {
// If they both exactly match, that means the next block will begin past the requested
// addr, so the function would fail anyway.
return freeSpace.addrOnePast() >= location.address;
});
if (iter != bankMem.end()) {
// We have the first block ending after the section's address, so all that's left is
// checking that the address does fall into the block, and then that the section fits.
if (location.address < iter->address
|| location.address + section.size > iter->addrOnePast()) {
return bankMem.end(); // Failed! Better luck next bank?
}
}
return iter;
} else {
// There are many possible locations within the bank, so we are going to iterate on free
// blocks. If it is impossible to fit at the earliest (constraint-satisfying) address, then
// no other address in the block will do; thus, we make only one attempt per block.
return std::find_if(RANGE(bankMem), [&location, &section](FreeSpace const &freeSpace) {
location.address = freeSpace.address;
if (section.isAlignFixed) {
location.makeAddressAligned(section.alignMask, section.alignOfs);
// Did it advance past the block? Or, rarely, overflowed?
if (location.address >= freeSpace.addrOnePast()
|| location.address < freeSpace.address) {
return false;
}
}
// Since `location.address` lies within the block,
// we only need to check that its end address also does.
return location.address + section.size <= freeSpace.addrOnePast();
});
}
}
// Place section using first-fit decreasing algorithm
// <https://en.wikipedia.org/wiki/Bin_packing_problem#First-fit_algorithm>
// Returns an iterator to within `freeSpaceOfBank(section, location.bank)`
// (guaranteeing that `location.bank` remains valid) pointing at the free block that
// the section can go into (at `location.address`).
// The iterator is an end iterator if and only if no suitable location was found.
// `location` is updated accordingly.
static FreeSpaceIter tryPlacing(Section const &section, MemoryLocation &location) {
if (section.isBankFixed) {
assume(location.bank == section.bank);
return tryPlacingInBank(section, location);
}
do {
if (FreeSpaceIter iter = tryPlacingInBank(section, location);
iter != freeSpaceOfBank(section, location.bank).end()) {
return iter; // Found one!
}
} while (location.goToNextApplicableBankFor(section));
// Return a deque's end iterator to signal failure.
// The exact deque doesn't matter, but the caller will use `freeSpaceOfBank` also.
return freeSpaceOfBank(section, location.bank).end();
}
static void
updateFreeSpace(FreeSpaceIter iter, std::deque<FreeSpace> &bankMem, Section const &section) {
assume(section.org + section.size <= UINT16_MAX);
uint16_t sectionEnd = section.org + section.size;
assume(section.org >= iter->address);
assume(sectionEnd <= iter->addrOnePast());
bool noLeftSpace = iter->address == section.org;
bool noRightSpace = iter->address + iter->size == sectionEnd;
if (noLeftSpace && noRightSpace) {
// The free space is entirely deleted
bankMem.erase(iter);
} else if (!noLeftSpace && !noRightSpace) {
// The free space is split in two
uint16_t size = static_cast<uint16_t>(iter->address + iter->size - sectionEnd);
// Resize the original space (address is unmodified)
iter->size = section.org - iter->address;
// Append the new space after the original one
bankMem.insert(iter + 1, {.address = sectionEnd, .size = size});
// `iter` cannot be reused from this point on, because `bankMem.insert`
// invalidates iterators to itself!
} else {
// The amount of free spaces doesn't change: resize!
iter->size -= section.size;
if (noLeftSpace) {
// The free space is moved *and* resized
iter->address += section.size;
}
}
}
// Assigns a section to a given memory location
static void assignSection(Section &section, MemoryLocation const &location) {
assume(location.address >= section.typeInfo().startAddr);
// Zero-sized sections can start one past the end of their region.
assume(location.address <= section.typeInfo().endAddr() + 1);
// This one is not redundant, it guards against overflow!
assume(location.address + section.size >= section.typeInfo().startAddr);
assume(location.address + section.size <= section.typeInfo().endAddr() + 1);
if (section.isAddressFixed) {
assume(location.address == section.org);
} else if (section.isAlignFixed) {
assume((location.address & section.alignMask) == section.alignOfs);
}
if (section.isBankFixed) {
assume(location.bank == section.bank);
}
// Propagate the assigned location to all UNIONs/FRAGMENTs
// so `jr` patches in them will have the correct offset
for (Section &piece : section.pieces()) {
@@ -47,267 +283,93 @@ static void assignSection(Section &section, MemoryLocation const &location) {
out_AddSection(section);
}
// Checks whether a given location is suitable for placing a given section
// This checks not only that the location has enough room for the section, but
// also that the constraints (alignment...) are respected.
static bool isLocationSuitable(
Section const &section, FreeSpace const &freeSpace, MemoryLocation const &location
) {
if (section.isAddressFixed && section.org != location.address) {
return false;
}
if (section.isAlignFixed && ((location.address - section.alignOfs) & section.alignMask)) {
return false;
}
if (location.address < freeSpace.address) {
return false;
}
return location.address + section.size <= freeSpace.address + freeSpace.size;
}
static MemoryLocation getStartLocation(Section const &section) {
static uint16_t curScrambleROM = 0;
static uint16_t curScrambleWRAM = 0;
static uint16_t curScrambleSRAM = 0;
MemoryLocation location;
// Determine which bank we should start searching in
if (section.isBankFixed) {
location.bank = section.bank;
} else {
location.bank = sectionTypeInfo[section.type].firstBank;
// Scramble the bank if applicable
if (options.scrambleROMX && section.type == SECTTYPE_ROMX) {
if (curScrambleROM == 0) {
curScrambleROM = options.scrambleROMX;
}
location.bank += --curScrambleROM;
} else if (options.scrambleWRAMX && section.type == SECTTYPE_WRAMX) {
if (curScrambleWRAM == 0) {
curScrambleWRAM = options.scrambleWRAMX;
}
location.bank += --curScrambleWRAM;
} else if (options.scrambleSRAM && section.type == SECTTYPE_SRAM) {
if (curScrambleSRAM == 0) {
curScrambleSRAM = options.scrambleSRAM;
}
location.bank += --curScrambleSRAM;
}
}
return location;
}
// Returns a suitable free space index into `memory[section->type]` at which to place the given
// section, or `std::nullopt` if none was found.
static std::optional<size_t> getPlacement(Section const &section, MemoryLocation &location) {
SectionTypeInfo const &typeInfo = sectionTypeInfo[section.type];
for (;;) {
if (location.bank < typeInfo.firstBank
|| location.bank >= memory[section.type].size() + typeInfo.firstBank) {
fatal(
"Invalid bank for %s section \"%s\": %" PRIu32,
sectionTypeInfo[section.type].name.c_str(),
section.name.c_str(),
location.bank
);
}
// Switch to the beginning of the next bank
std::deque<FreeSpace> &bankMem = memory[section.type][location.bank - typeInfo.firstBank];
size_t spaceIdx = 0;
if (spaceIdx < bankMem.size()) {
location.address = bankMem[spaceIdx].address;
}
// Process locations in that bank
while (spaceIdx < bankMem.size()) {
// If that location is OK, return it
if (isLocationSuitable(section, bankMem[spaceIdx], location)) {
return spaceIdx;
}
// Go to the next *possible* location
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);
static std::string describeConstraintsOf(Section const &section) {
std::string description = "\"" + section.name + "\" (" + section.typeInfo().name + " section) ";
if (section.isBankFixed && section.typeInfo().isBanked()) {
char bank[9];
sprintf_to_array(bank, "%02" PRIx32, section.bank);
if (section.isAddressFixed) {
char addr[8];
snprintf(addr, sizeof(addr), "%04" PRIx16, section.org);
char addr[5];
sprintf_to_array(addr, "%04" PRIx16, section.org);
description = description + "at $" + bank + ":" + addr;
} else if (section.isAlignFixed) {
char mask[8];
snprintf(mask, sizeof(mask), "%" PRIx16, static_cast<uint16_t>(~section.alignMask));
char mask[5];
sprintf_to_array(mask, "%" PRIx16, static_cast<uint16_t>(~section.alignMask));
description = description + "in bank $" + bank + " with align mask $" + mask;
} else {
description = description + "in bank $" + bank;
}
} else {
if (section.isAddressFixed) {
char addr[8];
snprintf(addr, sizeof(addr), "%04" PRIx16, section.org);
char addr[5];
sprintf_to_array(addr, "%04" PRIx16, section.org);
description = description + "at address $" + addr;
} else if (section.isAlignFixed) {
char mask[8], offset[8];
snprintf(mask, sizeof(mask), "%" PRIx16, static_cast<uint16_t>(~section.alignMask));
snprintf(offset, sizeof(offset), "%" PRIx16, section.alignOfs);
char mask[5], offset[5];
sprintf_to_array(mask, "%" PRIx16, static_cast<uint16_t>(~section.alignMask));
sprintf_to_array(offset, "%" PRIx16, section.alignOfs);
description = description + "with align mask $" + mask + " and offset $" + offset;
} else {
description = description + "anywhere";
}
if (auto info = scrambling.getInfoFor(section.type);
info.has_value() && info->maxOfs != 0) { // Only mention scrambling if it is enabled.
char size[6];
sprintf_to_array(size, "%" PRIu16, info->maxOfs);
description = description + " within the " + size + " scrambled banks";
}
}
return description;
}
// Places a section in a suitable location, or error out if it fails to.
// Due to the implemented algorithm, this should be called with sections of decreasing size!
static void placeSection(Section &section) {
// Specially handle 0-byte SECTIONs, as they can't overlap anything
MemoryLocation location = MemoryLocation::initFor(section);
// Specially handle 0-byte SECTIONs, as they ignore free space entirely.
if (section.size == 0) {
// Unless the SECTION's address was fixed, the starting address
// is fine for any alignment, as checked in sect_DoSanityChecks.
MemoryLocation location = {
.address =
section.isAddressFixed ? section.org : sectionTypeInfo[section.type].startAddr,
.bank = section.isBankFixed ? section.bank : sectionTypeInfo[section.type].firstBank,
};
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;
if (!section.isAddressFixed) {
location.address = section.typeInfo().startAddr;
if (section.isAlignFixed) {
location.makeAddressAligned(section.alignMask, section.alignOfs);
}
}
// This check safely handles sections with impossible alignment and no fixed address.
// The above `location.address = section.typeInfo().startAddr` would be valid on its own,
// but `location.makeAddressAligned(...)` can increase `location.address` above the valid
// range for its `section`, which would violate an assumption in `assignSection`.
// Note that sections with fixed addresses are handled earlier by `sect_DoSanityChecks`,
// but this check would safely handle them too if they ever reached it.
if (location.address <= section.typeInfo().endAddr() + 1) {
assignSection(section, location);
return;
}
}
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;
}
if (!section.isBankFixed || !section.isAddressFixed) {
// If a section failed to go to several places, nothing we can report
fatal("Unable to place %s", getSectionDescription(section).c_str());
} else if (section.org + section.size > sectTypeEndAddr(section.type) + 1) {
fatal("Unable to place %s", describeConstraintsOf(section).c_str());
} else if (uint16_t onePastEnd = section.typeInfo().endAddr() + 1;
section.org + section.size > onePastEnd) {
// If the section just can't fit the bank, report that
fatal(
"Unable to place %s: section runs past end of region ($%04x > $%04x)",
getSectionDescription(section).c_str(),
describeConstraintsOf(section).c_str(),
section.org + section.size,
sectTypeEndAddr(section.type) + 1
onePastEnd
);
} else {
// Otherwise there is overlap with another section
@@ -315,7 +377,7 @@ static void placeSection(Section &section) {
assume(overlap != nullptr);
fatal(
"Unable to place %s: section overlaps with \"%s\"",
getSectionDescription(section).c_str(),
describeConstraintsOf(section).c_str(),
overlap->name.c_str()
);
}
@@ -392,8 +454,9 @@ static void checkOverlayCompat() {
unfixedList += "\n- and ";
unfixedList += std::to_string(nbUnfixedSections - nbListed);
unfixedList += " more";
break;
goto finish;
}
unfixedList += "\n- \"";
unfixedList += section->name;
unfixedList += "\" (";
@@ -410,6 +473,7 @@ static void checkOverlayCompat() {
}
}
finish: // Can't `break` out of a nested loop
fatal(
"All sections must be fixed when using an overlay file; %zu %s not:%s",
nbUnfixedSections,
@@ -423,11 +487,12 @@ void assign_AssignSections() {
// Initialize the free space-modelling structs
for (SectionType type : EnumSeq(SECTTYPE_INVALID)) {
memory[type].resize(sectTypeBanks(type));
SectionTypeInfo const &typeInfo = sectionTypeInfo[type];
memory[type].resize(typeInfo.nbBanks());
for (std::deque<FreeSpace> &bankMem : memory[type]) {
bankMem.push_back({
.address = sectionTypeInfo[type].startAddr,
.size = sectionTypeInfo[type].size,
.address = typeInfo.startAddr,
.size = typeInfo.size,
});
}
}
+21 -20
View File
@@ -31,7 +31,7 @@ static void setActiveTypeAndIdx(SectionType type, uint32_t idx) {
}
void layout_SetFloatingSectionType(SectionType type) {
if (sectTypeBanks(type) == 1) {
if (!sectionTypeInfo[type].isBanked()) {
// There is only a single bank anyway, so just set the index to 0.
setActiveTypeAndIdx(type, 0);
} else {
@@ -46,8 +46,8 @@ void layout_SetFloatingSectionType(SectionType type) {
}
void layout_SetSectionType(SectionType type) {
if (sectTypeBanks(type) != 1) {
scriptError("A bank number must be specified for %s", sectionTypeInfo[type].name.c_str());
if (SectionTypeInfo const &typeInfo = sectionTypeInfo[type]; typeInfo.isBanked()) {
scriptError("A bank number must be specified for %s", typeInfo.name);
// Keep going with a default value for the bank index.
}
@@ -60,7 +60,7 @@ void layout_SetSectionType(SectionType type, uint32_t bank) {
if (bank < typeInfo.firstBank) {
scriptError(
"%s bank %" PRIu32 " does not exist (the minimum is %" PRIu32 ")",
typeInfo.name.c_str(),
typeInfo.name,
bank,
typeInfo.firstBank
);
@@ -68,10 +68,11 @@ void layout_SetSectionType(SectionType type, uint32_t bank) {
} else if (bank > typeInfo.lastBank) {
scriptError(
"%s bank %" PRIu32 " does not exist (the maximum is %" PRIu32 ")",
typeInfo.name.c_str(),
typeInfo.name,
bank,
typeInfo.lastBank
);
bank = typeInfo.lastBank;
}
setActiveTypeAndIdx(type, bank - typeInfo.firstBank);
@@ -87,22 +88,22 @@ void layout_SetAddr(uint32_t addr) {
return;
}
uint16_t &pc = curAddr[activeType][activeBankIdx];
SectionTypeInfo const &typeInfo = sectionTypeInfo[activeType];
if (addr < pc) {
if (uint16_t &pc = curAddr[activeType][activeBankIdx]; addr < pc) {
scriptError("Cannot decrease the current address (from $%04x to $%04x)", pc, addr);
} else if (addr > sectTypeEndAddr(activeType)) { // Allow "one past the end" sections.
} else if (addr > typeInfo.endAddr() + 1u) { // Allow "one past the end" zero-sized sections.
scriptError(
"Cannot set the current address to $%04" PRIx32 ": %s ends at $%04" PRIx16,
addr,
typeInfo.name.c_str(),
sectTypeEndAddr(activeType)
typeInfo.name,
typeInfo.endAddr()
);
pc = sectTypeEndAddr(activeType);
pc = typeInfo.endAddr();
} else {
pc = addr;
}
isPcFloating = false;
}
@@ -125,7 +126,7 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
if (isPcFloating) {
if (alignment >= 16) {
layout_SetAddr(floatingAlignOffset);
layout_SetAddr(alignOfs);
} else {
uint32_t alignSize = 1u << alignment;
uint32_t alignMask = alignSize - 1;
@@ -181,7 +182,7 @@ void layout_AlignTo(uint32_t alignment, uint32_t alignOfs) {
", past $%04" PRIx16,
pc,
static_cast<uint16_t>(pc + length),
static_cast<uint16_t>(sectTypeEndAddr(activeType) + 1)
static_cast<uint16_t>(typeInfo.endAddr() + 1)
);
return;
}
@@ -211,7 +212,7 @@ void layout_Pad(uint32_t length) {
"Cannot increase the current address by %u bytes: only %u bytes to $%04" PRIx16,
length,
typeInfo.size - offset,
static_cast<uint16_t>(sectTypeEndAddr(activeType) + 1)
static_cast<uint16_t>(typeInfo.endAddr() + 1)
);
} else {
pc += length;
@@ -241,7 +242,7 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
scriptError(
"\"%s\" is specified to be a %s section, but it contains data",
name.c_str(),
typeInfo.name.c_str()
typeInfo.name
);
} else if (sectTypeHasData(activeType) && section->data.empty() && section->size != 0) {
// A section that lacks data can only be assigned to a type that requires data
@@ -249,7 +250,7 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
scriptError(
"\"%s\" is specified to be a %s section, but it does not contain data",
name.c_str(),
typeInfo.name.c_str()
typeInfo.name
);
} else {
// SDCC areas don't have a type assigned yet, so the linker script gives them one.
@@ -261,8 +262,8 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
scriptError(
"\"%s\" is specified to be a %s section, but it is already a %s section",
name.c_str(),
typeInfo.name.c_str(),
sectionTypeInfo[section->type].name.c_str()
typeInfo.name,
section->typeInfo().name
);
}
@@ -275,7 +276,7 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
"The linker script places section \"%s\" in %s bank %" PRIu32
", but it was already defined in bank %" PRIu32,
name.c_str(),
sectionTypeInfo[section->type].name.c_str(),
section->typeInfo().name,
bank,
section->bank
);
@@ -320,7 +321,7 @@ void layout_PlaceSection(std::string const &name, bool isOptional) {
", but then it would overflow %s by %" PRIu16 " byte%s",
name.c_str(),
org,
typeInfo.name.c_str(),
typeInfo.name,
overflowSize,
overflowSize == 1 ? "" : "s"
);
+15 -6
View File
@@ -101,7 +101,7 @@ static void parseScrambleSpec(char *spec) {
// clang-format off: vertically align nested initializers
static UpperMap<std::pair<uint16_t *, uint16_t>> scrambleSpecs{
{"ROMX", std::pair{&options.scrambleROMX, 65535}},
{"SRAM", std::pair{&options.scrambleSRAM, 255 }},
{"SRAM", std::pair{&options.scrambleSRAM, 256 }},
{"WRAMX", std::pair{&options.scrambleWRAMX, 7 }},
};
// clang-format on
@@ -433,14 +433,14 @@ int main(int argc, char *argv[]) {
sectionTypeInfo[SECTTYPE_VRAM].lastBank = 0;
}
// Read all object files first,
// Read all object files...
size_t nbFiles = localOptions.inputFileNames.size();
obj_Setup(nbFiles);
for (size_t i = 0; i < nbFiles; ++i) {
obj_ReadFile(localOptions.inputFileNames[i], nbFiles - i - 1);
}
// apply the linker script's modifications,
// Apply the linker script's modifications...
if (localOptions.linkerScriptName) {
verbosePrint(VERB_NOTICE, "Reading linker script...\n");
@@ -453,16 +453,25 @@ int main(int argc, char *argv[]) {
requireZeroErrors();
}
// then process them,
// Attempt to generate a layout...
sect_DoSanityChecks();
requireZeroErrors();
assign_AssignSections();
// Emit the symbol and/or map files now (if requested on the CLI),
// so that more link-time info is available to debug any failing assertions.
// This is safe to do, since we have a valid layout at this point;
// assertions only check user-level assumptions made about it.
out_WriteSymIfAny();
out_WriteMapIfAny();
// Make any last-minute verifications...
patch_CheckAssertions();
// and finally output the result.
// And, finally, make the ROM and emit it!
patch_ApplyPatches();
requireZeroErrors();
out_WriteFiles();
out_WriteROMIfAny();
return 0;
}
+33 -44
View File
@@ -6,6 +6,7 @@
#include <deque>
#include <errno.h>
#include <inttypes.h>
#include <optional>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@@ -70,7 +71,7 @@ void out_AddSection(Section const &section) {
1, // SECTTYPE_OAM
};
uint32_t targetBank = section.bank - sectionTypeInfo[section.type].firstBank;
uint32_t targetBank = section.bank - section.typeInfo().firstBank;
if (targetBank >= maxNbBanks[section.type]) {
fatal(
"Section \"%s\" has an invalid bank range (%" PRIu32 " > %" PRIu32 ")",
@@ -95,7 +96,7 @@ void out_AddSection(Section const &section) {
}
Section const *out_OverlappingSection(Section const &section) {
uint32_t bank = section.bank - sectionTypeInfo[section.type].firstBank;
uint32_t bank = section.bank - section.typeInfo().firstBank;
for (Section const *ptr : sections[section.type][bank].sections) {
if (ptr->org < section.org + section.size && section.org < ptr->org + ptr->size) {
@@ -112,23 +113,23 @@ static uint32_t checkOverlaySize() {
return 0;
}
long overlaySize = seekSize(overlayFile);
std::optional<uint64_t> overlaySize = seekSize(overlayFile);
if (overlaySize == -1) {
if (!overlaySize.has_value()) {
warnx("Overlay file is not seekable, cannot check if properly formed");
return 0;
}
if (overlaySize % BANK_SIZE) {
if (*overlaySize % BANK_SIZE) {
warnx("Overlay file does not have a size multiple of 0x4000");
} else if (options.is32kMode && overlaySize != 0x8000) {
} else if (options.is32kMode && *overlaySize != 0x8000) {
warnx("Overlay is not exactly 0x8000 bytes large");
}
if (overlaySize < 0x8000) {
if (*overlaySize < 0x8000) {
warnx("Overlay is less than 0x8000 bytes large");
}
return (overlaySize + BANK_SIZE - 1) / BANK_SIZE;
return (*overlaySize + BANK_SIZE - 1) / BANK_SIZE;
}
// Expand `sections[SECTTYPE_ROMX]` to cover all the overlay banks.
@@ -203,7 +204,7 @@ static void
}
}
static void writeROM() {
void out_WriteROMIfAny() {
if (options.outputFileName) {
char const *outputFileName = options.outputFileName->c_str();
if (*options.outputFileName != "-") {
@@ -266,31 +267,27 @@ static void writeROM() {
}
static void writeSymName(std::string const &name, FILE *file) {
for (char const *ptr = name.c_str(); *ptr != '\0';) {
for (size_t i = 0; i < name.length();) {
// Output legal ASCII characters as-is
if (char c = *ptr; continuesIdentifier(c)) {
if (char c = name[i]; continuesIdentifier(c)) {
putc(c, file);
++ptr;
++i;
continue;
}
// Output illegal characters using Unicode escapes ('\u' or '\U')
// Decode the UTF-8 codepoint; or at least attempt to
Utf8Decoder decoder;
do {
if (decoder.update(*ptr) != UTF8_REJECT) {
++ptr;
continue;
while (i < name.length()) {
decoder.update(static_cast<uint8_t>(name[i++]));
if (decoder.state == UTF8_ACCEPT || decoder.state == UTF8_REJECT) {
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;
// 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(
file, decoder.codepoint <= 0xFFFF ? "\\u%04" PRIx32 : "\\U%08" PRIx32, decoder.codepoint
);
@@ -464,18 +461,19 @@ static void writeMapBank(SortedSections const &sectList, SectionType type, uint3
fprintf(
mapFile,
"\n%s bank #%" PRIu32 ":\n",
sectionTypeInfo[type].name.c_str(),
sectionTypeInfo[type].name,
bank + sectionTypeInfo[type].firstBank
);
uint16_t prevEndAddr = sectionTypeInfo[type].startAddr;
uint16_t used = forEachSection(sectList, [&](Section const &sect) {
assume(sect.offset == 0);
writeEmptySpace(prevEndAddr, sect.org);
assume(sect.org + sect.size <= UINT16_MAX);
prevEndAddr = sect.org + sect.size;
if (uint16_t endAddr = sect.org + sect.size; endAddr > prevEndAddr) {
writeEmptySpace(prevEndAddr, sect.org);
prevEndAddr = endAddr;
}
fprintf(mapFile, "\tSECTION: $%04" PRIx16, sect.org);
if (sect.size != 0) {
@@ -494,12 +492,10 @@ static void writeMapBank(SortedSections const &sectList, SectionType type, uint3
if (used == 0) {
fputs("\tEMPTY\n", mapFile);
} else {
uint16_t bankEndAddr = sectionTypeInfo[type].startAddr + sectionTypeInfo[type].size;
uint16_t bankEndAddr = sectionTypeInfo[type].endAddr() + 1;
writeEmptySpace(prevEndAddr, bankEndAddr);
uint16_t slack = sectionTypeInfo[type].size - used;
fprintf(mapFile, "\tTOTAL EMPTY: $%04" PRIx16 " byte%s\n", slack, slack == 1 ? "" : "s");
}
}
@@ -516,13 +512,12 @@ static void writeMapSummary() {
continue;
}
// Do not output unused section types
// Skip types which haven't been used at all.
if (nbBanks == 0) {
continue;
}
uint32_t usedTotal = 0;
for (uint32_t bank = 0; bank < nbBanks; ++bank) {
usedTotal += forEachSection(sections[type][bank], [](Section const &) {});
}
@@ -530,19 +525,19 @@ static void writeMapSummary() {
fprintf(
mapFile,
"\t%s: %" PRIu32 " byte%s used / %zu free",
sectionTypeInfo[type].name.c_str(),
sectionTypeInfo[type].name,
usedTotal,
usedTotal == 1 ? "" : "s",
static_cast<size_t>(nbBanks) * sectionTypeInfo[type].size - usedTotal
);
if (sectionTypeInfo[type].firstBank != sectionTypeInfo[type].lastBank || nbBanks > 1) {
if (sectionTypeInfo[type].isBanked()) {
fprintf(mapFile, " in %u bank%s", nbBanks, nbBanks == 1 ? "" : "s");
}
putc('\n', mapFile);
}
}
static void writeSym() {
void out_WriteSymIfAny() {
if (!options.symFileName) {
return;
}
@@ -593,7 +588,7 @@ static void writeSym() {
}
}
static void writeMap() {
void out_WriteMapIfAny() {
if (!options.mapFileName) {
return;
}
@@ -621,9 +616,3 @@ static void writeMap() {
}
}
}
void out_WriteFiles() {
writeROM();
writeSym();
writeMap();
}
+10 -5
View File
@@ -114,14 +114,14 @@ static int32_t computeRPNExpr(Patch const &patch, std::vector<Symbol> const &fil
int32_t value;
switch (command) {
case RPN_ADD:
value = popRPN(patch) + popRPN(patch);
value = op_add(popRPN(patch), popRPN(patch));
break;
case RPN_SUB:
value = popRPN(patch);
value = popRPN(patch) - value;
value = op_sub(popRPN(patch), value);
break;
case RPN_MUL:
value = popRPN(patch) * popRPN(patch);
value = op_mul(popRPN(patch), popRPN(patch));
break;
case RPN_DIV:
value = popRPN(patch);
@@ -585,15 +585,20 @@ static void applyFilePatches(Section &section, Section &dataSection) {
rpnErrorAt(patch, "PC has no value outside of a section");
dataSection.data[offset] = 0;
} else {
// A `jr` is *encoded* in ROM as a 1-byte (8-bit) offset, so here `typeSize == 8`,
// but the object's *value* size is a 16-bit absolute address, so we pass 16 here.
checkPatchSize(patch, value, 16);
// Offset is relative to the byte *after* the operand
// PC as operand to `jr` is lower than reference PC by 2
uint16_t address = patch.pcSection->org + patch.pcOffset + 2;
int32_t jumpOffset = value - address;
// The 16-bit truncation of `value - address` is intentional, since
// a low ROM0 address may `jr` backwards to a high HRAM one.
int16_t jumpOffset = static_cast<int16_t>(value - address);
if (jumpOffset < -128 || jumpOffset > 127) {
firstErrorAt(
patch,
"`JR` target must be between -128 and 127 bytes away, not %" PRId32
"`JR` target must be between -128 and 127 bytes away, not %" PRId16
"; use `JP` instead",
jumpOffset
);
+4 -10
View File
@@ -294,11 +294,7 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
uint32_t tmp = readInt(where, token, numberBase);
if (tmp > UINT16_MAX) {
fatalAt(
where,
"Area \"%s\" is larger than the GB address space",
curSection->name.c_str()
);
fatalAt(where, "Area \"%s\" is larger than the GB address space", sectName);
}
curSection->size = tmp;
@@ -575,9 +571,8 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
expectRelocation();
uint16_t idx = readByte(where, token, numberBase);
expectRelocation();
idx |= static_cast<uint16_t>(readByte(where, token, numberBase));
idx |= static_cast<uint16_t>(readByte(where, token, numberBase)) << 8;
// Loudly fail on unknown flags
if (flags & (1 << RELOC_ZPAGE | 1 << RELOC_NPAGE)) {
@@ -860,7 +855,6 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
// RAM sections can have a size, but don't get any data (they shouldn't have any)
if (section->type != SECTTYPE_INVALID) {
SectionTypeInfo const &typeInfo = sectionTypeInfo[section->type];
// Otherwise, how would the type already be known at this point?
assume(section->isAddressFixed);
@@ -871,7 +865,7 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
"\"%s\" is implicitly defined as a %s section (being at address $%04" PRIx16
"), but it has data! (Was a bad `__at()` value used?)",
section->name.c_str(),
typeInfo.name.c_str(),
section->typeInfo().name,
section->org
);
}
@@ -881,7 +875,7 @@ void sdobj_ReadFile(FileStackNode const &src, FILE *file, std::vector<Symbol> &f
"\"%s\" is implicitly defined as a %s section (being at address $%04" PRIx16
"), but it doesn't have any data! (Was a bad `__at()` value used?)",
section->name.c_str(),
typeInfo.name.c_str(),
section->typeInfo().name,
section->org
);
}
+39 -17
View File
@@ -56,6 +56,18 @@ static void checkPieceCompat(Section &target, Section const &other, size_t delta
other.org
);
}
} else if (other.org < delta) {
// Check that `org` did not underflow. Note that it's safe for the above checks to
// use an underflowed value, since their reported errors will still be accurate.
fatalTwoAt(
target,
other,
"Section \"%s\" contains %" PRIu16
" bytes, but is also defined with a lower fixed address $%04" PRIx16,
target.name.c_str(),
target.size,
other.org
);
}
target.isAddressFixed = true;
@@ -115,8 +127,8 @@ static void mergeSections(Section &target, std::unique_ptr<Section> &&other) {
*other,
"Section \"%s\" is defined with type `%s`, but also with type `%s`",
target.name.c_str(),
sectionTypeInfo[target.type].name.c_str(),
sectionTypeInfo[other->type].name.c_str()
target.typeInfo().name,
other->typeInfo().name
);
}
@@ -146,6 +158,15 @@ static void mergeSections(Section &target, std::unique_ptr<Section> &&other) {
case SECTION_FRAGMENT:
checkPieceCompat(target, *other, target.size);
// Check that `target.size += other->size` below will not overflow
if (target.size + other->size > UINT16_MAX) {
fatalTwoAt(
target,
*other,
"Section \"%s\" fragments combined are larger than the GB address space",
target.name.c_str()
);
}
// Append `other` to `target`
other->offset = target.size;
target.size += other->size;
@@ -181,7 +202,7 @@ void sect_AddSection(std::unique_ptr<Section> &&section) {
fatal(
"Section \"%s\" is of type `%s`, which cannot be `UNION`ized",
section->name.c_str(),
sectionTypeInfo[section->type].name.c_str()
section->typeInfo().name
);
} else {
sections.add(section->name, std::move(section));
@@ -214,6 +235,7 @@ static void doSanityChecks(Section &section) {
bankModeError = true;
} else {
section.type = SECTTYPE_ROM0;
section.isBankFixed = false;
}
}
if (options.isWRAM0Mode && section.type == SECTTYPE_WRAMX) {
@@ -226,6 +248,7 @@ static void doSanityChecks(Section &section) {
bankModeError = true;
} else {
section.type = SECTTYPE_WRAM0;
section.isBankFixed = false;
}
}
if (options.isDmgMode && section.type == SECTTYPE_VRAM && section.isBankFixed
@@ -243,18 +266,20 @@ static void doSanityChecks(Section &section) {
section.isAlignFixed = false;
}
// The section's type is determined now, so we can get its type info
SectionTypeInfo const &typeInfo = section.typeInfo();
// Too large an alignment may not be satisfiable
if (section.isAlignFixed && (section.alignMask & sectionTypeInfo[section.type].startAddr)) {
if (section.isAlignFixed && (section.alignMask & typeInfo.startAddr) > section.alignOfs) {
error(
"Section \"%s\" has type `%s`, which cannot be aligned to $%04x bytes",
section.name.c_str(),
sectionTypeInfo[section.type].name.c_str(),
typeInfo.name,
section.alignMask + 1
);
}
uint32_t minbank = sectionTypeInfo[section.type].firstBank,
maxbank = sectionTypeInfo[section.type].lastBank;
uint32_t minbank = typeInfo.firstBank, maxbank = typeInfo.lastBank;
if (!bankModeError && section.isBankFixed
&& (section.bank < minbank || section.bank > maxbank)) {
@@ -271,12 +296,12 @@ static void doSanityChecks(Section &section) {
}
// Check if section has a chance to be placed
if (section.size > sectionTypeInfo[section.type].size) {
if (section.size > typeInfo.size) {
error(
"Section \"%s\" is bigger than the max size for that type: $%" PRIx16 " > $%" PRIx16,
section.name.c_str(),
section.size,
sectionTypeInfo[section.type].size
typeInfo.size
);
}
@@ -300,24 +325,21 @@ static void doSanityChecks(Section &section) {
}
// Ensure the target address is valid
if (section.org < sectionTypeInfo[section.type].startAddr
|| section.org > sectTypeEndAddr(section.type)) {
if (section.org < typeInfo.startAddr || section.org > typeInfo.endAddr()) {
error(
"Section \"%s\"'s fixed address $%04" PRIx16 " is outside of range [$%04" PRIx16
"; $%04" PRIx16 "]",
section.name.c_str(),
section.org,
sectionTypeInfo[section.type].startAddr,
sectTypeEndAddr(section.type)
typeInfo.startAddr,
typeInfo.endAddr()
);
}
if (section.org + section.size > sectTypeEndAddr(section.type) + 1) {
} else if (section.org + section.size > typeInfo.endAddr() + 1) {
error(
"Section \"%s\"'s end address $%04x is greater than last address $%04x",
section.name.c_str(),
section.org + section.size,
sectTypeEndAddr(section.type) + 1
typeInfo.endAddr() + 1
);
}
}
+8 -12
View File
@@ -2,65 +2,61 @@
#include "linkdefs.hpp"
#include <string>
using namespace std::literals;
// The default values are the most lax, as they are used as-is by RGBASM; only RGBLINK has the full
// info, so RGBASM's job is only to catch unconditional errors earlier.
// clang-format off: nested initializers
SectionTypeInfo sectionTypeInfo[SECTTYPE_INVALID] = {
{
.name = "WRAM0"s,
.name = "WRAM0",
.startAddr = 0xC000,
.size = 0x2000, // Patched to 0x1000 if !isWRAM0Mode
.firstBank = 0,
.lastBank = 0,
},
{
.name = "VRAM"s,
.name = "VRAM",
.startAddr = 0x8000,
.size = 0x2000,
.firstBank = 0,
.lastBank = 1, // Patched to 0 if isDmgMode
},
{
.name = "ROMX"s,
.name = "ROMX",
.startAddr = 0x4000,
.size = 0x4000,
.firstBank = 1,
.lastBank = 65535,
},
{
.name = "ROM0"s,
.name = "ROM0",
.startAddr = 0x0000,
.size = 0x8000, // Patched to 0x4000 if !is32kMode
.firstBank = 0,
.lastBank = 0,
},
{
.name = "HRAM"s,
.name = "HRAM",
.startAddr = 0xFF80,
.size = 0x007F,
.firstBank = 0,
.lastBank = 0,
},
{
.name = "WRAMX"s,
.name = "WRAMX",
.startAddr = 0xD000,
.size = 0x1000,
.firstBank = 1,
.lastBank = 7,
},
{
.name = "SRAM"s,
.name = "SRAM",
.startAddr = 0xA000,
.size = 0x2000,
.firstBank = 0,
.lastBank = 255,
},
{
.name = "OAM"s,
.name = "OAM",
.startAddr = 0xFE00,
.size = 0x00A0,
.firstBank = 0,
+17
View File
@@ -8,6 +8,23 @@
#include "helpers.hpp" // assume, clz, ctz
// Signed overflow is UB, so these compute with unsigned arithmetic, which wraps around.
// Casting back is OK because the types implement two's complement behavior.
int32_t op_add(int32_t augend, int32_t addend) {
return static_cast<int32_t>(static_cast<uint32_t>(augend) + static_cast<uint32_t>(addend));
}
int32_t op_sub(int32_t minuend, int32_t subtrahend) {
return static_cast<int32_t>(static_cast<uint32_t>(minuend) - static_cast<uint32_t>(subtrahend));
}
int32_t op_mul(int32_t multiplicand, int32_t multiplier) {
return static_cast<int32_t>(
static_cast<uint32_t>(multiplicand) * static_cast<uint32_t>(multiplier)
);
}
int32_t op_divide(int32_t dividend, int32_t divisor) {
assume(divisor != 0); // Division by 0 is UB
assume(dividend != INT32_MIN || divisor != -1); // INT32_MIN / -1 is UB
+18 -13
View File
@@ -10,6 +10,7 @@
#include "helpers.hpp"
#include "platform.hpp"
#include "style.hpp"
#include "util.hpp" // parseWholeNumber
#include "version.hpp"
#if defined(_MSC_VER) || defined(__MINGW32__)
@@ -24,20 +25,19 @@ void Usage::printVersion(bool error) const {
}
void Usage::printAndExit(int code) const {
FILE *file;
bool isTerminal;
if (code) {
file = stderr;
isTerminal = isatty(STDERR_FILENO);
} else {
file = stdout;
isTerminal = isatty(STDOUT_FILENO);
// Usage flags can be long lines, so wrap them at a maximum line length
uint64_t maxLineLen = 0;
// Use the conventional COLUMNS environment variable, if it is defined and nonzero
if (char const *columnsStr = getenv("COLUMNS"); columnsStr) {
if (std::optional<uint64_t> columns = parseWholeNumber(columnsStr, BASE_10);
columns && *columns > 0) {
maxLineLen = *columns;
} else {
warnx("Ignoring invalid `COLUMNS` value \"%s\"", columnsStr);
}
}
// Use the console window width minus 1 as the maximum line length for flags,
// or the historically common 80 minus 1 if the output is not to a console TTY
size_t maxLineLen = 79;
if (isTerminal) {
// Otherwise, use the console window width minus 1, if the output is to a console TTY
if (maxLineLen == 0 && isatty(code ? STDERR_FILENO : STDOUT_FILENO)) {
// LCOV_EXCL_START
#if defined(_MSC_VER) || defined(__MINGW32__)
CONSOLE_SCREEN_BUFFER_INFO csbi;
@@ -54,8 +54,13 @@ void Usage::printAndExit(int code) const {
#endif
// LCOV_EXCL_STOP
}
// Otherwise, just use the historically common 80 minus 1
if (maxLineLen == 0) {
maxLineLen = 79;
}
// Print "Usage: <program name>"
FILE *file = code ? stderr : stdout;
style_Set(file, STYLE_GREEN, true);
fputs("Usage: ", file);
style_Set(file, STYLE_CYAN, true);
+6 -6
View File
@@ -25,18 +25,18 @@ int xclose(int fd) {
return close(fd);
}
long seekSize(FILE *file) {
std::optional<uint64_t> seekSize(FILE *file) {
if (fseek(file, 0, SEEK_END) != 0) {
return -1;
return std::nullopt;
}
long size = ftell(file);
auto size = ftell(file); // Use `auto` since Windows' `_ftelli64` returns `__int64`, not `long`
if (size < 0) {
return -1;
return std::nullopt;
}
if (fseek(file, 0, SEEK_SET) != 0) {
return -1;
return std::nullopt;
}
return size;
return static_cast<uint64_t>(size);
}
bool isNewline(int c) {
+22 -8
View File
@@ -4,23 +4,37 @@
#include "helpers.hpp"
// We do not build `make develop` with `-fsanitize=leak` because macOS clang++ does not support it.
// Instead, we enable ASan (`-fsanitize=address`) to check for memory leaks in all four programs.
#ifdef __clang__
#if __has_feature(address_sanitizer) && !defined(__SANITIZE_ADDRESS__)
#define __SANITIZE_ADDRESS__
#endif
#if __has_feature(address_sanitizer) && !defined(__SANITIZE_UNDEFINED__)
#define __SANITIZE_UNDEFINED__
#endif
#endif
#if !defined(NDEBUG) && defined(__SANITIZE_ADDRESS__) && !defined(__APPLE__)
#if !defined(NDEBUG) && defined(__SANITIZE_ADDRESS__)
extern "C" {
char const *__asan_default_options(void) {
return "detect_leaks=1"
":detect_stack_use_after_return=1"
return ":check_initialization_order=1"
":detect_invalid_pointer_pairs=2"
":check_initialization_order=1"
// `detect_leaks` is not supported on macOS.
#ifndef __APPLE__
":detect_leaks=1"
#endif
":detect_stack_use_after_return=1"
// ":fast_unwind_on_malloc=0" // Enable this if ASan outputs bad backtraces
":print_legend=0"
":strict_init_order=1"
":strict_string_checks=1"
":print_legend=0";
":strict_string_checks=1";
}
}
#endif
#if !defined(NDEBUG) && defined(__SANITIZE_UNDEFINED__)
extern "C" {
char const *__ubsan_default_options(void) {
return "print_stacktrace=1";
}
}
#endif
+3
View File
@@ -0,0 +1,3 @@
SECTION "test", ROM0
db 1, 2, 3
align 16, 2
+3
View File
@@ -0,0 +1,3 @@
error: Section already contains 3 bytes, higher than this aligned address $0002
at align-beyond-size.asm(3)
Assembly aborted with 1 error
+1
View File
@@ -0,0 +1 @@
assert x, "oops \0 null"
+2
View File
@@ -0,0 +1,2 @@
FATAL: Assertion messages cannot contain '\0' characters
at assert-nul.asm(1)
+20
View File
@@ -0,0 +1,20 @@
opt Q8
println strfmt("%f", 1.999)
println strfmt("%f", 2.999)
println strfmt("%f", 3.999)
println strfmt("%f", 16777215.999)
println strfmt("%f", 4294967295.999)
opt Q16
println strfmt("%f", 1.999999)
println strfmt("%f", 2.999999)
println strfmt("%f", 3.999999)
println strfmt("%f", 65535.999999)
println strfmt("%f", 4294967295.999999)
opt Q24
println strfmt("%f", 1.999999999)
println strfmt("%f", 2.999999999)
println strfmt("%f", 3.999999999)
println strfmt("%f", 255.999999999)
println strfmt("%f", 4294967295.999999999)
+12
View File
@@ -0,0 +1,12 @@
warning: Magnitude of fixed-point constant is too large [-Wlarge-constant]
at fixed-point-carry.asm(5)
warning: Magnitude of fixed-point constant is too large [-Wlarge-constant]
at fixed-point-carry.asm(6)
warning: Magnitude of fixed-point constant is too large [-Wlarge-constant]
at fixed-point-carry.asm(12)
warning: Magnitude of fixed-point constant is too large [-Wlarge-constant]
at fixed-point-carry.asm(13)
warning: Magnitude of fixed-point constant is too large [-Wlarge-constant]
at fixed-point-carry.asm(19)
warning: Magnitude of fixed-point constant is too large [-Wlarge-constant]
at fixed-point-carry.asm(20)
+15
View File
@@ -0,0 +1,15 @@
2.00000
3.00000
4.00000
0.00000
0.00000
2.00000
3.00000
4.00000
0.00000
0.00000
2.00000
3.00000
4.00000
0.00000
0.00000
+2 -2
View File
@@ -5,9 +5,9 @@ $18B
$62B85
$4
$10000
$20000
$10000
$10000
$10000
$20000
$C570A
$13333
$13333
+4
View File
@@ -0,0 +1,4 @@
SECTION FRAGMENT "test", ROM0
ds $11, 42
SECTION FRAGMENT "test", ROM0[$10]
+2
View File
@@ -0,0 +1,2 @@
FATAL: Section "test" already contains 17 bytes, higher than this fragment's fixed address $0010
at fragment-org-beyond-size.asm(4)
+3
View File
@@ -0,0 +1,3 @@
SECTION "test", ROM0
LOAD "test", WRAM0
ENDL
+4
View File
@@ -0,0 +1,4 @@
FATAL: Section "test" already defined
at load-already-defined.asm(2)
and also:
at load-already-defined.asm(1)
+1 -2
View File
@@ -12,9 +12,8 @@ error: Macro argument `\<2147483647>` not defined
at negative-macro-args.asm::mac(11) <- negative-macro-args.asm(21)
error: Macro argument `\<-2147483648>` not defined
at negative-macro-args.asm::mac(14) <- negative-macro-args.asm(21)
error: Number in bracketed macro argument is too large
error: Macro argument `\<-2147483648>` not defined
at negative-macro-args.asm::mac(14) <- negative-macro-args.asm(21)
while expanding symbol `i`
error: Macro argument `\<-2147483648>` not defined
at negative-macro-args.asm::mac(15) <- negative-macro-args.asm(21)
error: Number in bracketed macro argument is too large
+1 -1
View File
@@ -8,7 +8,7 @@
0: ==
-8: ==
2147483647: ==
-2147483648: == >
-2147483648: ==
2147483648: == >
-1: G == G
4294967295: G == >
+2 -4
View File
@@ -12,8 +12,6 @@ error: Macro argument `\1` not defined
at negative-shifted-macro-args.asm::test(15) <- negative-shifted-macro-args.asm(22)
error: Macro argument `\<-1>` not defined
at negative-shifted-macro-args.asm::test(16) <- negative-shifted-macro-args.asm(22)
error: Number in bracketed macro argument is too large
error: Macro argument `\<-2147483648>` not defined
at negative-shifted-macro-args.asm::test(19) <- negative-shifted-macro-args.asm(22)
error: syntax error, unexpected >
at negative-shifted-macro-args.asm::test(19) <- negative-shifted-macro-args.asm(22)
Assembly aborted with 9 errors
Assembly aborted with 8 errors
+1
View File
@@ -11,3 +11,4 @@ $0
$A
+2
View File
@@ -0,0 +1,2 @@
SECTION "test", ROM0
SECTION "test", WRAM0
+4
View File
@@ -0,0 +1,4 @@
FATAL: Section "test" already defined
at section-already-defined.asm(2)
and also:
at section-already-defined.asm(1)
+2
View File
@@ -0,0 +1,2 @@
SECTION "test\0 foo", ROM0
SECTION "test\0 bar", ROM0
+2
View File
@@ -0,0 +1,2 @@
FATAL: Section names cannot contain '\0' characters
at section-name-nul.asm(1)
+6
View File
@@ -0,0 +1,6 @@
MACRO m
println \1
shift $8000_0000 ; INT32_MIN
println \1
ENDM
m 1, 2, 3
+2
View File
@@ -0,0 +1,2 @@
warning: Cannot shift macro arguments past their beginning [-Wmacro-shift]
at shift-int-min.asm::m(3) <- shift-int-min.asm(6)
+2
View File
@@ -0,0 +1,2 @@
$1
$1
+22
View File
@@ -0,0 +1,22 @@
section "test", rom0
opt Wtruncation=1
; good
ld hl, sp + 0
ld hl, sp + 127
ld hl, sp - 128
ld hl, sp + -128
add sp, 0
add sp, 127
add sp, -128
; bad
ld hl, sp + 128
ld hl, sp - 129
ld hl, sp + 255
ld hl, sp - 256
add sp, 128
add sp, -129
add sp, 255
add sp, -256
+16
View File
@@ -0,0 +1,16 @@
warning: Expression must be signed 8-bit [-Wtruncation]
at sp-signed-truncation.asm(15)
warning: Expression must be signed 8-bit [-Wtruncation]
at sp-signed-truncation.asm(16)
warning: Expression must be signed 8-bit [-Wtruncation]
at sp-signed-truncation.asm(17)
warning: Expression must be signed 8-bit [-Wtruncation]
at sp-signed-truncation.asm(18)
warning: Expression must be signed 8-bit [-Wtruncation]
at sp-signed-truncation.asm(19)
warning: Expression must be signed 8-bit [-Wtruncation]
at sp-signed-truncation.asm(20)
warning: Expression must be signed 8-bit [-Wtruncation]
at sp-signed-truncation.asm(21)
warning: Expression must be signed 8-bit [-Wtruncation]
at sp-signed-truncation.asm(22)

Some files were not shown because too many files have changed in this diff Show More