Compare commits

..

140 Commits

Author SHA1 Message Date
ISSOtm
6b2dc37f43 Release 0.6.0-rc1 2022-07-02 17:08:54 +02:00
ISSOtm
2b83a81ceb Update completion scripts
Also correct minor blunders in the man page
2022-07-02 17:04:17 +02:00
ISSOtm
ca8693690a Cancel -U option
As it turns out, it is really difficult to implement, and can be dealt with later.
2022-07-02 17:04:17 +02:00
ISSOtm
87092208bc Stop passing options.columnMajor explicitly everywhere 2022-07-02 17:04:17 +02:00
ISSOtm
0d32775a1f Add more sanity checks when reversing an image 2022-07-02 17:04:17 +02:00
ISSOtm
0df07d3688 Add note on supporting extra pal formats 2022-07-02 17:04:17 +02:00
ISSOtm
3f70372308 Implement slicing input image 2022-07-02 17:04:17 +02:00
ISSOtm
9646f15b59 Add error message for nigh-impossible proto-palette overflow 2022-07-02 17:04:17 +02:00
ISSOtm
973fbb91bc Improve error message when a tile has too many colors 2022-07-02 17:04:17 +02:00
ISSOtm
903862c451 Improve flag set generation of RGBGFX tests
Less duplication = good
2022-07-02 17:04:17 +02:00
ISSOtm
3f5983358c Add proper error message for bad manual palettes 2022-07-02 17:04:17 +02:00
ISSOtm
7a7126f3b8 Implement bit flipping with a lookup table
Should improve performance.
This version is cooler, and also does not suffer from iteration limits
2022-07-02 17:04:17 +02:00
ISSOtm
b4dadd35b6 Use an iterator zip
Simplifies iterating over tiles and attributes at the same time
2022-07-02 17:04:17 +02:00
ISSOtm
d9b1402ef8 Fix RGBGFX number parsing 2022-07-02 17:04:17 +02:00
ISSOtm
832e0ec972 Report position in color errors 2022-07-02 17:04:17 +02:00
ISSOtm
caaf7a8444 Test VRA0 size as well in randtilegen tests 2022-07-02 17:04:17 +02:00
ISSOtm
a5ed0292b1 Reject colors with ambiguous alpha channel 2022-07-02 17:04:17 +02:00
ISSOtm
05e36767b0 Implement "palette map" output 2022-07-02 17:04:17 +02:00
ISSOtm
531092f5bd Add corrupted PNGs to check error handling
This is not intended to test libpng as much as checking that
we behave correctly if libpng gives us an error
2022-07-02 17:04:17 +02:00
ISSOtm
4c51792f15 Allow testing RGBGFX with specific images as input
No such tests yet, but the infrastructure will be there.
2022-07-02 17:04:17 +02:00
ISSOtm
c4359c1058 Ignore -b and -N when reversing without tilemap
These offsets should only be applied to a tile ID read as input... but this ain't one!
2022-07-02 17:04:17 +02:00
ISSOtm
159efe1257 Transfer test programs as well for cross-testing 2022-07-02 17:04:17 +02:00
ISSOtm
3cfe7800c7 Make randtilegen and rgbgfx_test compile with MSVC 2022-07-02 17:04:17 +02:00
ISSOtm
01cf0c5f98 Print RGBGFX test names 2022-07-02 17:04:17 +02:00
ISSOtm
0dbcebfeb4 Have CMake build and install steps be verbose 2022-07-02 17:04:17 +02:00
ISSOtm
491b6746ab Clean RGBGFX test programs with make clean 2022-07-02 17:04:17 +02:00
ISSOtm
cbf6fadcdb Add RGBGFX tests 2022-07-02 17:04:17 +02:00
ISSOtm
a77b0b396a Fix grayscale sorting not setting palette size
A black+white palette would turn into white+transparent, removing black pixels
from the palette (the third slot would be empty, and the 4th slot be set directly)
2022-07-02 17:04:17 +02:00
ISSOtm
568fb5e4c8 Only report "fusing" different colors once per pair 2022-07-02 17:04:17 +02:00
ISSOtm
82012f698e Fix alpha channel checking in RGBGFX test driver
Compare CGB colors only, including ignoring RGB components if alpha says
the color is transparent
2022-07-02 17:04:17 +02:00
ISSOtm
57ac07b03e Correctly handle fully-transparent tiles when outputting unoptimized tilemap 2022-07-02 17:04:17 +02:00
ISSOtm
c521233499 Fix ProtoPalette::compare
Some disjoint sets were mistakenly reported not as such
For example, {0} was considered to include {1}.
2022-07-02 17:04:17 +02:00
ISSOtm
bf869f6961 Fix memory leak in test driver program
Teeny tiny
2022-07-02 17:04:17 +02:00
ISSOtm
0f8cbb1faf Generate all necessary files for RGBGFX tests 2022-07-02 17:04:17 +02:00
ISSOtm
fcce42d3d2 Avoid sorting proto-palettes breaking mappings
The sorting was performed without updating the mappings, which broke the world.
We can instead sort the IDs as they are inserted into the packing queue,
which should also be faster than moving the actual proto-pal objects around.
2022-07-02 17:04:17 +02:00
Eievui
ed104a9f70 Add rgbgfx test program
Co-Authored-By: ISSOtm <eldredhabert0@gmail.com>
2022-07-02 17:04:17 +02:00
ISSOtm
f5d4126303 Report when an input "tile" contains too many colors
Which otherwise trips a later assertion in debug mode (phew!)
and crashes in release mode (oops)
2022-07-02 17:04:17 +02:00
ISSOtm
78e751f022 Fix reading interlaced PNGs
Either we let libpng handle the transform, or we don't.
But make up your mind!
2022-07-02 17:04:17 +02:00
ISSOtm
d569c6392c Avoid initializing the PNG row
It gets overwritten right after, and uncovers an error in the interlaced read
2022-07-02 17:04:17 +02:00
ISSOtm
75b9d48990 Make randtilegen report the file names it fails to open
Which of the two is erroring out can be inferred from the file name,
anyway.
2022-07-02 17:04:17 +02:00
ISSOtm
3aabe9c799 Move randtilegen to test/gfx subdir
It's tool-specific, so categorize it where it belongs
2022-07-02 17:04:17 +02:00
ISSOtm
5be2b96b40 Remove incorrect check for reversed image width
That check was when the image width was in tiles, and another check right below
is the correct equivalent for the new pixel-unit argument
2022-07-02 17:04:17 +02:00
ISSOtm
7fdfbbbbba Fix inverted condition in tile dedup warning 2022-07-02 17:04:17 +02:00
ISSOtm
7fdc6cbced Fix erroneous tabs in rgbgfx usage text
Thanks @GreenAndEievui!
2022-07-02 17:04:17 +02:00
ISSOtm
56115653ef Include cross-ref link to at-files where mentioned 2022-07-02 17:04:17 +02:00
ISSOtm
7927dfd2e3 Remove negative values in parseNumber checking
Including removal of a dead & useless check.
By the way, fuck integer promotion.
2022-07-02 17:04:17 +02:00
ISSOtm
b1aec91912 Assert that row size is not null
Silences some static analysis warning
2022-07-02 17:04:17 +02:00
ISSOtm
7defaad9d2 Remove placeholder palette output format from -p
This is now described in its own section
2022-07-02 17:04:17 +02:00
ISSOtm
dc9185e50b Make output format descriptions more succinct and link to Pan Docs 2022-07-02 17:04:17 +02:00
ISSOtm
02d957278d Document output formats 2022-07-02 17:04:17 +02:00
ISSOtm
6feb1fb73a Retire -f 2022-07-02 17:04:17 +02:00
ISSOtm
dc67f152a9 Document accepted external palette formats 2022-07-02 17:04:17 +02:00
ISSOtm
913c3dd711 Accept both colons and semicolons in inline pal spec 2022-07-02 17:04:17 +02:00
ISSOtm
32242e0ff2 Switch from colon separators to semicolon
As documented, oops
2022-07-02 17:04:17 +02:00
ISSOtm
91071009a8 Implement some external palette specs
PSP, ACT, and ACO are complete
2022-07-02 17:04:17 +02:00
ISSOtm
1da884db15 Handle processing errors appropriately
Alter the return status accordingly, and print the error counts on exit
2022-07-02 17:04:17 +02:00
ISSOtm
ef473de75a Explicitly error out if no input image is given 2022-07-02 17:04:17 +02:00
ISSOtm
6b0cab32a6 Implement inline palette spec parsing 2022-07-02 17:04:17 +02:00
ISSOtm
cc27169ecd Implement preliminary version of "reverse" feature
Not hooked to all RGBGFX flags yet, but good enough for most use cases
(and as a base for future development, should I need to `reset --hard`.)

TODOs marked appropriately.
2022-07-02 17:04:17 +02:00
ISSOtm
843022772b Clean up randtilegen
Apply review comments
2022-07-02 17:04:17 +02:00
ISSOtm
75f8b16f33 Implement "at-files" for RGBGFX
Useful for persisting flags outside of the build system
2022-07-02 17:04:17 +02:00
ISSOtm
188027bccc Rename convert to process
More consistent with its "main" function's name
2022-07-02 17:04:17 +02:00
ISSOtm
79adcdb7ea Ignore tested sub-projects 2022-07-02 17:04:17 +02:00
ISSOtm
8ed65078da Import fixes to random image generator 2022-07-02 17:04:17 +02:00
ISSOtm
7311fc9ef8 Fix transparency handling
Ensure that the color count is properly used, and that
transparency is not counted as a color when packing palettes
2022-07-02 17:04:17 +02:00
ISSOtm
7d54145e56 Record "seed" when generating images as well
For reproducibility
2022-07-02 17:04:17 +02:00
ISSOtm
e753b62d1a Add program by @aaaaaa123456789 to generate RGBGFX-able images 2022-07-02 17:04:17 +02:00
ISSOtm
6ed220b4c1 Make some style corrections
Co-Authored-By: Rangi <remy.oukaour+rangi42@gmail.com>
2022-07-02 17:04:17 +02:00
ISSOtm
e49fb457ea Factor out row bitplanes extraction into a common function 2022-07-02 17:04:17 +02:00
ISSOtm
3c9d5b05d6 Implement transparency handling
Though none of this has been tested so far...
2022-07-02 17:04:17 +02:00
ISSOtm
e86eb9337a Check the range of the CLI-specified palette size 2022-07-02 17:04:17 +02:00
ISSOtm
b0f8e04fb7 Do not do anything if option parsing goes wrong 2022-07-02 17:04:17 +02:00
ISSOtm
493b94919f Fix -TAP being non-functional 2022-07-02 17:04:17 +02:00
ISSOtm
71e22f3bfe Correct "is this palette empty?" function
This notably caused decantation to delete non-empty palettes, which crashes
2022-07-02 17:04:17 +02:00
ISSOtm
ac02382632 Clean up palette packing a bit
Rename a poorly-named attribute, and add a bunch of debug logging
2022-07-02 17:04:17 +02:00
ISSOtm
943d631701 Implement max palette count 2022-07-02 17:04:17 +02:00
ISSOtm
d2f9cc7e8c Document accepted number formats 2022-07-02 17:04:17 +02:00
ISSOtm
76bb950be5 Parse bank capacities 2022-07-02 17:04:17 +02:00
ISSOtm
f29c5d81ec Provide a per-option sane default when number parsing fails 2022-07-02 17:04:17 +02:00
ISSOtm
2307981878 Parse base tile IDs 2022-07-02 17:04:17 +02:00
ISSOtm
6bab2ea5c8 Add different verbosity levels
And also some ASCII art, perhaps?
2022-07-02 17:04:17 +02:00
ISSOtm
35e57a55c9 Handle base tile IDs in "optimized" output 2022-07-02 17:04:17 +02:00
ISSOtm
21e9a65f0b Remove old C headers 2022-07-02 17:04:17 +02:00
ISSOtm
779c8c9368 Implement -m
Though none of the code involved has been tested in any capacity yet, lol
2022-07-02 17:04:17 +02:00
ISSOtm
e855b6f622 Include libstdc++-6.dll in 32-bit MinGW build 2022-07-02 17:04:17 +02:00
ISSOtm
3b1808cc8f Fix Windows-breaking use of struct vs class
MSVC's (broken) ABI breaks otherwise.
What the f@!$#ck, Microsoft?
(Thank you based Clang for warning, by the way.)
2022-07-02 17:04:17 +02:00
ISSOtm
71cb2854e8 Use cinttypes instead of inttypes.h
Fixes build on some compilers for some reason I fail to understand
2022-07-02 17:04:17 +02:00
ISSOtm
2099a25ee0 Avoid using transform_reduce
Not available in libstdc++ 7, apparently
2022-07-02 17:04:17 +02:00
ISSOtm
b9de65c9a2 Set C++ compiler in CI as well
Duh
2022-07-02 17:04:17 +02:00
ISSOtm
c82cce6d95 Explicitly specify queue template param
GCC 7 fails to deduce the contained type
2022-07-02 17:04:17 +02:00
ISSOtm
97965c9766 Revamp number parsing and remove <charconv>
Support all intended formats and allow partial parsing
<charconv> is not available in GCC 7
2022-07-02 17:04:17 +02:00
ISSOtm
5efc49cb12 Change UseTab to ForIndentation
This fixes many whitespace issues
2022-07-02 17:04:17 +02:00
ISSOtm
c4361b965c Remove <filesystem>
Should fix compilation with GCC before 9
2022-07-02 17:04:17 +02:00
ISSOtm
8d00a61602 Flesh out man page
Describe current options, and add some TODOs for functionality to be implemented
2022-07-02 17:04:17 +02:00
ISSOtm
20442c8a43 Add compatibility hack for unused colors in indexed PNGs 2022-07-02 17:04:17 +02:00
ISSOtm
b95c26c886 Fully implement decanting step 2022-07-02 17:04:17 +02:00
ISSOtm
a96aa1725f Optimize AssignedProtos::empty() to stop early
Also allow counting an `AssignedProtos`'s number of proto-palettes
2022-07-02 17:04:17 +02:00
ISSOtm
3d79f76e41 Avoid calling skipEmpty() for AssignedProtos::end() 2022-07-02 17:04:17 +02:00
ISSOtm
fdfedc45a6 Allow computing the combined weight of any proto-palette set 2022-07-02 17:04:17 +02:00
ISSOtm
c98d92a4c4 Implement -C 2022-07-02 17:04:17 +02:00
ISSOtm
d438838db4 Correctly handle "multiple" overlap between proto-palettes
As explained by the comment
2022-07-02 17:04:17 +02:00
ISSOtm
d675523e49 Skip initializing the PNG pixel array
We are about to set all of the pixels anyway
2022-07-02 17:04:17 +02:00
ISSOtm
ad07c9deb9 Implement (stub) handling for all options 2022-07-02 17:04:17 +02:00
ISSOtm
bf9f99ebf5 Reorder Options members by usage order 2022-07-02 17:04:17 +02:00
ISSOtm
f0eca86c52 Update checkdiff to check for gfx/main.cpp 2022-07-02 17:04:17 +02:00
ISSOtm
e8d8ae4c78 Print all configuration on verbose startup 2022-07-02 17:04:17 +02:00
ISSOtm
0cc62824b9 Use MinGW C++ compiler as well 2022-07-02 17:04:17 +02:00
ISSOtm
2fb1eb9136 Print file paths in a platform-independent way
Conversion may be unspecified, but Windows better do the right thing (or else.)
2022-07-02 17:04:17 +02:00
ISSOtm
bde380f38b Fix unchecked narrowing conversion of tile IDs
The conversion is OK because of the tile amount cap
2022-07-02 17:04:17 +02:00
ISSOtm
38e8024ffa Add missing return type for unreachable_() 2022-07-02 17:04:17 +02:00
ISSOtm
3bd6078537 Tag AssignedProtos iterator as forward
That's what it actually is, oops
(Required for `minmax_element`)
2022-07-02 17:04:17 +02:00
ISSOtm
5409d0d15a Add missing header for strcasecmp 2022-07-02 17:04:17 +02:00
ISSOtm
9262fefd07 Fix compiling with Clang
Force version.c to be compiled as C++ (bodge, will need a proper fix for `version.c`)
Remove user-defined `ProtoPalette` assignment operator (same as default, anyway)
2022-07-02 17:04:17 +02:00
ISSOtm
638d024040 Update subprojects and patch pokecrystal 2022-07-02 17:04:17 +02:00
ISSOtm
3fa1854332 Implement enough functionality to compile & match pokecrystal 2022-07-02 17:04:17 +02:00
ISSOtm
6e406b22bb Implement more features and fix bugs 2022-07-02 17:04:17 +02:00
ISSOtm
d30e507270 Fix duplicated CGB color calculation in pal sorting
And also the inverted alpha channel condition causing the wrong colors
to be treated as transparent
2022-07-02 17:04:17 +02:00
ISSOtm
373a22660b Switch clang-format to C++17
Be consistent with what we settled on in code
2022-07-02 17:04:17 +02:00
ISSOtm
8c62e80c18 Reimplement basic RGBGFX features in C++
Currently missing from the old version:
- `-f` ("fixing" the input image to be indexed)
- `-m` (the code for detecting mirrored tiles is missing, but all of the
        "plumbing" is otherwise there)
- `-C`
- `-d`
- `-x` (though I need to check the exact functionality the old one has)
- Also the man page is still a draft and needs to be fleshed out

More planned features are not implemented yet either:
- Explicit palette spec
- Better error messages, also error "images"
- Better 8x16 support, as well as other "dedup unit" sizes
- Support for arbitrary number of palettes & colors per palette
- Other output formats (for example, a "full" palette map for "streaming"
  use cases like gb-open-world)
- Quantization?

Some things may also be bugged:
- Transparency support
- Tile offsets (not exposed yet)
- Tile counts per bank (not exposed yet)

...and performance remains to be checked.
We need to set up some tests, honestly.
2022-07-02 17:04:17 +02:00
DaKnig
34bc650341 Fix shebangs (#992) 2022-07-01 22:59:41 +02:00
ISSOtm
43ba7d0efb Explain to static analyzer that initial symbol creation cannot fail 2022-06-12 16:49:06 +02:00
ISSOtm
685ea5feed Fix RGBLINK leaking partial string at EOF
Does not really do much, but silences some static analyzers
2022-06-12 16:39:55 +02:00
ISSOtm
f5ac268989 Fix unchecked realloc in makeUnknown 2022-06-12 16:36:35 +02:00
ISSOtm
d51ab35203 Trim macro arg whitespace after line continuations 2022-06-09 00:07:27 +02:00
aaaaaa123456789
3a71910312 Update link to TPP1 spec in RGBFIX man page (#1004)
To match the repo transfer from late 2021.
2022-06-05 23:02:19 +02:00
ISSOtm
ec25b4ac0e Disable -Winline
<@aaaaaa123456789> I mean, `-Winline -Werror` is the "make my builds fail at random for no reason" switch
2022-06-04 11:14:07 +02:00
ISSOtm
83222a8147 Fix "building from source" link in README
Fixes #1001
2022-05-27 11:56:52 +02:00
ISSOtm
97c326942f Warn about automatic instruction alterations
Step 1 of #986
2022-05-21 21:49:07 +02:00
ISSOtm
b037d54f64 Remove deprecated symbols
Fixes #896
2022-05-21 21:45:06 +02:00
ISSOtm
80df6640e3 Update checkdiff to check new man page locations 2022-05-21 11:01:32 +02:00
ISSOtm
68d3ef8e76 Only test a few random padding values each time
It's unlikely that a specific value matters, though in theory possible;
however, an exhaustive test every time is really slow, and testing a couple
random values should, over time, cover everything.

It is fine to test random values because those tested will be logged,
so the problem can be manually reproduced later.
It does make CI technically not deterministic... but that should be fine.
2022-05-19 22:47:48 +02:00
Eldred Habert
01777c96c8 Link to rgbds-www repo in README
Should make it easier to find..!
2022-05-18 19:27:55 +02:00
ISSOtm
28f9183d80 Fix formatting of rgbasm -D documentation 2022-05-17 20:11:00 +02:00
ISSOtm
74c31f7c0f Update docs CI workflows to sync with new rgbds-www repo 2022-05-17 18:36:31 +02:00
ISSOtm
7e94ecbfe6 Move all man pages to a separate directory
Simplifies processing all around, and makes more sense
2022-05-15 13:34:35 +02:00
ISSOtm
0195196425 Cache built deps in Windows CI runs 2022-05-01 16:40:11 +02:00
ISSOtm
19c85a7c2e Update uCity commit to fix deprecation warnings
Cleaning up the logs!
See #991 for context.
2022-04-30 20:42:55 +02:00
ISSOtm
59e73e64ca Unconditionally output ROM0 even without any sections
Required for example when "overlaying" over a ROM and only patching ROMX.
Fixes #993
2022-04-30 14:27:36 +02:00
ISSOtm
972d06bb41 Fix gbdiff script
Fix issues with spaces in input filenames,
as well as a bunch more lint warnings.
2022-04-30 14:27:36 +02:00
ISSOtm
e27da737c6 Print name of up to 10 floating sections on overlay error 2022-04-14 23:58:32 +02:00
125 changed files with 6218 additions and 2735 deletions

View File

@@ -93,7 +93,7 @@ SpacesInConditionalStatement: false
SpacesInContainerLiterals: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: c++20
Standard: c++17
TabWidth: 4
UseCRLF: false
UseTab: AlignWithSpaces
UseTab: ForIndentation

View File

@@ -1,56 +0,0 @@
#!/usr/bin/awk -f
/^\s+<td><b class="Sy">.+<\/b><\/td>$/ {
# Assuming that all cells whose contents are bold are heading cells,
# use the HTML tag for those
sub(/td><b class="Sy"/, "th");
sub(/b><\/td/, "th");
}
# The whole page is being generated, so it's not meant to contain any Liquid
BEGIN {
print "{% raw %}"
}
END {
print "{% endraw %}"
}
BEGIN {
in_synopsis = 0
}
/<table class="Nm">/ {
in_synopsis = 1
}
/<\/table>/ {
# Resets synopsis state even when already reset, but whatever
in_synopsis = 0
}
/<code class="Fl">-[a-zA-Z]/ {
# Add links to arg descr in synopsis section
if (in_synopsis) {
while (match($0, /<code class="Fl">-[a-zA-Z]+/)) {
# 123456789012345678 -> 18 chars
optchars = substr($0, RSTART + 18, RLENGTH - 18)
i = length(optchars)
while (i) {
end = RSTART + 18 + i
i -= 1
len = i ? 1 : 2
$0 = sprintf("%s<a href=\"#%s\">%s</a>%s",
substr($0, 0, end - len - 1),
substr($0, end - 1, 1),
substr($0, end - len, len),
substr($0, end))
}
}
}
}
{
# Make long opts (defined using `Fl Fl`) into a single tag
gsub(/<code class="Fl">-<\/code>\s*<code class="Fl">/, "<code class=\"Fl\">-")
}
{
print
}

View File

@@ -1,113 +0,0 @@
#!/bin/bash
usage() {
cat <<EOF
Usage: $0 [-h] [-r] <rgbds-www> <version>
Copy renders from RGBDS repository to rgbds-www documentation
Execute from the root folder of the RGBDS repo, checked out at the desired tag
<rgbds-www> : Path to the rgbds-www repository
<version> : Version to be copied, such as 'v0.4.1' or 'master'
-h Display this help message
-r Update "latest stable" redirection pages and add a new entry to the index
(use for releases, not master)
EOF
}
is_release=0
bad_usage=0
while getopts ":hr" opt; do
case $opt in
r)
is_release=1
;;
h)
usage
exit 0
;;
\?)
echo "Unknown option '$OPTARG'"
bad_usage=1
;;
esac
done
if [ $bad_usage -ne 0 ]; then
usage
exit 1
fi
shift $(($OPTIND - 1))
declare -A PAGES
PAGES=(
[rgbasm.1.html]=src/asm/rgbasm.1
[rgbasm.5.html]=src/asm/rgbasm.5
[rgblink.1.html]=src/link/rgblink.1
[rgblink.5.html]=src/link/rgblink.5
[rgbfix.1.html]=src/fix/rgbfix.1
[rgbgfx.1.html]=src/gfx/rgbgfx.1
[rgbds.5.html]=src/rgbds.5
[rgbds.7.html]=src/rgbds.7
[gbz80.7.html]=src/gbz80.7
)
WWWPATH="/docs"
mkdir -p "$1/_documentation/$2"
# `mandoc` uses a different format for referring to man pages present in the **current** directory.
# We want that format for RGBDS man pages, and the other one for the rest;
# we thus need to copy all pages to a temporary directory, and process them there.
# Copy all pages to current dir
cp "${PAGES[@]}" .
for page in "${!PAGES[@]}"; do
stem="${page%.html}"
manpage="${stem%.?}(${stem#*.})"
descr="$(awk -v 'FS=.Nd ' '/.Nd/ { print $2; }' "${PAGES[$page]}")"
cat >"$1/_documentation/$2/$page" <<EOF
---
layout: doc
title: $manpage [$2]
description: RGBDS $2 — $descr
---
EOF
options=fragment,man='%N.%S;https://linux.die.net/man/%S/%N'
if [ $stem = rgbasm.5 ]; then
options+=,toc
fi
mandoc -Thtml -I os=Linux -O$options "${PAGES[$page]##*/}" | .github/actions/doc_postproc.awk >> "$1/_documentation/$2/$page"
groff -Tpdf -mdoc -wall "${PAGES[$page]##*/}" >"$1/_documentation/$2/$stem.pdf"
if [ $is_release -ne 0 ]; then
cat - >"$1/_documentation/$page" <<EOF
---
redirect_to: $WWWPATH/$2/${page%.html}
permalink: $WWWPATH/${page%.html}/
title: $manpage [latest stable]
description: RGBDS latest stable — $descr
---
EOF
fi
done
cat - >"$1/_documentation/$2/index.html" <<EOF
---
layout: doc_index
permalink: /docs/$2/
title: RGBDS online manual [$2]
description: RGBDS $2 - Online manual
---
EOF
# If making a release, add a new entry right after `master`
if [ $is_release -ne 0 ]; then
awk '{ print }
/"name": "master"/ { print "\t\t{\"name\": \"'$2'\", \"text\": \"'$2'\" }," }
' "$1/_data/doc.json" >"$1/_data/doc.json.tmp"
mv "$1/_data/doc.json"{.tmp,}
fi
# Clean up
rm "${PAGES[@]##*/}"

View File

@@ -23,16 +23,16 @@ jobs:
run: |
sudo apt-get -qq update
sudo apt-get install -yq groff zlib1g-dev
wget 'http://mandoc.bsd.lv/snapshots/mandoc-1.14.5.tar.gz'
tar xf mandoc-1.14.5.tar.gz
cd mandoc-1.14.5
wget 'http://mandoc.bsd.lv/snapshots/mandoc-1.14.6.tar.gz'
tar xf mandoc-1.14.6.tar.gz
cd mandoc-1.14.6
./configure
make
sudo make install
- name: Update pages
working-directory: rgbds
working-directory: rgbds/man
run: | # The ref appears to be in the format "refs/tags/<version>", so strip that
./.github/actions/get-pages.sh -r ../rgbds-www ${GITHUB_REF##*/}
../../rgbds-www/.github/actions/get-pages.sh ${GITHUB_REF##*/} *
- name: Push new pages
working-directory: rgbds-www
run: |

View File

@@ -16,6 +16,11 @@ jobs:
cc: gcc
- os: macos-11.0
cc: gcc
include:
- cc: gcc
cxx: g++
- cc: clang
cxx: clang++
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
@@ -28,25 +33,25 @@ jobs:
# Apple's base version is severely outdated, not even supporting -Wall,
# but it overrides Homebrew's version nonetheless...
- name: Build & install using Make
run: |
export PATH="/usr/local/opt/bison/bin:$PATH"
make develop -j Q= CC=${{ matrix.cc }}
sudo make install -j Q=
if: matrix.buildsys == 'make'
- name: Build & install using CMake
run: |
export PATH="/usr/local/opt/bison/bin:$PATH"
cmake -S . -B build -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=${{ matrix.cc }} -DSANITIZERS=ON -DMORE_WARNINGS=ON
cmake --build build -j
cp build/src/rgb{asm,link,fix,gfx} .
sudo cmake --install build
make develop -j Q= CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
sudo make install -j Q=
- name: Build & install using CMake
if: matrix.buildsys == 'cmake'
run: |
export PATH="/usr/local/opt/bison/bin:$PATH"
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=${{ matrix.cc }} -DCMAKE_CXX_COMPILER=${{ matrix.cxx }} -DSANITIZERS=ON -DMORE_WARNINGS=ON
cmake --build build -j --verbose
cp build/src/rgb{asm,link,fix,gfx} .
sudo cmake --install build --verbose
- name: Package binaries
run: |
mkdir bins
cp rgb{asm,link,fix,gfx} bins
- name: Upload binaries
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v3
with:
name: rgbds-canary-${{ matrix.os }}-${{ matrix.cc }}-${{ matrix.buildsys }}
path: bins
@@ -71,7 +76,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Get zlib, libpng and bison
run: | # TODO: use an array
run: | # TODO: use an array; remember to update the versions being downloaded, *and* the paths being extracted! (`Move-Item`)
$wc = New-Object System.Net.WebClient
$wc.DownloadFile('https://www.zlib.net/zlib1212.zip', 'zlib.zip')
$hash = (Get-FileHash "zlib.zip" -Algorithm SHA256).Hash
@@ -95,28 +100,41 @@ jobs:
Expand-Archive -DestinationPath install_dir "winflexbison.zip"
Move-Item zlib-1.2.12 zlib
Move-Item lpng1637 libpng
- uses: actions/cache@v3
id: cache
with:
path: |
zbuild
pngbuild
key: ${{ matrix.arch }}-${{ hashFiles('zlib/**', 'libpng/**') }}
- name: Build zlib
run: | # BUILD_SHARED_LIBS causes the output DLL to be correctly called `zlib1.dll`
cmake -S zlib -B zbuild -A ${{ matrix.platform }} -DCMAKE_INSTALL_PREFIX=install_dir -DBUILD_SHARED_LIBS=ON
cmake --build zbuild --config Release -j
if: steps.cache.outputs.cache-hit != 'true'
- name: Install zlib
run: |
cmake --install zbuild
- name: Build libpng
run: |
cmake -S libpng -B pngbuild -A ${{ matrix.platform }} -DCMAKE_INSTALL_PREFIX=install_dir -DPNG_SHARED=ON -DPNG_STATIC=ON -DPNG_TESTS=OFF
cmake --build pngbuild --config Release -j
if: steps.cache.outputs.cache-hit != 'true'
- name: Install libpng
run: |
cmake --install pngbuild
- name: Build Windows binaries
run: |
cmake -S . -B build -A ${{ matrix.platform }} -DCMAKE_INSTALL_PREFIX=install_dir -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j
cmake --install build
cmake --build build --config Release -j --verbose
cmake --install build --verbose --prefix install_dir
- name: Package binaries
shell: bash
run: |
mkdir bins
cp install_dir/bin/{rgbasm.exe,rgblink.exe,rgbfix.exe,rgbgfx.exe,zlib1.dll,libpng16.dll} bins
- name: Upload Windows binaries
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v3
with:
name: rgbds-canary-win${{ matrix.bits }}
path: bins
@@ -124,6 +142,7 @@ jobs:
shell: bash
run: |
cp bins/* .
cp bins/*.dll test/gfx
test/run-tests.sh
windows-xbuild:
@@ -150,7 +169,7 @@ jobs:
./.github/actions/install_deps.sh ${{ matrix.os }}
- name: Install MinGW
run: |
sudo apt-get install gcc-mingw-w64-${{ matrix.arch }} mingw-w64-tools libz-mingw-w64-dev
sudo apt-get install {gcc,g++}-mingw-w64-${{ matrix.arch }} mingw-w64-tools libz-mingw-w64-dev
- name: Install libpng dev headers for MinGW
run: |
sudo ./.github/actions/mingw-w64-libpng-dev.sh ${{ matrix.triplet }}
@@ -166,12 +185,21 @@ jobs:
mv rgbgfx bins/rgbgfx.exe
cp /usr/${{ matrix.triplet }}/lib/zlib1.dll bins
cp /usr/${{ matrix.triplet }}/bin/libpng16-16.dll bins
if [ ${{ matrix.bits }} -eq 32 ]; then cp /usr/lib/gcc/${{ matrix.triplet }}/7.3-win32/libgcc_s_sjlj-1.dll bins; fi
if [ ${{ matrix.bits }} -eq 32 ]; then cp /usr/lib/gcc/${{ matrix.triplet }}/7.3-win32/lib{gcc_s_sjlj-1,stdc++-6}.dll bins; fi
mv test/gfx/randtilegen{,.exe}
mv test/gfx/rgbgfx_test{,.exe}
- name: Upload Windows binaries
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v3
with:
name: rgbds-canary-mingw-win${{ matrix.bits }}
path: bins
- name: Upload Windows test binaries
uses: actions/upload-artifact@v3
with:
name: testing-programs-mingw-win${{ matrix.bits }}
path: |
test/gfx/randtilegen.exe
test/gfx/rgbgfx_test.exe
windows-xtesting:
needs: windows-xbuild
@@ -183,14 +211,20 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Retrieve binaries
uses: actions/download-artifact@v1
uses: actions/download-artifact@v3
with:
name: rgbds-canary-mingw-win${{ matrix.bits }}
path: bins
- name: Retrieve test binaries
uses: actions/download-artifact@v3
with:
name: testing-programs-mingw-win${{ matrix.bits }}
path: test/gfx
- name: Extract binaries
shell: bash
run: |
cp bins/* .
cp bins/*.dll test/gfx
- name: Run tests
shell: bash
run: |

View File

@@ -4,16 +4,15 @@ on:
branches:
- master
paths:
- .github/actions/get-pages.sh
- src/gbz80.7
- src/rgbds.5
- src/rgbds.7
- src/asm/rgbasm.1
- src/asm/rgbasm.5
- src/link/rgblink.1
- src/link/rgblink.5
- src/fix/rgbfix.1
- src/gfx/rgbgfx.1
- man/gbz80.7
- man/rgbds.5
- man/rgbds.7
- man/rgbasm.1
- man/rgbasm.5
- man/rgblink.1
- man/rgblink.5
- man/rgbfix.1
- man/rgbgfx.1
jobs:
build:
@@ -36,16 +35,16 @@ jobs:
run: |
sudo apt-get -qq update
sudo apt-get install -yq groff zlib1g-dev
wget 'http://mandoc.bsd.lv/snapshots/mandoc-1.14.5.tar.gz'
tar xf mandoc-1.14.5.tar.gz
cd mandoc-1.14.5
wget 'http://mandoc.bsd.lv/snapshots/mandoc-1.14.6.tar.gz'
tar xf mandoc-1.14.6.tar.gz
cd mandoc-1.14.6
./configure
make
sudo make install
- name: Update pages
working-directory: rgbds
working-directory: rgbds/man
run: |
./.github/actions/get-pages.sh ../rgbds-www master
../../rgbds-www/maintainer/man_to_html.sh master *
- name: Push new pages
working-directory: rgbds-www
run: |
@@ -56,7 +55,7 @@ jobs:
ssh-add ~/.ssh/id_ed25519
git config --global user.name "GitHub Action"
git config --global user.email "community@gbdev.io"
git add .
git add -A
git commit -m "Update RGBDS master documentation"
if git remote | grep -q origin; then
git remote set-url origin git@github.com:gbdev/rgbds-www.git

View File

@@ -10,7 +10,7 @@
cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
project(rgbds
LANGUAGES C)
LANGUAGES C CXX)
# get real path of source and binary directories
get_filename_component(srcdir "${CMAKE_SOURCE_DIR}" REALPATH)
@@ -29,8 +29,18 @@ option(MORE_WARNINGS "Turn on more warnings" OFF) # Ignored on MSVC
if(MSVC)
# MSVC's standard library triggers warning C5105,
# "macro expansion producing 'defined' has undefined behavior"
add_compile_options(/std:c11 /W1 /MP /wd5105)
add_compile_options(/MP /wd5105)
add_definitions(/D_CRT_SECURE_NO_WARNINGS)
# Also, CMake appears not to pass the C11-enabling flag, so we must add it manually... but only for C!
if(NOT CMAKE_C_FLAGS MATCHES "std:c11") # The flag may already have been injected by an earlier CMake invocation, so don't add it twice
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std:c11" CACHE STRING "Flags used by the C compiler during all build types." FORCE)
endif()
if(SANITIZERS)
set(SAN_FLAGS /fsanitize=address)
add_compile_options(${SAN_FLAGS})
add_link_options(${SAN_FLAGS})
endif()
else()
add_compile_options(-Wall -pedantic)
add_definitions(-D_POSIX_C_SOURCE=200809L -D_ISOC11_SOURCE)
@@ -41,18 +51,18 @@ else()
-fsanitize=object-size -fsanitize=bool -fsanitize=enum
-fsanitize=alignment -fsanitize=null -fsanitize=address)
add_compile_options(${SAN_FLAGS})
link_libraries(${SAN_FLAGS})
add_link_options(${SAN_FLAGS})
# A non-zero optimization level is desired in debug mode, but allow overriding it nonetheless
# TODO: this overrides anything previously set... that's a bit sloppy!
set(CMAKE_C_FLAGS_DEBUG "-g -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_DEBUG "-g -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE STRING "" FORCE)
endif()
if(MORE_WARNINGS)
add_compile_options(-Werror -Wextra
-Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond
-Wfloat-equal -Winline -Wlogical-op -Wnested-externs -Wnull-dereference
-Wold-style-definition -Wshift-overflow=2 -Wstrict-overflow=5
-Wstrict-prototypes -Wstringop-overflow=4 -Wundef -Wuninitialized -Wunused
-Wfloat-equal -Wlogical-op -Wnull-dereference -Wshift-overflow=2
-Wstringop-overflow=4 -Wstrict-overflow=5 -Wundef -Wuninitialized -Wunused
-Wshadow # TODO: -Wshadow=compatible-local ?
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1
-Wno-format-nonliteral # We have a couple of "dynamic" prints
@@ -77,12 +87,23 @@ else(GIT)
message(STATUS "Cannot determine RGBDS version (Git not installed), falling back")
endif(GIT)
find_package(PkgConfig)
if(MSVC OR NOT PKG_CONFIG_FOUND)
# fallback to find_package
find_package(PNG REQUIRED)
else()
pkg_check_modules(LIBPNG REQUIRED libpng)
endif()
include_directories("${PROJECT_SOURCE_DIR}/include")
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED True)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
add_subdirectory(src)
add_subdirectory(test)
# By default, build in Release mode; Debug mode must be explicitly requested
# (You may want to augment it with the options above)
@@ -97,3 +118,19 @@ if(CMAKE_BUILD_TYPE STREQUAL "Release")
message(CHECK_FAIL "no")
endif()
endif()
set(MANDIR "share/man")
set(man1 "man/rgbasm.1"
"man/rgbfix.1"
"man/rgbgfx.1"
"man/rgblink.1")
set(man5 "man/rgbasm.5"
"man/rgblink.5"
"man/rgbds.5")
set(man7 "man/gbz80.7"
"man/rgbds.7")
foreach(SECTION "man1" "man5" "man7")
set(DEST "${MANDIR}/${SECTION}")
install(FILES ${${SECTION}} DESTINATION ${DEST})
endforeach()

View File

@@ -7,7 +7,7 @@
#
.SUFFIXES:
.SUFFIXES: .h .y .c .o
.SUFFIXES: .h .y .c .cpp .o
# User-defined variables
@@ -34,10 +34,13 @@ WARNFLAGS := -Wall -pedantic
# Overridable CFLAGS
CFLAGS ?= -O3 -flto -DNDEBUG
CXXFLAGS ?= -O3 -flto -DNDEBUG
# Non-overridable CFLAGS
# _ISOC11_SOURCE is required on certain platforms to get C11 on top of the C99-based POSIX 2008
REALCFLAGS := ${CFLAGS} ${WARNFLAGS} -std=gnu11 -I include \
-D_POSIX_C_SOURCE=200809L -D_ISOC11_SOURCE
REALCXXFLAGS := ${CXXFLAGS} ${WARNFLAGS} -std=c++17 -I include \
-D_POSIX_C_SOURCE=200809L -fno-exceptions -fno-rtti
# Overridable LDFLAGS
LDFLAGS ?=
# Non-overridable LDFLAGS
@@ -102,9 +105,14 @@ rgbfix_obj := \
src/error.o
rgbgfx_obj := \
src/gfx/gb.o \
src/gfx/main.o \
src/gfx/makepng.o \
src/gfx/pal_packing.o \
src/gfx/pal_sorting.o \
src/gfx/pal_spec.o \
src/gfx/process.o \
src/gfx/proto_palette.o \
src/gfx/reverse.o \
src/gfx/rgba.o \
src/extern/getopt.o \
src/error.o
@@ -118,7 +126,13 @@ rgbfix: ${rgbfix_obj}
$Q${CC} ${REALLDFLAGS} -o $@ ${rgbfix_obj} ${REALCFLAGS} src/version.c
rgbgfx: ${rgbgfx_obj}
$Q${CC} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ ${rgbgfx_obj} ${REALCFLAGS} src/version.c ${PNGLDLIBS}
$Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ ${rgbgfx_obj} ${REALCXXFLAGS} -x c++ src/version.c ${PNGLDLIBS}
test/gfx/randtilegen: test/gfx/randtilegen.c
$Q${CC} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCFLAGS} ${PNGCFLAGS} ${PNGLDLIBS}
test/gfx/rgbgfx_test: test/gfx/rgbgfx_test.cpp
$Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCXXFLAGS} ${PNGLDLIBS}
# Rules to process files
@@ -145,7 +159,10 @@ src/asm/parser.c: src/asm/parser.y
${BISON} $$DEFS -d ${YFLAGS} -o $@ $<
.c.o:
$Q${CC} ${REALCFLAGS} ${PNGCFLAGS} -c -o $@ $<
$Q${CC} ${REALCFLAGS} -c -o $@ $<
.cpp.o:
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
# Target used to remove all files generated by other Makefile targets
@@ -157,6 +174,7 @@ clean:
$Qfind src/ -name "*.o" -exec rm {} \;
$Q${RM} rgbshim.sh
$Q${RM} src/asm/parser.c src/asm/parser.h
$Q${RM} test/gfx/randtilegen test/gfx/rgbgfx_test
# Target used to install the binaries and man pages.
@@ -167,15 +185,15 @@ install: all
$Qinstall ${STRIP} -m ${BINMODE} rgblink ${DESTDIR}${bindir}/rgblink
$Qinstall ${STRIP} -m ${BINMODE} rgbgfx ${DESTDIR}${bindir}/rgbgfx
$Qmkdir -p ${DESTDIR}${mandir}/man1 ${DESTDIR}${mandir}/man5 ${DESTDIR}${mandir}/man7
$Qinstall -m ${MANMODE} src/rgbds.7 ${DESTDIR}${mandir}/man7/rgbds.7
$Qinstall -m ${MANMODE} src/gbz80.7 ${DESTDIR}${mandir}/man7/gbz80.7
$Qinstall -m ${MANMODE} src/rgbds.5 ${DESTDIR}${mandir}/man5/rgbds.5
$Qinstall -m ${MANMODE} src/asm/rgbasm.1 ${DESTDIR}${mandir}/man1/rgbasm.1
$Qinstall -m ${MANMODE} src/asm/rgbasm.5 ${DESTDIR}${mandir}/man5/rgbasm.5
$Qinstall -m ${MANMODE} src/fix/rgbfix.1 ${DESTDIR}${mandir}/man1/rgbfix.1
$Qinstall -m ${MANMODE} src/link/rgblink.1 ${DESTDIR}${mandir}/man1/rgblink.1
$Qinstall -m ${MANMODE} src/link/rgblink.5 ${DESTDIR}${mandir}/man5/rgblink.5
$Qinstall -m ${MANMODE} src/gfx/rgbgfx.1 ${DESTDIR}${mandir}/man1/rgbgfx.1
$Qinstall -m ${MANMODE} man/rgbds.7 ${DESTDIR}${mandir}/man7/rgbds.7
$Qinstall -m ${MANMODE} man/gbz80.7 ${DESTDIR}${mandir}/man7/gbz80.7
$Qinstall -m ${MANMODE} man/rgbds.5 ${DESTDIR}${mandir}/man5/rgbds.5
$Qinstall -m ${MANMODE} man/rgbasm.1 ${DESTDIR}${mandir}/man1/rgbasm.1
$Qinstall -m ${MANMODE} man/rgbasm.5 ${DESTDIR}${mandir}/man5/rgbasm.5
$Qinstall -m ${MANMODE} man/rgbfix.1 ${DESTDIR}${mandir}/man1/rgbfix.1
$Qinstall -m ${MANMODE} man/rgblink.1 ${DESTDIR}${mandir}/man1/rgblink.1
$Qinstall -m ${MANMODE} man/rgblink.5 ${DESTDIR}${mandir}/man5/rgblink.5
$Qinstall -m ${MANMODE} man/rgbgfx.1 ${DESTDIR}${mandir}/man1/rgbgfx.1
# Target used to check the coding style of the whole codebase.
# `extern/` is excluded, as it contains external code that should not be patched
@@ -214,11 +232,9 @@ checkdiff:
develop:
$Qenv ${MAKE} WARNFLAGS="-Werror -Wextra \
-Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond \
-Wfloat-equal -Winline -Wlogical-op -Wnested-externs -Wold-style-definition \
-Wshift-overflow=2 \
-Wstrict-overflow=5 -Wstrict-prototypes -Wundef -Wuninitialized -Wunused \
-Wfloat-equal -Wlogical-op -Wnull-dereference -Wshift-overflow=2 \
-Wstringop-overflow=4 -Wstrict-overflow=5 -Wundef -Wuninitialized -Wunused \
-Wshadow \
-Wnull-dereference -Wstringop-overflow=4 \
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1 \
-Wno-format-nonliteral \
-Wno-type-limits -Wno-tautological-constant-out-of-range-compare \
@@ -229,7 +245,8 @@ develop:
-fsanitize=signed-integer-overflow -fsanitize=bounds \
-fsanitize=object-size -fsanitize=bool -fsanitize=enum \
-fsanitize=alignment -fsanitize=null -fsanitize=address" \
CFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls"
CFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" \
CXXFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls"
# Targets for the project maintainer to easily create Windows exes.
# This is not for Windows users!
@@ -237,12 +254,14 @@ develop:
# install instructions instead.
mingw32:
$Q${MAKE} CC=i686-w64-mingw32-gcc BISON=bison \
PKG_CONFIG=i686-w64-mingw32-pkg-config -j
$Q${MAKE} all test/gfx/randtilegen test/gfx/rgbgfx_test \
CC=i686-w64-mingw32-gcc CXX=i686-w64-mingw32-g++ \
BISON=bison PKG_CONFIG=i686-w64-mingw32-pkg-config -j
mingw64:
$Q${MAKE} CC=x86_64-w64-mingw32-gcc BISON=bison \
PKG_CONFIG=x86_64-w64-mingw32-pkg-config -j
$Q${MAKE} all test/gfx/randtilegen test/gfx/rgbgfx_test \
CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ \
BISON=bison PKG_CONFIG=x86_64-w64-mingw32-pkg-config -j
wine-shim:
$Qecho '#!/bin/bash' > rgbshim.sh

View File

@@ -12,11 +12,10 @@ for the Game Boy and Game Boy Color. It consists of:
This is a fork of the original RGBDS which aims to make the programs more like
other UNIX tools.
This toolchain is maintained on `GitHub <https://github.com/rednex/rgbds>`__.
This toolchain is maintained `on GitHub <https://github.com/gbdev/rgbds>`__.
The documentation of this toolchain can be viewed online
`here <https://rgbds.gbdev.io/docs/>`__, it is generated from the man pages
found in this repository.
The documentation of this toolchain can be viewed online `here <https://rgbds.gbdev.io/docs/>`__, it is generated from the man pages found in this repository.
The source code of the website itself is on GitHub as well under the repo `rgbds-www <https://github.com/gbdev/rgbds-www>`__.
If you want to contribute or maintain RGBDS, and have questions regarding the code, its organisation, etc. you can find me `on GBDev <https://gbdev.io/chat>`__ or via mail at ``rgbds at eldred dot fr``.
@@ -24,7 +23,7 @@ If you want to contribute or maintain RGBDS, and have questions regarding the co
-------------------
The `installation procedure <https://rgbds.gbdev.io/install>`__ is available
online for various platforms. `Building from source <https://rgbds.gbdev.io/install/source>`__
online for various platforms. `Building from source <https://rgbds.gbdev.io/install/#building-from-source>`__
is possible using ``make`` or ``cmake``; follow the link for more detailed instructions.
.. code:: sh
@@ -57,6 +56,8 @@ The RGBDS source code file structure somewhat resembles the following:
│ └── ...
├── include/
│ └── ...
├── man/
│ └── ...
├── src/
│ ├── asm/
│ │ └── ...
@@ -96,7 +97,9 @@ The RGBDS source code file structure somewhat resembles the following:
- ``include/`` - header files for each respective C files in `src`.
- ``src/`` - source code and manual pages for RGBDS.
- ``man/`` - manual pages.
- ``src/`` - source code of RGBDS.
* Note that the code unique to each RGBDS tool is stored in its respective subdirectory
(rgbasm -> ``src/asm/``, for example). ``src/extern/`` contains code imported from external sources.
@@ -126,3 +129,11 @@ The RGBDS source code file structure somewhat resembles the following:
- 2018, codebase relicensed under the MIT license.
- 2020, repository is moved to the `gbdev <https://github.com/gbdev>`__ organisation. The `rgbds.gbdev.io <https://rgbds.gbdev.io>`__ website serving documentation and downloads is created.
4. Acknowledgements
-------------------
RGBGFX generates palettes using algorithms found in the paper
`"Algorithms for the Pagination Problem, a Bin Packing with Overlapping Items" <http://arxiv.org/abs/1605.00558>`__
(`GitHub <https://github.com/pagination-problem/pagination>`__, MIT license),
by Aristide Grange, Imed Kacem, and Sébastien Martin.

View File

@@ -11,19 +11,24 @@ _rgbgfx_completions() {
declare -A opts=(
[V]="version:normal"
[C]="color-curve:normal"
[D]="debug:normal"
[h]="horizontal:normal"
[m]="mirror-tiles:normal"
[u]="unique-tiles:normal"
[v]="verbose:normal"
[f]="fix:normal"
[F]="fix-and-save:normal"
[Z]="columns:normal"
[a]="attr-map:*.attrmap"
[A]="output-attr-map:normal"
[b]="base-tiles:unk"
[d]="depth:unk"
[L]="slice:unk"
[N]="nb-tiles:unk"
[n]="nb-palettes:unk"
[o]="output:glob *.2bpp"
[p]="palette:glob *.pal"
[P]="output-palette:normal"
[q]="palette-map:glob *.palmap"
[Q]="output-palette-map:normal"
[r]="reverse:unk"
[s]="palette-size:unk"
[t]="tilemap:glob *.tilemap"
[T]="output-tilemap:normal"
[x]="trim-end:unk"

View File

@@ -25,7 +25,7 @@
declare -A FILES
while read -r -d '' file; do
FILES["$file"]="true"
done < <(git diff --name-only -z $1 HEAD)
done < <(git diff --name-only -z "$1" HEAD)
edited () {
${FILES["$1"]:-"false"}
@@ -40,13 +40,13 @@ dependency () {
# Pull requests that edit the first file without the second may be correct,
# but are suspicious enough to require review.
dependency include/linkdefs.h src/rgbds.5 \
dependency include/linkdefs.h man/rgbds.5 \
"Was the object file format changed?"
dependency src/asm/parser.y src/asm/rgbasm.5 \
dependency src/asm/parser.y man/rgbasm.5 \
"Was the rgbasm grammar changed?"
dependency include/asm/warning.h src/asm/rgbasm.1 \
dependency include/asm/warning.h man/rgbasm.1 \
"Were the rgbasm warnings changed?"
dependency src/asm/object.c include/linkdefs.h \
@@ -59,27 +59,27 @@ dependency Makefile CMakeLists.txt \
dependency Makefile src/CMakeLists.txt \
"Did the build process change?"
dependency src/asm/main.c src/asm/rgbasm.1 \
dependency src/asm/main.c man/rgbasm.1 \
"Did the rgbasm CLI change?"
dependency src/asm/main.c contrib/zsh_compl/_rgbasm \
"Did the rgbasm CLI change?"
dependency src/asm/main.c contrib/bash_compl/_rgbasm.bash \
"Did the rgbasm CLI change?"
dependency src/link/main.c src/link/rgblink.1 \
dependency src/link/main.c man/rgblink.1 \
"Did the rgblink CLI change?"
dependency src/link/main.c contrib/zsh_compl/_rgblink \
"Did the rgblink CLI change?"
dependency src/link/main.c contrib/bash_compl/_rgblink.bash \
"Did the rgblink CLI change?"
dependency src/fix/main.c src/fix/rgbfix.1 \
dependency src/fix/main.c man/rgbfix.1 \
"Did the rgbfix CLI change?"
dependency src/fix/main.c contrib/zsh_compl/_rgbfix \
"Did the rgbfix CLI change?"
dependency src/fix/main.c contrib/bash_compl/_rgbfix.bash \
"Did the rgbfix CLI change?"
dependency src/gfx/main.c src/gfx/rgbgfx.1 \
dependency src/gfx/main.cpp man/rgbgfx.1 \
"Did the rgbgfx CLI change?"
dependency src/gfx/main.c contrib/zsh_compl/_rgbgfx \
dependency src/gfx/main.cpp contrib/zsh_compl/_rgbgfx \
"Did the rgbgfx CLI change?"
dependency src/gfx/main.c contrib/bash_compl/_rgbgfx.bash \
dependency src/gfx/main.cpp contrib/bash_compl/_rgbgfx.bash \
"Did the rgbgfx CLI change?"

View File

@@ -23,51 +23,51 @@
# SOFTWARE.
STATE=0
diff <(xxd $1) <(xxd $2) | while read -r LINE; do
if [ $STATE -eq 0 ]; then
diff <(xxd "$1") <(xxd "$2") | while read -r LINE; do
if [[ $STATE -eq 0 ]]; then
# Discard first line (line info)
STATE=1
elif [ "$LINE" = '---' ]; then
elif [[ "$LINE" = '---' ]]; then
# Separator between files switches states
echo $LINE
echo "$LINE"
STATE=3
elif grep -Eq '^[0-9]+(,[0-9]+)?[cd][0-9]+(,[0-9]+)?' <<< "$LINE"; then
# Line info resets the whole thing
STATE=1
elif [ $STATE -eq 1 -o $STATE -eq 3 ]; then
elif [[ $STATE -eq 1 || $STATE -eq 3 ]]; then
# Compute the GB address from the ROM offset
OFS=$(cut -d ' ' -f 2 <<< "$LINE" | tr -d ':')
BANK=$((0x$OFS / 0x4000))
ADDR=$((0x$OFS % 0x4000 + ($BANK != 0) * 0x4000))
ADDR=$((0x$OFS % 0x4000 + (BANK != 0) * 0x4000))
# Try finding the preceding symbol closest to the diff
if [ $STATE -eq 1 ]; then
if [[ $STATE -eq 1 ]]; then
STATE=2
SYMFILE=${1%.*}.sym
else
STATE=4
SYMFILE=${2%.*}.sym
fi
EXTRA=$(if [ -f "$SYMFILE" ]; then
EXTRA=$(if [[ -f "$SYMFILE" ]]; then
# Read the sym file for such a symbol
# Ignore comment lines, only pick matching bank
# (The bank regex ignores comments already, make `cut` and `tr` process less lines)
grep -Ei $(printf "^%02x:" $BANK) "$SYMFILE" |
grep -Ei "$(printf "^%02x:" $BANK)" "$SYMFILE" |
cut -d ';' -f 1 |
tr -d "\r" |
while read -r SYMADDR SYM; do
SYMADDR=$((0x${SYMADDR#*:}))
if [ $SYMADDR -le $ADDR ]; then
printf " (%s+%#x)\n" "$SYM" $(($ADDR - $SYMADDR))
if [[ $SYMADDR -le $ADDR ]]; then
printf " (%s+%#x)\n" "$SYM" $((ADDR - SYMADDR))
fi
# TODO: assumes sorted sym files
done | tail -n 1
fi)
printf "%02x:%04x %s\n" $BANK $ADDR $EXTRA
printf "%02x:%04x %s\n" $BANK $ADDR "$EXTRA"
fi
if [ $STATE -eq 2 -o $STATE -eq 4 ]; then
if [[ $STATE -eq 2 || $STATE -eq 4 ]]; then
OFS=$(cut -d ' ' -f 2 <<< "$LINE" | tr -d ':')
BANK=$((0x$OFS / 0x4000))
ADDR=$((0x$OFS % 0x4000 + ($BANK != 0) * 0x4000))
ADDR=$((0x$OFS % 0x4000 + (BANK != 0) * 0x4000))
printf "%s %02x:%04x: %s\n" "${LINE:0:1}" $BANK $ADDR "${LINE#*: }"
fi
done

View File

@@ -15,20 +15,25 @@ local args=(
'(-a --attr-map -A --output-attr-map)'{-A,--output-attr-map}'[Shortcut for -a <file>.attrmap]'
'(-C --color-curve)'{-C,--color-curve}'[Generate palettes using GBC color curve]'
'(-D --debug)'{-D,--debug}'[Enable debug features]'
'(-f --fix -F --fix-and-save)'{-f,--fix}'[Fix input PNG into an indexed image]'
'(-f --fix -F --fix-and-save)'{-F,--fix-and-save}'[Like -f but also save CLI params within the PNG]'
'(-h --horizontal)'{-h,--horizontal}'[Lay out tiles horizontally instead of vertically]'
'(-m --mirror-tiles)'{-m,--mirror-tiles}'[Eliminate mirrored tiles from output]'
'(-p --palette -P --output-palette)'{-P,--output-palette}'[Shortcut for -p <file>.pal]'
'(-q --palette-map -Q --output-palette-map)'{-Q,--output-palette-map}'[Shortcut for -p <file>.palmap]'
'(-t --tilemap -T --output-tilemap)'{-T,--output-tilemap}'[Shortcut for -t <file>.tilemap]'
'(-u --unique-tiles)'{-u,--unique-tiles}'[Eliminate redundant tiles]'
'(-v --verbose)'{-v,--verbose}'[Enable verbose output]'
{-v,--verbose}'[Enable verbose output]'
'(-h --horizontal -Z --columns)'{-Z,--columns}'[Read the image in column-major order]'
'(-a --attr-map -A --output-attr-map)'{-a,--attr-map}'+[Generate a map of tile attributes (mirroring)]:attrmap file:_files'
'(-b --base-tiles)'{-b,--base-tiles}'+[Base tile IDs for tile map output]:base tile IDs:'
'(-d --depth)'{-d,--depth}'+[Set bit depth]:bit depth:_depths'
'(-L --slice)'{-L,--slice}'+[Only process a portion of the image]:input slice:'
'(-N --nb-tiles)'{-n,--nb-tiles}'+[Limit number of tiles]:tile count:'
'(-n --nb-palettes)'{-n,--nb-palettes}'+[Limit number of palettes]:palette count:'
'(-o --output)'{-o,--output}'+[Set output file]:output file:_files'
'(-p --palette -P --output-palette)'{-p,--palette}"+[Output the image's palette in little-endian native RGB555 format]:palette file:_files"
'(-q --palette-map -Q --output-palette-map)'{-p,--palette-map}"+[Output the image's palette map]:palette map file:_files"
'(-r --reverse)'{-r,--reverse}'+[Yield an image from binary data]:image width (in tiles):'
'(-s --palette-size)'{-s,--palette-size}'+[Limit palette size]:palette size:'
'(-t --tilemap -T --output-tilemap)'{-t,--tilemap}'+[Generate a map of tile indices]:tilemap file:_files'
'(-x --trim-end)'{-x,--trim-end}'+[Trim end of output by this many tiles]:tile count:'

View File

@@ -11,7 +11,6 @@
#include <stdint.h>
int32_t fix_Callback_PI(void);
void fix_Print(int32_t i);
int32_t fix_Sin(int32_t i);
int32_t fix_Cos(int32_t i);

View File

@@ -16,7 +16,9 @@
#include "helpers.h"
extern bool haltnop;
extern bool warnOnHaltNop;
extern bool optimizeLoads;
extern bool warnOnLdOpt;
extern bool verbose;
extern bool warnings; /* True to enable warnings, false to disable them. */

View File

@@ -0,0 +1,39 @@
/**
* Allocator adaptor that interposes construct() calls to convert value-initialization
* (which is what you get with e.g. `vector::resize`) into default-initialization (which does not
* zero out non-class types).
* From
* https://stackoverflow.com/questions/21028299/is-this-behavior-of-vectorresizesize-type-n-under-c11-and-boost-container/21028912#21028912
*/
#ifndef DEFAULT_INIT_ALLOC_H
#define DEFAULT_INIT_ALLOC_H
#include <memory>
#include <vector>
template<typename T, typename A = std::allocator<T>>
class default_init_allocator : public A {
using a_t = std::allocator_traits<A>;
public:
template<typename U>
struct rebind {
using other = default_init_allocator<U, typename a_t::template rebind_alloc<U>>;
};
using A::A; // Inherit the allocator's constructors
template<typename U>
void construct(U *ptr) noexcept(std::is_nothrow_default_constructible_v<U>) {
::new (static_cast<void *>(ptr)) U;
}
template<typename U, typename... Args>
void construct(U *ptr, Args &&...args) {
a_t::construct(static_cast<A &>(*this), ptr, std::forward<Args>(args)...);
}
};
template<typename T>
using DefaultInitVec = std::vector<T, default_init_allocator<T>>;
#endif

View File

@@ -12,10 +12,18 @@
#include "helpers.h"
#include "platform.h"
#ifdef __cplusplus
extern "C" {
#endif
void warn(char const NONNULL(fmt), ...) format_(printf, 1, 2);
void warnx(char const NONNULL(fmt), ...) format_(printf, 1, 2);
_Noreturn void err(char const NONNULL(fmt), ...) format_(printf, 1, 2);
_Noreturn void errx(char const NONNULL(fmt), ...) format_(printf, 1, 2);
#ifdef __cplusplus
}
#endif
#endif /* RGBDS_ERROR_H */

View File

@@ -26,6 +26,10 @@
#ifndef RGBDS_EXTERN_GETOPT_H
#define RGBDS_EXTERN_GETOPT_H
#ifdef __cplusplus
extern "C" {
#endif
extern char *musl_optarg;
extern int musl_optind, musl_opterr, musl_optopt, musl_optreset;
@@ -43,4 +47,8 @@ int musl_getopt_long_only(int argc, char **argv, char const *optstring,
#define required_argument 1
#define optional_argument 2
#ifdef __cplusplus
} // extern "C"
#endif
#endif

View File

@@ -1,36 +0,0 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2013-2018, stag019 and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#ifndef RGBDS_GFX_GB_H
#define RGBDS_GFX_GB_H
#include <stdint.h>
#include "gfx/main.h"
#define XFLIP 0x40
#define YFLIP 0x20
void raw_to_gb(const struct RawIndexedImage *raw_image, struct GBImage *gb);
void output_file(const struct Options *opts, const struct GBImage *gb);
int get_tile_index(uint8_t *tile, uint8_t **tiles, int num_tiles,
int tile_size);
uint8_t reverse_bits(uint8_t b);
void xflip(uint8_t *tile, uint8_t *tile_xflip, int tile_size);
void yflip(uint8_t *tile, uint8_t *tile_yflip, int tile_size);
int get_mirrored_tile_index(uint8_t *tile, uint8_t **tiles, int num_tiles,
int tile_size, int *flags);
void create_mapfiles(const struct Options *opts, struct GBImage *gb,
struct Mapfile *tilemap, struct Mapfile *attrmap);
void output_tilemap_file(const struct Options *opts,
const struct Mapfile *tilemap);
void output_attrmap_file(const struct Options *opts,
const struct Mapfile *attrmap);
void output_palette_file(const struct Options *opts,
const struct RawIndexedImage *raw_image);
#endif

View File

@@ -1,91 +0,0 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2013-2018, stag019 and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#ifndef RGBDS_GFX_MAIN_H
#define RGBDS_GFX_MAIN_H
#include <png.h>
#include <stdbool.h>
#include <stdint.h>
#include "error.h"
struct Options {
bool debug;
bool verbose;
bool hardfix;
bool fix;
bool horizontal;
bool mirror;
bool unique;
bool colorcurve;
unsigned int trim;
char *tilemapfile;
bool tilemapout;
char *attrmapfile;
bool attrmapout;
char *palfile;
bool palout;
char *outfile;
char *infile;
};
struct RGBColor {
uint8_t red;
uint8_t green;
uint8_t blue;
};
struct ImageOptions {
bool horizontal;
unsigned int trim;
char *tilemapfile;
bool tilemapout;
char *attrmapfile;
bool attrmapout;
char *palfile;
bool palout;
};
struct PNGImage {
png_struct *png;
png_info *info;
png_byte **data;
int width;
int height;
png_byte depth;
png_byte type;
};
struct RawIndexedImage {
uint8_t **data;
struct RGBColor *palette;
int num_colors;
unsigned int width;
unsigned int height;
};
struct GBImage {
uint8_t *data;
int size;
bool horizontal;
int trim;
};
struct Mapfile {
uint8_t *data;
int size;
};
extern int depth, colors;
#include "gfx/makepng.h"
#include "gfx/gb.h"
#endif /* RGBDS_GFX_MAIN_H */

123
include/gfx/main.hpp Normal file
View File

@@ -0,0 +1,123 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#ifndef RGBDS_GFX_MAIN_HPP
#define RGBDS_GFX_MAIN_HPP
#include <array>
#include <limits.h>
#include <stdint.h>
#include <string>
#include <utility>
#include <vector>
#include "helpers.h"
#include "gfx/rgba.hpp"
struct Options {
uint8_t reversedWidth = 0; // -r, in pixels
bool reverse() const { return reversedWidth != 0; }
bool useColorCurve = false; // -C
bool allowMirroring = false; // -m
bool allowDedup = false; // -u
bool columnMajor = false; // -Z, previously -h
uint8_t verbosity = 0; // -v
std::string attrmap{}; // -a, -A
std::array<uint8_t, 2> baseTileIDs{0, 0}; // -b
enum {
NO_SPEC,
EXPLICIT,
EMBEDDED,
} palSpecType = NO_SPEC; // -c
std::vector<std::array<Rgba, 4>> palSpec{};
uint8_t bitDepth = 2; // -d
struct {
uint16_t left;
uint16_t top;
uint16_t width;
uint16_t height;
} inputSlice{0, 0, 0, 0}; // -L (margins in clockwise order, like CSS)
std::array<uint16_t, 2> maxNbTiles{UINT16_MAX, 0}; // -N
uint8_t nbPalettes = 8; // -n
std::string output{}; // -o
std::string palettes{}; // -p, -P
std::string palmap{}; // -q, -Q
uint8_t nbColorsPerPal = 0; // -s; 0 means "auto" = 1 << bitDepth;
std::string tilemap{}; // -t, -T
uint64_t trim = 0; // -x
std::string input{}; // positional arg
static constexpr uint8_t VERB_NONE = 0; // Normal, no extra output
static constexpr uint8_t VERB_CFG = 1; // Print configuration after parsing options
static constexpr uint8_t VERB_LOG_ACT = 2; // Log actions before doing them
static constexpr uint8_t VERB_INTERM = 3; // Print some intermediate results
static constexpr uint8_t VERB_DEBUG = 4; // Internals are logged
static constexpr uint8_t VERB_UNMAPPED = 5; // Unused so far
static constexpr uint8_t VERB_VVVVVV = 6; // What, can't I have a little fun?
format_(printf, 3, 4) void verbosePrint(uint8_t level, char const *fmt, ...) const;
mutable bool hasTransparentPixels = false;
uint8_t maxOpaqueColors() const { return nbColorsPerPal - hasTransparentPixels; }
};
extern Options options;
/**
* Prints the error count, and exits with failure
*/
[[noreturn]] void giveUp();
/**
* Prints a warning, and does not change the error count
*/
void warning(char const *fmt, ...);
/**
* Prints an error, and increments the error count
*/
void error(char const *fmt, ...);
/**
* Prints a fatal error, increments the error count, and gives up
*/
[[noreturn]] void fatal(char const *fmt, ...);
struct Palette {
// An array of 4 GBC-native (RGB555) colors
std::array<uint16_t, 4> colors{UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX};
void addColor(uint16_t color);
uint8_t indexOf(uint16_t color) const;
uint16_t &operator[](size_t index) { return colors[index]; }
uint16_t const &operator[](size_t index) const { return colors[index]; }
decltype(colors)::iterator begin();
decltype(colors)::iterator end();
decltype(colors)::const_iterator begin() const;
decltype(colors)::const_iterator end() const;
uint8_t size() const;
};
namespace detail {
template<typename T, T... i>
static constexpr auto flipTable(std::integer_sequence<T, i...>) {
return std::array{[](uint8_t byte) {
// To flip all the bits, we'll flip both nibbles, then each nibble half, etc.
byte = (byte & 0b0000'1111) << 4 | (byte & 0b1111'0000) >> 4;
byte = (byte & 0b0011'0011) << 2 | (byte & 0b1100'1100) >> 2;
byte = (byte & 0b0101'0101) << 1 | (byte & 0b1010'1010) >> 1;
return byte;
}(i)...};
}
}
// Flipping tends to happen fairly often, so take a bite out of dcache to speed it up
static constexpr auto flipTable = detail::flipTable(std::make_integer_sequence<uint16_t, 256>());
#endif /* RGBDS_GFX_MAIN_HPP */

View File

@@ -1,21 +0,0 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2013-2018, stag019 and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#ifndef RGBDS_GFX_PNG_H
#define RGBDS_GFX_PNG_H
#include "gfx/main.h"
struct RawIndexedImage *input_png_file(const struct Options *opts,
struct ImageOptions *png_options);
void output_png_file(const struct Options *opts,
const struct ImageOptions *png_options,
const struct RawIndexedImage *raw_image);
void destroy_raw_image(struct RawIndexedImage **raw_image_ptr_ptr);
#endif /* RGBDS_GFX_PNG_H */

View File

@@ -0,0 +1,32 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#ifndef RGBDS_GFX_PAL_PACKING_HPP
#define RGBDS_GFX_PAL_PACKING_HPP
#include <tuple>
#include <vector>
#include "defaultinitalloc.hpp"
#include "gfx/main.hpp"
struct Palette;
class ProtoPalette;
namespace packing {
/**
* Returns which palette each proto-palette maps to, and how many palettes are necessary
*/
std::tuple<DefaultInitVec<size_t>, size_t>
overloadAndRemove(std::vector<ProtoPalette> const &protoPalettes);
}
#endif /* RGBDS_GFX_PAL_PACKING_HPP */

View File

@@ -0,0 +1,32 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#ifndef RGBDS_GFX_PAL_SORTING_HPP
#define RGBDS_GFX_PAL_SORTING_HPP
#include <array>
#include <assert.h>
#include <optional>
#include <png.h>
#include <vector>
#include "gfx/rgba.hpp"
struct Palette;
namespace sorting {
void indexed(std::vector<Palette> &palettes, int palSize, png_color const *palRGB,
png_byte *palAlpha);
void grayscale(std::vector<Palette> &palettes,
std::array<std::optional<Rgba>, 0x8001> const &colors);
void rgb(std::vector<Palette> &palettes);
}
#endif /* RGBDS_GFX_PAL_SORTING_HPP */

15
include/gfx/pal_spec.hpp Normal file
View File

@@ -0,0 +1,15 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#ifndef RGBDS_GFX_PAL_SPEC_HPP
#define RGBDS_GFX_PAL_SPEC_HPP
void parseInlinePalSpec(char const * const arg);
void parseExternalPalSpec(char const *arg);
#endif /* RGBDS_GFX_PAL_SPEC_HPP */

14
include/gfx/process.hpp Normal file
View File

@@ -0,0 +1,14 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#ifndef RGBDS_GFX_CONVERT_HPP
#define RGBDS_GFX_CONVERT_HPP
void process();
#endif /* RGBDS_GFX_CONVERT_HPP */

View File

@@ -0,0 +1,44 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#ifndef RGBDS_GFX_PROTO_PALETTE_HPP
#define RGBDS_GFX_PROTO_PALETTE_HPP
#include <algorithm>
#include <array>
#include <stddef.h>
#include <stdint.h>
class ProtoPalette {
// Up to 4 colors, sorted, and where SIZE_MAX means the slot is empty
// (OK because it's not a valid color index)
// Sorting is done on the raw numerical values to lessen `compare`'s complexity
std::array<uint16_t, 4> _colorIndices{UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX};
public:
/**
* Adds the specified color to the set
* Returns false if the set is full
*/
bool add(uint16_t color);
enum ComparisonResult {
NEITHER,
WE_BIGGER,
THEY_BIGGER = -1,
};
ComparisonResult compare(ProtoPalette const &other) const;
size_t size() const;
bool empty() const;
decltype(_colorIndices)::const_iterator begin() const;
decltype(_colorIndices)::const_iterator end() const;
};
#endif /* RGBDS_GFX_PROTO_PALETTE_HPP */

14
include/gfx/reverse.hpp Normal file
View File

@@ -0,0 +1,14 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#ifndef RGBDS_GFX_REVERSE_HPP
#define RGBDS_GFX_REVERSE_HPP
void reverse();
#endif /* RGBDS_GFX_REVERSE_HPP */

67
include/gfx/rgba.hpp Normal file
View File

@@ -0,0 +1,67 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#ifndef RGBDS_GFX_RGBA_HPP
#define RGBDS_GFX_RGBA_HPP
#include <cstdint>
#include <stdint.h>
struct Rgba {
uint8_t red;
uint8_t green;
uint8_t blue;
uint8_t alpha;
constexpr Rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
: red(r), green(g), blue(b), alpha(a) {}
/**
* Constructs the color from a "packed" RGBA representation (0xRRGGBBAA)
*/
explicit constexpr Rgba(uint32_t rgba = 0)
: red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {}
static constexpr Rgba fromCGBColor(uint16_t cgbColor) {
constexpr auto _5to8 = [](uint8_t fiveBpp) -> uint8_t {
fiveBpp &= 0b11111; // For caller's convenience
return fiveBpp << 3 | fiveBpp >> 2;
};
return {_5to8(cgbColor), _5to8(cgbColor >> 5), _5to8(cgbColor >> 10),
(uint8_t)(cgbColor & 0x8000 ? 0x00 : 0xFF)};
}
/**
* Returns this RGBA as a 32-bit number that can be printed in hex (`%08x`) to yield its CSS
* representation
*/
uint32_t toCSS() const {
auto shl = [](uint8_t val, unsigned shift) { return static_cast<uint32_t>(val) << shift; };
return shl(red, 24) | shl(green, 16) | shl(blue, 8) | shl(alpha, 0);
}
friend bool operator!=(Rgba const &lhs, Rgba const &rhs) { return lhs.toCSS() != rhs.toCSS(); }
/**
* CGB colors are RGB555, so we use bit 15 to signify that the color is transparent instead
* Since the rest of the bits don't matter then, we return 0x8000 exactly.
*/
static constexpr uint16_t transparent = 0b1'00000'00000'00000;
static constexpr uint8_t transparency_threshold = 0x10;
bool isTransparent() const { return alpha < transparency_threshold; }
static constexpr uint8_t opacity_threshold = 0xF0;
bool isOpaque() const { return alpha >= opacity_threshold; }
/**
* Computes the equivalent CGB color, respects the color curve depending on options
*/
uint16_t cgbColor() const;
bool isGray() const { return red == green && green == blue; }
uint8_t grayIndex() const;
};
#endif /* RGBDS_GFX_RGBA_HPP */

View File

@@ -31,7 +31,7 @@
#define attr_(...)
// This seems to generate similar code to __builtin_unreachable, despite different semantics
// Note that executing this is undefined behavior (declared _Noreturn, but does return)
static inline _Noreturn unreachable_(void) {}
static inline _Noreturn void unreachable_(void) {}
#endif
// Use builtins whenever possible, and shim them otherwise

90
include/itertools.hpp Normal file
View File

@@ -0,0 +1,90 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#ifndef RGBDS_ITERTOOLS_HPP
#define RGBDS_ITERTOOLS_HPP
#include <tuple>
#include <utility>
template<typename... Ts>
static inline void report() {
puts(__PRETTY_FUNCTION__);
}
// This is not a fully generic implementation; its current use cases only require for-loop behavior.
// We also assume that all iterators have the same length.
template<typename... Iters>
class Zip {
std::tuple<Iters...> _iters;
public:
explicit Zip(std::tuple<Iters...> &&iters) : _iters(iters) {}
Zip &operator++() {
std::apply([](auto &&...it) { (++it, ...); }, _iters);
return *this;
}
auto operator*() const {
return std::apply([](auto &&...it) { return std::tuple<decltype(*it)...>(*it...); },
_iters);
}
friend auto operator==(Zip const &lhs, Zip const &rhs) {
return std::get<0>(lhs._iters) == std::get<0>(rhs._iters);
}
friend auto operator!=(Zip const &lhs, Zip const &rhs) {
return std::get<0>(lhs._iters) != std::get<0>(rhs._iters);
}
};
namespace detail {
template<typename... Containers>
class ZipContainer {
std::tuple<Containers...> _containers;
public:
ZipContainer(Containers &&...containers)
: _containers(std::forward<Containers>(containers)...) {}
auto begin() {
return Zip(std::apply(
[](auto &&...containers) {
using std::begin;
return std::make_tuple(begin(containers)...);
},
_containers));
}
auto end() {
return Zip(std::apply(
[](auto &&...containers) {
using std::end;
return std::make_tuple(end(containers)...);
},
_containers));
}
};
// Take ownership of objects and rvalue refs passed to us, but not lvalue refs
template<typename T>
using Holder = std::conditional_t<std::is_lvalue_reference_v<T>, T,
std::remove_cv_t<std::remove_reference_t<T>>>;
}
/**
* Does the same number of iterations as the first container's iterator!
*/
template<typename... Containers>
static constexpr auto zip(Containers &&...cs) {
return detail::ZipContainer<detail::Holder<Containers>...>(std::forward<Containers>(cs)...);
}
#endif /* RGBDS_ITERTOOLS_HPP */

View File

@@ -46,12 +46,14 @@
# include <unistd.h>
#endif
/* MSVC doesn't support `[static N]` for array arguments from C99 */
/* MSVC doesn't support `[static N]` for array arguments from C99 or C11 */
#ifdef _MSC_VER
# define MIN_NB_ELMS(N)
# define ARR_QUALS(...)
# define NONNULL(ptr) *ptr
#else
# define MIN_NB_ELMS(N) static (N)
# define ARR_QUALS(...) __VA_ARGS__
# define NONNULL(ptr) ptr[static 1]
#endif

View File

@@ -9,10 +9,19 @@
#ifndef EXTERN_VERSION_H
#define EXTERN_VERSION_H
#ifdef __cplusplus
extern "C" {
#endif
#define PACKAGE_VERSION_MAJOR 0
#define PACKAGE_VERSION_MINOR 5
#define PACKAGE_VERSION_PATCH 2
#define PACKAGE_VERSION_MINOR 6
#define PACKAGE_VERSION_PATCH 0
#define PACKAGE_VERSION_RC 1
char const *get_package_version_string(void);
#ifdef __cplusplus
}
#endif
#endif /* EXTERN_VERSION_H */

File diff suppressed because it is too large Load Diff

View File

@@ -55,9 +55,9 @@ The defaults are 01.
.It Fl D Ar name Ns Oo = Ns Ar value Oc , Fl Fl define Ar name Ns Oo = Ns Ar value Oc
Add a string symbol to the compiled source code.
This is equivalent to
.Ql Ar name Ic EQUS \(dq Ns Ar value Ns \(dq
.Ql Ar name Ic EQUS No \(dq Ns Ar value Ns \(dq
in code, or
.Ql Ar name Ic EQUS \(dq1\(dq
.Ql Ar name Ic EQUS No \(dq1\(dq
if
.Ar value
is not specified.

View File

@@ -192,7 +192,7 @@ SurvivalKids.gbc
.Sh TPP1
TPP1 is a homebrew mapper designed as a functional superset of the common traditional MBCs, allowing larger ROM and RAM sizes combined with other hardware features.
Its specification, as well as more resources, can be found online at
.Lk https://github.com/TwitchPlaysPokemon/tpp1 .
.Lk https://github.com/aaaaaa123456789/tpp1 .
.Ss MBC name
The MBC name for TPP1 is more complex than standard mappers.
It must be followed with the revision number, of the form

591
man/rgbgfx.1 Normal file
View File

@@ -0,0 +1,591 @@
'\" e
.\"
.\" This file is part of RGBDS.
.\"
.\" Copyright (c) 2013-2021, stag019 and RGBDS contributors.
.\"
.\" SPDX-License-Identifier: MIT
.\"
.Dd March 28, 2021
.Dt RGBGFX 1
.Os
.Sh NAME
.Nm rgbgfx
.Nd Game Boy graphics converter
.Sh SYNOPSIS
.Nm
.Op Fl r Ar stride
.Op Fl CmuVZ
.Op Fl v Op Fl v No ...
.Op Fl a Ar attrmap | Fl A
.Op Fl b Ar base_ids
.Op Fl c Ar color_spec
.Op Fl d Ar depth
.Op Fl L Ar slice
.Op Fl N Ar nb_tiles
.Op Fl n Ar nb_pals
.Op Fl o Ar out_file
.Op Fl p Ar pal_file | Fl P
.Op Fl q Ar pal_map | Fl Q
.Op Fl s Ar nb_colors
.Op Fl t Ar tilemap | Fl T
.Op Fl x Ar quantity
.Ar file
.Sh DESCRIPTION
The
.Nm
program converts PNG images into data suitable for display on the Game Boy and Game Boy Color, or vice-versa.
.Pp
The main function of
.Nm
is to divide the input PNG into 8\[tmu]8 pixel
.Em squares ,
convert each of those squares into 1bpp or 2bpp tile data, and save all of the tile data in a file.
It also has options to generate a tile map, attribute map, and/or palette set as well; more on that and how the conversion process can be tweaked below.
.Sh ARGUMENTS
Note that options can be abbreviated as long as the abbreviation is unambiguous:
.Fl Fl verb
is
.Fl Fl verbose ,
but
.Fl Fl ver
is invalid because it could also be
.Fl Fl version .
.Pp
.Nm
accepts decimal, binary, and hexadecimal numbers in option arguments.
Decimal numbers are written as usual; binary numbers must be prefixed with either
.Ql %
or
.Ql 0b ,
and hexadecimal numbers must be prefixed with either
.Ql $
(which will likely need escaping or quoting to avoid being interpreted by the shell), or
.Ql 0x .
Leading zeros (after the base prefix, if any) are accepted, and letters are not case-sensitive.
All of these are equivalent:
.Ql 42 ,
.Ql 042 ,
.Ql 0b00101010 ,
.Ql 0B101010 ,
.Ql 0x2A ,
.Ql 0X2A ,
.Ql 0x2a .
.Pp
The following options are accepted:
.Bl -tag -width Ds
.It Fl a Ar attrmap , Fl Fl attr-map Ar attrmap
Generate an attribute map, which is a file containing tile
.Dq attributes .
For each square of the input image, its corresponding attribute map byte contains the mirroring bits (if
.Fl m
was specified), the bank bit
.Pq see Fl N ,
and the palette index.
See
.Lk https://gbdev.io/pandocs/Tile_Maps#bg-map-attributes-cgb-mode-only Pan Docs
for the individual bytes' format.
The output is written just like the tile map (see
.Fl t ) ,
follows the same order
.Pq Fl Z ,
and has the same size.
.It Fl A , Fl Fl output-attr-map
Same as
.Fl a Ar path ,
where
.Ar path
is the input image's path with the extension set to
.Pa .attrmap .
.It Fl b Ar base_ids , Fl Fl base-tiles Ar base_ids
Set the base IDs for tile map output.
.Ar base_ids
should be one or two numbers between 0 and 255, separated by a comma; they are for bank 0 and bank 1 respectively.
Both default to 0.
.It Fl C , Fl Fl color-curve
When generating palettes, use a color curve mimicking the Game Boy Color's screen.
The resulting colors may look closer to the input image's
.Sy on hardware and accurate emulators .
.It Fl c Ar color_spec , Fl Fl colors Ar color_spec
Use the specified color palettes instead of having
.Nm
automatically determine some.
.Ar color_spec
can be one of the following:
.Bl -tag -width Ds
.It Sy inline palette spec
If
.Ar color_spec
begins with a hash character
.Ql # ,
it is treated as an inline palette specification.
It should contain a comma-separated list of hexadecimal colors, each beginning with a hash.
Colors in are accepted either as
.Ql #rgb
or
.Ql #rrggbb
format.
Palettes must be separated by a colon or semicolon (the latter may require quoting to avoid special handling by the shell), and spaces are allowed around colons, semicolons and commas; trailing commas and semicolons are allowed.
See
.Sx EXAMPLES
for an example of an inline palette specification.
.It Sy embedded palette spec
If
.Ar color_spec
is the case-insensitive word
.Cm embedded ,
then the first four colors of the input PNG's embedded palette are used.
It is an error if the PNG is not indexed, or if colors other than these 4 are used.
.Pq This is different from the default behavior of indexed PNGs, as then unused entries in the embedded palette are ignored, whereas they are not with Fl c Cm embedded .
.It Sy external palette spec
Otherwise,
.Ar color_spec
is assumed to be an external palette specification.
The expected format is
.Ql format:path ,
where
.Ar path
is a path to a file, which will be processed according to the
.Ar format .
See
.Sx PALETTE SPECIFICATION FORMATS
for a list of formats and their descriptions.
.El
.It Fl d Ar depth , Fl Fl depth Ar depth
Set the bit depth of the output tile data, in bits per pixel (bpp), either 1 or 2 (the default).
This changes how tile data is output, and the maximum number of colors per palette (2 and 4 respectively).
.It Fl L Ar slice , Fl Fl slice Ar slice
Only process a given rectangle of the image.
This is useful for example if the input image is a sheet of some sort, and you want to convert each cel individually.
The default is to process the whole image as-is.
.Pp
.Ar slice
must be two number pairs, separated by a colon.
The numbers must be separated by commas; space is allowed around all punctuation.
The first number pair specifies the X and Y coordinates of the top-left pixel that will be processed (anything above it or to its left will be ignored).
The second number pair specifies how many tiles to process horizontally and vertically, respectively.
.Pp
.Sy Fl L Sy is ignored in reverse mode , No no padding is inserted .
.It Fl m , Fl Fl mirror-tiles
Deduplicate tiles that are mirrors of each other.
Tiles are checked for horizontal, vertical, and horizontal-vertical mirroring.
Useful with a tile map and attribute map together to keep track of the duplicated tiles and the dimension(s) mirrored.
Implies
.Fl u .
.It Fl N Ar nb_tiles , Fl Fl nb-tiles Ar nb_tiles
Set a maximum number of tiles that can be placed in each VRAM bank.
.Ar nb_tiles
should be one or two numbers between 0 and 256, separated by a comma; if the latter is omitted, it defaults to 0.
Setting either number to 0 prevents any tiles from being output in that bank.
.Pp
If more tiles are generated than can fit in the two banks combined,
.Nm
will abort.
If
.Fl N
is not specified, no limit will be set on the amount of tiles placed in bank 0, and tiles will not be placed in bank 1.
.It Fl n Ar nb_pals , Fl Fl nb-palettes Ar nb_pals
Abort if more than
.Ar nb_pals
palettes are generated.
This may not be more than 256.
.Pp
Note that attribute map output only has 3 bits for the palette ID, so a limit higher than 8 may yield incomplete data unless relying on a palette map
.Pq see Fl q .
.It Fl o Ar out_file , Fl Fl output Ar out_file
Output the tile data in native 2bpp format or in 1bpp
.Pq depending on Fl d
to this file.
.It Fl p Ar pal_file , Fl Fl palette Ar pal_file
Output the image's palette set to this file.
.It Fl P , Fl Fl output-palette
Same as
.Fl p Ar path ,
where
.Ar path
is the input image's path with the extension set to
.Pa .pal .
.It Fl q Ar pal_file , Fl Fl palette-map Ar pal_file
Output the image's palette map to this file.
This is useful if the input image contains more than 8 palettes, as the attribute map only contains the lower 3 bits of the palette indices.
.It Fl Q , Fl Fl output-palette-map
Same as
.Fl q Ar path ,
where
.Ar path
is the input image's path with the extension set to
.Pa .palmap .
.It Fl r Ar width , Fl Fl reverse Ar width
Switches
.Nm
into
.Dq Sy reverse
mode.
In this mode, instead of converting a PNG image into Game Boy data,
.Nm
will attempt to reverse the process, and render Game Boy data into an image.
See
.Sx REVERSE MODE
below for details.
.Pp
.Ar width
is the image's width, in tiles
.Pq including any margins specified by Fl L .
.It Fl s Ar nb_colors , Fl Fl palette-size Ar nb_colors
Specify how many colors each palette contains, including the transparent one if any.
.Ar nb_colors
cannot be more than
.Ql 1 << Ar depth
.Pq see Fl d .
.It Fl t Ar tilemap , Fl Fl tilemap Ar tilemap
Generate a file of tile indices.
For each square of the input image, its corresponding tile map byte contains the index of the associated tile in the tile data file.
The IDs wrap around from 255 back to 0, and do not include the bank bit; use
.Fl a
for that.
Useful in combination with
.Fl u
and/or
.Fl m
to keep track of duplicate tiles.
.It Fl T , Fl Fl output-tilemap
Same as
.Fl t Ar path ,
where
.Ar path
is the input image's path with the extension set to
.Pa .tilemap .
.It Fl u , Fl Fl unique-tiles
Deduplicate identical tiles, and omit the duplicates from the tile data file.
Useful with a tile map
.Pq see Fl t
to keep track of the duplicated tiles.
.Pp
Note that if this option is enabled, no guarantee is made on the order in which tiles are output; while it
.Em should
be consistent across identical runs of a given
.Nm
release, the same is not true for different releases.
.It Fl V , Fl Fl version
Print the version of the program and exit.
.It Fl v , Fl Fl verbose
Be verbose.
The verbosity level is increased by one each time the flag is specified, with each level including the previous:
.Bl -enum -width 2n -compact
.It
.Nm
prints out its configuration before doing anything.
.It
A generic message is printed before doing most actions.
.It
Some of the actions' intermediate results are printed.
.It
Some internal debug printing is enabled.
.El
The verbosity level does not go past 6.
.Pp
Note that verbose output is only intended to be consumed by humans, and may change without notice between RGBDS releases; relying on those for scripts is not advised.
.It Fl x Ar quantity , Fl Fl trim-end Ar quantity
Do not output the last
.Ar quantity
tiles to the tile data file; no other output is affected.
This is useful for trimming
.Dq filler
/ blank squares at the end of an image.
If fewer than
.Ar quantity
tiles would have been emitted, the file will be empty.
.Pp
Note that this is done
.Em after
deduplication if
.Fl u
was enabled, so you probably don't want to use this option in combination with
.Fl u .
Note also that the tiles that don't get output will not count towards
.Fl N Ap s
limit.
.It Fl Z , Fl Fl columns
Read squares from the PNG in column-major order (column by column), instead of the default row-major order (line by line).
This primarily affects tile map and attribute map output, although it may also change generated tile data and palettes.
.El
.Ss At-files
In a given project, many images are to be converted with different flags.
The traditional way of solving this problem has been to specify the different flags for each image in the Makefile / build script; this can be inconvenient, as it centralizes all those flags away from the images they concern.
.Pp
To avoid these drawbacks,
.Nm
supports
.Dq at-files :
any command-line argument that begins with an at sign
.Pq Ql @
is interpreted as one.
The rest of the argument (without the @, that is) is interpreted as the path to a file, whose contents are interpreted as if given on the command line.
At-files can be stored right next to the corresponding image, for example.
.Pp
Since the contents of at-files are interpreted by
.Nm ,
.Sy no shell processing is performed ;
for example, shell variables are not expanded
.Ql ( $PWD ,
.Ql %WINDIR% ,
etc.).
In at-files, lines that are empty or contain only whitespace are ignored; lines that begin with a hash sign
.Pq Ql # ,
optionally preceded by whitespace, are considered comments and also ignored.
Each line can contain any number of arguments, which are separated by whitespace.
.Pq \&No quoting feature to prevent this is provided.
.Pp
Note that this special meaning given to arguments has less precedence than option arguments, and that the standard
.Ql --
to stop option processing also disables at-file processing.
For example, the following command line processes
.Ql @tilesets/town.png ,
outputs tile data to
.Ql @tilesets/town.2bpp ,
and reads command-line options from
.Ql tilesets/town.flags
then
.Ql tilesets.flags :
.Pp
.Dl $ rgbgfx -o @tilesets/town.2bpp @tilesets/town.flags @tilesets.flags -- @tilesets/town.png
.Pp
At-files can also specify the input image directly, and call for more at-files, both using the regular syntax.
Note that while
.Ql --
can be used in an at-file (with identical semantics), it is only effective inside of it\(emnormal option processing continues in the parent scope.
.Sh PALETTE SPECIFICATION FORMATS
The following formats are supported:
.Bl -tag -width Ds
.It Sy act
.Lk https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1070626 Adobe Photoshop color table .
.It Sy aco
.Lk https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1055819 Adobe Photoshop color swatch .
.It Sy psp
.Lk https://www.selapa.net/swatches/colors/fileformats.php#psp_pal Paint Shop Pro palette .
.El
.Pp
If you wish for another format to be supported, please open an issue (see
.Sx BUGS
below) or contact us, and supply a few sample files.
.Sh PALETTE GENERATION
.Nm
must generate palettes from the colors in the input image, unless
.Fl c
was used; in that case, the provided palettes will be used.
.Sy If the order of colors in the palettes is important to you ,
for example because you want to use palette swaps, please use
.Fl c
to specify the palette explicitly.
.Pp
First, if the image contains
.Em any
transparent pixel, color #0 of
.Em all
palettes will be allocated to it.
This is done
.Sy even if palettes were explicitly specified using Fl c ;
then the specification only covers color #1 onwards.
.Pq If you do not want this, ask your image editor to remove the alpha channel.
.Pp
After generating palettes,
.Nm
sorts colors within those palettes using the following rules:
.EQ
delim $$
.EN
.Bl -bullet -offset indent
.It
If the PNG file internally contains a palette (often dubbed an
.Dq indexed
PNG), then colors in each output palette will be sorted according to their order in the PNG's palette.
Any unused entries will be ignored, and only the first entry is considered if there are any duplicates.
.Po If you want a given color to appear more than once, or an unused color to appear at all, you should specify the palettes explicitly instead using Fl c ;
.Fl c Cm embedded
may be appropriate.
.Pc
.It
Otherwise, if the PNG only contains shades of gray, they will be categorized into as many
.Dq bins
as there are colors per palette, and the palette is set to these bins.
The darkest gray will end up in bin #0, and so on; note that this is the opposite of the RGB method below.
If two distinct grays end up in the same bin, the RGB method is used instead.
.Pp
Be careful that
.Nm
is picky about what it considers
.Dq grays :
the red, green, and blue components of each color must
.Em all
be
.Em exactly
the same.
.It
If none of the above apply, colors are sorted from lightest to darkest.
The definition of luminance that
.Nm
uses is
.Do
$2126 times red + 7152 times green + 722 times blue$
.Dc .
.El
.EQ
delim off
.EN
.Pp
Note that the
.Dq indexed
behavior depends on an internal detail of how the PNG is saved, specifically its
.Ql PLTE
chunk.
Since few image editors (such as GIMP) expose that detail, this behavior is only kept for compatibility and should be considered deprecated.
.Sh OUTPUT FILES
All files output by
.Nm
are binary files, and designed to follow the Game Boy and Game Boy Color's native formats.
What follows is succinct descriptions of those formats, including
.Nm Ns -specific
details.
For more complete, beginner-friendly descriptions of the native formats with illustrations, please check out
.Lk https://gbdev.io/pandocs/Rendering Pan Docs .
.Ss Tile data
Tile data is output like a binary dump of VRAM, with no padding between tiles.
Each tile is 16 bytes, 2 per row of 8 pixels; the bits of color IDs are split into each byte
.Pq or Dq bitplane .
The leftmost pixel's color ID is stored in the two bytes' most significant bits, and the rightmost pixel's color ID in their least significant bits.
.Pp
When the bit depth
.Pq Fl d
is set to 1, the most significant bitplane (second byte) of each row, being all zeros, is simply not output.
.Ss Palette data
Palette data is output like a dump of palette memory.
Each color is written as GBC-native little-endian RGB555, with the unused bit 15 set to 0.
There is no padding between colors, nor between palettes; however, empty colors in the palettes are output as 0xFFFF.
.EQ
delim $$
.EN
For example, if 5 palettes are generated with
.Fl s Cm 4 ,
the palette data file will be $2 times 4 times 5 = 40$ bytes long, even if some palettes contain less than 3 colors.
.EQ
delim off
.EN
Note that
.Fl n
only puts a limit on the amount of palettes, but does not fix this file's size.
.Ss Tile map data
A tile map is an array of tile IDs, with one byte per tile ID.
The first byte always corresponds to the ID of the tile in top-left corner of the input image; the second byte is either the ID of the tile to its right (by default), or below it
.Pq with Fl Z ;
and so on, continuing in the same direction.
Rows / columns (respectively) are stored consecutively, with no padding.
.Ss Attribute map data
Attribute maps mirror the format of tile maps, like on the GBC, especially the order in which bytes are output.
The contents of individual bytes follows the GBC's native format:
.Bl -column "Bit 2\(en0" "Background Palette number"
.It Bit 7 Ta BG-to-OAM Priority Ta Set to 0
.It Bit 6 Ta Vertical Flip Ta 0=Normal, 1=Mirror vertically
.It Bit 5 Ta Horizontal Flip Ta 0=Normal, 1=Mirror horizontally
.It Bit 4 Ta Not used Ta Set to 0
.It Bit 3 Ta Tile VRAM Bank number Ta 0=Bank 0, 1=Bank 1
.It Bit 2\(en0 Ta Background Palette number Ta BGP0-7
.El
.Pp
Note that if more than 8 palettes are used, only the lowest 3 bits of the palette ID are output.
.Sh REVERSE MODE
.Nm
can produce a PNG image from valid data.
This may be useful for ripping graphics, recovering lost source images, etc.
An important caveat on that last one, though: the conversion process is
.Sy lossy
both ways, so the
.Do reversed Dc image won't be perfectly identical to the original\(embut it should be close to a Game Boy's output .
.Pq Keep in mind that many of consoles output different colors, so there is no true reference rendering.
.Pp
When using reverse mode, make sure to pass the same flags that were given when generating the data, especially
.Fl C , d , N , s , x ,
and
.Fl Z .
.Do Sx At-files Dc may help with this .
.Nm
will warn about any inconsistencies it detects.
.Pp
Files that are normally outputs
.Pq Fl a , p , t
become inputs, and
.Ar file
will be written to instead of read from, and thus needs not exist beforehand.
Any of these inputs not passed is assumed to be some default:
.Bl -column "attribute map"
.It palettes Ta Unspecified palette data makes
.Nm
assume DMG (monochrome Game Boy) mode: a single palette of 4 grays.
It is possible to pass palettes using
.Fl c
instead of
.Fl p .
.It tile data Ta Tile data must be provided, as there is no reasonable assumption to fall back on.
.It tile map Ta A missing tile map makes
.Nm
assume that tiles were not deduplicated, and should be laid out in the order they are stored.
.It attribute map Ta Without an attribute map,
.Nm
assumes that no tiles were mirrored.
.El
.Sh NOTES
Some flags have had their functionality removed.
.Fl D , f ,
and
.Fl F
are now ignored, and
.Fl h
is an alias for the new (and less confusingly named)
.Fl Z .
These will be removed and/or repurposed in future versions of
.Nm ,
so relying on them is not recommended.
The same applies to the corresponding long options.
.Pp
If you are curious, you may find out that palette generation is an NP-complete problem, so
.Nm
does not attempt to find the optimal solution, but instead to find a good one in a reasonable amount of time.
It is possible to compute the optimal solution externally (using a solver, for example), and then provide it to
.Nm
via
.Fl c .
.Sh EXAMPLES
The following will only validate the PNG (check its size, that all tiles have a suitable amount of colors, etc.), but output nothing:
.Pp
.Dl $ rgbgfx src/res/maps/overworld/tileset.png
.Pp
The following will convert the image using the two given palettes (and only those), and store the generated 2bpp tile data in
.Ql tileset.2bpp ,
and the attribute map in
.Ql tileset.attrmap .
.Pp
.Dl $ rgbgfx -c '#ffffff,#8d05de, #dc7905,#000000 ; #fff,#8d05de, #7e0000 \&, #000' -A -o tileset.2bpp tileset.png
.Pp
TODO: more examples.
.Sh BUGS
Please report bugs and mistakes in this man page on
.Lk https://github.com/gbdev/rgbds/issues GitHub .
Bug reports and feature requests about RGBDS are also welcome!
.Sh SEE ALSO
.Xr rgbds 7 ,
.Xr rgbasm 1 ,
.Xr rgblink 1 ,
.Xr rgbfix 1 ,
.Xr gbz80 7
.Pp
The Game Boy hardware reference
.Lk https://gbdev.io/pandocs/Rendering.html Pan Docs ,
particularly the section about graphics.
.Sh HISTORY
.Nm
was originally created by
.An stag019
to be included in RGBDS.
It was later rewritten by
.An ISSOtm ,
and is now maintained by a number of contributors at
.Lk https://github.com/gbdev/rgbds .

View File

@@ -14,14 +14,6 @@ set(common_src
"_version.c"
)
find_package(PkgConfig)
if(MSVC OR NOT PKG_CONFIG_FOUND)
# fallback to find_package
find_package(PNG REQUIRED)
else()
pkg_check_modules(LIBPNG REQUIRED libpng)
endif()
find_package(BISON REQUIRED)
set(BISON_FLAGS "-Wall")
# Set sompe optimization flags on versions that support them
@@ -70,9 +62,16 @@ set(rgbfix_src
)
set(rgbgfx_src
"gfx/gb.c"
"gfx/main.c"
"gfx/makepng.c"
"gfx/main.cpp"
"gfx/pal_packing.cpp"
"gfx/pal_sorting.cpp"
"gfx/pal_spec.cpp"
"gfx/process.cpp"
"gfx/proto_palette.cpp"
"gfx/reverse.cpp"
"gfx/rgba.cpp"
"extern/getopt.c"
"error.c"
)
set(rgblink_src
@@ -97,22 +96,6 @@ foreach(PROG "asm" "fix" "gfx" "link")
install(TARGETS rgb${PROG} RUNTIME DESTINATION bin)
endforeach()
set(MANDIR "share/man")
set(man1 "asm/rgbasm.1"
"fix/rgbfix.1"
"gfx/rgbgfx.1"
"link/rgblink.1")
set(man5 "asm/rgbasm.5"
"link/rgblink.5"
"rgbds.5")
set(man7 "gbz80.7"
"rgbds.7")
foreach(SECTION "man1" "man5" "man7")
set(DEST "${MANDIR}/${SECTION}")
install(FILES ${${SECTION}} DESTINATION ${DEST})
endforeach()
if(LIBPNG_FOUND) # pkg-config
target_include_directories(rgbgfx PRIVATE ${LIBPNG_INCLUDE_DIRS})
target_link_directories(rgbgfx PRIVATE ${LIBPNG_LIBRARY_DIRS})

View File

@@ -30,16 +30,6 @@
#define M_PI 3.14159265358979323846
#endif
/*
* Return the _PI symbol value
*/
int32_t fix_Callback_PI(void)
{
warning(WARNING_OBSOLETE, "`_PI` is deprecated; use 3.14159\n");
return double2fix(M_PI);
}
/*
* Print a fixed point value
*/

View File

@@ -109,7 +109,7 @@ static struct KeywordMapping {
{"DEC", T_Z80_DEC},
{"DI", T_Z80_DI},
{"EI", T_Z80_EI},
{"HALT", T_Z80_HALT},
{"HALT", T_Z80_HALT},
{"INC", T_Z80_INC},
{"JP", T_Z80_JP},
{"JR", T_Z80_JR},
@@ -118,9 +118,8 @@ static struct KeywordMapping {
{"LDD", T_Z80_LDD},
{"LDIO", T_Z80_LDH},
{"LDH", T_Z80_LDH},
{"NOPE", T_Z80_NOP},
{"NOP", T_Z80_NOP},
{"OR", T_Z80_OR},
{"OWO", T_OWO},
{"POP", T_Z80_POP},
{"PUSH", T_Z80_PUSH},
{"RES", T_Z80_RES},
@@ -141,7 +140,7 @@ static struct KeywordMapping {
{"SLA", T_Z80_SLA},
{"SRA", T_Z80_SRA},
{"SRL", T_Z80_SRL},
{"STOP!!🛑", T_Z80_STOP},
{"STOP", T_Z80_STOP},
{"SUB", T_Z80_SUB},
{"SWAP", T_Z80_SWAP},
{"XOR", T_Z80_XOR},
@@ -149,29 +148,24 @@ static struct KeywordMapping {
{"NZ", T_CC_NZ},
{"Z", T_CC_Z},
{"NC", T_CC_NC},
{"C", T_CC_C},
/* Handled after as T_TOKEN_C */
/* { "C", T_CC_C }, */
{"•̀A•́)𝓕𝓾𝓬𝓴", T_MODE_AF},
// {"BC", T_MODE_BC},
// {"DE", T_MODE_DE},
{"н∠(", T_MODE_HL_START},
{"AF", T_MODE_AF},
{"BC", T_MODE_BC},
{"DE", T_MODE_DE},
{"HL", T_MODE_HL},
{"SP", T_MODE_SP},
{"н∠( ᐛ 」∠)_👁", T_MODE_HL_DEC},
{"н∠( ᐛ 」∠)_👎", T_MODE_HL_INC},
{"HLD", T_MODE_HL_DEC},
{"HLI", T_MODE_HL_INC},
// HACK: normally this is surrounded by parens, but this is annoying to special-case,
// so we use cooperation from the parser.
{"•̀A•́", T_TOKEN_A},
// {"=B", T_TOKEN_B}, HACK: This begins with a non-identifier character, so we'll cheat
{"♥(˘⌣˘", T_TOKEN_C}, // HACK: same for "C" after the space & closing paren
// {";D", T_TOKEN_D}, HACK: also needs to be special-cased. God I feel dirty.
{"(´ε`", T_TOKEN_E},
{"", T_TOKEN_E_HEART},
{"н", T_TOKEN_H},
{"∠(", T_TOKEN_L_ARM},
{"", T_TOKEN_L_FACE},
{"」∠", T_TOKEN_L_BODY},
{"_", T_TOKEN_L_LEG},
{"A", T_TOKEN_A},
{"B", T_TOKEN_B},
{"C", T_TOKEN_C},
{"D", T_TOKEN_D},
{"E", T_TOKEN_E},
{"H", T_TOKEN_H},
{"L", T_TOKEN_L},
{"DEF", T_OP_DEF},
@@ -218,10 +212,6 @@ static struct KeywordMapping {
{"INCLUDE", T_POP_INCLUDE},
{"PRINT", T_POP_PRINT},
{"PRINTLN", T_POP_PRINTLN},
{"PRINTT", T_POP_PRINTT},
{"PRINTI", T_POP_PRINTI},
{"PRINTV", T_POP_PRINTV},
{"PRINTF", T_POP_PRINTF},
{"EXPORT", T_POP_EXPORT},
{"DS", T_POP_DS},
{"DB", T_POP_DB},
@@ -584,16 +574,16 @@ struct KeywordDictNode {
* In turn, this allows greatly simplifying checking an index into this array,
* which should help speed up the lexer.
*/
uint16_t children[256]; // HACK: we "support" UTF-8 as input now
uint16_t children[0x60 - ' '];
struct KeywordMapping const *keyword;
/* Since the keyword structure is invariant, the min number of nodes is known at compile time */
} keywordDict[690] = {0}; /* Nice */
} keywordDict[365] = {0}; /* Make sure to keep this correct when adding keywords! */
/* Convert a char into its index into the dict */
static uint8_t dictIndex(char c)
{
/* Translate uppercase to lowercase (roughly) */
if (c > 0x60 && c < 0x80)
if (c > 0x60)
c = c - ('a' - 'A');
return c - ' ';
}
@@ -615,9 +605,8 @@ void lexer_Init(void)
/* Walk the dictionary, creating intermediate nodes for the keyword */
for (char const *ptr = keywords[i].name; *ptr; ptr++) {
unsigned char index = (unsigned char)*ptr - ' ';
/* We should be able to assume all entries are well-formed */
if (keywordDict[nodeID].children[index] == 0) {
if (keywordDict[nodeID].children[*ptr - ' '] == 0) {
/*
* If this gets tripped up, set the size of keywordDict to
* something high, compile with `-DPRINT_NODE_COUNT` (see below),
@@ -626,10 +615,10 @@ void lexer_Init(void)
assert(usedNodes < sizeof(keywordDict) / sizeof(*keywordDict));
/* There is no node at that location, grab one from the pool */
keywordDict[nodeID].children[index] = usedNodes;
keywordDict[nodeID].children[*ptr - ' '] = usedNodes;
usedNodes++;
}
nodeID = keywordDict[nodeID].children[index];
nodeID = keywordDict[nodeID].children[*ptr - ' '];
}
/* This assumes that no two keywords have the same name */
@@ -1296,16 +1285,12 @@ static uint32_t readGfxConstant(void)
static bool startsIdentifier(int c)
{
// Anonymous labels internally start with '!'
return (c <= 'Z' && c >= 'A') || (c <= 'z' && c >= 'a') || c == '.' || c == '_' || c >= 0x80 || c == '(';
return (c <= 'Z' && c >= 'A') || (c <= 'z' && c >= 'a') || c == '.' || c == '_';
}
static bool continuesIdentifier(int c)
{
// April Fools HACK: allow UTF-8 :D
// This would normally be quite unsafe (hello, RTL control codes?),
// but since this is for a joke I'll also make the code a joke
// Also, hi if you're reading this!
return startsIdentifier(c) || (c <= '9' && c >= '0') || c == '#' || c == '@' || c == '!';
return startsIdentifier(c) || (c <= '9' && c >= '0') || c == '#' || c == '@';
}
static int readIdentifier(char firstChar)
@@ -1785,10 +1770,6 @@ static int yylex_NORMAL(void)
/* Ignore whitespace and comments */
case ';':
if (peek() == 'D') {
shiftChar();
return T_TOKEN_D;
}
discardComment();
/* fallthrough */
case ' ':
@@ -1809,6 +1790,8 @@ static int yylex_NORMAL(void)
return T_LBRACK;
case ']':
return T_RBRACK;
case '(':
return T_LPAREN;
case ')':
return T_RPAREN;
case ',':
@@ -1876,14 +1859,9 @@ static int yylex_NORMAL(void)
return T_OP_XOR;
case '=': /* Either assignment or EQ */
switch (peek()) {
case '=':
if (peek() == '=') {
shiftChar();
return T_OP_LOGICEQU;
case 'b':
case 'B':
shiftChar();
return T_TOKEN_B;
}
return T_POP_EQUAL;
@@ -2022,12 +2000,6 @@ static int yylex_NORMAL(void)
/* Handle identifiers... or report garbage characters */
case '(':
if (peek() != (unsigned char)"´"[0]) {
return T_LPAREN;
}
// fallthrough
default:
if (startsIdentifier(c)) {
int tokenType = readIdentifier(c);
@@ -2080,9 +2052,23 @@ static int yylex_RAW(void)
size_t i = 0;
int c;
/* Trim left whitespace (stops at a block comment or line continuation) */
while (isWhitespace(peek()))
shiftChar();
/* Trim left whitespace (stops at a block comment) */
for (;;) {
c = peek();
if (isWhitespace(c)) {
shiftChar();
} else if (c == '\\') {
shiftChar();
c = peek();
// If not a line continuation, handle as a normal char
if (!isWhitespace(c) && c != '\n' && c != '\r')
goto backslash;
// Line continuations count as "whitespace"
readLineContinuation();
} else {
break;
}
}
for (;;) {
c = peek();
@@ -2131,6 +2117,7 @@ static int yylex_RAW(void)
shiftChar();
c = peek();
backslash:
switch (c) {
case ',': /* Escapes only valid inside a macro arg */
case '(':

View File

@@ -59,7 +59,9 @@ bool generatePhonyDeps;
char *targetFileName;
bool haltnop;
bool warnOnHaltNop;
bool optimizeLoads;
bool warnOnLdOpt;
bool verbose;
bool warnings; /* True to enable warnings, false to disable them. */
@@ -84,7 +86,7 @@ static char *make_escape(char const *str)
}
/* Short options */
static const char *optstring = "b:D:Eg:hi:LM:o:p:r:VvW:w";
static const char *optstring = "b:D:Eg:Hhi:LlM:o:p:r:VvW:w";
/* Variables for the long-only options */
static int depType; /* Variants of `-M` */
@@ -104,9 +106,11 @@ static struct option const longopts[] = {
{ "define", required_argument, NULL, 'D' },
{ "export-all", no_argument, NULL, 'E' },
{ "gfx-chars", required_argument, NULL, 'g' },
{ "nop-after-halt", no_argument, NULL, 'H' },
{ "halt-without-nop", no_argument, NULL, 'h' },
{ "include", required_argument, NULL, 'i' },
{ "preserve-ld", no_argument, NULL, 'L' },
{ "auto-ldh", no_argument, NULL, 'l' },
{ "dependfile", required_argument, NULL, 'M' },
{ "MG", no_argument, &depType, 'G' },
{ "MP", no_argument, &depType, 'P' },
@@ -142,9 +146,6 @@ static void print_usage(void)
int main(int argc, char *argv[])
{
#if YYDEBUG
yydebug = 1;
#endif
int ch;
char *ep;
@@ -173,8 +174,10 @@ int main(int argc, char *argv[])
opt_B("01");
opt_G("0123");
opt_P(0);
optimizeLoads = true;
haltnop = true;
warnOnHaltNop = true;
optimizeLoads = true;
warnOnLdOpt = true;
verbose = false;
warnings = true;
sym_SetExportAll(false);
@@ -212,7 +215,14 @@ int main(int argc, char *argv[])
errx("Must specify exactly 4 characters for option 'g'");
break;
case 'H':
if (!haltnop)
errx("`-H` and `-h` don't make sense together");
warnOnHaltNop = false;
break;
case 'h':
if (!warnOnHaltNop)
errx("`-H` and `-h` don't make sense together");
haltnop = false;
break;
@@ -221,8 +231,15 @@ int main(int argc, char *argv[])
break;
case 'L':
if (!warnOnLdOpt)
errx("`-L` and `-l` don't make sense together");
optimizeLoads = false;
break;
case 'l':
if (!optimizeLoads)
errx("`-L` and `-l` don't make sense together");
warnOnLdOpt = false;
break;
case 'M':
if (!strcmp("-", musl_optarg))

View File

@@ -17,7 +17,9 @@ struct OptStackEntry {
char gbgfx[4];
int32_t fillByte;
bool haltnop;
bool warnOnHaltNop;
bool optimizeLoads;
bool warnOnLdOpt;
bool warningsAreErrors;
size_t maxRecursionDepth;
// Don't be confused: we use the size of the **global variable** `warningStates`!
@@ -48,6 +50,11 @@ void opt_R(size_t newDepth)
lexer_CheckRecursionDepth();
}
void opt_H(bool warn)
{
warnOnHaltNop = warn;
}
void opt_h(bool halt)
{
haltnop = halt;
@@ -58,6 +65,11 @@ void opt_L(bool optimize)
optimizeLoads = optimize;
}
void opt_l(bool warn)
{
warnOnLdOpt = warn;
}
void opt_W(char *flag)
{
processWarningFlag(flag);
@@ -118,6 +130,13 @@ void opt_Parse(char *s)
break;
}
case 'H':
if (s[1] == '\0')
opt_H(false);
else
error("Option 'H' does not take an argument\n");
break;
case 'h':
if (s[1] == '\0')
opt_h(false);
@@ -132,6 +151,13 @@ void opt_Parse(char *s)
error("Option 'L' does not take an argument\n");
break;
case 'l':
if (s[1] == '\0')
opt_l(false);
else
error("Option 'l' does not take an argument\n");
break;
case 'W':
if (strlen(&s[1]) > 0)
opt_W(&s[1]);
@@ -186,8 +212,10 @@ void opt_Push(void)
entry->fillByte = fillByte; // Pulled from section.h
entry->haltnop = haltnop; // Pulled from main.h
entry->warnOnHaltNop = warnOnHaltNop;
entry->optimizeLoads = optimizeLoads; // Pulled from main.h
entry->warnOnLdOpt = warnOnLdOpt;
// Both of these pulled from warning.h
entry->warningsAreErrors = warningsAreErrors;
@@ -209,8 +237,10 @@ void opt_Pop(void)
opt_B(entry->binary);
opt_G(entry->gbgfx);
opt_P(entry->fillByte);
opt_H(entry->warnOnHaltNop);
opt_h(entry->haltnop);
opt_L(entry->optimizeLoads);
opt_l(entry->warnOnLdOpt);
// opt_W does not apply a whole warning state; it processes one flag string
warningsAreErrors = entry->warningsAreErrors;

View File

@@ -599,7 +599,6 @@ enum {
%token T_POP_INCLUDE "INCLUDE"
%token T_POP_PRINT "PRINT" T_POP_PRINTLN "PRINTLN"
%token T_POP_PRINTF "PRINTF" T_POP_PRINTT "PRINTT" T_POP_PRINTV "PRINTV" T_POP_PRINTI "PRINTI"
%token T_POP_IF "IF" T_POP_ELIF "ELIF" T_POP_ELSE "ELSE" T_POP_ENDC "ENDC"
%token T_POP_EXPORT "EXPORT"
%token T_POP_DB "DB" T_POP_DS "DS" T_POP_DW "DW" T_POP_DL "DL"
@@ -646,32 +645,31 @@ enum {
%token T_Z80_CALL "call" T_Z80_CCF "ccf" T_Z80_CP "cp" T_Z80_CPL "cpl"
%token T_Z80_DAA "daa" T_Z80_DEC "dec" T_Z80_DI "di"
%token T_Z80_EI "ei"
%token T_Z80_HALT "halt"
%token T_Z80_HALT "halt"
%token T_Z80_INC "inc"
%token T_Z80_JP "jp" T_Z80_JR "jr"
%token T_Z80_LD "ld"
%token T_Z80_LDI "ldi"
%token T_Z80_LDD "ldd"
%token T_Z80_LDH "ldh"
%token T_Z80_NOP "nope"
%token T_Z80_NOP "nop"
%token T_Z80_OR "or"
%token T_OWO "owo"
%token T_Z80_POP "pop" T_Z80_PUSH "push"
%token T_Z80_RES "res" T_Z80_RET "ret" T_Z80_RETI "reti" T_Z80_RST "rst"
%token T_Z80_RL "rl" T_Z80_RLA "rla" T_Z80_RLC "rlc" T_Z80_RLCA "rlca"
%token T_Z80_RR "rr" T_Z80_RRA "rra" T_Z80_RRC "rrc" T_Z80_RRCA "rrca"
%token T_Z80_SBC "sbc" T_Z80_SCF "scf" T_Z80_STOP "stop!!🛑"
%token T_Z80_SBC "sbc" T_Z80_SCF "scf" T_Z80_STOP "stop"
%token T_Z80_SLA "sla" T_Z80_SRA "sra" T_Z80_SRL "srl" T_Z80_SUB "sub"
%token T_Z80_SWAP "swap"
%token T_Z80_XOR "xor"
%token T_TOKEN_A "( •̀A•́)" T_TOKEN_F "𝓕𝓾𝓬𝓴"
%token T_TOKEN_B "=B" T_TOKEN_C "♥(˘⌣˘ C)"
%token T_TOKEN_D ";D" T_TOKEN_E "(´ε` )♡" T_TOKEN_E_HEART "(´ε` )♡"
%token T_TOKEN_H "н" T_TOKEN_L_ARM "∠( ᐛ 」∠)_" T_TOKEN_L_FACE "∠( ᐛ 」∠)_" T_TOKEN_L_BODY "∠( ᐛ 」∠)_" T_TOKEN_L_LEG "∠( ᐛ 」∠)_"
%token T_MODE_AF "af" /* T_MODE_BC "bc" T_MODE_DE "de" */ T_MODE_SP "sp"
%token T_MODE_HL_START "н∠( ᐛ 」∠)_" T_MODE_HL_DEC "hld/hl-" T_MODE_HL_INC "hli/hl+"
%token T_CC_NZ "nz" T_CC_Z "z" T_CC_NC "nc" T_CC_C "c"
%token T_TOKEN_A "a"
%token T_TOKEN_B "b" T_TOKEN_C "c"
%token T_TOKEN_D "d" T_TOKEN_E "e"
%token T_TOKEN_H "h" T_TOKEN_L "l"
%token T_MODE_AF "af" T_MODE_BC "bc" T_MODE_DE "de" T_MODE_SP "sp"
%token T_MODE_HL "hl" T_MODE_HL_DEC "hld/hl-" T_MODE_HL_INC "hli/hl+"
%token T_CC_NZ "nz" T_CC_Z "z" T_CC_NC "nc" // There is no T_CC_C, only T_TOKEN_C
%type <constValue> reg_r
%type <constValue> reg_ss
@@ -857,7 +855,7 @@ macroargs : %empty {
/* These commands start with a T_LABEL. */
assignment_directive : equ
| set
| assignment
| rb
| rw
| rl
@@ -867,10 +865,6 @@ assignment_directive : equ
directive : endc
| print
| println
| printf
| printt
| printv
| printi
| export
| db
| dw
@@ -928,12 +922,8 @@ compoundeq : T_POP_ADDEQ { $$ = RPN_ADD; }
equ : T_LABEL T_POP_EQU const { sym_AddEqu($1, $3); }
;
set : T_LABEL T_POP_EQUAL const { sym_AddVar($1, $3); }
assignment : T_LABEL T_POP_EQUAL const { sym_AddVar($1, $3); }
| T_LABEL compoundeq const { compoundAssignment($1, $2, $3); }
| T_LABEL T_POP_SET const {
warning(WARNING_OBSOLETE, "`SET` for variables is deprecated; use `=`\n");
sym_AddVar($1, $3);
}
;
equs : T_LABEL T_POP_EQUS string { sym_AddString($1, $3); }
@@ -1175,14 +1165,6 @@ def_set : def_id T_POP_EQUAL const { sym_AddVar($1, $3); }
| redef_id T_POP_EQUAL const { sym_AddVar($1, $3); }
| def_id compoundeq const { compoundAssignment($1, $2, $3); }
| redef_id compoundeq const { compoundAssignment($1, $2, $3); }
| def_id T_POP_SET const {
warning(WARNING_OBSOLETE, "`SET` for variables is deprecated; use `=`\n");
sym_AddVar($1, $3);
}
| redef_id T_POP_SET const {
warning(WARNING_OBSOLETE, "`SET` for variables is deprecated; use `=`\n");
sym_AddVar($1, $3);
}
;
def_rb : def_id T_POP_RB rs_uconst {
@@ -1296,30 +1278,6 @@ print_expr : const_no_str { printf("$%" PRIX32, $1); }
| string { printf("%s", $1); }
;
printt : T_POP_PRINTT string {
warning(WARNING_OBSOLETE, "`PRINTT` is deprecated; use `PRINT`\n");
printf("%s", $2);
}
;
printv : T_POP_PRINTV const {
warning(WARNING_OBSOLETE, "`PRINTV` is deprecated; use `PRINT`\n");
printf("$%" PRIX32, $2);
}
;
printi : T_POP_PRINTI const {
warning(WARNING_OBSOLETE, "`PRINTI` is deprecated; use `PRINT` with `STRFMT` \"%%d\"\n");
printf("%" PRId32, $2);
}
;
printf : T_POP_PRINTF const {
warning(WARNING_OBSOLETE, "`PRINTF` is deprecated; use `PRINT` with `STRFMT` \"%%f\"\n");
fix_Print($2);
}
;
const_3bit : const {
int32_t value = $1;
@@ -1770,7 +1728,6 @@ cpu_command : z80_adc
| z80_sub
| z80_swap
| z80_xor
| T_OWO { fatalerror("*BONK* go to horny jail\n"); }
;
z80_adc : T_Z80_ADC op_a_n {
@@ -1844,8 +1801,13 @@ z80_ei : T_Z80_EI { sect_AbsByte(0xFB); }
z80_halt : T_Z80_HALT {
sect_AbsByte(0x76);
if (haltnop)
if (haltnop) {
if (warnOnHaltNop) {
warnOnHaltNop = false;
warning(WARNING_OBSOLETE, "`nop` after `halt` will stop being the default; pass `-H` to opt into it\n");
}
sect_AbsByte(0x00);
}
}
;
@@ -1953,6 +1915,10 @@ z80_ld_mem : T_Z80_LD op_mem_ind T_COMMA T_MODE_SP {
| T_Z80_LD op_mem_ind T_COMMA T_MODE_A {
if (optimizeLoads && rpn_isKnown(&$2)
&& $2.val >= 0xFF00) {
if (warnOnLdOpt) {
warnOnLdOpt = false;
warning(WARNING_OBSOLETE, "ld optimization will stop being the default; pass `-l` to opt into it\n");
}
sect_AbsByte(0xE0);
sect_AbsByte($2.val & 0xFF);
rpn_Free(&$2);
@@ -2001,6 +1967,10 @@ z80_ld_a : T_Z80_LD reg_r T_COMMA c_ind {
if ($2 == REG_A) {
if (optimizeLoads && rpn_isKnown(&$4)
&& $4.val >= 0xFF00) {
if (warnOnLdOpt) {
warnOnLdOpt = false;
warning(WARNING_OBSOLETE, "ld optimization will stop being the default; pass `-l` to opt into it\n");
}
sect_AbsByte(0xF0);
sect_AbsByte($4.val & 0xFF);
rpn_Free(&$4);
@@ -2179,7 +2149,7 @@ op_a_n : reloc_8bit
| T_MODE_A T_COMMA reloc_8bit { $$ = $3; }
;
T_MODE_A : T_LPAREN T_TOKEN_A T_RPAREN
T_MODE_A : T_TOKEN_A
| T_OP_HIGH T_LPAREN T_MODE_AF T_RPAREN
;
@@ -2187,7 +2157,7 @@ T_MODE_B : T_TOKEN_B
| T_OP_HIGH T_LPAREN T_MODE_BC T_RPAREN
;
T_MODE_C : T_TOKEN_C T_CC_C T_RPAREN
T_MODE_C : T_TOKEN_C
| T_OP_LOW T_LPAREN T_MODE_BC T_RPAREN
;
@@ -2195,7 +2165,7 @@ T_MODE_D : T_TOKEN_D
| T_OP_HIGH T_LPAREN T_MODE_DE T_RPAREN
;
T_MODE_E : T_TOKEN_E T_RPAREN T_TOKEN_E_HEART
T_MODE_E : T_TOKEN_E
| T_OP_LOW T_LPAREN T_MODE_DE T_RPAREN
;
@@ -2203,19 +2173,10 @@ T_MODE_H : T_TOKEN_H
| T_OP_HIGH T_LPAREN T_MODE_HL T_RPAREN
;
T_MODE_L : T_TOKEN_L_ARM T_TOKEN_L_FACE T_TOKEN_L_BODY T_RPAREN T_TOKEN_L_LEG
T_MODE_L : T_TOKEN_L
| T_OP_LOW T_LPAREN T_MODE_HL T_RPAREN
;
T_MODE_BC : T_TOKEN_B T_TOKEN_C T_CC_C T_RPAREN
;
T_MODE_DE : T_TOKEN_D T_TOKEN_E T_RPAREN T_TOKEN_E_HEART
;
T_MODE_HL : T_MODE_HL_START T_TOKEN_L_FACE T_TOKEN_L_BODY T_RPAREN T_TOKEN_L_LEG
;
ccode_expr : ccode
| T_OP_LOGICNOT ccode_expr {
$$ = $2 ^ 1;
@@ -2225,7 +2186,7 @@ ccode_expr : ccode
ccode : T_CC_NZ { $$ = CC_NZ; }
| T_CC_Z { $$ = CC_Z; }
| T_CC_NC { $$ = CC_NC; }
| T_CC_C { $$ = CC_C; }
| T_TOKEN_C { $$ = CC_C; }
;
reg_r : T_MODE_B { $$ = REG_B; }
@@ -2241,7 +2202,7 @@ reg_r : T_MODE_B { $$ = REG_B; }
reg_tt : T_MODE_BC { $$ = REG_BC; }
| T_MODE_DE { $$ = REG_DE; }
| T_MODE_HL { $$ = REG_HL; }
| T_LPAREN T_TOKEN_A T_RPAREN T_TOKEN_F { $$ = REG_AF; }
| T_MODE_AF { $$ = REG_AF; }
;
reg_ss : T_MODE_BC { $$ = REG_BC; }

View File

@@ -39,6 +39,8 @@
int size = snprintf(_expr->reason, 128, __VA_ARGS__); \
if (size >= 128) { /* If this wasn't enough, try again */ \
_expr->reason = realloc(_expr->reason, size + 1); \
if (!_expr->reason) \
fatalerror("Can't allocate err string: %s\n", strerror(errno)); \
sprintf(_expr->reason, __VA_ARGS__); \
} \
} while (0)

View File

@@ -743,8 +743,13 @@ void sym_Init(time_t now)
sym_AddVar("_RS", 0)->isBuiltin = true;
#define addNumber(name, val) sym_AddEqu(name, val)->isBuiltin = true
#define addString(name, val) sym_AddString(name, val)->isBuiltin = true
#define addSym(fn, name, val) do { \
struct Symbol *sym = fn(name, val); \
assert(sym); \
sym->isBuiltin = true; \
} while (0)
#define addNumber(name, val) addSym(sym_AddEqu, name, val)
#define addString(name, val) addSym(sym_AddString, name, val)
addString("__RGBDS_VERSION__", get_package_version_string());
addNumber("__RGBDS_MAJOR__", PACKAGE_VERSION_MAJOR);
@@ -788,16 +793,7 @@ void sym_Init(time_t now)
#undef addNumber
#undef addString
#undef addSym
sym_SetCurrentSymbolScope(NULL);
anonLabelID = 0;
/* _PI is deprecated */
struct Symbol *_PISymbol = createBuiltinSymbol("_PI");
_PISymbol->type = SYM_EQU;
_PISymbol->src = NULL;
_PISymbol->fileLine = 0;
_PISymbol->hasCallback = true;
_PISymbol->numCallback = fix_Callback_PI;
}
anonLabelID = 0;}

View File

@@ -1,4 +1,4 @@
#!/bin/sh
#!/usr/bin/env bash
bison -V | awk -v major="$1" -v minor="$2" '
/^bison.*[0-9]+(\.[0-9]+)(\.[0-9]+)?$/ {
match($0, /[0-9]+(\.[0-9]+)(\.[0-9]+)?$/);

View File

@@ -1,385 +0,0 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2013-2018, stag019 and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include "gfx/gb.h"
void transpose_tiles(struct GBImage *gb, int width)
{
uint8_t *newdata;
int i;
int newbyte;
newdata = calloc(gb->size, 1);
if (!newdata)
err("%s: Failed to allocate memory for new data", __func__);
for (i = 0; i < gb->size; i++) {
newbyte = i / (8 * depth) * width * 8 * depth;
newbyte = newbyte % gb->size
+ 8 * depth * (newbyte / gb->size)
+ i % (8 * depth);
newdata[newbyte] = gb->data[i];
}
free(gb->data);
gb->data = newdata;
}
void raw_to_gb(const struct RawIndexedImage *raw_image, struct GBImage *gb)
{
uint8_t index;
for (unsigned int y = 0; y < raw_image->height; y++) {
for (unsigned int x = 0; x < raw_image->width; x++) {
index = raw_image->data[y][x];
index &= (1 << depth) - 1;
unsigned int byte = y * depth
+ x / 8 * raw_image->height / 8 * 8 * depth;
gb->data[byte] |= (index & 1) << (7 - x % 8);
if (depth == 2) {
gb->data[byte + 1] |=
(index >> 1) << (7 - x % 8);
}
}
}
if (!gb->horizontal)
transpose_tiles(gb, raw_image->width / 8);
}
void output_file(const struct Options *opts, const struct GBImage *gb)
{
FILE *f;
f = fopen(opts->outfile, "wb");
if (!f)
err("%s: Opening output file '%s' failed", __func__,
opts->outfile);
fwrite(gb->data, 1, gb->size - gb->trim * 8 * depth, f);
fclose(f);
}
int get_tile_index(uint8_t *tile, uint8_t **tiles, int num_tiles, int tile_size)
{
int i, j;
for (i = 0; i < num_tiles; i++) {
for (j = 0; j < tile_size; j++) {
if (tile[j] != tiles[i][j])
break;
}
if (j >= tile_size)
return i;
}
return -1;
}
uint8_t reverse_bits(uint8_t b)
{
uint8_t rev = 0;
rev |= (b & 0x80) >> 7;
rev |= (b & 0x40) >> 5;
rev |= (b & 0x20) >> 3;
rev |= (b & 0x10) >> 1;
rev |= (b & 0x08) << 1;
rev |= (b & 0x04) << 3;
rev |= (b & 0x02) << 5;
rev |= (b & 0x01) << 7;
return rev;
}
void xflip(uint8_t *tile, uint8_t *tile_xflip, int tile_size)
{
int i;
for (i = 0; i < tile_size; i++)
tile_xflip[i] = reverse_bits(tile[i]);
}
void yflip(uint8_t *tile, uint8_t *tile_yflip, int tile_size)
{
int i;
for (i = 0; i < tile_size; i++)
tile_yflip[i] = tile[(tile_size - i - 1) ^ (depth - 1)];
}
/*
* get_mirrored_tile_index looks for `tile` in tile array `tiles`, also
* checking x-, y-, and xy-mirrored versions of `tile`. If one is found,
* `*flags` is set according to the type of mirroring and the index of the
* matched tile is returned. If no match is found, -1 is returned.
*/
int get_mirrored_tile_index(uint8_t *tile, uint8_t **tiles, int num_tiles,
int tile_size, int *flags)
{
int index;
uint8_t *tile_xflip;
uint8_t *tile_yflip;
index = get_tile_index(tile, tiles, num_tiles, tile_size);
if (index >= 0) {
*flags = 0;
return index;
}
tile_yflip = malloc(tile_size);
if (!tile_yflip)
err("%s: Failed to allocate memory for Y flip of tile",
__func__);
yflip(tile, tile_yflip, tile_size);
index = get_tile_index(tile_yflip, tiles, num_tiles, tile_size);
if (index >= 0) {
*flags = YFLIP;
free(tile_yflip);
return index;
}
tile_xflip = malloc(tile_size);
if (!tile_xflip)
err("%s: Failed to allocate memory for X flip of tile",
__func__);
xflip(tile, tile_xflip, tile_size);
index = get_tile_index(tile_xflip, tiles, num_tiles, tile_size);
if (index >= 0) {
*flags = XFLIP;
free(tile_yflip);
free(tile_xflip);
return index;
}
yflip(tile_xflip, tile_yflip, tile_size);
index = get_tile_index(tile_yflip, tiles, num_tiles, tile_size);
if (index >= 0)
*flags = XFLIP | YFLIP;
free(tile_yflip);
free(tile_xflip);
return index;
}
void create_mapfiles(const struct Options *opts, struct GBImage *gb,
struct Mapfile *tilemap, struct Mapfile *attrmap)
{
int i, j;
int gb_i;
int tile_size;
int max_tiles;
int num_tiles;
int index;
int flags;
int gb_size;
uint8_t *tile;
uint8_t **tiles;
tile_size = sizeof(*tile) * depth * 8;
gb_size = gb->size - (gb->trim * tile_size);
max_tiles = gb_size / tile_size;
/* If the input image doesn't fill the last tile, increase the count. */
if (gb_size > max_tiles * tile_size)
max_tiles++;
tiles = calloc(max_tiles, sizeof(*tiles));
if (!tiles)
err("%s: Failed to allocate memory for tiles", __func__);
num_tiles = 0;
if (*opts->tilemapfile) {
tilemap->data = calloc(max_tiles, sizeof(*tilemap->data));
if (!tilemap->data)
err("%s: Failed to allocate memory for tilemap data",
__func__);
tilemap->size = 0;
}
if (*opts->attrmapfile) {
attrmap->data = calloc(max_tiles, sizeof(*attrmap->data));
if (!attrmap->data)
err("%s: Failed to allocate memory for attrmap data",
__func__);
attrmap->size = 0;
}
gb_i = 0;
while (gb_i < gb_size) {
flags = 0;
tile = malloc(tile_size);
if (!tile)
err("%s: Failed to allocate memory for tile",
__func__);
/*
* If the input image doesn't fill the last tile,
* `gb_i` will reach `gb_size`.
*/
for (i = 0; i < tile_size && gb_i < gb_size; i++) {
tile[i] = gb->data[gb_i];
gb_i++;
}
if (opts->unique) {
if (opts->mirror) {
index = get_mirrored_tile_index(tile, tiles,
num_tiles,
tile_size,
&flags);
} else {
index = get_tile_index(tile, tiles, num_tiles,
tile_size);
}
if (index < 0) {
index = num_tiles;
tiles[num_tiles] = tile;
num_tiles++;
} else {
free(tile);
}
} else {
index = num_tiles;
tiles[num_tiles] = tile;
num_tiles++;
}
if (*opts->tilemapfile) {
tilemap->data[tilemap->size] = index;
tilemap->size++;
}
if (*opts->attrmapfile) {
attrmap->data[attrmap->size] = flags;
attrmap->size++;
}
}
if (opts->unique) {
free(gb->data);
gb->data = malloc(tile_size * num_tiles);
if (!gb->data)
err("%s: Failed to allocate memory for tile data",
__func__);
for (i = 0; i < num_tiles; i++) {
tile = tiles[i];
for (j = 0; j < tile_size; j++)
gb->data[i * tile_size + j] = tile[j];
}
gb->size = i * tile_size;
}
for (i = 0; i < num_tiles; i++)
free(tiles[i]);
free(tiles);
}
void output_tilemap_file(const struct Options *opts,
const struct Mapfile *tilemap)
{
FILE *f;
f = fopen(opts->tilemapfile, "wb");
if (!f)
err("%s: Opening tilemap file '%s' failed", __func__,
opts->tilemapfile);
fwrite(tilemap->data, 1, tilemap->size, f);
fclose(f);
if (opts->tilemapout)
free(opts->tilemapfile);
}
void output_attrmap_file(const struct Options *opts,
const struct Mapfile *attrmap)
{
FILE *f;
f = fopen(opts->attrmapfile, "wb");
if (!f)
err("%s: Opening attrmap file '%s' failed", __func__,
opts->attrmapfile);
fwrite(attrmap->data, 1, attrmap->size, f);
fclose(f);
if (opts->attrmapout)
free(opts->attrmapfile);
}
/*
* based on the Gaussian-like curve used by SameBoy since commit
* 65dd02cc52f531dbbd3a7e6014e99d5b24e71a4c (Oct 2017)
* with ties resolved by comparing the difference of the squares.
*/
static int reverse_curve[] = {
0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4,
5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7,
7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13,
13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18,
18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24,
24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26,
26, 27, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 30, 30, 31,
};
void output_palette_file(const struct Options *opts,
const struct RawIndexedImage *raw_image)
{
FILE *f;
int i, color;
uint8_t cur_bytes[2];
f = fopen(opts->palfile, "wb");
if (!f)
err("%s: Opening palette file '%s' failed", __func__,
opts->palfile);
for (i = 0; i < raw_image->num_colors; i++) {
int r = raw_image->palette[i].red;
int g = raw_image->palette[i].green;
int b = raw_image->palette[i].blue;
if (opts->colorcurve) {
g = (g * 4 - b) / 3;
if (g < 0)
g = 0;
r = reverse_curve[r];
g = reverse_curve[g];
b = reverse_curve[b];
} else {
r >>= 3;
g >>= 3;
b >>= 3;
}
color = b << 10 | g << 5 | r;
cur_bytes[0] = color & 0xFF;
cur_bytes[1] = color >> 8;
fwrite(cur_bytes, 2, 1, f);
}
fclose(f);
if (opts->palout)
free(opts->palfile);
}

View File

@@ -1,358 +0,0 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2013-2018, stag019 and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#include <png.h>
#include <stdlib.h>
#include <string.h>
#include "gfx/main.h"
#include "extern/getopt.h"
#include "version.h"
int depth, colors;
/* Short options */
static char const *optstring = "Aa:CDd:Ffhmo:Pp:Tt:uVvx:";
/*
* Equivalent long options
* Please keep in the same order as short opts
*
* Also, make sure long opts don't create ambiguity:
* A long opt's name should start with the same letter as its short opt,
* except if it doesn't create any ambiguity (`verbose` versus `version`).
* This is because long opt matching, even to a single char, is prioritized
* over short opt matching
*/
static struct option const longopts[] = {
{ "output-attr-map", no_argument, NULL, 'A' },
{ "attr-map", required_argument, NULL, 'a' },
{ "color-curve", no_argument, NULL, 'C' },
{ "debug", no_argument, NULL, 'D' },
{ "depth", required_argument, NULL, 'd' },
{ "fix", no_argument, NULL, 'f' },
{ "fix-and-save", no_argument, NULL, 'F' },
{ "horizontal", no_argument, NULL, 'h' },
{ "mirror-tiles", no_argument, NULL, 'm' },
{ "output", required_argument, NULL, 'o' },
{ "output-palette", no_argument, NULL, 'P' },
{ "palette", required_argument, NULL, 'p' },
{ "output-tilemap", no_argument, NULL, 'T' },
{ "tilemap", required_argument, NULL, 't' },
{ "unique-tiles", no_argument, NULL, 'u' },
{ "version", no_argument, NULL, 'V' },
{ "verbose", no_argument, NULL, 'v' },
{ "trim-end", required_argument, NULL, 'x' },
{ NULL, no_argument, NULL, 0 }
};
static void print_usage(void)
{
fputs(
"Usage: rgbgfx [-CDhmuVv] [-f | -F] [-a <attr_map> | -A] [-d <depth>]\n"
" [-o <out_file>] [-p <pal_file> | -P] [-t <tile_map> | -T]\n"
" [-x <tiles>] <file>\n"
"Useful options:\n"
" -f, --fix make the input image an indexed PNG\n"
" -m, --mirror-tiles optimize out mirrored tiles\n"
" -o, --output <path> set the output binary file\n"
" -t, --tilemap <path> set the output tilemap file\n"
" -u, --unique-tiles optimize out identical tiles\n"
" -V, --version print RGBGFX version and exit\n"
"\n"
"For help, use `man rgbgfx' or go to https://rgbds.gbdev.io/docs/\n",
stderr);
exit(1);
}
int main(int argc, char *argv[])
{
int ch, size;
struct Options opts = {0};
struct ImageOptions png_options = {0};
struct RawIndexedImage *raw_image;
struct GBImage gb = {0};
struct Mapfile tilemap = {0};
struct Mapfile attrmap = {0};
char *ext;
opts.tilemapfile = "";
opts.attrmapfile = "";
opts.palfile = "";
opts.outfile = "";
depth = 2;
while ((ch = musl_getopt_long_only(argc, argv, optstring, longopts,
NULL)) != -1) {
switch (ch) {
case 'A':
opts.attrmapout = true;
break;
case 'a':
opts.attrmapfile = musl_optarg;
break;
case 'C':
opts.colorcurve = true;
break;
case 'D':
opts.debug = true;
break;
case 'd':
depth = strtoul(musl_optarg, NULL, 0);
break;
case 'F':
opts.hardfix = true;
/* fallthrough */
case 'f':
opts.fix = true;
break;
case 'h':
opts.horizontal = true;
break;
case 'm':
opts.mirror = true;
opts.unique = true;
break;
case 'o':
opts.outfile = musl_optarg;
break;
case 'P':
opts.palout = true;
break;
case 'p':
opts.palfile = musl_optarg;
break;
case 'T':
opts.tilemapout = true;
break;
case 't':
opts.tilemapfile = musl_optarg;
break;
case 'u':
opts.unique = true;
break;
case 'V':
printf("rgbgfx %s\n", get_package_version_string());
exit(0);
case 'v':
opts.verbose = true;
break;
case 'x':
opts.trim = strtoul(musl_optarg, NULL, 0);
break;
default:
print_usage();
/* NOTREACHED */
}
}
argc -= musl_optind;
argv += musl_optind;
if (argc == 0) {
fputs("FATAL: no input files\n", stderr);
print_usage();
}
#define WARN_MISMATCH(property) \
warnx("The PNG's " property \
" setting doesn't match the one defined on the command line")
opts.infile = argv[argc - 1];
if (depth != 1 && depth != 2)
errx("Depth option must be either 1 or 2.");
colors = 1 << depth;
raw_image = input_png_file(&opts, &png_options);
png_options.tilemapfile = "";
png_options.attrmapfile = "";
png_options.palfile = "";
if (png_options.horizontal != opts.horizontal) {
if (opts.verbose)
WARN_MISMATCH("horizontal");
if (opts.hardfix)
png_options.horizontal = opts.horizontal;
}
if (png_options.horizontal)
opts.horizontal = png_options.horizontal;
if (png_options.trim != opts.trim) {
if (opts.verbose)
WARN_MISMATCH("trim");
if (opts.hardfix)
png_options.trim = opts.trim;
}
if (png_options.trim)
opts.trim = png_options.trim;
if (raw_image->width % 8) {
errx("Input PNG file %s not sized correctly. The image's width must be a multiple of 8.",
opts.infile);
}
if (raw_image->width / 8 > 1 && raw_image->height % 8) {
errx("Input PNG file %s not sized correctly. If the image is more than 1 tile wide, its height must be a multiple of 8.",
opts.infile);
}
if (opts.trim &&
opts.trim > (raw_image->width / 8) * (raw_image->height / 8) - 1) {
errx("Trim (%d) for input raw_image file '%s' too large (max: %u)",
opts.trim, opts.infile,
(raw_image->width / 8) * (raw_image->height / 8) - 1);
}
if (strcmp(png_options.tilemapfile, opts.tilemapfile) != 0) {
if (opts.verbose)
WARN_MISMATCH("tilemap file");
if (opts.hardfix)
png_options.tilemapfile = opts.tilemapfile;
}
if (!*opts.tilemapfile)
opts.tilemapfile = png_options.tilemapfile;
if (png_options.tilemapout != opts.tilemapout) {
if (opts.verbose)
WARN_MISMATCH("tilemap file");
if (opts.hardfix)
png_options.tilemapout = opts.tilemapout;
}
if (png_options.tilemapout)
opts.tilemapout = png_options.tilemapout;
if (strcmp(png_options.attrmapfile, opts.attrmapfile) != 0) {
if (opts.verbose)
WARN_MISMATCH("attrmap file");
if (opts.hardfix)
png_options.attrmapfile = opts.attrmapfile;
}
if (!*opts.attrmapfile)
opts.attrmapfile = png_options.attrmapfile;
if (png_options.attrmapout != opts.attrmapout) {
if (opts.verbose)
WARN_MISMATCH("attrmap file");
if (opts.hardfix)
png_options.attrmapout = opts.attrmapout;
}
if (png_options.attrmapout)
opts.attrmapout = png_options.attrmapout;
if (strcmp(png_options.palfile, opts.palfile) != 0) {
if (opts.verbose)
WARN_MISMATCH("palette file");
if (opts.hardfix)
png_options.palfile = opts.palfile;
}
if (!*opts.palfile)
opts.palfile = png_options.palfile;
if (png_options.palout != opts.palout) {
if (opts.verbose)
WARN_MISMATCH("palette file");
if (opts.hardfix)
png_options.palout = opts.palout;
}
#undef WARN_MISMATCH
if (png_options.palout)
opts.palout = png_options.palout;
if (!*opts.tilemapfile && opts.tilemapout) {
ext = strrchr(opts.infile, '.');
if (ext != NULL) {
size = ext - opts.infile + 9;
opts.tilemapfile = malloc(size);
strncpy(opts.tilemapfile, opts.infile, size);
*strrchr(opts.tilemapfile, '.') = '\0';
strcat(opts.tilemapfile, ".tilemap");
} else {
opts.tilemapfile = malloc(strlen(opts.infile) + 9);
strcpy(opts.tilemapfile, opts.infile);
strcat(opts.tilemapfile, ".tilemap");
}
}
if (!*opts.attrmapfile && opts.attrmapout) {
ext = strrchr(opts.infile, '.');
if (ext != NULL) {
size = ext - opts.infile + 9;
opts.attrmapfile = malloc(size);
strncpy(opts.attrmapfile, opts.infile, size);
*strrchr(opts.attrmapfile, '.') = '\0';
strcat(opts.attrmapfile, ".attrmap");
} else {
opts.attrmapfile = malloc(strlen(opts.infile) + 9);
strcpy(opts.attrmapfile, opts.infile);
strcat(opts.attrmapfile, ".attrmap");
}
}
if (!*opts.palfile && opts.palout) {
ext = strrchr(opts.infile, '.');
if (ext != NULL) {
size = ext - opts.infile + 5;
opts.palfile = malloc(size);
strncpy(opts.palfile, opts.infile, size);
*strrchr(opts.palfile, '.') = '\0';
strcat(opts.palfile, ".pal");
} else {
opts.palfile = malloc(strlen(opts.infile) + 5);
strcpy(opts.palfile, opts.infile);
strcat(opts.palfile, ".pal");
}
}
gb.size = raw_image->width * raw_image->height * depth / 8;
gb.data = calloc(gb.size, 1);
gb.trim = opts.trim;
gb.horizontal = opts.horizontal;
if (*opts.outfile || *opts.tilemapfile || *opts.attrmapfile) {
raw_to_gb(raw_image, &gb);
create_mapfiles(&opts, &gb, &tilemap, &attrmap);
}
if (*opts.outfile)
output_file(&opts, &gb);
if (*opts.tilemapfile)
output_tilemap_file(&opts, &tilemap);
if (*opts.attrmapfile)
output_attrmap_file(&opts, &attrmap);
if (*opts.palfile)
output_palette_file(&opts, raw_image);
if (opts.fix || opts.debug)
output_png_file(&opts, &png_options, raw_image);
destroy_raw_image(&raw_image);
free(gb.data);
return 0;
}

813
src/gfx/main.cpp Normal file
View File

@@ -0,0 +1,813 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#include "gfx/main.hpp"
#include <algorithm>
#include <assert.h>
#include <cinttypes>
#include <cstdint>
#include <ctype.h>
#include <fstream>
#include <ios>
#include <limits>
#include <numeric>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string_view>
#include "extern/getopt.h"
#include "platform.h"
#include "version.h"
#include "gfx/pal_spec.hpp"
#include "gfx/process.hpp"
#include "gfx/reverse.hpp"
using namespace std::literals::string_view_literals;
Options options;
char const *externalPalSpec = nullptr;
static uintmax_t nbErrors;
[[noreturn]] void giveUp() {
fprintf(stderr, "Conversion aborted after %ju error%s\n", nbErrors, nbErrors == 1 ? "" : "s");
exit(1);
}
void warning(char const *fmt, ...) {
va_list ap;
fputs("warning: ", stderr);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
putc('\n', stderr);
}
void error(char const *fmt, ...) {
va_list ap;
fputs("error: ", stderr);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
putc('\n', stderr);
if (nbErrors != std::numeric_limits<decltype(nbErrors)>::max())
nbErrors++;
}
[[noreturn]] void fatal(char const *fmt, ...) {
va_list ap;
fputs("FATAL: ", stderr);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
putc('\n', stderr);
if (nbErrors != std::numeric_limits<decltype(nbErrors)>::max())
nbErrors++;
giveUp();
}
void Options::verbosePrint(uint8_t level, char const *fmt, ...) const {
if (verbosity >= level) {
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}
}
// Short options
static char const *optstring = "-Aa:b:Cc:Dd:FfhL:mN:n:o:Pp:Qq:r:s:Tt:U:uVvx:Z";
/*
* Equivalent long options
* Please keep in the same order as short opts
*
* Also, make sure long opts don't create ambiguity:
* A long opt's name should start with the same letter as its short opt,
* except if it doesn't create any ambiguity (`verbose` versus `version`).
* This is because long opt matching, even to a single char, is prioritized
* over short opt matching
*/
static struct option const longopts[] = {
{"output-attr-map", no_argument, NULL, 'A'},
{"attr-map", required_argument, NULL, 'a'},
{"base-tiles", required_argument, NULL, 'b'},
{"color-curve", no_argument, NULL, 'C'},
{"colors", required_argument, NULL, 'c'},
{"debug", no_argument, NULL, 'D'}, // Ignored
{"depth", required_argument, NULL, 'd'},
{"fix", no_argument, NULL, 'f'},
{"fix-and-save", no_argument, NULL, 'F'}, // Deprecated
{"horizontal", no_argument, NULL, 'h'}, // Deprecated
{"slice", required_argument, NULL, 'L'},
{"mirror-tiles", no_argument, NULL, 'm'},
{"nb-tiles", required_argument, NULL, 'N'},
{"nb-palettes", required_argument, NULL, 'n'},
{"output", required_argument, NULL, 'o'},
{"output-palette", no_argument, NULL, 'P'},
{"palette", required_argument, NULL, 'p'},
{"output-palette-map", no_argument, NULL, 'Q'},
{"palette-map", required_argument, NULL, 'q'},
{"reverse", required_argument, NULL, 'r'},
{"output-tilemap", no_argument, NULL, 'T'},
{"tilemap", required_argument, NULL, 't'},
{"unit-size", required_argument, NULL, 'U'},
{"unique-tiles", no_argument, NULL, 'u'},
{"version", no_argument, NULL, 'V'},
{"verbose", no_argument, NULL, 'v'},
{"trim-end", required_argument, NULL, 'x'},
{"columns", no_argument, NULL, 'Z'},
{NULL, no_argument, NULL, 0 }
};
static void printUsage(void) {
fputs("Usage: rgbgfx [-r stride] [-CmuVZ] [-v [-v ...]] [-a <attr_map> | -A]\n"
" [-b <base_ids>] [-c <colors>] [-d <depth>] [-L <slice>] [-N <nb_tiles>]\n"
" [-n <nb_pals>] [-o <out_file>] [-p <pal_file> | -P] [-q <pal_map> | -Q]\n"
" [-s <nb_colors>] [-t <tile_map> | -T] [-x <nb_tiles>] <file>\n"
"Useful options:\n"
" -m, --mirror-tiles optimize out mirrored tiles\n"
" -o, --output <path> output the tile data to this path\n"
" -t, --tilemap <path> output the tile map to this path\n"
" -u, --unique-tiles optimize out identical tiles\n"
" -V, --version print RGBGFX version and exit\n"
"\n"
"For help, use `man rgbgfx' or go to https://rgbds.gbdev.io/docs/\n",
stderr);
exit(1);
}
/**
* Parses a number at the beginning of a string, moving the pointer to skip the parsed characters
* Returns the provided errVal on error
*/
static uint16_t parseNumber(char *&string, char const *errPrefix, uint16_t errVal = UINT16_MAX) {
uint8_t base = 10;
if (*string == '\0') {
error("%s: expected number, but found nothing", errPrefix);
return errVal;
} else if (*string == '$') {
base = 16;
++string;
} else if (*string == '%') {
base = 2;
++string;
} else if (*string == '0' && string[1] != '\0') {
// Check if we have a "0x" or "0b" here
if (string[1] == 'x' || string[1] == 'X') {
base = 16;
string += 2;
} else if (string[1] == 'b' || string[1] == 'B') {
base = 2;
string += 2;
}
}
/**
* Turns a digit into its numeric value in the current base, if it has one.
* Maximum is inclusive. The string_view is modified to "consume" all digits.
* Returns 255 on parse failure (including wrong char for base), in which case
* the string_view may be pointing on garbage.
*/
auto charIndex = [&base](unsigned char c) -> uint8_t {
unsigned char index = c - '0'; // Use wrapping semantics
if (base == 2 && index >= 2) {
return 255;
} else if (index < 10) {
return index;
} else if (base != 16) {
return 255; // Letters are only valid in hex
}
index = tolower(c) - 'a'; // OK because we pass an `unsigned char`
if (index < 6) {
return index + 10;
}
return 255;
};
if (charIndex(*string) == 255) {
error("%s: expected digit%s, but found nothing", errPrefix,
base != 10 ? " after base" : "");
return errVal;
}
uint16_t number = 0;
do {
// Read a character, and check if it's valid in the given base
uint8_t index = charIndex(*string);
if (index == 255) {
break; // Found an invalid character, end
}
++string;
number *= base;
number += index;
// The lax check covers the addition on top of the multiplication
if (number >= UINT16_MAX / base) {
error("%s: the number is too large!", errPrefix);
return errVal;
}
} while (*string != '\0'); // No more characters?
return number;
}
static void skipWhitespace(char *&arg) {
arg += strspn(arg, " \t");
}
static void registerInput(char const *arg) {
if (!options.input.empty()) {
fprintf(stderr,
"FATAL: input image specified more than once! (first \"%s\", then "
"\"%s\")\n",
options.input.c_str(), arg);
printUsage();
exit(1);
} else if (arg[0] == '\0') { // Empty input path
fprintf(stderr, "FATAL: input image path cannot be empty!\n");
printUsage();
exit(1);
} else {
options.input = arg;
}
}
/**
* Turn an "at-file"'s contents into an argv that `getopt` can handle
* @param argPool Argument characters will be appended to this vector, for storage purposes.
*/
static std::vector<size_t> readAtFile(std::string const &path, std::vector<char> &argPool) {
std::filebuf file;
file.open(path, std::ios_base::in);
static_assert(decltype(file)::traits_type::eof() == EOF,
"isblank(char_traits<...>::eof()) is UB!");
std::vector<size_t> argvOfs;
for (;;) {
int c;
// First, discard any leading whitespace
do {
c = file.sbumpc();
if (c == EOF) {
return argvOfs;
}
} while (isblank(c));
switch (c) {
case '#': // If it's a comment, discard everything until EOL
while ((c = file.sbumpc()) != '\n') {
if (c == EOF) {
return argvOfs;
}
}
continue; // Start processing the next line
// If it's an empty line, ignore it
case '\r': // Assuming CRLF here
file.sbumpc(); // Discard the upcoming '\n'
[[fallthrough]];
case '\n':
continue; // Start processing the next line
}
// Alright, now we can parse the line
do {
// Read one argument (until the next whitespace char).
// We know there is one because we already have its first character in `c`.
argvOfs.push_back(argPool.size());
// Reading and appending characters one at a time may be inefficient, but I'm counting
// on `vector` and `sbumpc` to do the right thing here.
argPool.push_back(c); // Push the character we've already read
for (;;) {
c = file.sbumpc();
if (isblank(c) || c == '\n' || c == EOF) {
break;
} else if (c == '\r') {
file.sbumpc(); // Discard the '\n'
break;
}
argPool.push_back(c);
}
argPool.push_back('\0');
// Discard whitespace until the next argument (candidate)
while (isblank(c)) {
c = file.sbumpc();
}
if (c == '\r') {
c = file.sbumpc(); // Skip the '\n'
}
} while (c != '\n' && c != EOF); // End if we reached EOL
}
}
/**
* Parses an arg vector, modifying `options` as options are read.
* The three booleans are for the "auto path" flags, since their processing must be deferred to the
* end of option parsing.
*
* Returns NULL if the vector was fully parsed, or a pointer (which is part of the arg vector) to an
* "at-file" path if one is encountered.
*/
static char *parseArgv(int argc, char **argv, bool &autoAttrmap, bool &autoTilemap,
bool &autoPalettes, bool &autoPalmap) {
int opt;
while ((opt = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1) {
char *arg = musl_optarg; // Make a copy for scanning
switch (opt) {
case 'A':
autoAttrmap = true;
break;
case 'a':
autoAttrmap = false;
options.attrmap = musl_optarg;
break;
case 'b':
options.baseTileIDs[0] = parseNumber(arg, "Bank 0 base tile ID", 0);
if (options.baseTileIDs[0] >= 256) {
error("Bank 0 base tile ID must be below 256");
}
if (*arg == '\0') {
options.baseTileIDs[1] = 0;
break;
}
skipWhitespace(arg);
if (*arg != ',') {
error("Base tile IDs must be one or two comma-separated numbers, not \"%s\"",
musl_optarg);
break;
}
++arg; // Skip comma
skipWhitespace(arg);
options.baseTileIDs[1] = parseNumber(arg, "Bank 1 base tile ID", 0);
if (options.baseTileIDs[1] >= 256) {
error("Bank 1 base tile ID must be below 256");
}
if (*arg != '\0') {
error("Base tile IDs must be one or two comma-separated numbers, not \"%s\"",
musl_optarg);
break;
}
break;
case 'C':
options.useColorCurve = true;
break;
case 'c':
if (musl_optarg[0] == '#') {
options.palSpecType = Options::EXPLICIT;
parseInlinePalSpec(musl_optarg);
} else if (strcasecmp(musl_optarg, "embedded") == 0) {
// Use PLTE, error out if missing
options.palSpecType = Options::EMBEDDED;
} else {
options.palSpecType = Options::EXPLICIT;
// Can't parse the file yet, as "flat" color collections need to know the palette
// size to be split; thus, we defer that
// TODO: this does not validate the `fmt` part of any external spec but the last
// one, but I guess that's okay
externalPalSpec = musl_optarg;
}
break;
case 'd':
options.bitDepth = parseNumber(arg, "Bit depth", 2);
if (*arg != '\0') {
error("Bit depth (-b) argument must be a valid number, not \"%s\"", musl_optarg);
} else if (options.bitDepth != 1 && options.bitDepth != 2) {
error("Bit depth must be 1 or 2, not %" PRIu8);
options.bitDepth = 2;
}
break;
case 'L':
options.inputSlice.left = parseNumber(arg, "Input slice left coordinate");
if (options.inputSlice.left > INT16_MAX) {
error("Input slice left coordinate is out of range!");
break;
}
skipWhitespace(arg);
if (*arg != ',') {
error("Missing comma after left coordinate in \"%s\"", musl_optarg);
break;
}
++arg;
skipWhitespace(arg);
options.inputSlice.top = parseNumber(arg, "Input slice upper coordinate");
skipWhitespace(arg);
if (*arg != ':') {
error("Missing colon after upper coordinate in \"%s\"", musl_optarg);
break;
}
++arg;
skipWhitespace(arg);
options.inputSlice.width = parseNumber(arg, "Input slice width");
skipWhitespace(arg);
if (options.inputSlice.width == 0) {
error("Input slice width may not be 0!");
}
if (*arg != ',') {
error("Missing comma after width in \"%s\"", musl_optarg);
break;
}
++arg;
skipWhitespace(arg);
options.inputSlice.height = parseNumber(arg, "Input slice height");
if (options.inputSlice.height == 0) {
error("Input slice height may not be 0!");
}
if (*arg != '\0') {
error("Unexpected extra characters after slice spec in \"%s\"", musl_optarg);
}
break;
case 'm':
options.allowMirroring = true;
[[fallthrough]]; // Imply `-u`
case 'u':
options.allowDedup = true;
break;
case 'N':
options.maxNbTiles[0] = parseNumber(arg, "Number of tiles in bank 0", 256);
if (options.maxNbTiles[0] > 256) {
error("Bank 0 cannot contain more than 256 tiles");
}
if (*arg == '\0') {
options.maxNbTiles[1] = 0;
break;
}
skipWhitespace(arg);
if (*arg != ',') {
error("Bank capacity must be one or two comma-separated numbers, not \"%s\"",
musl_optarg);
break;
}
++arg; // Skip comma
skipWhitespace(arg);
options.maxNbTiles[1] = parseNumber(arg, "Number of tiles in bank 1", 256);
if (options.maxNbTiles[1] > 256) {
error("Bank 1 cannot contain more than 256 tiles");
}
if (*arg != '\0') {
error("Bank capacity must be one or two comma-separated numbers, not \"%s\"",
musl_optarg);
break;
}
break;
case 'n':
options.nbPalettes = parseNumber(arg, "Number of palettes", 256);
if (*arg != '\0') {
error("Number of palettes (-n) must be a valid number, not \"%s\"", musl_optarg);
}
if (options.nbPalettes > 256) {
error("Number of palettes (-n) must not exceed 256!");
} else if (options.nbPalettes == 0) {
error("Number of palettes (-n) may not be 0!");
}
break;
case 'o':
options.output = musl_optarg;
break;
case 'P':
autoPalettes = true;
break;
case 'p':
autoPalettes = false;
options.palettes = musl_optarg;
break;
case 'Q':
autoPalmap = true;
break;
case 'q':
autoPalmap = false;
options.palmap = musl_optarg;
break;
case 'r':
options.reversedWidth = parseNumber(arg, "Reversed image stride");
if (*arg != '\0') {
error("Reversed image stride (-r) must be a valid number, not \"%s\"", musl_optarg);
}
if (options.reversedWidth == 0) {
error("Reversed image stride (-r) may not be 0!");
}
break;
case 's':
options.nbColorsPerPal = parseNumber(arg, "Number of colors per palette", 4);
if (*arg != '\0') {
error("Palette size (-s) must be a valid number, not \"%s\"", musl_optarg);
}
if (options.nbColorsPerPal > 4) {
error("Palette size (-s) must not exceed 4!");
} else if (options.nbColorsPerPal == 0) {
error("Palette size (-s) may not be 0!");
}
break;
case 'T':
autoTilemap = true;
break;
case 't':
autoTilemap = false;
options.tilemap = musl_optarg;
break;
case 'V':
printf("rgbgfx %s\n", get_package_version_string());
exit(0);
case 'v':
if (options.verbosity < Options::VERB_VVVVVV) {
++options.verbosity;
}
break;
case 'x':
options.trim = parseNumber(arg, "Number of tiles to trim", 0);
if (*arg != '\0') {
error("Tile trim (-x) argument must be a valid number, not \"%s\"", musl_optarg);
}
break;
case 'h':
warning("`-h` is deprecated, use `-Z` instead");
[[fallthrough]];
case 'Z':
options.columnMajor = true;
break;
case 1: // Positional argument, requested by leading `-` in opt string
if (musl_optarg[0] == '@') {
// Instruct the caller to process that at-file
return &musl_optarg[1];
} else {
registerInput(musl_optarg);
}
break;
case 'D':
case 'F':
case 'f':
warning("Ignoring retired option `-%c`", opt);
break;
default:
printUsage();
exit(1);
}
}
return nullptr; // Done processing this argv
}
int main(int argc, char *argv[]) {
bool autoAttrmap = false, autoTilemap = false, autoPalettes = false, autoPalmap = false;
struct AtFileStackEntry {
int parentInd; // Saved offset into parent argv
std::vector<char *> argv; // This context's arg pointer vec
std::vector<char> argPool;
AtFileStackEntry(int parentInd_, std::vector<char *> argv_)
: parentInd(parentInd_), argv(argv_) {}
};
std::vector<AtFileStackEntry> atFileStack;
int curArgc = argc;
char **curArgv = argv;
for (;;) {
char *atFileName =
parseArgv(curArgc, curArgv, autoAttrmap, autoTilemap, autoPalettes, autoPalmap);
if (atFileName) {
// Copy `argv[0]` for error reporting, and because option parsing skips it
AtFileStackEntry &stackEntry =
atFileStack.emplace_back(musl_optind, std::vector{atFileName});
// It would be nice to compute the char pointers on the fly, but reallocs don't allow
// that; so we must compute the offsets after the pool is fixed
auto offsets = readAtFile(&musl_optarg[1], stackEntry.argPool);
stackEntry.argv.reserve(offsets.size() + 2); // Avoid a bunch of reallocs
for (size_t ofs : offsets) {
stackEntry.argv.push_back(&stackEntry.argPool.data()[ofs]);
}
stackEntry.argv.push_back(nullptr); // Don't forget the arg vector terminator!
curArgc = stackEntry.argv.size() - 1;
curArgv = stackEntry.argv.data();
musl_optind = 1; // Don't use 0 because we're not scanning a different argv per se
continue; // Begin scanning that arg vector
}
if (musl_optind != curArgc) {
// This happens if `--` is passed, process the remaining arg(s) as positional
assert(musl_optind < curArgc);
for (int i = musl_optind; i < curArgc; ++i) {
registerInput(argv[i]);
}
}
// Pop off the top stack entry, or end parsing if none
if (atFileStack.empty()) {
break;
}
// OK to restore `optind` directly, because `optpos` must be 0 right now.
// (Providing 0 would be a "proper" reset, but we want to resume parsing)
musl_optind = atFileStack.back().parentInd;
atFileStack.pop_back();
if (atFileStack.empty()) {
curArgc = argc;
curArgv = argv;
} else {
auto &vec = atFileStack.back().argv;
curArgc = vec.size();
curArgv = vec.data();
}
}
if (options.nbColorsPerPal == 0) {
options.nbColorsPerPal = 1u << options.bitDepth;
} else if (options.nbColorsPerPal > 1u << options.bitDepth) {
error("%" PRIu8 "bpp palettes can only contain %u colors, not %" PRIu8, options.bitDepth,
1u << options.bitDepth, options.nbColorsPerPal);
}
auto autoOutPath = [](bool autoOptEnabled, std::string &path, char const *extension) {
if (autoOptEnabled) {
constexpr std::string_view chars =
// Both must start with a dot!
#if defined(_MSC_VER) || defined(__MINGW32__)
"./\\"sv;
#else
"./"sv;
#endif
size_t len = options.input.npos;
size_t i = options.input.find_last_of(chars);
if (i != options.input.npos && options.input[i] == '.') {
// We found the last dot, but check if it's part of a stem
// (There must be a non-path separator character before it)
if (i != 0 && chars.find(options.input[i - 1], 1) == chars.npos) {
// We can replace the extension
len = i;
}
}
path.assign(options.input, 0, len);
path.append(extension);
}
};
autoOutPath(autoAttrmap, options.attrmap, ".attrmap");
autoOutPath(autoTilemap, options.tilemap, ".tilemap");
autoOutPath(autoPalettes, options.palettes, ".pal");
autoOutPath(autoPalmap, options.palmap, ".palmap");
// Execute deferred external pal spec parsing, now that all other params are known
if (externalPalSpec) {
parseExternalPalSpec(externalPalSpec);
}
if (options.verbosity >= Options::VERB_CFG) {
fprintf(stderr, "rgbgfx %s\n", get_package_version_string());
if (options.verbosity >= Options::VERB_VVVVVV) {
fputc('\n', stderr);
static std::array<uint16_t, 21> gfx{
0x1FE, 0x3FF, 0x399, 0x399, 0x3FF, 0x3FF, 0x381, 0x3C3, 0x1FE, 0x078, 0x1FE,
0x3FF, 0x3FF, 0x3FF, 0x37B, 0x37B, 0x0FC, 0x0CC, 0x1CE, 0x1CE, 0x1CE,
};
static std::array<char const *, 3> textbox{
" ,----------------------------------------.",
" | Augh, dimensional interference again?! |",
" `----------------------------------------'"};
for (size_t i = 0; i < gfx.size(); ++i) {
uint16_t row = gfx[i];
for (uint8_t _ = 0; _ < 10; ++_) {
unsigned char c = row & 1 ? '0' : ' ';
fputc(c, stderr);
// Double the pixel horizontally, otherwise the aspect ratio looks wrong
fputc(c, stderr);
row >>= 1;
}
if (i < textbox.size()) {
fputs(textbox[i], stderr);
}
fputc('\n', stderr);
}
fputc('\n', stderr);
}
fputs("Options:\n", stderr);
if (options.columnMajor)
fputs("\tVisit image in column-major order\n", stderr);
if (options.allowMirroring)
fputs("\tAllow mirroring tiles\n", stderr);
if (options.allowDedup)
fputs("\tAllow deduplicating tiles\n", stderr);
if (options.useColorCurve)
fputs("\tUse color curve\n", stderr);
fprintf(stderr, "\tBit depth: %" PRIu8 "bpp\n", options.bitDepth);
if (options.trim != 0)
fprintf(stderr, "\tTrim the last %" PRIu64 " tiles\n", options.trim);
fprintf(stderr, "\tMaximum %" PRIu8 " palettes\n", options.nbPalettes);
fprintf(stderr, "\tPalettes contain %" PRIu8 " colors\n", options.nbColorsPerPal);
fprintf(stderr, "\t%s palette spec\n", []() {
switch (options.palSpecType) {
case Options::NO_SPEC:
return "No";
case Options::EXPLICIT:
return "Explicit";
case Options::EMBEDDED:
return "Embedded";
}
return "???";
}());
if (options.palSpecType == Options::EXPLICIT) {
fputs("\t[\n", stderr);
for (std::array<Rgba, 4> const &pal : options.palSpec) {
fprintf(stderr, "\t\t#%06x, #%06x, #%06x, #%06x,\n", pal[0].toCSS() >> 8,
pal[1].toCSS() >> 8, pal[2].toCSS() >> 8, pal[3].toCSS() >> 8);
}
fputs("\t]\n", stderr);
}
fprintf(stderr,
"\tInput image slice: %" PRIu32 "x%" PRIu32 " pixels starting at (%" PRIi32
", %" PRIi32 ")\n",
options.inputSlice.width, options.inputSlice.height, options.inputSlice.left,
options.inputSlice.top);
fprintf(stderr, "\tBase tile IDs: [%" PRIu8 ", %" PRIu8 "]\n", options.baseTileIDs[0],
options.baseTileIDs[1]);
fprintf(stderr, "\tMaximum %" PRIu16 " tiles in bank 0, %" PRIu16 " in bank 1\n",
options.maxNbTiles[0], options.maxNbTiles[1]);
auto printPath = [](char const *name, std::string const &path) {
if (!path.empty()) {
fprintf(stderr, "\t%s: %s\n", name, path.c_str());
}
};
printPath("Input image", options.input);
printPath("Output tile data", options.output);
printPath("Output tilemap", options.tilemap);
printPath("Output attrmap", options.attrmap);
printPath("Output palettes", options.palettes);
fputs("Ready.\n", stderr);
}
if (options.input.empty()) {
fatal("No input image specified");
}
// Do not do anything if option parsing went wrong
if (nbErrors) {
giveUp();
}
if (options.reverse()) {
reverse();
} else {
process();
}
if (nbErrors) {
giveUp();
}
return 0;
}
void Palette::addColor(uint16_t color) {
for (size_t i = 0; true; ++i) {
assert(i < colors.size()); // The packing should guarantee this
if (colors[i] == color) { // The color is already present
break;
} else if (colors[i] == UINT16_MAX) { // Empty slot
colors[i] = color;
break;
}
}
}
/**
* Returns the ID of the color in the palette, or `size()` if the color is not in
*/
uint8_t Palette::indexOf(uint16_t color) const {
return std::find(colors.begin(), colors.end(), color) - colors.begin();
}
auto Palette::begin() -> decltype(colors)::iterator {
// Skip the first slot if reserved for transparency
return colors.begin() + options.hasTransparentPixels;
}
auto Palette::end() -> decltype(colors)::iterator {
return std::find(begin(), colors.end(), UINT16_MAX);
}
auto Palette::begin() const -> decltype(colors)::const_iterator {
// Skip the first slot if reserved for transparency
return colors.begin() + options.hasTransparentPixels;
}
auto Palette::end() const -> decltype(colors)::const_iterator {
return std::find(begin(), colors.end(), UINT16_MAX);
}
uint8_t Palette::size() const {
return indexOf(UINT16_MAX);
}

View File

@@ -1,806 +0,0 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2013-2018, stag019 and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#include <png.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gfx/makepng.h"
static void initialize_png(struct PNGImage *img, FILE * f);
static struct RawIndexedImage *indexed_png_to_raw(struct PNGImage *img);
static struct RawIndexedImage *grayscale_png_to_raw(struct PNGImage *img);
static struct RawIndexedImage *truecolor_png_to_raw(struct PNGImage *img);
static void get_text(const struct PNGImage *img,
struct ImageOptions *png_options);
static void set_text(const struct PNGImage *img,
const struct ImageOptions *png_options);
static void free_png_data(const struct PNGImage *png);
struct RawIndexedImage *input_png_file(const struct Options *opts,
struct ImageOptions *png_options)
{
struct PNGImage img;
struct RawIndexedImage *raw_image;
FILE *f;
f = fopen(opts->infile, "rb");
if (!f)
err("Opening input png file '%s' failed", opts->infile);
initialize_png(&img, f);
if (img.depth != depth) {
if (opts->verbose) {
warnx("Image bit depth is not %d (is %d).",
depth, img.depth);
}
}
switch (img.type) {
case PNG_COLOR_TYPE_PALETTE:
raw_image = indexed_png_to_raw(&img); break;
case PNG_COLOR_TYPE_GRAY:
case PNG_COLOR_TYPE_GRAY_ALPHA:
raw_image = grayscale_png_to_raw(&img); break;
case PNG_COLOR_TYPE_RGB:
case PNG_COLOR_TYPE_RGB_ALPHA:
raw_image = truecolor_png_to_raw(&img); break;
default:
/* Shouldn't happen, but might as well handle just in case. */
errx("Input PNG file is of invalid color type.");
}
get_text(&img, png_options);
png_destroy_read_struct(&img.png, &img.info, NULL);
fclose(f);
free_png_data(&img);
return raw_image;
}
void output_png_file(const struct Options *opts,
const struct ImageOptions *png_options,
const struct RawIndexedImage *raw_image)
{
FILE *f;
char *outfile;
struct PNGImage img;
png_color *png_palette;
int i;
/*
* TODO: Variable outfile is for debugging purposes. Eventually,
* opts.infile will be used directly.
*/
if (opts->debug) {
outfile = malloc(strlen(opts->infile) + 5);
if (!outfile)
err("%s: Failed to allocate memory for outfile",
__func__);
strcpy(outfile, opts->infile);
strcat(outfile, ".out");
} else {
outfile = opts->infile;
}
f = fopen(outfile, "wb");
if (!f)
err("Opening output png file '%s' failed", outfile);
if (opts->debug)
free(outfile);
img.png = png_create_write_struct(PNG_LIBPNG_VER_STRING,
NULL, NULL, NULL);
if (!img.png)
errx("Creating png structure failed");
img.info = png_create_info_struct(img.png);
if (!img.info)
errx("Creating png info structure failed");
if (setjmp(png_jmpbuf(img.png)))
exit(1);
png_init_io(img.png, f);
png_set_IHDR(img.png, img.info, raw_image->width, raw_image->height,
8, PNG_COLOR_TYPE_PALETTE, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_palette = malloc(sizeof(*png_palette) * raw_image->num_colors);
if (!png_palette)
err("%s: Failed to allocate memory for PNG palette",
__func__);
for (i = 0; i < raw_image->num_colors; i++) {
png_palette[i].red = raw_image->palette[i].red;
png_palette[i].green = raw_image->palette[i].green;
png_palette[i].blue = raw_image->palette[i].blue;
}
png_set_PLTE(img.png, img.info, png_palette, raw_image->num_colors);
free(png_palette);
if (opts->fix)
set_text(&img, png_options);
png_write_info(img.png, img.info);
png_write_image(img.png, (png_byte **) raw_image->data);
png_write_end(img.png, NULL);
png_destroy_write_struct(&img.png, &img.info);
fclose(f);
}
void destroy_raw_image(struct RawIndexedImage **raw_image_ptr_ptr)
{
struct RawIndexedImage *raw_image = *raw_image_ptr_ptr;
for (unsigned int y = 0; y < raw_image->height; y++)
free(raw_image->data[y]);
free(raw_image->data);
free(raw_image->palette);
free(raw_image);
*raw_image_ptr_ptr = NULL;
}
static void initialize_png(struct PNGImage *img, FILE *f)
{
img->png = png_create_read_struct(PNG_LIBPNG_VER_STRING,
NULL, NULL, NULL);
if (!img->png)
errx("Creating png structure failed");
img->info = png_create_info_struct(img->png);
if (!img->info)
errx("Creating png info structure failed");
if (setjmp(png_jmpbuf(img->png)))
exit(1);
png_init_io(img->png, f);
png_read_info(img->png, img->info);
img->width = png_get_image_width(img->png, img->info);
img->height = png_get_image_height(img->png, img->info);
img->depth = png_get_bit_depth(img->png, img->info);
img->type = png_get_color_type(img->png, img->info);
}
static void read_png(struct PNGImage *img);
static struct RawIndexedImage *create_raw_image(int width, int height,
int num_colors);
static void set_raw_image_palette(struct RawIndexedImage *raw_image,
png_color const *palette, int num_colors);
static struct RawIndexedImage *indexed_png_to_raw(struct PNGImage *img)
{
struct RawIndexedImage *raw_image;
png_color *palette;
int colors_in_PLTE;
int colors_in_new_palette;
png_byte *trans_alpha;
int num_trans;
png_color_16 *trans_color;
png_color *original_palette;
uint8_t *old_to_new_palette;
int i, x, y;
if (img->depth < 8)
png_set_packing(img->png);
png_get_PLTE(img->png, img->info, &palette, &colors_in_PLTE);
raw_image = create_raw_image(img->width, img->height, colors);
/*
* Transparent palette entries are removed, and the palette is
* collapsed. Transparent pixels are then replaced with palette index 0.
* This way, an indexed PNG can contain transparent pixels in *addition*
* to 4 normal colors.
*/
if (png_get_tRNS(img->png, img->info, &trans_alpha, &num_trans,
&trans_color)) {
original_palette = palette;
palette = malloc(sizeof(*palette) * colors_in_PLTE);
if (!palette)
err("%s: Failed to allocate memory for palette",
__func__);
colors_in_new_palette = 0;
old_to_new_palette = malloc(sizeof(*old_to_new_palette)
* colors_in_PLTE);
if (!old_to_new_palette)
err("%s: Failed to allocate memory for new palette",
__func__);
for (i = 0; i < num_trans; i++) {
if (trans_alpha[i] == 0) {
old_to_new_palette[i] = 0;
} else {
old_to_new_palette[i] = colors_in_new_palette;
palette[colors_in_new_palette++] =
original_palette[i];
}
}
for (i = num_trans; i < colors_in_PLTE; i++) {
old_to_new_palette[i] = colors_in_new_palette;
palette[colors_in_new_palette++] = original_palette[i];
}
if (colors_in_new_palette != colors_in_PLTE) {
palette = realloc(palette,
sizeof(*palette) *
colors_in_new_palette);
if (!palette)
err("%s: Failed to allocate memory for palette",
__func__);
}
/*
* Setting and validating palette before reading
* allows us to error out *before* doing the data
* transformation if the palette is too long.
*/
set_raw_image_palette(raw_image, palette,
colors_in_new_palette);
read_png(img);
for (y = 0; y < img->height; y++) {
for (x = 0; x < img->width; x++) {
raw_image->data[y][x] =
old_to_new_palette[img->data[y][x]];
}
}
free(palette);
free(old_to_new_palette);
} else {
set_raw_image_palette(raw_image, palette, colors_in_PLTE);
read_png(img);
for (y = 0; y < img->height; y++) {
for (x = 0; x < img->width; x++)
raw_image->data[y][x] = img->data[y][x];
}
}
return raw_image;
}
static struct RawIndexedImage *grayscale_png_to_raw(struct PNGImage *img)
{
if (img->depth < 8)
png_set_expand_gray_1_2_4_to_8(img->png);
png_set_gray_to_rgb(img->png);
return truecolor_png_to_raw(img);
}
static void rgba_png_palette(struct PNGImage *img,
png_color **palette_ptr_ptr, int *num_colors);
static struct RawIndexedImage
*processed_rgba_png_to_raw(const struct PNGImage *img,
png_color const *palette,
int colors_in_palette);
static struct RawIndexedImage *truecolor_png_to_raw(struct PNGImage *img)
{
struct RawIndexedImage *raw_image;
png_color *palette;
int colors_in_palette;
if (img->depth == 16) {
#if PNG_LIBPNG_VER >= 10504
png_set_scale_16(img->png);
#else
png_set_strip_16(img->png);
#endif
}
if (!(img->type & PNG_COLOR_MASK_ALPHA)) {
if (png_get_valid(img->png, img->info, PNG_INFO_tRNS))
png_set_tRNS_to_alpha(img->png);
else
png_set_add_alpha(img->png, 0xFF, PNG_FILLER_AFTER);
}
read_png(img);
rgba_png_palette(img, &palette, &colors_in_palette);
raw_image = processed_rgba_png_to_raw(img, palette, colors_in_palette);
free(palette);
return raw_image;
}
static void rgba_PLTE_palette(struct PNGImage *img,
png_color **palette_ptr_ptr, int *num_colors);
static void rgba_build_palette(struct PNGImage *img,
png_color **palette_ptr_ptr, int *num_colors);
static void rgba_png_palette(struct PNGImage *img,
png_color **palette_ptr_ptr, int *num_colors)
{
if (png_get_valid(img->png, img->info, PNG_INFO_PLTE))
rgba_PLTE_palette(img, palette_ptr_ptr, num_colors);
else
rgba_build_palette(img, palette_ptr_ptr, num_colors);
}
static void rgba_PLTE_palette(struct PNGImage *img,
png_color **palette_ptr_ptr, int *num_colors)
{
png_get_PLTE(img->png, img->info, palette_ptr_ptr, num_colors);
/*
* Lets us free the palette manually instead of leaving it to libpng,
* which lets us handle a PLTE and a built palette the same way.
*/
png_data_freer(img->png, img->info,
PNG_USER_WILL_FREE_DATA, PNG_FREE_PLTE);
}
static void update_built_palette(png_color *palette,
png_color const *pixel_color, png_byte alpha,
int *num_colors, bool *only_grayscale);
static int fit_grayscale_palette(png_color *palette, int *num_colors);
static void order_color_palette(png_color *palette, int num_colors);
static void rgba_build_palette(struct PNGImage *img,
png_color **palette_ptr_ptr, int *num_colors)
{
png_color *palette;
int y, value_index;
png_color cur_pixel_color;
png_byte cur_alpha;
bool only_grayscale = true;
/*
* By filling the palette up with black by default, if the image
* doesn't have enough colors, the palette gets padded with black.
*/
*palette_ptr_ptr = calloc(colors, sizeof(**palette_ptr_ptr));
if (!*palette_ptr_ptr)
err("%s: Failed to allocate memory for palette", __func__);
palette = *palette_ptr_ptr;
*num_colors = 0;
for (y = 0; y < img->height; y++) {
value_index = 0;
while (value_index < img->width * 4) {
cur_pixel_color.red = img->data[y][value_index++];
cur_pixel_color.green = img->data[y][value_index++];
cur_pixel_color.blue = img->data[y][value_index++];
cur_alpha = img->data[y][value_index++];
update_built_palette(palette, &cur_pixel_color,
cur_alpha,
num_colors, &only_grayscale);
}
}
/* In order not to count 100% transparent images as grayscale. */
only_grayscale = *num_colors ? only_grayscale : false;
if (!only_grayscale || !fit_grayscale_palette(palette, num_colors))
order_color_palette(palette, *num_colors);
}
static void update_built_palette(png_color *palette,
png_color const *pixel_color, png_byte alpha,
int *num_colors, bool *only_grayscale)
{
bool color_exists;
png_color cur_palette_color;
int i;
/*
* Transparent pixels don't count toward the palette,
* as they'll be replaced with color #0 later.
*/
if (alpha == 0)
return;
if (*only_grayscale && !(pixel_color->red == pixel_color->green &&
pixel_color->red == pixel_color->blue)) {
*only_grayscale = false;
}
color_exists = false;
for (i = 0; i < *num_colors; i++) {
cur_palette_color = palette[i];
if (pixel_color->red == cur_palette_color.red &&
pixel_color->green == cur_palette_color.green &&
pixel_color->blue == cur_palette_color.blue) {
color_exists = true;
break;
}
}
if (!color_exists) {
if (*num_colors == colors) {
errx("Too many colors in input PNG file to fit into a %d-bit palette (max %d).",
depth, colors);
}
palette[*num_colors] = *pixel_color;
(*num_colors)++;
}
}
static int fit_grayscale_palette(png_color *palette, int *num_colors)
{
int interval = 256 / colors;
png_color *fitted_palette = malloc(sizeof(*fitted_palette) * colors);
bool *set_indices = calloc(colors, sizeof(*set_indices));
int i, shade_index;
if (!fitted_palette)
err("%s: Failed to allocate memory for palette", __func__);
if (!set_indices)
err("%s: Failed to allocate memory for indices", __func__);
fitted_palette[0].red = 0xFF;
fitted_palette[0].green = 0xFF;
fitted_palette[0].blue = 0xFF;
fitted_palette[colors - 1].red = 0;
fitted_palette[colors - 1].green = 0;
fitted_palette[colors - 1].blue = 0;
if (colors == 4) {
fitted_palette[1].red = 0xA9;
fitted_palette[1].green = 0xA9;
fitted_palette[1].blue = 0xA9;
fitted_palette[2].red = 0x55;
fitted_palette[2].green = 0x55;
fitted_palette[2].blue = 0x55;
}
for (i = 0; i < *num_colors; i++) {
shade_index = colors - 1 - palette[i].red / interval;
if (set_indices[shade_index]) {
free(fitted_palette);
free(set_indices);
return false;
}
fitted_palette[shade_index] = palette[i];
set_indices[shade_index] = true;
}
for (i = 0; i < colors; i++)
palette[i] = fitted_palette[i];
*num_colors = colors;
free(fitted_palette);
free(set_indices);
return true;
}
/* A combined struct is needed to sort csolors in order of luminance. */
struct ColorWithLuminance {
png_color color;
int luminance;
};
static int compare_luminance(void const *a, void const *b)
{
const struct ColorWithLuminance *x, *y;
x = (const struct ColorWithLuminance *)a;
y = (const struct ColorWithLuminance *)b;
return y->luminance - x->luminance;
}
static void order_color_palette(png_color *palette, int num_colors)
{
int i;
struct ColorWithLuminance *palette_with_luminance =
malloc(sizeof(*palette_with_luminance) * num_colors);
if (!palette_with_luminance)
err("%s: Failed to allocate memory for palette", __func__);
for (i = 0; i < num_colors; i++) {
/*
* Normally this would be done with floats, but since it's only
* used for comparison, we might as well use integer math.
*/
palette_with_luminance[i].color = palette[i];
palette_with_luminance[i].luminance = 2126 * palette[i].red +
7152 * palette[i].green +
722 * palette[i].blue;
}
qsort(palette_with_luminance, num_colors,
sizeof(*palette_with_luminance), compare_luminance);
for (i = 0; i < num_colors; i++)
palette[i] = palette_with_luminance[i].color;
free(palette_with_luminance);
}
static void put_raw_image_pixel(struct RawIndexedImage *raw_image,
const struct PNGImage *img,
int *value_index, int x, int y,
png_color const *palette,
int colors_in_palette);
static struct RawIndexedImage
*processed_rgba_png_to_raw(const struct PNGImage *img,
png_color const *palette,
int colors_in_palette)
{
struct RawIndexedImage *raw_image;
int x, y, value_index;
raw_image = create_raw_image(img->width, img->height, colors);
set_raw_image_palette(raw_image, palette, colors_in_palette);
for (y = 0; y < img->height; y++) {
x = raw_image->width - 1;
value_index = img->width * 4 - 1;
while (x >= 0) {
put_raw_image_pixel(raw_image, img,
&value_index, x, y,
palette, colors_in_palette);
x--;
}
}
return raw_image;
}
static uint8_t palette_index_of(png_color const *palette,
int num_colors, png_color const *color);
static void put_raw_image_pixel(struct RawIndexedImage *raw_image,
const struct PNGImage *img,
int *value_index, int x, int y,
png_color const *palette,
int colors_in_palette)
{
png_color pixel_color;
png_byte alpha;
alpha = img->data[y][*value_index];
if (alpha == 0) {
raw_image->data[y][x] = 0;
*value_index -= 4;
} else {
(*value_index)--;
pixel_color.blue = img->data[y][(*value_index)--];
pixel_color.green = img->data[y][(*value_index)--];
pixel_color.red = img->data[y][(*value_index)--];
raw_image->data[y][x] = palette_index_of(palette,
colors_in_palette,
&pixel_color);
}
}
static uint8_t palette_index_of(png_color const *palette,
int num_colors, png_color const *color)
{
uint8_t i;
for (i = 0; i < num_colors; i++) {
if (palette[i].red == color->red &&
palette[i].green == color->green &&
palette[i].blue == color->blue) {
return i;
}
}
errx("The input PNG file contains colors that don't appear in its embedded palette.");
}
static void read_png(struct PNGImage *img)
{
int y;
png_read_update_info(img->png, img->info);
img->data = malloc(sizeof(*img->data) * img->height);
if (!img->data)
err("%s: Failed to allocate memory for image data",
__func__);
for (y = 0; y < img->height; y++) {
img->data[y] = malloc(png_get_rowbytes(img->png, img->info));
if (!img->data[y])
err("%s: Failed to allocate memory for image data",
__func__);
}
png_read_image(img->png, img->data);
png_read_end(img->png, img->info);
}
static struct RawIndexedImage *create_raw_image(int width, int height,
int num_colors)
{
struct RawIndexedImage *raw_image;
int y;
raw_image = malloc(sizeof(*raw_image));
if (!raw_image)
err("%s: Failed to allocate memory for raw image",
__func__);
raw_image->width = width;
raw_image->height = height;
raw_image->num_colors = num_colors;
raw_image->palette = malloc(sizeof(*raw_image->palette) * num_colors);
if (!raw_image->palette)
err("%s: Failed to allocate memory for raw image palette",
__func__);
raw_image->data = malloc(sizeof(*raw_image->data) * height);
if (!raw_image->data)
err("%s: Failed to allocate memory for raw image data",
__func__);
for (y = 0; y < height; y++) {
raw_image->data[y] = malloc(sizeof(*raw_image->data[y])
* width);
if (!raw_image->data[y])
err("%s: Failed to allocate memory for raw image data",
__func__);
}
return raw_image;
}
static void set_raw_image_palette(struct RawIndexedImage *raw_image,
png_color const *palette, int num_colors)
{
int i;
if (num_colors > raw_image->num_colors) {
errx("Too many colors in input PNG file's palette to fit into a %d-bit palette (%d in input palette, max %d).",
raw_image->num_colors >> 1,
num_colors, raw_image->num_colors);
}
for (i = 0; i < num_colors; i++) {
raw_image->palette[i].red = palette[i].red;
raw_image->palette[i].green = palette[i].green;
raw_image->palette[i].blue = palette[i].blue;
}
for (i = num_colors; i < raw_image->num_colors; i++) {
raw_image->palette[i].red = 0;
raw_image->palette[i].green = 0;
raw_image->palette[i].blue = 0;
}
}
static void get_text(const struct PNGImage *img,
struct ImageOptions *png_options)
{
png_text *text;
int i, numtxts, numremoved;
png_get_text(img->png, img->info, &text, &numtxts);
for (i = 0; i < numtxts; i++) {
if (strcmp(text[i].key, "h") == 0 && !*text[i].text) {
png_options->horizontal = true;
png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
} else if (strcmp(text[i].key, "x") == 0) {
png_options->trim = strtoul(text[i].text, NULL, 0);
png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
} else if (strcmp(text[i].key, "t") == 0) {
png_options->tilemapfile = text[i].text;
png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
} else if (strcmp(text[i].key, "T") == 0 && !*text[i].text) {
png_options->tilemapout = true;
png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
} else if (strcmp(text[i].key, "a") == 0) {
png_options->attrmapfile = text[i].text;
png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
} else if (strcmp(text[i].key, "A") == 0 && !*text[i].text) {
png_options->attrmapout = true;
png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
} else if (strcmp(text[i].key, "p") == 0) {
png_options->palfile = text[i].text;
png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
} else if (strcmp(text[i].key, "P") == 0 && !*text[i].text) {
png_options->palout = true;
png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
}
}
/*
* TODO: Remove this and simply change the warning function not to warn
* instead.
*/
for (i = 0, numremoved = 0; i < numtxts; i++) {
if (text[i].key == NULL)
numremoved++;
text[i].key = text[i + numremoved].key;
text[i].text = text[i + numremoved].text;
text[i].compression = text[i + numremoved].compression;
}
png_set_text(img->png, img->info, text, numtxts - numremoved);
}
static void set_text(const struct PNGImage *img,
const struct ImageOptions *png_options)
{
png_text *text;
char buffer[3];
text = malloc(sizeof(*text));
if (!text)
err("%s: Failed to allocate memory for PNG text",
__func__);
if (png_options->horizontal) {
text[0].key = "h";
text[0].text = "";
text[0].compression = PNG_TEXT_COMPRESSION_NONE;
png_set_text(img->png, img->info, text, 1);
}
if (png_options->trim) {
text[0].key = "x";
snprintf(buffer, 3, "%d", png_options->trim);
text[0].text = buffer;
text[0].compression = PNG_TEXT_COMPRESSION_NONE;
png_set_text(img->png, img->info, text, 1);
}
if (*png_options->tilemapfile) {
text[0].key = "t";
text[0].text = "";
text[0].compression = PNG_TEXT_COMPRESSION_NONE;
png_set_text(img->png, img->info, text, 1);
}
if (png_options->tilemapout) {
text[0].key = "T";
text[0].text = "";
text[0].compression = PNG_TEXT_COMPRESSION_NONE;
png_set_text(img->png, img->info, text, 1);
}
if (*png_options->attrmapfile) {
text[0].key = "a";
text[0].text = "";
text[0].compression = PNG_TEXT_COMPRESSION_NONE;
png_set_text(img->png, img->info, text, 1);
}
if (png_options->attrmapout) {
text[0].key = "A";
text[0].text = "";
text[0].compression = PNG_TEXT_COMPRESSION_NONE;
png_set_text(img->png, img->info, text, 1);
}
if (*png_options->palfile) {
text[0].key = "p";
text[0].text = "";
text[0].compression = PNG_TEXT_COMPRESSION_NONE;
png_set_text(img->png, img->info, text, 1);
}
if (png_options->palout) {
text[0].key = "P";
text[0].text = "";
text[0].compression = PNG_TEXT_COMPRESSION_NONE;
png_set_text(img->png, img->info, text, 1);
}
free(text);
}
static void free_png_data(const struct PNGImage *img)
{
int y;
for (y = 0; y < img->height; y++)
free(img->data[y]);
free(img->data);
}

512
src/gfx/pal_packing.cpp Normal file
View File

@@ -0,0 +1,512 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#include "gfx/pal_packing.hpp"
#include <algorithm>
#include <assert.h>
#include <bitset>
#include <cinttypes>
#include <deque>
#include <numeric>
#include <optional>
#include <queue>
#include <tuple>
#include <type_traits>
#include <unordered_set>
#include <vector>
#include "defaultinitalloc.hpp"
#include "gfx/main.hpp"
#include "gfx/proto_palette.hpp"
using std::swap;
namespace packing {
// The solvers here are picked from the paper at http://arxiv.org/abs/1605.00558:
// "Algorithms for the Pagination Problem, a Bin Packing with Overlapping Items"
// Their formulation of the problem consists in packing "tiles" into "pages"; here is a
// correspondence table for our application of it:
// Paper | RGBGFX
// ------+-------
// Tile | Proto-palette
// Page | Palette
/**
* A reference to a proto-palette, and attached attributes for sorting purposes
*/
struct ProtoPalAttrs {
size_t const protoPalIndex;
/**
* Pages from which we are banned (to prevent infinite loops)
* This is dynamic because we wish not to hard-cap the amount of palettes
*/
std::vector<bool> bannedPages;
ProtoPalAttrs(size_t index) : protoPalIndex(index) {}
bool isBannedFrom(size_t index) const {
return index < bannedPages.size() && bannedPages[index];
}
void banFrom(size_t index) {
if (bannedPages.size() <= index) {
bannedPages.resize(index + 1);
}
bannedPages[index] = true;
}
};
/**
* A collection of proto-palettes assigned to a palette
* Does not contain the actual color indices because we need to be able to remove elements
*/
class AssignedProtos {
// We leave room for emptied slots to avoid copying the structs around on removal
std::vector<std::optional<ProtoPalAttrs>> _assigned;
// For resolving proto-palette indices
std::vector<ProtoPalette> const *_protoPals;
public:
template<typename... Ts>
AssignedProtos(std::vector<ProtoPalette> const &protoPals, Ts &&...elems)
: _assigned{std::forward<Ts>(elems)...}, _protoPals{&protoPals} {}
private:
template<typename Inner, template<typename> typename Constness>
class Iter {
public:
friend class AssignedProtos;
// For `iterator_traits`
using difference_type = typename std::iterator_traits<Inner>::difference_type;
using value_type = ProtoPalAttrs;
using pointer = Constness<value_type> *;
using reference = Constness<value_type> &;
using iterator_category = std::forward_iterator_tag;
private:
Constness<decltype(_assigned)> *_array = nullptr;
Inner _iter{};
Iter(decltype(_array) array, decltype(_iter) &&iter) : _array(array), _iter(iter) {}
Iter &skipEmpty() {
while (_iter != _array->end() && !_iter->has_value()) {
++_iter;
}
return *this;
}
public:
Iter() = default;
bool operator==(Iter const &other) const { return _iter == other._iter; }
bool operator!=(Iter const &other) const { return !(*this == other); }
Iter &operator++() {
++_iter;
skipEmpty();
return *this;
}
Iter operator++(int) {
Iter it = *this;
++(*this);
return it;
}
reference operator*() const {
assert((*_iter).has_value());
return **_iter;
}
pointer operator->() const {
return &(**this); // Invokes the operator above, not quite a no-op!
}
friend void swap(Iter &lhs, Iter &rhs) {
swap(lhs._array, rhs._array);
swap(lhs._iter, rhs._iter);
}
};
public:
using iterator = Iter<decltype(_assigned)::iterator, std::remove_const_t>;
iterator begin() { return iterator{&_assigned, _assigned.begin()}.skipEmpty(); }
iterator end() { return iterator{&_assigned, _assigned.end()}; }
using const_iterator = Iter<decltype(_assigned)::const_iterator, std::add_const_t>;
const_iterator begin() const {
return const_iterator{&_assigned, _assigned.begin()}.skipEmpty();
}
const_iterator end() const { return const_iterator{&_assigned, _assigned.end()}; }
/**
* Assigns a new ProtoPalAttrs in a free slot, assuming there is one
* Args are passed to the `ProtoPalAttrs`'s constructor
*/
template<typename... Ts>
void assign(Ts &&...args) {
auto freeSlot = std::find_if_not(
_assigned.begin(), _assigned.end(),
[](std::optional<ProtoPalAttrs> const &slot) { return slot.has_value(); });
if (freeSlot == _assigned.end()) { // We are full, use a new slot
_assigned.emplace_back(std::forward<Ts>(args)...);
} else { // Reuse a free slot
freeSlot->emplace(std::forward<Ts>(args)...);
}
}
void remove(iterator const &iter) {
iter._iter->reset(); // This time, we want to access the `optional` itself
}
void clear() { _assigned.clear(); }
bool empty() const {
return std::find_if(
_assigned.begin(), _assigned.end(),
[](std::optional<ProtoPalAttrs> const &slot) { return slot.has_value(); })
== _assigned.end();
}
size_t nbProtoPals() const { return std::distance(begin(), end()); }
private:
template<typename Iter>
static void addUniqueColors(std::unordered_set<uint16_t> &colors, Iter iter, Iter const &end,
std::vector<ProtoPalette> const &protoPals) {
for (; iter != end; ++iter) {
ProtoPalette const &protoPal = protoPals[iter->protoPalIndex];
colors.insert(protoPal.begin(), protoPal.end());
}
}
// This function should stay private because it returns a reference to a unique object
std::unordered_set<uint16_t> &uniqueColors() const {
// We check for *distinct* colors by stuffing them into a `set`; this should be
// faster than "back-checking" on every element (O(n²))
//
// TODO: calc84maniac suggested another approach; try implementing it, see if it
// performs better:
// > So basically you make a priority queue that takes iterators into each of your sets
// > (paired with end iterators so you'll know where to stop), and the comparator tests the
// > values pointed to by each iterator
// > Then each iteration you pop from the queue,
// > optionally add one to your count, increment the iterator and push it back into the
// > queue if it didn't reach the end
// > And you do this until the priority queue is empty
static std::unordered_set<uint16_t> colors;
colors.clear();
addUniqueColors(colors, begin(), end(), *_protoPals);
return colors;
}
public:
/**
* Returns the number of distinct colors
*/
size_t volume() const { return uniqueColors().size(); }
bool canFit(ProtoPalette const &protoPal) const {
auto &colors = uniqueColors();
colors.insert(protoPal.begin(), protoPal.end());
return colors.size() <= options.maxOpaqueColors();
}
/**
* Computes the "relative size" of a proto-palette on this palette
*/
double relSizeOf(ProtoPalette const &protoPal) const {
// NOTE: this function must not call `uniqueColors`, or one of its callers will break!
double relSize = 0.;
for (uint16_t color : protoPal) {
auto n = std::count_if(begin(), end(), [this, &color](ProtoPalAttrs const &attrs) {
ProtoPalette const &pal = (*_protoPals)[attrs.protoPalIndex];
return std::find(pal.begin(), pal.end(), color) != pal.end();
});
// NOTE: The paper and the associated code disagree on this: the code has
// this `1 +`, whereas the paper does not; its lack causes a division by 0
// if the symbol is not found anywhere, so I'm assuming the paper is wrong.
relSize += 1. / (1 + n);
}
return relSize;
}
/**
* Computes the "relative size" of a set of proto-palettes on this palette
*/
template<typename Iter>
auto combinedVolume(Iter &&begin, Iter const &end,
std::vector<ProtoPalette> const &protoPals) const {
auto &colors = uniqueColors();
addUniqueColors(colors, std::forward<Iter>(begin), end, protoPals);
return colors.size();
}
/**
* Computes the "relative size" of a set of colors on this palette
*/
template<typename Iter>
auto combinedVolume(Iter &&begin, Iter &&end) const {
auto &colors = uniqueColors();
colors.insert(std::forward<Iter>(begin), std::forward<Iter>(end));
return colors.size();
}
};
static void decant(std::vector<AssignedProtos> &assignments,
std::vector<ProtoPalette> const &protoPalettes) {
// "Decanting" is the process of moving all *things* that can fit in a lower index there
auto decantOn = [&assignments](auto const &tryDecanting) {
// No need to attempt decanting on palette #0, as there are no palettes to decant to
for (size_t from = assignments.size(); --from;) {
// Scan all palettes before this one
for (size_t to = 0; to < from; ++to) {
tryDecanting(assignments[to], assignments[from]);
}
// If the proto-palette is now empty, remove it
// Doing this now reduces the number of iterations performed by later steps
// NB: order is intentionally preserved so as not to alter the "decantation"'s
// properties
// NB: this does mean that the first step might get empty palettes as its input!
// NB: this is safe to do because we go towards the beginning of the vector, thereby not
// invalidating our iteration (thus, iterators should not be used to drivethe outer
// loop)
if (assignments[from].empty()) {
assignments.erase(assignments.begin() + from);
}
}
};
options.verbosePrint(Options::VERB_DEBUG, "%zu palettes before decanting\n",
assignments.size());
// Decant on palettes
decantOn([&protoPalettes](AssignedProtos &to, AssignedProtos &from) {
// If the entire palettes can be merged, move all of `from`'s proto-palettes
if (to.combinedVolume(from.begin(), from.end(), protoPalettes)
<= options.maxOpaqueColors()) {
for (ProtoPalAttrs &attrs : from) {
to.assign(attrs.protoPalIndex);
}
from.clear();
}
});
options.verbosePrint(Options::VERB_DEBUG, "%zu palettes after decanting on palettes\n",
assignments.size());
// Decant on "components" (= proto-pals sharing colors)
decantOn([&protoPalettes](AssignedProtos &to, AssignedProtos &from) {
// We need to iterate on all the "components", which are groups of proto-palettes sharing at
// least one color with another proto-palettes in the group.
// We do this by adding the first available proto-palette, and then looking for palettes
// with common colors. (As an optimization, we know we can skip palettes already scanned.)
std::vector<bool> processed(from.nbProtoPals(), false);
std::unordered_set<uint16_t> colors;
std::vector<size_t> members;
while (true) {
auto iter = std::find(processed.begin(), processed.end(), true);
if (iter == processed.end()) { // Processed everything!
break;
}
auto attrs = from.begin();
std::advance(attrs, (iter - processed.begin()));
// Build up the "component"...
colors.clear();
members.clear();
assert(members.empty()); // Compiler optimization hint
do {
ProtoPalette const &protoPal = protoPalettes[attrs->protoPalIndex];
// If this is the first proto-pal, or if at least one color matches, add it
if (members.empty()
|| std::find_first_of(colors.begin(), colors.end(), protoPal.begin(),
protoPal.end())
!= colors.end()) {
colors.insert(protoPal.begin(), protoPal.end());
members.push_back(iter - processed.begin());
*iter = true; // Mark that proto-pal as processed
}
++iter;
++attrs;
} while (iter != processed.end());
if (to.combinedVolume(colors.begin(), colors.end()) <= options.maxOpaqueColors()) {
// Iterate through the component's proto-palettes, and transfer them
auto member = from.begin();
size_t curIndex = 0;
for (size_t index : members) {
std::advance(member, index - curIndex);
curIndex = index;
to.assign(std::move(*member));
from.remove(member); // Removing does not shift elements, so it's cheap
}
}
}
});
options.verbosePrint(Options::VERB_DEBUG, "%zu palettes after decanting on \"components\"\n",
assignments.size());
// Decant on individual proto-palettes
decantOn([&protoPalettes](AssignedProtos &to, AssignedProtos &from) {
for (auto iter = from.begin(); iter != from.end(); ++iter) {
if (to.canFit(protoPalettes[iter->protoPalIndex])) {
to.assign(std::move(*iter));
from.remove(iter);
}
}
});
options.verbosePrint(Options::VERB_DEBUG, "%zu palettes after decanting on proto-palettes\n",
assignments.size());
}
std::tuple<DefaultInitVec<size_t>, size_t>
overloadAndRemove(std::vector<ProtoPalette> const &protoPalettes) {
options.verbosePrint(Options::VERB_LOG_ACT,
"Paginating palettes using \"overload-and-remove\" strategy...\n");
// Sort the proto-palettes by size, which improves the packing algorithm's efficiency
DefaultInitVec<size_t> sortedProtoPalIDs(protoPalettes.size());
sortedProtoPalIDs.clear();
for (size_t i = 0; i < protoPalettes.size(); ++i) {
sortedProtoPalIDs.insert(
std::lower_bound(sortedProtoPalIDs.begin(), sortedProtoPalIDs.end(), i), i);
}
// Begin with all proto-palettes queued up for insertion
std::queue<ProtoPalAttrs> queue(
std::deque<ProtoPalAttrs>(sortedProtoPalIDs.begin(), sortedProtoPalIDs.end()));
// Begin with no pages
std::vector<AssignedProtos> assignments{};
for (; !queue.empty(); queue.pop()) {
ProtoPalAttrs const &attrs = queue.front(); // Valid until the `queue.pop()`
options.verbosePrint(Options::VERB_DEBUG, "Handling proto-pal %zu\n", attrs.protoPalIndex);
ProtoPalette const &protoPal = protoPalettes[attrs.protoPalIndex];
size_t bestPalIndex = assignments.size();
// We're looking for a palette where the proto-palette's relative size is less than
// its actual size; so only overwrite the "not found" index on meeting that criterion
double bestRelSize = protoPal.size();
for (size_t i = 0; i < assignments.size(); ++i) {
// Skip the page if this one is banned from it
if (attrs.isBannedFrom(i)) {
continue;
}
options.verbosePrint(Options::VERB_DEBUG, "%zu/%zu: Rel size: %f (size = %zu)\n", i + 1,
assignments.size(), assignments[i].relSizeOf(protoPal),
protoPal.size());
if (assignments[i].relSizeOf(protoPal) < bestRelSize) {
bestPalIndex = i;
}
}
if (bestPalIndex == assignments.size()) {
// Found nowhere to put it, create a new page containing just that one
assignments.emplace_back(protoPalettes, std::move(attrs));
} else {
auto &bestPal = assignments[bestPalIndex];
// Add the color to that palette
bestPal.assign(std::move(attrs));
// If this overloads the palette, get it back to normal (if possible)
while (bestPal.volume() > options.maxOpaqueColors()) {
options.verbosePrint(Options::VERB_DEBUG,
"Palette %zu is overloaded! (%zu > %" PRIu8 ")\n",
bestPalIndex, bestPal.volume(), options.maxOpaqueColors());
// Look for a proto-pal minimizing "efficiency" (size / rel_size)
auto efficiency = [&bestPal](ProtoPalette const &pal) {
return pal.size() / bestPal.relSizeOf(pal);
};
auto [minEfficiencyIter, maxEfficiencyIter] =
std::minmax_element(bestPal.begin(), bestPal.end(),
[&efficiency, &protoPalettes](ProtoPalAttrs const &lhs,
ProtoPalAttrs const &rhs) {
return efficiency(protoPalettes[lhs.protoPalIndex])
< efficiency(protoPalettes[rhs.protoPalIndex]);
});
// All efficiencies are identical iff min equals max
// TODO: maybe not ideal to re-compute these two?
// TODO: yikes for float comparison! I *think* this threshold is OK?
if (efficiency(protoPalettes[maxEfficiencyIter->protoPalIndex])
- efficiency(protoPalettes[minEfficiencyIter->protoPalIndex])
< .001) {
break;
}
// Remove the proto-pal with minimal efficiency
queue.emplace(std::move(*minEfficiencyIter));
queue.back().banFrom(bestPalIndex); // Ban it from this palette
bestPal.remove(minEfficiencyIter);
}
}
}
// Deal with palettes still overloaded, by emptying them
for (AssignedProtos &pal : assignments) {
if (pal.volume() > options.maxOpaqueColors()) {
for (ProtoPalAttrs &attrs : pal) {
queue.emplace(std::move(attrs));
}
pal.clear();
}
}
// Place back any proto-palettes now in the queue via first-fit
while (!queue.empty()) {
ProtoPalAttrs const &attrs = queue.front();
ProtoPalette const &protoPal = protoPalettes[attrs.protoPalIndex];
auto iter =
std::find_if(assignments.begin(), assignments.end(),
[&protoPal](AssignedProtos const &pal) { return pal.canFit(protoPal); });
if (iter == assignments.end()) { // No such page, create a new one
options.verbosePrint(Options::VERB_DEBUG,
"Adding new palette (%zu) for overflowing proto-pal %zu\n",
assignments.size(), attrs.protoPalIndex);
assignments.emplace_back(protoPalettes, std::move(attrs));
} else {
options.verbosePrint(Options::VERB_DEBUG,
"Assigning overflowing proto-pal %zu to palette %zu\n",
attrs.protoPalIndex, iter - assignments.begin());
iter->assign(std::move(attrs));
}
queue.pop();
}
if (options.verbosity >= Options::VERB_INTERM) {
for (auto &&assignment : assignments) {
fprintf(stderr, "{ ");
for (auto &&attrs : assignment) {
fprintf(stderr, "[%zu] ", attrs.protoPalIndex);
for (auto &&colorIndex : protoPalettes[attrs.protoPalIndex]) {
fprintf(stderr, "%04" PRIx16 ", ", colorIndex);
}
}
fprintf(stderr, "} (volume = %zu)\n", assignment.volume());
}
}
// "Decant" the result
decant(assignments, protoPalettes);
// Note that the result does not contain any empty palettes
if (options.verbosity >= Options::VERB_INTERM) {
for (auto &&assignment : assignments) {
fprintf(stderr, "{ ");
for (auto &&attrs : assignment) {
fprintf(stderr, "[%zu] ", attrs.protoPalIndex);
for (auto &&colorIndex : protoPalettes[attrs.protoPalIndex]) {
fprintf(stderr, "%04" PRIx16 ", ", colorIndex);
}
}
fprintf(stderr, "} (volume = %zu)\n", assignment.volume());
}
}
DefaultInitVec<size_t> mappings(protoPalettes.size());
for (size_t i = 0; i < assignments.size(); ++i) {
for (ProtoPalAttrs const &attrs : assignments[i]) {
mappings[attrs.protoPalIndex] = i;
}
}
return {mappings, assignments.size()};
}
} // namespace packing

105
src/gfx/pal_sorting.cpp Normal file
View File

@@ -0,0 +1,105 @@
#include "gfx/pal_sorting.hpp"
#include <algorithm>
#include <png.h>
#include <vector>
#include "helpers.h"
#include "gfx/main.hpp"
#include "gfx/process.hpp"
namespace sorting {
void indexed(std::vector<Palette> &palettes, int palSize, png_color const *palRGB,
png_byte *palAlpha) {
options.verbosePrint(Options::VERB_LOG_ACT, "Sorting palettes using embedded palette...\n");
auto pngToRgb = [&palRGB, &palAlpha](int index) {
auto const &c = palRGB[index];
return Rgba(c.red, c.green, c.blue, palAlpha ? palAlpha[index] : 0xFF);
};
// HACK: for compatibility with old versions, add unused colors if:
// - there is only one palette, and
// - only some of the first N colors are being used
if (palettes.size() == 1) {
Palette &palette = palettes[0];
// Build our candidate array of colors
decltype(palette.colors) colors{UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX};
for (int i = 0; i < options.maxOpaqueColors(); ++i) {
colors[i + options.hasTransparentPixels] = pngToRgb(i).cgbColor();
}
// Check that the palette only uses those colors
if (std::all_of(palette.begin(), palette.end(), [&colors](uint16_t color) {
return std::find(colors.begin(), colors.end(), color) != colors.end();
})) {
if (palette.size() != options.maxOpaqueColors()) {
warning("Unused color in PNG embedded palette was re-added; please use `-c "
"embedded` to get this in future versions");
}
// Overwrite the palette, and return with that (it's already sorted)
palette.colors = colors;
return;
}
}
for (Palette &pal : palettes) {
std::sort(pal.begin(), pal.end(), [&](uint16_t lhs, uint16_t rhs) {
// Iterate through the PNG's palette, looking for either of the two
for (int i = 0; i < palSize; ++i) {
uint16_t color = pngToRgb(i).cgbColor();
if (color == Rgba::transparent) {
continue;
}
// Return whether lhs < rhs
if (color == rhs) {
return false;
}
if (color == lhs) {
return true;
}
}
unreachable_(); // This should not be possible
});
}
}
void grayscale(std::vector<Palette> &palettes,
std::array<std::optional<Rgba>, 0x8001> const &colors) {
options.verbosePrint(Options::VERB_LOG_ACT, "Sorting grayscale-only palette...\n");
// This method is only applicable if there are at most as many colors as colors per palette, so
// we should only have a single palette.
assert(palettes.size() == 1);
Palette &palette = palettes[0];
std::fill(palette.colors.begin(), palette.colors.end(), Rgba::transparent);
for (auto const &slot : colors) {
if (!slot.has_value() || slot->isTransparent()) {
continue;
}
palette[slot->grayIndex()] = slot->cgbColor();
}
}
static unsigned int legacyLuminance(uint16_t color) {
uint8_t red = color & 0b11111;
uint8_t green = color >> 5 & 0b11111;
uint8_t blue = color >> 10;
return 2126 * red + 7152 * green + 722 * blue;
}
void rgb(std::vector<Palette> &palettes) {
options.verbosePrint(Options::VERB_LOG_ACT, "Sorting palettes by \"\"\"luminance\"\"\"...\n");
for (Palette &pal : palettes) {
std::sort(pal.begin(), pal.end(), [](uint16_t lhs, uint16_t rhs) {
return legacyLuminance(lhs) < legacyLuminance(rhs);
});
}
}
} // namespace sorting

451
src/gfx/pal_spec.cpp Normal file
View File

@@ -0,0 +1,451 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#include "gfx/pal_spec.hpp"
#include <algorithm>
#include <cassert>
#include <cinttypes>
#include <climits>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <ostream>
#include <streambuf>
#include <string>
#include <string_view>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include "platform.h"
#include "gfx/main.hpp"
using namespace std::string_view_literals;
constexpr uint8_t nibble(char c) {
if (c >= 'a') {
assert(c <= 'f');
return c - 'a' + 10;
} else if (c >= 'A') {
assert(c <= 'F');
return c - 'A' + 10;
} else {
assert(c >= '0' && c <= '9');
return c - '0';
}
}
constexpr uint8_t toHex(char c1, char c2) {
return nibble(c1) * 16 + nibble(c2);
}
constexpr uint8_t singleToHex(char c) {
return toHex(c, c);
}
template<typename Str> // Should be std::string or std::string_view
static void skipWhitespace(Str const &str, typename Str::size_type &pos) {
pos = std::min(str.find_first_not_of(" \t", pos), str.length());
}
void parseInlinePalSpec(char const * const rawArg) {
// List of #rrggbb/#rgb colors, comma-separated, palettes are separated by colons
std::string_view arg(rawArg);
using size_type = decltype(arg)::size_type;
auto parseError = [&rawArg, &arg](size_type ofs, size_type len, char const *fmt,
auto &&...args) {
(void)arg; // With NDEBUG, `arg` is otherwise not used
assert(ofs <= arg.length());
assert(len <= arg.length());
error(fmt, args...);
fprintf(stderr,
"In inline palette spec: %s\n"
" ",
rawArg);
for (auto i = ofs; i; --i) {
putc(' ', stderr);
}
for (auto i = len; i; --i) {
putc('^', stderr);
}
putc('\n', stderr);
};
options.palSpec.clear();
options.palSpec.emplace_back(); // Value-initialized, not default-init'd, so we get zeros
size_type n = 0; // Index into the argument
// TODO: store max `nbColors` ever reached, and compare against palette size later
size_t nbColors = 0; // Number of colors in the current palette
for (;;) {
++n; // Ignore the '#' (checked either by caller or previous loop iteration)
Rgba &color = options.palSpec.back()[nbColors];
auto pos = std::min(arg.find_first_not_of("0123456789ABCDEFabcdef"sv, n), arg.length());
switch (pos - n) {
case 3:
color = Rgba(singleToHex(arg[n + 0]), singleToHex(arg[n + 1]), singleToHex(arg[n + 2]),
0xFF);
break;
case 6:
color = Rgba(toHex(arg[n + 0], arg[n + 1]), toHex(arg[n + 2], arg[n + 3]),
toHex(arg[n + 4], arg[n + 5]), 0xFF);
break;
case 0:
parseError(n - 1, 1, "Missing color after '#'");
return;
default:
parseError(n, pos - n, "Unknown color specification");
return;
}
n = pos;
// Skip whitespace, if any
skipWhitespace(arg, n);
// Skip comma/semicolon, or end
if (n == arg.length()) {
break;
}
switch (arg[n]) {
case ',':
++n; // Skip it
++nbColors;
// A trailing comma may be followed by a semicolon
skipWhitespace(arg, n);
if (n == arg.length()) {
break;
} else if (arg[n] != ';' && arg[n] != ':') {
if (nbColors == 4) {
parseError(n, 1, "Each palette can only contain up to 4 colors");
return;
}
break;
}
[[fallthrough]];
case ':':
case ';':
++n;
skipWhitespace(arg, n);
nbColors = 0; // Start a new palette
// Avoid creating a spurious empty palette
if (n != arg.length()) {
options.palSpec.emplace_back();
}
break;
default:
parseError(n, 1, "Unexpected character, expected ',', ';', or end of argument");
return;
}
// Check again to allow trailing a comma/semicolon
if (n == arg.length()) {
break;
}
if (arg[n] != '#') {
parseError(n, 1, "Unexpected character, expected '#'");
return;
}
}
}
/**
* Tries to read some magic bytes from the provided `file`.
* Returns whether the magic was correctly read.
*/
template<size_t n>
static bool readMagic(std::filebuf &file, char const *magic) {
assert(strlen(magic) == n);
char magicBuf[n];
return file.sgetn(magicBuf, n) == n && memcmp(magicBuf, magic, n);
}
// Like `readMagic`, but automatically determines the size from the string literal's length.
// Don't worry if you make a mistake, an `assert`'s got your back!
#define READ_MAGIC(file, magic) \
readMagic<sizeof(magic) - 1>(file, magic) // Don't count the terminator
template<typename T, typename U>
static T readBE(U const *bytes) {
T val = 0;
for (size_t i = 0; i < sizeof(val); ++i) {
val = val << 8 | static_cast<uint8_t>(bytes[i]);
}
return val;
}
/**
* **Appends** the first line read from `file` to the end of the provided `buffer`.
*/
static void readLine(std::filebuf &file, std::string &buffer) {
// TODO: maybe this can be optimized to bulk reads?
for (;;) {
auto c = file.sbumpc();
if (c == std::filebuf::traits_type::eof()) {
return;
}
if (c == '\n') {
// Discard a trailing CRLF
if (!buffer.empty() && buffer.back() == '\r') {
buffer.pop_back();
}
return;
}
buffer.push_back(c);
}
}
// FIXME: Normally we'd use `std::from_chars`, but that's not available with GCC 7
/**
* Parses the initial part of a string_view, advancing the "read index" as it does
*/
static uint16_t parseDec(std::string const &str, std::string::size_type &n) {
uint32_t value = 0; // Use a larger type to handle overflow more easily
for (auto end = std::min(str.length(), str.find_first_not_of("0123456789", n)); n < end; ++n) {
value = std::min<uint32_t>(value * 10 + (str[n] - '0'), UINT16_MAX);
}
return value;
}
static void parsePSPFile(std::filebuf &file) {
// https://www.selapa.net/swatches/colors/fileformats.php#psp_pal
std::string line;
readLine(file, line);
if (line != "JASC-PAL") {
error("Palette file does not appear to be a PSP palette file");
return;
}
line.clear();
readLine(file, line);
if (line != "0100") {
error("Unsupported PSP palette file version \"%s\"", line.c_str());
return;
}
line.clear();
readLine(file, line);
std::string::size_type n = 0;
uint16_t nbColors = parseDec(line, n);
if (n != line.length()) {
error("Invalid \"number of colors\" line in PSP file (%s)", line.c_str());
return;
}
if (nbColors > options.nbColorsPerPal * options.nbPalettes) {
warning("PSP file contains %" PRIu16 " colors, but there can only be %" PRIu16
"; ignoring extra",
nbColors, options.nbColorsPerPal * options.nbPalettes);
nbColors = options.nbColorsPerPal * options.nbPalettes;
}
options.palSpec.clear();
for (uint16_t i = 0; i < nbColors; ++i) {
line.clear();
readLine(file, line);
n = 0;
uint8_t r = parseDec(line, n);
skipWhitespace(line, n);
if (n == line.length()) {
error("Failed to parse color #%" PRIu16 " (\"%s\"): missing green component", i + 1,
line.c_str());
return;
}
uint8_t g = parseDec(line, n);
if (n == line.length()) {
error("Failed to parse color #%" PRIu16 " (\"%s\"): missing green component", i + 1,
line.c_str());
return;
}
skipWhitespace(line, n);
uint8_t b = parseDec(line, n);
if (n != line.length()) {
error("Failed to parse color #%" PRIu16
" (\"%s\"): trailing characters after blue component",
i + 1, line.c_str());
return;
}
if (i % options.nbColorsPerPal == 0) {
options.palSpec.emplace_back();
}
options.palSpec.back()[i % options.nbColorsPerPal] = Rgba(r, g, b, 0xFF);
}
}
void parseACTFile(std::filebuf &file) {
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1070626
std::array<char, 772> buf;
auto len = file.sgetn(buf.data(), buf.size());
uint16_t nbColors = 256;
if (len == 772) {
nbColors = readBE<uint16_t>(&buf[768]);
// TODO: apparently there is a "transparent color index"? What?
if (nbColors > 256 || nbColors == 0) {
error("Invalid number of colors in ACT file (%" PRIu16 ")", nbColors);
return;
}
} else if (len != 768) {
error("Invalid file size for ACT file (expected 768 or 772 bytes, got %zu", len);
return;
}
if (nbColors > options.nbColorsPerPal * options.nbPalettes) {
warning("ACT file contains %" PRIu16 " colors, but there can only be %" PRIu16
"; ignoring extra",
nbColors, options.nbColorsPerPal * options.nbPalettes);
nbColors = options.nbColorsPerPal * options.nbPalettes;
}
options.palSpec.clear();
options.palSpec.emplace_back();
char const *ptr = buf.data();
size_t colorIdx = 0;
for (uint16_t i = 0; i < nbColors; ++i) {
Rgba &color = options.palSpec.back()[colorIdx];
color = Rgba(ptr[0], ptr[1], ptr[2], 0xFF);
ptr += 3;
++colorIdx;
if (colorIdx == options.nbColorsPerPal) {
options.palSpec.emplace_back();
colorIdx = 0;
}
}
// Remove the spurious empty palette if there is one
if (colorIdx == 0) {
options.palSpec.pop_back();
}
}
void parseACOFile(std::filebuf &file) {
// rhttps://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1055819
// http://www.nomodes.com/aco.html
char buf[10];
if (file.sgetn(buf, 2) != 2) {
error("Couldn't read ACO file version");
return;
}
if (readBE<uint16_t>(buf) != 1) {
error("Palette file does not appear to be an ACO file");
return;
}
if (file.sgetn(buf, 2) != 2) {
error("Couldn't read number of colors in palette file");
return;
}
uint16_t nbColors = readBE<uint16_t>(buf);
if (nbColors > options.nbColorsPerPal * options.nbPalettes) {
warning("ACO file contains %" PRIu16 " colors, but there can only be %" PRIu16
"; ignoring extra",
nbColors, options.nbColorsPerPal * options.nbPalettes);
nbColors = options.nbColorsPerPal * options.nbPalettes;
}
options.palSpec.clear();
for (uint16_t i = 0; i < nbColors; ++i) {
if (file.sgetn(buf, 10) != 10) {
error("Failed to read color #%" PRIu16 " from palette file", i + 1);
return;
}
if (i % options.nbColorsPerPal == 0) {
options.palSpec.emplace_back();
}
Rgba &color = options.palSpec.back()[i % options.nbColorsPerPal];
uint16_t colorType = readBE<uint16_t>(buf);
switch (colorType) {
case 0: // RGB
color = Rgba(buf[0], buf[2], buf[4], 0xFF);
break;
case 1: // HSB
error("Unsupported color type (HSB) for ACO file");
return;
case 2: // CMYK
error("Unsupported color type (CMYK) for ACO file");
return;
case 7: // Lab
error("Unsupported color type (lab) for ACO file");
return;
case 8: // Grayscale
error("Unsupported color type (grayscale) for ACO file");
return;
default:
error("Unknown color type (%" PRIu16 ") for ACO file", colorType);
return;
}
}
// TODO: maybe scan the v2 data instead (if present)
// `codecvt` can be used to convert from UTF-16 to UTF-8
}
void parseExternalPalSpec(char const *arg) {
// `fmt:path`, parse the file according to the given format
// Split both parts, error out if malformed
char const *ptr = strchr(arg, ':');
if (ptr == nullptr) {
error("External palette spec must have format `fmt:path` (missing colon)");
return;
}
char const *path = ptr + 1;
static std::array parsers{
std::tuple{"PSP", &parsePSPFile, std::ios::in },
std::tuple{"ACT", &parseACTFile, std::ios::binary},
std::tuple{"ACO", &parseACOFile, std::ios::binary},
};
auto iter = std::find_if(parsers.begin(), parsers.end(),
[&arg, &ptr](decltype(parsers)::value_type const &parser) {
return strncasecmp(arg, std::get<0>(parser), ptr - arg) == 0;
});
if (iter == parsers.end()) {
error("Unknown external palette format \"%.*s\"",
static_cast<int>(std::min(ptr - arg, static_cast<decltype(ptr - arg)>(INT_MAX))),
arg);
return;
}
std::filebuf file;
// Some parsers read the file in text mode, others in binary mode
if (!file.open(path, std::ios::in | std::get<2>(*iter))) {
error("Failed to open palette file \"%s\"", path);
return;
}
std::get<1> (*iter)(file);
}

1114
src/gfx/process.cpp Normal file

File diff suppressed because it is too large Load Diff

83
src/gfx/proto_palette.cpp Normal file
View File

@@ -0,0 +1,83 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#include "gfx/proto_palette.hpp"
#include <algorithm>
#include <array>
#include <cassert>
#include <stddef.h>
#include <stdint.h>
bool ProtoPalette::add(uint16_t color) {
size_t i = 0;
// Seek the first slot greater than our color
// (A linear search is better because we don't store the array size,
// and there are very few slots anyway)
while (_colorIndices[i] < color) {
++i;
if (i == _colorIndices.size())
return false;
}
// If we found ourselves, great!
if (_colorIndices[i] == color)
return true;
// Swap entries until the end
while (_colorIndices[i] != UINT16_MAX) {
std::swap(_colorIndices[i], color);
++i;
if (i == _colorIndices.size())
return false; // Oh well
}
// Write that last one into the new slot
_colorIndices[i] = color;
return true;
}
ProtoPalette::ComparisonResult ProtoPalette::compare(ProtoPalette const &other) const {
// This works because the sets are sorted numerically
assert(std::is_sorted(_colorIndices.begin(), _colorIndices.end()));
assert(std::is_sorted(other._colorIndices.begin(), other._colorIndices.end()));
auto ours = _colorIndices.begin(), theirs = other._colorIndices.begin();
bool weBigger = true, theyBigger = true;
while (ours != _colorIndices.end() && theirs != other._colorIndices.end()) {
if (*ours == *theirs) {
++ours;
++theirs;
} else if (*ours < *theirs) {
++ours;
theyBigger = false;
} else { // *ours > *theirs
++theirs;
weBigger = false;
}
}
weBigger &= theirs == other._colorIndices.end();
theyBigger &= ours == _colorIndices.end();
return theyBigger ? THEY_BIGGER : (weBigger ? WE_BIGGER : NEITHER);
}
size_t ProtoPalette::size() const {
return std::distance(begin(), end());
}
bool ProtoPalette::empty() const {
return _colorIndices[0] == UINT16_MAX;
}
auto ProtoPalette::begin() const -> decltype(_colorIndices)::const_iterator {
return _colorIndices.begin();
}
auto ProtoPalette::end() const -> decltype(_colorIndices)::const_iterator {
return std::find(_colorIndices.begin(), _colorIndices.end(), UINT16_MAX);
}

324
src/gfx/reverse.cpp Normal file
View File

@@ -0,0 +1,324 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#include "gfx/reverse.hpp"
#include <algorithm>
#include <array>
#include <assert.h>
#include <cinttypes>
#include <errno.h>
#include <fstream>
#include <optional>
#include <png.h>
#include <string.h>
#include <tuple>
#include <vector>
#include "defaultinitalloc.hpp"
#include "helpers.h"
#include "itertools.hpp"
#include "gfx/main.hpp"
static DefaultInitVec<uint8_t> readInto(std::string path) {
std::filebuf file;
file.open(path, std::ios::in | std::ios::binary);
DefaultInitVec<uint8_t> data(128 * 16); // Begin with some room pre-allocated
size_t curSize = 0;
for (;;) {
size_t oldSize = curSize;
curSize = data.size();
// Fill the new area ([oldSize; curSize[) with bytes
size_t nbRead =
file.sgetn(reinterpret_cast<char *>(&data.data()[oldSize]), curSize - oldSize);
if (nbRead != curSize - oldSize) {
// Shrink the vector to discard bytes that weren't read
data.resize(oldSize + nbRead);
break;
}
// If the vector has some capacity left, use it; otherwise, double the current size
// Arbitrary, but if you got a better idea...
size_t newSize = oldSize != data.capacity() ? data.capacity() : oldSize * 2;
assert(oldSize != newSize);
data.resize(newSize);
}
return data;
}
[[noreturn]] static void pngError(png_structp png, char const *msg) {
fatal("Error writing reversed image (\"%s\"): %s",
static_cast<char const *>(png_get_error_ptr(png)), msg);
}
static void pngWarning(png_structp png, char const *msg) {
warning("While writing reversed image (\"%s\"): %s",
static_cast<char const *>(png_get_error_ptr(png)), msg);
}
void writePng(png_structp png, png_bytep data, size_t length) {
auto &pngFile = *static_cast<std::filebuf *>(png_get_io_ptr(png));
pngFile.sputn(reinterpret_cast<char *>(data), length);
}
void flushPng(png_structp png) {
auto &pngFile = *static_cast<std::filebuf *>(png_get_io_ptr(png));
pngFile.pubsync();
}
void reverse() {
options.verbosePrint(Options::VERB_CFG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
// Check for weird flag combinations
if (options.output.empty()) {
fatal("Tile data must be provided when reversing an image!");
}
if (options.allowDedup && options.tilemap.empty()) {
warning("Tile deduplication is enabled, but no tilemap is provided?");
}
if (options.useColorCurve) {
warning("The color curve is not yet supported in reverse mode...");
}
if (options.inputSlice.left != 0 || options.inputSlice.top != 0
|| options.inputSlice.height != 0) {
warning("\"Sliced-off\" pixels are ignored in reverse mode");
}
if (options.inputSlice.width != 0 && options.inputSlice.width != options.reversedWidth * 8) {
warning("Specified input slice width (%" PRIu16
") doesn't match provided reversing width (%" PRIu8 " * 8)",
options.inputSlice.width, options.reversedWidth);
}
options.verbosePrint(Options::VERB_LOG_ACT, "Reading tiles...\n");
auto const tiles = readInto(options.output);
uint8_t tileSize = 8 * options.bitDepth;
if (tiles.size() % tileSize != 0) {
fatal("Tile data size must be a multiple of %" PRIu8 " bytes! (Read %zu)", tileSize,
tiles.size());
}
// By default, assume tiles are not deduplicated, and add the (allegedly) trimmed tiles
size_t nbTileInstances = tiles.size() / tileSize + options.trim; // Image size in tiles
options.verbosePrint(Options::VERB_INTERM, "Read %zu tiles.\n", nbTileInstances);
std::optional<DefaultInitVec<uint8_t>> tilemap;
if (!options.tilemap.empty()) {
tilemap = readInto(options.tilemap);
nbTileInstances = tilemap->size();
}
if (nbTileInstances > options.maxNbTiles[0] + options.maxNbTiles[1]) {
warning("Read %zu tiles, more than the limit of %zu + %zu", nbTileInstances,
options.maxNbTiles[0], options.maxNbTiles[1]);
}
size_t width = options.reversedWidth, height; // In tiles
if (nbTileInstances % width != 0) {
fatal("Total number of tiles read (%zu) cannot be divided by image width (%zu tiles)",
nbTileInstances, width);
}
height = nbTileInstances / width;
options.verbosePrint(Options::VERB_INTERM, "Reversed image dimensions: %zux%zu tiles\n", width,
height);
// TODO: -U
std::vector<std::array<Rgba, 4>> palettes{
{Rgba(0xffffffff), Rgba(0xaaaaaaff), Rgba(0x555555ff), Rgba(0x000000ff)}
};
if (!options.palettes.empty()) {
std::filebuf file;
file.open(options.palettes, std::ios::in | std::ios::binary);
palettes.clear();
std::array<uint8_t, sizeof(uint16_t) * 4> buf; // 4 colors
size_t nbRead;
do {
nbRead = file.sgetn(reinterpret_cast<char *>(buf.data()), buf.size());
if (nbRead == buf.size()) {
// Expand the colors
auto &palette = palettes.emplace_back();
std::generate(palette.begin(), palette.begin() + options.nbColorsPerPal,
[&buf, i = 0]() mutable {
i += 2;
return Rgba::fromCGBColor(buf[i - 2] + (buf[i - 1] << 8));
});
} else if (nbRead != 0) {
fatal("Palette data size (%zu) is not a multiple of %zu bytes!\n",
palettes.size() * buf.size() + nbRead, buf.size());
}
} while (nbRead != 0);
if (palettes.size() > options.nbPalettes) {
warning("Read %zu palettes, more than the specified limit of %zu", palettes.size(),
options.nbPalettes);
}
}
std::optional<DefaultInitVec<uint8_t>> attrmap;
if (!options.attrmap.empty()) {
attrmap = readInto(options.attrmap);
if (attrmap->size() != nbTileInstances) {
fatal("Attribute map size (%zu tiles) doesn't match image's (%zu)", attrmap->size(),
nbTileInstances);
}
// Scan through the attributes for inconsistencies
// We do this now for two reasons:
// 1. Checking those during the main loop is harmful to optimization, and
// 2. It clutters the code more, and it's not in great shape to begin with
bool bad = false;
for (auto attr : *attrmap) {
if ((attr & 0b111) > palettes.size()) {
error("Referencing palette %u, but there are only %zu!");
bad = true;
}
if (attr & 0x08 && !tilemap) {
warning("Tile in bank 1 but no tilemap specified; ignoring the bank bit");
}
}
if (bad) {
giveUp();
}
}
if (tilemap) {
if (attrmap) {
for (auto [id, attr] : zip(*tilemap, *attrmap)) {
bool bank = attr & 1 << 3;
if (id >= options.maxNbTiles[bank]) {
warning("Tile #%" PRIu8
" was referenced, but the limit for bank %u is %" PRIu16,
id, bank, options.maxNbTiles[bank]);
}
}
} else {
for (auto id : *tilemap) {
if (id >= options.maxNbTiles[0]) {
warning("Tile #%" PRIu8 " was referenced, but the limit is %" PRIu16, id,
options.maxNbTiles[0]);
}
}
}
}
std::optional<DefaultInitVec<uint8_t>> palmap;
if (!options.palmap.empty()) {
palmap = readInto(options.palmap);
if (palmap->size() != nbTileInstances) {
fatal("Palette map size (%zu tiles) doesn't match image's (%zu)", palmap->size(),
nbTileInstances);
}
}
options.verbosePrint(Options::VERB_LOG_ACT, "Writing image...\n");
std::filebuf pngFile;
pngFile.open(options.input, std::ios::out | std::ios::binary);
png_structp png = png_create_write_struct(
PNG_LIBPNG_VER_STRING,
const_cast<png_voidp>(static_cast<void const *>(options.input.c_str())), pngError,
pngWarning);
if (!png) {
fatal("Couldn't create PNG write struct: %s", strerror(errno));
}
png_infop pngInfo = png_create_info_struct(png);
if (!pngInfo) {
fatal("Couldn't create PNG info struct: %s", strerror(errno));
}
png_set_write_fn(png, &pngFile, writePng, flushPng);
png_set_IHDR(png, pngInfo, options.reversedWidth * 8, height * 8, 8, PNG_COLOR_TYPE_RGB_ALPHA,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png, pngInfo);
png_color_8 sbitChunk;
sbitChunk.red = 5;
sbitChunk.green = 5;
sbitChunk.blue = 5;
sbitChunk.alpha = 1;
png_set_sBIT(png, pngInfo, &sbitChunk);
constexpr uint8_t SIZEOF_PIXEL = 4; // Each pixel is 4 bytes (RGBA @ 8 bits/component)
size_t const SIZEOF_ROW = options.reversedWidth * 8 * SIZEOF_PIXEL;
std::vector<uint8_t> tileRow(8 * SIZEOF_ROW, 0xFF); // Data for 8 rows of pixels
uint8_t * const rowPtrs[8] = {
&tileRow.data()[0 * SIZEOF_ROW], &tileRow.data()[1 * SIZEOF_ROW],
&tileRow.data()[2 * SIZEOF_ROW], &tileRow.data()[3 * SIZEOF_ROW],
&tileRow.data()[4 * SIZEOF_ROW], &tileRow.data()[5 * SIZEOF_ROW],
&tileRow.data()[6 * SIZEOF_ROW], &tileRow.data()[7 * SIZEOF_ROW],
};
for (size_t ty = 0; ty < height; ++ty) {
for (size_t tx = 0; tx < width; ++tx) {
size_t index = options.columnMajor ? ty + tx * width : ty * width + tx;
// By default, a tile is unflipped, in bank 0, and uses palette #0
uint8_t attribute = attrmap.has_value() ? (*attrmap)[index] : 0x00;
bool bank = attribute & 0x08;
// Get the tile ID at this location
uint8_t tileID = index;
if (tilemap.has_value()) {
tileID =
(*tilemap)[index] - options.baseTileIDs[bank] + bank * options.maxNbTiles[0];
}
assert(tileID < nbTileInstances); // Should have been checked earlier
size_t palID = palmap ? (*palmap)[index] : attribute & 0b111;
assert(palID < palettes.size()); // Should be ensured on data read
// We do not have data for tiles trimmed with `-x`, so assume they are "blank"
static std::array<uint8_t, 16> const trimmedTile{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
uint8_t const *tileData = tileID > nbTileInstances - options.trim
? trimmedTile.data()
: &tiles[tileID * tileSize];
auto const &palette = palettes[palID];
for (uint8_t y = 0; y < 8; ++y) {
// If vertically mirrored, fetch the bytes from the other end
uint8_t realY = attribute & 0x40 ? 7 - y : y;
uint8_t bitplane0 = tileData[realY * 2], bitplane1 = tileData[realY * 2 + 1];
if (attribute & 0x20) { // Handle horizontal flip
bitplane0 = flipTable[bitplane0];
bitplane1 = flipTable[bitplane1];
}
uint8_t *ptr = &rowPtrs[y][tx * 8 * SIZEOF_PIXEL];
for (uint8_t x = 0; x < 8; ++x) {
uint8_t bit0 = bitplane0 & 0x80, bit1 = bitplane1 & 0x80;
Rgba const &pixel = palette[bit0 >> 7 | bit1 >> 6];
*ptr++ = pixel.red;
*ptr++ = pixel.green;
*ptr++ = pixel.blue;
*ptr++ = pixel.alpha;
// Shift the pixel out
bitplane0 <<= 1;
bitplane1 <<= 1;
}
}
}
// We never modify the pointers, and neither should libpng, despite the overly lax function
// signature.
// (AIUI, casting away const-ness is okay as long as you don't actually modify the
// pointed-to data)
png_write_rows(png, const_cast<png_bytepp>(rowPtrs), 8);
}
// Finalize the write
png_write_end(png, pngInfo);
png_destroy_write_struct(&png, &pngInfo);
pngFile.close();
}

52
src/gfx/rgba.cpp Normal file
View File

@@ -0,0 +1,52 @@
#include "gfx/rgba.hpp"
#include <assert.h>
#include <stdint.h>
#include "gfx/main.hpp" // options
/*
* based on the Gaussian-like curve used by SameBoy since commit
* 65dd02cc52f531dbbd3a7e6014e99d5b24e71a4c (Oct 2017)
* with ties resolved by comparing the difference of the squares.
*/
static std::array<uint8_t, 256> reverse_curve{
0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, // These
5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, // comments
7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, // prevent
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, // clang-format
10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, // from
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, // reflowing
13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, // these
14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, // sixteen
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, // 16-item
17, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, // lines,
18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, // which,
19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, // in
21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, // my
22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, // opinion,
24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, // help
26, 27, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 30, 30, 31, // visualization!
};
uint16_t Rgba::cgbColor() const {
if (isTransparent()) {
return transparent;
}
assert(isOpaque());
uint8_t r = red, g = green, b = blue;
if (options.useColorCurve) {
g = g * 4 < b ? 0 : (g * 4 - b) / 3;
r = reverse_curve[r];
g = reverse_curve[g];
b = reverse_curve[b];
}
return (r >> 3) | (g >> 3) << 5 | (b >> 3) << 10;
}
uint8_t Rgba::grayIndex() const {
assert(isGray());
// Convert from [0; 256[ to [0; maxOpaqueColors[
return static_cast<uint16_t>(255 - red) * options.maxOpaqueColors() / 256;
}

View File

@@ -1,160 +0,0 @@
.\"
.\" This file is part of RGBDS.
.\"
.\" Copyright (c) 2013-2021, stag019 and RGBDS contributors.
.\"
.\" SPDX-License-Identifier: MIT
.\"
.Dd March 28, 2021
.Dt RGBGFX 1
.Os
.Sh NAME
.Nm rgbgfx
.Nd Game Boy graphics converter
.Sh SYNOPSIS
.Nm
.Op Fl CDhmuVv
.Op Fl f | Fl F
.Op Fl a Ar attrmap | Fl A
.Op Fl d Ar depth
.Op Fl o Ar out_file
.Op Fl p Ar pal_file | Fl P
.Op Fl t Ar tilemap | Fl T
.Op Fl x Ar tiles
.Ar file
.Sh DESCRIPTION
The
.Nm
program converts PNG images into the Nintendo Game Boy's planar tile format.
.Pp
The resulting colors and their palette indices are determined differently depending on the input PNG file:
.Bl -dash -width Ds
.It
If the file has an embedded palette, that palette's color and order are used.
.It
If not, and the image only contains shades of gray, rgbgfx maps them to the indices appropriate for each shade.
Any undetermined indices are set to respective default shades of gray.
For example: if the bit depth is 2 and the image contains light gray and black, they become the second and fourth colors, and the first and third colors get set to default white and dark gray.
If the image has multiple shades that map to the same index, the palette is instead determined as if the image had color.
.It
If the image has color (or the grayscale method failed), the colors are sorted from lightest to darkest.
.El
.Pp
The input image may not contain more colors than the selected bit depth allows.
Transparent pixels are set to palette index 0.
.Sh ARGUMENTS
Note that options can be abbreviated as long as the abbreviation is unambiguous:
.Fl Fl verb
is
.Fl Fl verbose ,
but
.Fl Fl ver
is invalid because it could also be
.Fl Fl version .
The arguments are as follows:
.Bl -tag -width Ds
.It Fl a Ar attrmap , Fl Fl attr-map Ar attrmap
Generate a file of tile mirroring attributes for OAM or (CGB-only) background tiles.
For each tile in the input file, a byte is written representing the dimensions that the associated tile in the output file should be mirrored.
Useful in combination with
.Fl m
to keep track the mirror direction of mirrored duplicate tiles.
.It Fl A , Fl Fl output-attr-map
Same as
.Fl a ,
but the attrmap file output name is made by taking the input filename, removing the file extension, and appending
.Pa .attrmap .
.It Fl C , Fl Fl color-curve
Use the color curve of the Game Boy Color when generating palettes.
.It Fl D , Fl Fl debug
Debug features are enabled.
.It Fl d Ar depth , Fl Fl depth Ar depth
The bit depth of the output image (either 1 or 2).
By default, the bit depth is 2 (two bits per pixel).
.It Fl f , Fl Fl fix
Fix the input PNG file to be a correctly indexed image.
.It Fl F , Fl Fl fix-and-save
Same as
.Fl f ,
but additionally, the supplied command line parameters are saved within the PNG and will be loaded and automatically used next time.
.It Fl h , Fl Fl horizontal
Lay out tiles in column-major order (column by column), instead of the default row-major order (line by line).
Especially useful for "8x16" OBJ mode, if the input image is 16 pixels tall.
.It Fl m , Fl Fl mirror-tiles
Truncate tiles by checking for tiles that are mirrored versions of others and omitting these from the output file.
Useful with tilemaps and attrmaps together to keep track of the duplicated tiles and the dimension mirrored.
Tiles are checked for horizontal, vertical, and horizontal-vertical mirroring.
Implies
.Fl u .
.It Fl o Ar out_file , Fl Fl output Ar out_file
The name of the output file.
.It Fl p Ar pal_file , Fl Fl palette Ar pal_file
Output the image's palette in standard GBC palette format: bytes (8 bytes for two bits per pixel, 4 bytes for one bit per pixel) containing the RGB15 values in little-endian byte order.
If the palette contains too few colors, the remaining entries are set to black.
.It Fl P , Fl Fl output-palette
Same as
.Fl p ,
but the palette file output name is made by taking the input PNG file's filename, removing the file extension, and appending
.Pa .pal .
.It Fl t Ar tilemap , Fl Fl tilemap Ar tilemap
Generate a file of tile indices.
For each tile in the input file, a byte is written representing the index of the associated tile in the output file.
Useful in combination with
.Fl u
or
.Fl m
to keep track of duplicate tiles.
.It Fl T , Fl Fl output-tilemap
Same as
.Fl t ,
but the tilemap file output name is made by taking the input filename, removing the file extension, and appending
.Pa .tilemap .
.It Fl u , Fl Fl unique-tiles
Truncate tiles by checking for tiles that are exact duplicates of others and omitting these from the output file.
Useful with tilemaps to keep track of the duplicated tiles.
.It Fl V , Fl Fl version
Print the version of the program and exit.
.It Fl v , Fl Fl verbose
Verbose.
Print errors when the command line parameters and the parameters in the PNG file don't match.
.It Fl x Ar tiles , Fl Fl trim-end Ar tiles
Trim the end of the output file by this many tiles.
.El
.Sh EXAMPLES
The following will take a PNG file with a bit depth of 1, 2, or 8, and output planar 2bpp data:
.Pp
.D1 $ rgbgfx -o out.2bpp in.png
.Pp
The following creates a planar 2bpp file with only unique tiles, and its tilemap
.Pa out.tilemap :
.Pp
.D1 $ rgbgfx -T -u -o out.2bpp in.png
.Pp
The following creates a planar 2bpp file with only unique tiles
.Pa accounting for tile mirroring
and its associated tilemap
.Pa out.tilemap
and attrmap
.Pa out.attrmap :
.Pp
.D1 $ rgbgfx -A -T -m -o out.2bpp in.png
.Pp
The following will do nothing:
.Pp
.D1 $ rgbgfx in.png
.Sh BUGS
Please report bugs on
.Lk https://github.com/gbdev/rgbds/issues GitHub .
.Sh SEE ALSO
.Xr rgbds 7 ,
.Xr rgbasm 1 ,
.Xr rgblink 1 ,
.Xr rgbfix 1 ,
.Xr gbz80 7
.Sh HISTORY
.Nm
was created by
.An stag019
to be included in RGBDS.
It is now maintained by a number of contributors at
.Lk https://github.com/gbdev/rgbds .

View File

@@ -8,6 +8,7 @@
#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -446,9 +447,27 @@ void assign_AssignSections(void)
/* Overlaying requires only fully-constrained sections */
verbosePrint("Assigning other sections...\n");
if (overlayFileName)
errx("All sections must be fixed when using an overlay file; %" PRIu64 " %sn't",
nbSectionsToAssign, nbSectionsToAssign == 1 ? "is" : "are");
if (overlayFileName) {
fprintf(stderr, "FATAL: All sections must be fixed when using an overlay file");
uint8_t nbSections = 0;
for (int8_t constraints = BANK_CONSTRAINED | ALIGN_CONSTRAINED; constraints >= 0; constraints--) {
for (sectionPtr = unassignedSections[constraints];
sectionPtr;
sectionPtr = sectionPtr->next) {
fprintf(stderr, "%c \"%s\"",
nbSections == 0 ? ';': ',', sectionPtr->section->name);
nbSections++;
if (nbSections == 10)
goto max_out;
}
}
max_out:
if (nbSectionsToAssign != nbSections)
fprintf(stderr, " and %" PRIu64 " more", nbSectionsToAssign - nbSections);
fprintf(stderr, " %sn't\n", nbSectionsToAssign == 1 ? "is" : "are");
exit(1);
}
/* Assign all remaining sections by decreasing constraint order */
for (int8_t constraints = BANK_CONSTRAINED | ALIGN_CONSTRAINED;

View File

@@ -151,8 +151,10 @@ static char *readstr(FILE *file)
/* Read char */
int byte = getc(file);
if (byte == EOF)
if (byte == EOF) {
free(str);
return NULL;
}
str[index] = byte;
} while (str[index]);
return str;

View File

@@ -39,7 +39,7 @@ struct SortedSymbol {
};
static struct {
uint32_t nbBanks;
uint32_t nbBanks; // Size of the array below (which may be NULL if this is 0)
struct SortedSections {
struct SortedSection *sections;
struct SortedSection *zeroLenSections;
@@ -243,9 +243,9 @@ static void writeROM(void)
coverOverlayBanks(nbOverlayBanks);
if (outputFile) {
if (sections[SECTTYPE_ROM0].nbBanks > 0)
writeBank(sections[SECTTYPE_ROM0].banks[0].sections,
startaddr[SECTTYPE_ROM0], maxsize[SECTTYPE_ROM0]);
writeBank(sections[SECTTYPE_ROM0].banks ? sections[SECTTYPE_ROM0].banks[0].sections
: NULL,
startaddr[SECTTYPE_ROM0], maxsize[SECTTYPE_ROM0]);
for (uint32_t i = 0 ; i < sections[SECTTYPE_ROMX].nbBanks; i++)
writeBank(sections[SECTTYPE_ROMX].banks[i].sections,

18
test/CMakeLists.txt Normal file
View File

@@ -0,0 +1,18 @@
add_executable(randtilegen gfx/randtilegen.c)
add_executable(rgbgfx_test gfx/rgbgfx_test.cpp)
install(TARGETS randtilegen rgbgfx_test DESTINATION ${rgbds_SOURCE_DIR}/test/gfx)
foreach(TARGET randtilegen rgbgfx_test)
if(LIBPNG_FOUND) # pkg-config
target_include_directories(${TARGET} PRIVATE ${LIBPNG_INCLUDE_DIRS})
target_link_directories(${TARGET} PRIVATE ${LIBPNG_LIBRARY_DIRS})
target_link_libraries(${TARGET} PRIVATE ${LIBPNG_LIBRARIES})
else()
target_compile_definitions(${TARGET} PRIVATE ${PNG_DEFINITIONS})
target_include_directories(${TARGET} PRIVATE ${PNG_INCLUDE_DIRS})
target_link_libraries(${TARGET} PRIVATE ${PNG_LIBRARIES})
endif()
endforeach()

View File

@@ -1,3 +0,0 @@
; Remove this test case when _PI is removed.
PRINTLN "{f:_PI}"
PURGE _PI

View File

@@ -1,5 +0,0 @@
warning: deprecated-pi.asm(2): [-Wobsolete]
`_PI` is deprecated; use 3.14159
error: deprecated-pi.asm(3):
Built-in symbol '_PI' cannot be purged
error: Assembly aborted (1 error)!

View File

@@ -1 +0,0 @@
3.14159

View File

@@ -4,8 +4,8 @@
\1: < 1>
\2: <2>
'mac c,d':
\1: < c>
'mac c,d':
\1: <c>
\2: <d>
'mac 1,2 + 2,3':

View File

@@ -10,6 +10,8 @@ pusho
println $8000_0000 / -1
popo
opt H, l
ds 1
ld [$ff88], a
halt

View File

@@ -1,2 +1,2 @@
warning: opt.asm(16): [-Wdiv]
warning: opt.asm(18): [-Wdiv]
Division of -2147483648 by -1 yields -2147483648

View File

@@ -1,4 +1,4 @@
#!/bin/sh
#!/usr/bin/env bash
export LC_ALL=C

View File

@@ -0,0 +1,19 @@
MACRO print_all
REPT _NARG
PRINTLN "{d:_NARG}: \"\1\""
SHIFT
ENDR
ENDM
print_all a, \
b \
, c
DEF EMPTY equs ""
print_all a, \
{EMPTY} b \
{EMPTY}, c
print_all a, \
/* . */ b \
/* . */, c

View File

View File

@@ -0,0 +1,9 @@
3: "a"
2: "b"
1: "c"
3: "a"
2: "b"
1: "c"
3: "a"
2: " b"
1: "c"

View File

@@ -1,4 +1,4 @@
#!/bin/sh
#!/usr/bin/env bash
fname=$(mktemp)
for i in *.asm; do

View File

@@ -80,22 +80,23 @@ echo "${bold}Checking padding...${resbold}"
cp "$src"/padding{,-large,-larger}.bin .
touch padding{,-large,-larger}.err
progress=0
for b in {0..254}; do
printf "\r$b..."
for (( i=0; i < 10; ++i )); do
(( padding = RANDOM % 256 ))
echo "$padding..."
for suffix in '' -large -larger; do
cat <<<'-p $b' >padding$suffix.flags
tr '\377' \\$(($b / 64))$((($b / 8) % 8))$(($b % 8)) <"$src/padding$suffix.gb" >padding$suffix.gb # OK because $FF bytes are only used for padding
cat <<<"-p $padding" >padding$suffix.flags
tr '\377' \\$((padding / 64))$(((padding / 8) % 8))$((padding % 8)) <"$src/padding$suffix.gb" >padding$suffix.gb # OK because $FF bytes are only used for padding
runTest padding${suffix} .
done
done
printf "\rDone! \n"
echo "Done!"
# TODO: check MBC names
# Check that RGBFIX errors out when inputting a non-existent file...
$RGBFIX noexist 2>out.err
rc=$(($rc || $? != 1))
rc=$((rc || $? != 1))
tryDiff "$src/noexist.err" out.err noexist.err
rc=$(($rc || $?))
rc=$((rc || $?))
exit $rc

11
test/gfx/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
# Test binaries
/randtilegen
/rgbgfx_test
# Generated by randtilegen
/out*.png
/*.rng
# Generated by the test program
/result.2bpp
/result.pal
/result.attrmap
/result.png

View File

@@ -0,0 +1,6 @@
error: Could not fit tile colors [$6c8a, $7f55, $7fff] in specified palettes
error: Could not fit tile colors [$6c8a, $7f55] in specified palettes
note: The following palettes were specified:
[$7fff, $7f55]
[$7fff, $6c8a]
Conversion aborted after 2 errors

View File

@@ -0,0 +1 @@
-c #fff,#a9d4fe:#fff,#5721d9

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

1
test/gfx/crop.flags Normal file
View File

@@ -0,0 +1 @@
-L 2,1:1,1

BIN
test/gfx/crop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

2
test/gfx/damaged1.err Normal file
View File

@@ -0,0 +1,2 @@
FATAL: Error reading input image ("damaged1.png"): IDAT: invalid code -- missing end-of-block
Conversion aborted after 1 error

BIN
test/gfx/damaged1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

2
test/gfx/damaged2.err Normal file
View File

@@ -0,0 +1,2 @@
FATAL: Error reading input image ("damaged2.png"): IDAT: invalid code -- missing end-of-block
Conversion aborted after 1 error

BIN
test/gfx/damaged2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

2
test/gfx/damaged9.err Normal file
View File

@@ -0,0 +1,2 @@
FATAL: Error reading input image ("damaged9.png"): IDAT: invalid code -- missing end-of-block
Conversion aborted after 1 error

BIN
test/gfx/damaged9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

320
test/gfx/randtilegen.c Normal file
View File

@@ -0,0 +1,320 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*
* Originally:
* // This program is hereby released to the public domain.
* // ~aaaaaa123456789, released 2022-03-15
* https://gist.github.com/aaaaaa123456789/3feccf085ab4f82d144d9a47fb1b4bdf
*
* This was modified to use libpng instead of libplum, as well as comments and style changes.
*/
#include <assert.h>
#include <errno.h>
#include <limits.h>
#include <png.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "platform.h"
#define STR(x) #x
#define XSTR(x) STR(x)
struct Attributes {
unsigned char palette;
unsigned char nbColors;
};
static unsigned long long randbits = 0;
static unsigned char randcount = 0;
static _Noreturn void fatal(char const *error) {
fprintf(stderr, "FATAL: %s\n", error);
exit(1);
}
static FILE *seed;
static unsigned long long getRandomBits(unsigned count) {
while (count > randcount) {
// Get new random bytes from stdin (assumed to be a stream of random data) to fulfill the
// random bits request
int data = getc(seed);
if (data == EOF) {
exit(0);
}
randbits |= (unsigned long long)data << randcount;
randcount += 8;
}
unsigned long long result = randbits & ((1ull << count) - 1);
randbits >>= count;
randcount -= count;
return result;
}
static void generate_tile_attributes(struct Attributes * restrict attributes) {
/*
* Images have ten colors, grouped into two groups of 5 colors. The palette index indicates two
* things: which one of those groups will be used, and which colors out of those 5 will be used
* by the tile. The low bit indicates the group, and the rest of the value indicates the subset
* of colors. The remainder of the number is treated as a bitfield, where each bit represents a
* color: for instance, a value of 13 in the upper bits (binary 01101) indicates that colors 0,
* 2 and 3 from that group will be used. Values of 0 and 31 are naturally invalid because they
* indicate zero and five colors respectively, and 30 is also excluded to ensure that the
* particular subset of colors 1, 2, 3 and 4 never shows up. This guarantees that every tile
* will be representable using a palette containing color 0 (since those that don't contain
* color 0 will have three colors at most), which in turn ensures that only 4 palettes per group
* (and thus 8 total) are needed to cover the image: 0, 1, 2, 3; 0, 1, 2, 4; 0, 1, 3, 4; and 0,
* 2, 3, 4. This also implies that making color 0 transparent (in both groups) adds a
* transparent color to every palette.
*/
unsigned char pal;
do {
pal = getRandomBits(5);
} while (pal == 0 || (pal > 29));
attributes->palette = 2 * pal + getRandomBits(1);
// Use an array to look up the number of colors in the palette; this is faster (and simpler)
// than doing a population count over the bits
static char const popcount[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3,
4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4};
attributes->nbColors = popcount[pal];
}
static void generate_tile_data(unsigned char tiledata[ARR_QUALS(restrict) MIN_NB_ELMS(8)][8],
unsigned colorcount) {
switch (colorcount) {
case 2: // 1bpp
for (uint8_t y = 0; y < 8; y++) {
for (uint8_t x = 0; x < 8; x++) {
tiledata[y][x] = getRandomBits(1);
}
}
break;
case 4: // 2bpp
for (uint8_t y = 0; y < 8; y++) {
for (uint8_t x = 0; x < 8; x++) {
tiledata[y][x] = getRandomBits(2);
}
}
break;
case 3: // 2bpp with resampling
for (uint8_t y = 0; y < 8; y++) {
for (uint8_t x = 0; x < 8; x++) {
do {
tiledata[y][x] = getRandomBits(2);
} while (tiledata[y][x] == 3);
}
}
}
}
// Can't mark as `const`, as the array type is otherwise not compatible (augh)
static void
copy_tile_data(unsigned char destination[ARR_QUALS(restrict) MIN_NB_ELMS(8)][8],
unsigned char /* const */ source[ARR_QUALS(restrict) MIN_NB_ELMS(8)][8]) {
// Apply a random rotation to the copy
// coord ^ 7 = inverted coordinate; coord ^ 0 = regular coordinate
unsigned xmask = getRandomBits(1) * 7;
unsigned ymask = getRandomBits(1) * 7;
for (unsigned y = 0; y < 8; y++) {
for (unsigned x = 0; x < 8; x++) {
destination[y][x] = source[y ^ ymask][x ^ xmask];
}
}
}
static void generate_palettes(uint16_t palettes[ARR_QUALS(restrict) MIN_NB_ELMS(60)][4]) {
uint16_t colors[10];
// Generate 10 random colors (two groups of 5 colors)
for (unsigned p = 0; p < 10; p++) {
colors[p] = getRandomBits(15);
}
// Potentially make the first color of each group transparent
if (!getRandomBits(2)) {
colors[0] |= 0x8000;
colors[5] |= 0x8000;
}
for (unsigned p = 0; p < 60; p++) {
uint16_t const *group = colors + 5 * (p & 1);
uint16_t *palette = palettes[p];
for (unsigned index = 0; index < 5; index++) {
if (p & (2 << index)) {
*(palette++) = group[index];
}
}
}
}
/**
* Expand a 5-bit color component to 8 bits with minimal bias
*/
static uint8_t _5to8(uint8_t five) {
return five << 3 | five >> 2;
}
// Can't mark as `const`, as the array type is otherwise not compatible (augh)
static void write_image(char const *filename, uint16_t /* const */ palettes[MIN_NB_ELMS(60)][4],
unsigned char /* const */ (*tileData)[8][8],
struct Attributes const *attributes, uint8_t width, uint8_t height) {
uint8_t const nbTiles = width * height;
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
png_infop pngInfo = png_create_info_struct(png);
if (setjmp(png_jmpbuf(png))) {
fprintf(stderr, "FATAL: An error occurred while writing image \"%s\"", filename);
exit(1);
}
FILE *file = fopen(filename, "wb");
if (file == NULL) {
fprintf(stderr, "FATAL: Failed to open \"%s\": %s\n", filename, strerror(errno));
exit(1);
}
png_init_io(png, file);
png_set_IHDR(png, pngInfo, width * 8, height * 8, 8, PNG_COLOR_TYPE_RGB_ALPHA,
getRandomBits(1) ? PNG_INTERLACE_NONE : PNG_INTERLACE_ADAM7,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
// While it would be nice to write the image little by little, I really don't want to handle
// interlacing myself. (We're doing interlacing to test that RGBGFX correctly handles it.)
uint8_t const SIZEOF_PIXEL = 4; // Each pixel is 4 bytes (RGBA @ 8 bits/component)
assert(width != 0);
assert(height != 0);
uint8_t *data = malloc(height * 8 * width * 8 * SIZEOF_PIXEL);
uint8_t **rowPtrs = malloc(height * 8 * sizeof(*rowPtrs));
if (data == NULL || rowPtrs == NULL) {
fatal("Out of memory");
}
for (uint8_t y = 0; y < height * 8; ++y) {
rowPtrs[y] = &data[y * width * 8 * SIZEOF_PIXEL];
}
for (uint8_t p = 0; p < nbTiles; p++) {
uint8_t const tx = 8 * (p % width), ty = 8 * (p / width);
for (uint8_t y = 0; y < 8; y++) {
uint8_t * const row = rowPtrs[ty + y];
for (uint8_t x = 0; x < 8; x++) {
uint8_t * const pixel = &row[(tx + x) * SIZEOF_PIXEL];
uint16_t const color = palettes[attributes[p].palette][tileData[p][y][x]];
pixel[0] = _5to8(color & 0x1F);
pixel[1] = _5to8(color >> 5 & 0x1F);
pixel[2] = _5to8(color >> 10 & 0x1F);
pixel[3] = color & 0x8000 ? 0x00 : 0xFF;
}
}
}
png_set_rows(png, pngInfo, rowPtrs);
png_write_png(png, pngInfo, PNG_TRANSFORM_IDENTITY, NULL);
fclose(file);
free(rowPtrs);
free(data);
png_destroy_write_struct(&png, &pngInfo);
}
static void generate_random_image(char const *filename) {
#define MIN_TILES_PER_SIDE 3
#define MAX_TILES ((MIN_TILES_PER_SIDE + 7) * (MIN_TILES_PER_SIDE + 7))
struct Attributes attributes[MAX_TILES];
unsigned char tileData[MAX_TILES][8][8];
uint8_t width = getRandomBits(3) + MIN_TILES_PER_SIDE,
height = getRandomBits(3) + MIN_TILES_PER_SIDE;
for (uint8_t tile = 0; tile < (width * height); tile++) {
generate_tile_attributes(attributes + tile);
// If a tile contains only one color, then there's no tile data to generate: all pixels will
// use color 0
if (attributes[tile].nbColors < 2) {
memset(tileData[tile], 0, sizeof(tileData[tile]));
continue;
}
// Find tiles with the same number of colors
unsigned index = 0, total = 0;
for (; index < tile; index++) {
if (attributes[index].nbColors == attributes[tile].nbColors) {
total++;
}
}
assert(index == tile); // This is used as a sentinel later on to indicate no tile was found
if (total) {
// If there are such tiles, there's a random chance that this tile will replicate one of
// those tiles (potentially rotated)
index = getRandomBits(8);
if (index < total) {
total = index + 1;
for (index = 0; total; index++) {
if (attributes[index].nbColors == attributes[tile].nbColors) {
total--;
}
}
if (total == 0) {
index--;
}
} else {
index = tile; // Restore the sentinel
}
}
if (index == tile) {
generate_tile_data(tileData[tile], attributes[index].nbColors);
} else {
copy_tile_data(tileData[tile], tileData[index]);
}
}
uint16_t palettes[60][4];
generate_palettes(palettes);
write_image(filename, palettes, tileData, attributes, width, height);
}
int main(int argc, char **argv) {
if (argc < 3 || argc > 4) {
fprintf(stderr, "usage: %s <input file> <basename> [<maxcount>]\n", argv[0]);
return 2;
}
seed = fopen(argv[1], "rb");
if (!seed) {
fprintf(stderr, "FATAL: Cannot open seed file (%s)\n", strerror(errno));
return 1;
}
size_t const nameLen = strlen(argv[2]);
unsigned long long maxcount = ULLONG_MAX;
if (argc > 3) {
char *error;
maxcount = strtoull(argv[3], &error, 0);
if (*error != '\0') {
fatal("invalid count");
}
}
char *filename = malloc(nameLen + sizeof(XSTR(ULLONG_MAX) ".png"));
if (!filename) {
fatal("out of memory");
}
memcpy(filename, argv[2], nameLen);
for (unsigned long long count = 0; count < maxcount; count++) {
sprintf(&filename[nameLen], "%llu.png", count);
generate_random_image(filename);
// Reset the global random state so that subsequent images don't share a random byte
randbits = 0;
randcount = 0;
}
return 0;
}

457
test/gfx/rgbgfx_test.cpp Normal file
View File

@@ -0,0 +1,457 @@
// For `execProg` (Windows is its special little snowflake again)
#if !defined(_MSC_VER) && !defined(__MINGW32__)
#include <sys/stat.h>
#include <sys/wait.h>
#include <spawn.h>
#include <unistd.h>
#else
#define WIN32_LEAN_AND_MEAN // Include less from `windows.h` to avoid conflicts
#include <windows.h>
#include <errhandlingapi.h>
#include <processthreadsapi.h>
#undef max // This macro conflicts with `std::numeric_limits<...>::max()`
#endif
#include <algorithm>
#include <array>
#include <cassert>
#include <cinttypes>
#include <fcntl.h>
#include <fstream>
#include <limits>
#include <memory>
#include <png.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <vector>
#include "defaultinitalloc.hpp"
#include "gfx/rgba.hpp" // Reused from RGBGFX
static uintmax_t nbErrors;
static void warning(char const *fmt, ...) {
va_list ap;
fputs("warning: ", stderr);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
putc('\n', stderr);
}
static void error(char const *fmt, ...) {
va_list ap;
fputs("error: ", stderr);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
putc('\n', stderr);
if (nbErrors != std::numeric_limits<decltype(nbErrors)>::max()) {
nbErrors++;
}
}
[[noreturn]] static void fatal(char const *fmt, ...) {
va_list ap;
fputs("FATAL: ", stderr);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
putc('\n', stderr);
if (nbErrors != std::numeric_limits<decltype(nbErrors)>::max()) {
nbErrors++;
}
fprintf(stderr, "Test aborted after %ju error%s\n", nbErrors, nbErrors == 1 ? "" : "s");
exit(1);
}
// Copy-pasted from RGBGFX
class Png {
std::string const &path;
std::filebuf file{};
png_structp png = nullptr;
png_infop info = nullptr;
// These are cached for speed
uint32_t width, height;
DefaultInitVec<Rgba> pixels;
int colorType;
int nbColors;
png_colorp embeddedPal = nullptr;
png_bytep transparencyPal = nullptr;
[[noreturn]] static void handleError(png_structp png, char const *msg) {
Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png));
fatal("Error reading input image (\"%s\"): %s", self->path.c_str(), msg);
}
static void handleWarning(png_structp png, char const *msg) {
Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png));
warning("In input image (\"%s\"): %s", self->path.c_str(), msg);
}
static void readData(png_structp png, png_bytep data, size_t length) {
Png *self = reinterpret_cast<Png *>(png_get_io_ptr(png));
std::streamsize expectedLen = length;
std::streamsize nbBytesRead = self->file.sgetn(reinterpret_cast<char *>(data), expectedLen);
if (nbBytesRead != expectedLen) {
fatal("Error reading input image (\"%s\"): file too short (expected at least %zd more "
"bytes after reading %lld)",
self->path.c_str(), length - nbBytesRead,
self->file.pubseekoff(0, std::ios_base::cur));
}
}
public:
uint32_t getWidth() const { return width; }
uint32_t getHeight() const { return height; }
Rgba &pixel(uint32_t x, uint32_t y) { return pixels[y * width + x]; }
Rgba const &pixel(uint32_t x, uint32_t y) const { return pixels[y * width + x]; }
/**
* Reads a PNG and notes all of its colors
*
* This code is more complicated than strictly necessary, but that's because of the API
* being used: the "high-level" interface doesn't provide all the transformations we need,
* so we use the "lower-level" one instead.
* We also use that occasion to only read the PNG one line at a time, since we store all of
* the pixel data in `pixels`, which saves on memory allocations.
*/
explicit Png(std::string const &filePath) : path(filePath) {
if (file.open(path, std::ios_base::in | std::ios_base::binary) == nullptr) {
fatal("Failed to open input image (\"%s\"): %s", path.c_str(), strerror(errno));
}
std::array<unsigned char, 8> pngHeader;
if (file.sgetn(reinterpret_cast<char *>(pngHeader.data()), pngHeader.size())
!= static_cast<std::streamsize>(pngHeader.size()) // Not enough bytes?
|| png_sig_cmp(pngHeader.data(), 0, pngHeader.size()) != 0) {
fatal("Input file (\"%s\") is not a PNG image!", path.c_str());
}
png = png_create_read_struct(PNG_LIBPNG_VER_STRING, (png_voidp)this, handleError,
handleWarning);
if (!png) {
fatal("Failed to allocate PNG structure: %s", strerror(errno));
}
info = png_create_info_struct(png);
if (!info) {
png_destroy_read_struct(&png, nullptr, nullptr);
fatal("Failed to allocate PNG info structure: %s", strerror(errno));
}
png_set_read_fn(png, this, readData);
png_set_sig_bytes(png, pngHeader.size());
// TODO: png_set_crc_action(png, PNG_CRC_ERROR_QUIT, PNG_CRC_WARN_DISCARD);
// Skipping chunks we don't use should improve performance
// TODO: png_set_keep_unknown_chunks(png, ...);
// Process all chunks up to but not including the image data
png_read_info(png, info);
int bitDepth, interlaceType; //, compressionType, filterMethod;
png_get_IHDR(png, info, &width, &height, &bitDepth, &colorType, &interlaceType, nullptr,
nullptr);
if (width % 8 != 0) {
fatal("Image width (%" PRIu32 " pixels) is not a multiple of 8!", width);
}
if (height % 8 != 0) {
fatal("Image height (%" PRIu32 " pixels) is not a multiple of 8!", height);
}
pixels.resize(static_cast<size_t>(width) * static_cast<size_t>(height));
if (png_get_PLTE(png, info, &embeddedPal, &nbColors) != 0) {
int nbTransparentEntries;
if (png_get_tRNS(png, info, &transparencyPal, &nbTransparentEntries, nullptr)) {
assert(nbTransparentEntries == nbColors);
}
}
// Set up transformations; to turn everything into RGBA888
// TODO: it's not necessary to uniformize the pixel data (in theory), and not doing
// so *might* improve performance, and should reduce memory usage.
// Convert grayscale to RGB
switch (colorType & ~PNG_COLOR_MASK_ALPHA) {
case PNG_COLOR_TYPE_GRAY:
png_set_gray_to_rgb(png); // This also converts tRNS to alpha
break;
case PNG_COLOR_TYPE_PALETTE:
png_set_palette_to_rgb(png);
break;
}
if (png_get_valid(png, info, PNG_INFO_tRNS)) {
// If we read a tRNS chunk, convert it to alpha
png_set_tRNS_to_alpha(png);
} else if (!(colorType & PNG_COLOR_MASK_ALPHA)) {
// Otherwise, if we lack an alpha channel, default to full opacity
png_set_add_alpha(png, 0xFFFF, PNG_FILLER_AFTER);
}
// Scale 16bpp back to 8 (we don't need all of that precision anyway)
if (bitDepth == 16) {
png_set_scale_16(png);
} else if (bitDepth < 8) {
png_set_packing(png);
}
// Do NOT call `png_set_interlace_handling`. We want to expand the rows ourselves.
// Update `info` with the transformations
png_read_update_info(png, info);
// These shouldn't have changed
assert(png_get_image_width(png, info) == width);
assert(png_get_image_height(png, info) == height);
// These should have changed, however
assert(png_get_color_type(png, info) == PNG_COLOR_TYPE_RGBA);
assert(png_get_bit_depth(png, info) == 8);
// Now that metadata has been read, we can process the image data
size_t nbRowBytes = png_get_rowbytes(png, info);
assert(nbRowBytes != 0);
DefaultInitVec<png_byte> row(nbRowBytes);
if (interlaceType == PNG_INTERLACE_NONE) {
for (png_uint_32 y = 0; y < height; ++y) {
png_read_row(png, row.data(), nullptr);
for (png_uint_32 x = 0; x < width; ++x) {
Rgba rgba(row[x * 4], row[x * 4 + 1], row[x * 4 + 2], row[x * 4 + 3]);
pixel(x, y) = rgba;
}
}
} else {
assert(interlaceType == PNG_INTERLACE_ADAM7);
// For interlace to work properly, we must read the image `nbPasses` times
for (int pass = 0; pass < PNG_INTERLACE_ADAM7_PASSES; ++pass) {
// The interlacing pass must be skipped if its width or height is reported as zero
if (PNG_PASS_COLS(width, pass) == 0 || PNG_PASS_ROWS(height, pass) == 0) {
continue;
}
png_uint_32 xStep = 1u << PNG_PASS_COL_SHIFT(pass);
png_uint_32 yStep = 1u << PNG_PASS_ROW_SHIFT(pass);
for (png_uint_32 y = PNG_PASS_START_ROW(pass); y < height; y += yStep) {
png_bytep ptr = row.data();
png_read_row(png, ptr, nullptr);
for (png_uint_32 x = PNG_PASS_START_COL(pass); x < width; x += xStep) {
Rgba rgba(ptr[0], ptr[1], ptr[2], ptr[3]);
pixel(x, y) = rgba;
ptr += 4;
}
}
}
}
// We don't care about chunks after the image data (comments, etc.)
png_read_end(png, nullptr);
}
~Png() { png_destroy_read_struct(&png, &info, nullptr); }
};
static char *execProg(char const *name, char * const *argv) {
#if !defined(_MSC_VER) && !defined(__MINGW32__)
pid_t pid;
int err = posix_spawn(&pid, argv[0], nullptr, nullptr, argv, nullptr);
if (err != 0) {
return strerror(err);
}
siginfo_t info;
if (waitid(P_PID, pid, &info, WEXITED) != 0) {
fatal("Error waiting for %s: %s", name, strerror(errno));
} else if (info.si_code != CLD_EXITED) {
assert(info.si_code == CLD_KILLED || info.si_code == CLD_DUMPED);
fatal("%s was terminated by signal %s%s", name, strsignal(info.si_status),
info.si_code == CLD_DUMPED ? " (core dumped)" : "");
} else if (info.si_status != 0) {
fatal("%s returned with status %d", name, info.si_status);
}
#else /* defined(_MSC_VER) || defined(__MINGW32__) */
auto winStrerror = [](DWORD errnum) {
LPTSTR buf;
if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
| FORMAT_MESSAGE_MAX_WIDTH_MASK,
nullptr, errnum, 0, (LPTSTR)&buf, 0, nullptr)
== 0) {
fatal("Failed to get error message for error 0x%x", errnum);
}
return buf;
};
char cmdLine[32768]; // Max command line size on Windows
char *ptr = cmdLine;
for (size_t i = 0; argv[i]; ++i) {
char const *src = argv[i];
// I miss you, `stpcpy`
while (*src) {
*ptr++ = *src++;
}
*ptr++ = ' ';
}
*ptr = '\0';
STARTUPINFOA startupInfo;
GetStartupInfoA(&startupInfo);
STARTUPINFOA childStartupInfo{sizeof(startupInfo),
nullptr,
nullptr,
nullptr,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
nullptr,
0,
0,
0};
PROCESS_INFORMATION child;
if (CreateProcessA(nullptr, cmdLine, nullptr, nullptr, true, 0, nullptr, nullptr,
&childStartupInfo, &child)
== 0) {
return winStrerror(GetLastError());
}
DWORD status;
do {
if (GetExitCodeProcess(child.hProcess, &status) == 0) {
fatal("Error waiting for %s: %ls", name, winStrerror(GetLastError()));
}
} while (status == STILL_ACTIVE);
CloseHandle(child.hProcess);
CloseHandle(child.hThread);
if (status != 0) {
fatal("%s returned with status %ld", name, status);
}
#endif
return nullptr;
}
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "usage: %s <rng file> [rgbgfx flags]\n", argv[0]);
exit(0);
}
{
char path[] = "./randtilegen", file[] = "out";
char *args[] = {path, argv[1], file, nullptr};
if (auto ret = execProg("randtilegen", args); ret != nullptr) {
fatal("Failed to excute ./randtilegen (%s). Is it in the current working directory?",
ret);
}
}
{
char path[] = "../../rgbgfx", out_opt[] = "-o", out_file[] = "result.2bpp",
pal_opt[] = "-p", pal_file[] = "result.pal", attr_opt[] = "-a",
attr_file[] = "result.attrmap", in_file[] = "out0.png";
std::vector<char *> args(
{path, out_opt, out_file, pal_opt, pal_file, attr_opt, attr_file, in_file});
// Also copy the trailing `nullptr`
std::copy_n(&argv[2], argc - 1, std::back_inserter(args));
if (auto ret = execProg("rgbgfx conversion", args.data()); ret != nullptr) {
fatal("Failed to execute ../../rgbgfx (%s). Is it in the parent directory?", ret);
}
}
Png image0{"out0.png"};
{
char path[] = "../../rgbgfx", reverse_opt[] = "-r", out_opt[] = "-o",
out_file[] = "result.2bpp", pal_opt[] = "-p", pal_file[] = "result.pal",
attr_opt[] = "-a", attr_file[] = "result.attrmap", in_file[] = "result.png";
auto width_string = std::to_string(image0.getWidth() / 8);
std::vector<char *> args = {
path, reverse_opt, width_string.data(), out_opt, out_file, pal_opt,
pal_file, attr_opt, attr_file, in_file};
// Also copy the trailing `nullptr`
std::copy_n(&argv[2], argc - 1, std::back_inserter(args));
if (auto ret = execProg("rgbgfx reversal", args.data()); ret != nullptr) {
fatal("Failed to execute ../../rgbgfx -r (%s)", ret);
}
}
Png image1{"result.png"};
if (image0.getWidth() != image1.getWidth()) {
fatal("Image widths do not match!");
}
if (image0.getHeight() != image1.getHeight()) {
fatal("Image heights do not match!");
}
for (uint32_t y = 0; y < image0.getHeight(); y++) {
for (uint32_t x = 0; x < image0.getWidth(); x++) {
Rgba px0 = image0.pixel(x, y);
Rgba px1 = image1.pixel(x, y);
auto cgbColor = [](Rgba const &rgba) {
auto field = [](uint16_t component, uint8_t shift) {
return (component & 0x1F) << shift;
};
return rgba.isTransparent()
? Rgba::transparent
: field(rgba.red, 0) | field(rgba.green, 5) | field(rgba.blue, 10);
};
if (cgbColor(px0) != cgbColor(px1)) {
error("Color mismatch at (%" PRIu32 ", %" PRIu32
"): (%u,%u,%u,%u) became (%u,%u,%u,%u) after round-tripping",
x, y, px0.red, px0.green, px0.blue, px0.alpha, px1.red, px1.green, px1.blue,
px1.alpha);
}
}
}
if (nbErrors > 0) {
fprintf(stderr, "Test failed with %ju error%s\n", nbErrors, nbErrors == 1 ? "" : "s");
exit(1);
}
return 0;
}

BIN
test/gfx/seed0.bin Normal file

Binary file not shown.

BIN
test/gfx/seed1.bin Normal file

Binary file not shown.

2
test/gfx/seed10.bin Normal file
View File

@@ -0,0 +1,2 @@
2<EFBFBD>.<2E><>C<><43><EFBFBD>pA22<32>VD<56><44>u2IEU<45><55><EFBFBD>_<EFBFBD>/!<21><05>p<EFBFBD><EFBFBD><C281>!<21>o-/^<5E><><EFBFBD>uNV0<56><30>g<12><>T <20><>?<3F>%<25><16>IA#1<>|<7C>$<24>tt<74><05>τ$<24><>W<10>^ ف;W<><57>JP!<21><pm<18><17><><EFBFBD><EFBFBD>;HZw<5A><77>MvUx<55><78>+<2B><><16><>e<EFBFBD><65><EFBFBD>'[$<24><>؋g<D88B><08>HcL<15><><EFBFBD>\u<>E,ԥ_<><5F><EFBFBD>!<21><><EFBFBD>;lw<6C>?<3F><><15><><EFBFBD>?܃<>k<>{Z<><5A><EFBFBD><EFBFBD>AR<41><52><57><CBB1><EFBFBD><EFBFBD>h*<05>b<10><>C$<24><><EFBFBD>[2x(<28>E`<60>8,<2C>=<3D>,ᾡ<>i<EFBFBD>;<3B><><EFBFBD><EFBFBD>7<EFBFBD>!<21><>0<EFBFBD><30>m<EFBFBD><6D><EFBFBD>fy+<2B><>wk<77>PYS<59><53>&<26><>Wb<><62>A<>x<EFBFBD><02>O<EFBFBD><4F>F\y<05><><EFBFBD><EFBFBD>r>f<>q.ժ<>#<23>]<5D>i<03><>D<>.m<><13>v<EFBFBD><<3C><11><>\5c<35>JW<4A>N<><4E>=nq<6E><71><EFBFBD>gY9<59>Aa<41><61>a<EFBFBD><61>8?<3F><><EFBFBD><11>~uc<>[<5B><><EFBFBD>
<17><>(<28><>[<5B>q<EFBFBD><71><EFBFBD>B>QM<51><05>z<EFBFBD> _.<2E><>p<EFBFBD>r<EFBFBD><72><EFBFBD>y<EFBFBD><79><EFBFBD>w<EFBFBD><77><EFBFBD><EFBFBD>~<7E>s<EFBFBD>;<0F><><4D>]/<2F>hX<68>+6fq<13><><EFBFBD>0Go<47><6F>BE<42><45><EFBFBD>R<EFBFBD>D<><44>b<EFBFBD><62>Am<35><C4B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD>g<EFBFBD><67>l<EFBFBD><1F><>:H<18><>

BIN
test/gfx/seed11.bin Normal file

Binary file not shown.

BIN
test/gfx/seed12.bin Normal file

Binary file not shown.

1
test/gfx/seed13.bin Normal file
View File

@@ -0,0 +1 @@
?x<><78>)&<<3C>av<61><01>z<><0F>ߕx_;<3B>yc& o<><6F>$<24><>w<EFBFBD><77><EFBFBD>x<>TST+<2B><>ǑX<><58>g<EFBFBD><67>t<07><><EFBFBD><EFBFBD><EFBFBD>|c <0B>UJ=<3D><>u<lP<6C><50>>І<><D086><EFBFBD><EFBFBD>[g$Ca<43><07>I޿<49>ÃsI5;<3B><>D2<44><32>jD[ <20>S+<2B><><EFBFBD>c<EFBFBD><63><0F>8[j<12>?<3F><>D<EFBFBD><44><EFBFBD><19><><01><><EFBFBD>D,<2C><><EFBFBD>r<EFBFBD><72><49>N<EFBFBD>

BIN
test/gfx/seed14.bin Normal file

Binary file not shown.

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