mirror of
https://github.com/gbdev/rgbds.git
synced 2025-11-21 02:32:06 +00:00
Compare commits
140 Commits
v0.6.0-wel
...
v0.6.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b2dc37f43 | ||
|
|
2b83a81ceb | ||
|
|
ca8693690a | ||
|
|
87092208bc | ||
|
|
0d32775a1f | ||
|
|
0df07d3688 | ||
|
|
3f70372308 | ||
|
|
9646f15b59 | ||
|
|
973fbb91bc | ||
|
|
903862c451 | ||
|
|
3f5983358c | ||
|
|
7a7126f3b8 | ||
|
|
b4dadd35b6 | ||
|
|
d9b1402ef8 | ||
|
|
832e0ec972 | ||
|
|
caaf7a8444 | ||
|
|
a5ed0292b1 | ||
|
|
05e36767b0 | ||
|
|
531092f5bd | ||
|
|
4c51792f15 | ||
|
|
c4359c1058 | ||
|
|
159efe1257 | ||
|
|
3cfe7800c7 | ||
|
|
01cf0c5f98 | ||
|
|
0dbcebfeb4 | ||
|
|
491b6746ab | ||
|
|
cbf6fadcdb | ||
|
|
a77b0b396a | ||
|
|
568fb5e4c8 | ||
|
|
82012f698e | ||
|
|
57ac07b03e | ||
|
|
c521233499 | ||
|
|
bf869f6961 | ||
|
|
0f8cbb1faf | ||
|
|
fcce42d3d2 | ||
|
|
ed104a9f70 | ||
|
|
f5d4126303 | ||
|
|
78e751f022 | ||
|
|
d569c6392c | ||
|
|
75b9d48990 | ||
|
|
3aabe9c799 | ||
|
|
5be2b96b40 | ||
|
|
7fdfbbbbba | ||
|
|
7fdc6cbced | ||
|
|
56115653ef | ||
|
|
7927dfd2e3 | ||
|
|
b1aec91912 | ||
|
|
7defaad9d2 | ||
|
|
dc9185e50b | ||
|
|
02d957278d | ||
|
|
6feb1fb73a | ||
|
|
dc67f152a9 | ||
|
|
913c3dd711 | ||
|
|
32242e0ff2 | ||
|
|
91071009a8 | ||
|
|
1da884db15 | ||
|
|
ef473de75a | ||
|
|
6b0cab32a6 | ||
|
|
cc27169ecd | ||
|
|
843022772b | ||
|
|
75f8b16f33 | ||
|
|
188027bccc | ||
|
|
79adcdb7ea | ||
|
|
8ed65078da | ||
|
|
7311fc9ef8 | ||
|
|
7d54145e56 | ||
|
|
e753b62d1a | ||
|
|
6ed220b4c1 | ||
|
|
e49fb457ea | ||
|
|
3c9d5b05d6 | ||
|
|
e86eb9337a | ||
|
|
b0f8e04fb7 | ||
|
|
493b94919f | ||
|
|
71e22f3bfe | ||
|
|
ac02382632 | ||
|
|
943d631701 | ||
|
|
d2f9cc7e8c | ||
|
|
76bb950be5 | ||
|
|
f29c5d81ec | ||
|
|
2307981878 | ||
|
|
6bab2ea5c8 | ||
|
|
35e57a55c9 | ||
|
|
21e9a65f0b | ||
|
|
779c8c9368 | ||
|
|
e855b6f622 | ||
|
|
3b1808cc8f | ||
|
|
71cb2854e8 | ||
|
|
2099a25ee0 | ||
|
|
b9de65c9a2 | ||
|
|
c82cce6d95 | ||
|
|
97965c9766 | ||
|
|
5efc49cb12 | ||
|
|
c4361b965c | ||
|
|
8d00a61602 | ||
|
|
20442c8a43 | ||
|
|
b95c26c886 | ||
|
|
a96aa1725f | ||
|
|
3d79f76e41 | ||
|
|
fdfedc45a6 | ||
|
|
c98d92a4c4 | ||
|
|
d438838db4 | ||
|
|
d675523e49 | ||
|
|
ad07c9deb9 | ||
|
|
bf9f99ebf5 | ||
|
|
f0eca86c52 | ||
|
|
e8d8ae4c78 | ||
|
|
0cc62824b9 | ||
|
|
2fb1eb9136 | ||
|
|
bde380f38b | ||
|
|
38e8024ffa | ||
|
|
3bd6078537 | ||
|
|
5409d0d15a | ||
|
|
9262fefd07 | ||
|
|
638d024040 | ||
|
|
3fa1854332 | ||
|
|
6e406b22bb | ||
|
|
d30e507270 | ||
|
|
373a22660b | ||
|
|
8c62e80c18 | ||
|
|
34bc650341 | ||
|
|
43ba7d0efb | ||
|
|
685ea5feed | ||
|
|
f5ac268989 | ||
|
|
d51ab35203 | ||
|
|
3a71910312 | ||
|
|
ec25b4ac0e | ||
|
|
83222a8147 | ||
|
|
97c326942f | ||
|
|
b037d54f64 | ||
|
|
80df6640e3 | ||
|
|
68d3ef8e76 | ||
|
|
01777c96c8 | ||
|
|
28f9183d80 | ||
|
|
74c31f7c0f | ||
|
|
7e94ecbfe6 | ||
|
|
0195196425 | ||
|
|
19c85a7c2e | ||
|
|
59e73e64ca | ||
|
|
972d06bb41 | ||
|
|
e27da737c6 |
@@ -93,7 +93,7 @@ SpacesInConditionalStatement: false
|
|||||||
SpacesInContainerLiterals: false
|
SpacesInContainerLiterals: false
|
||||||
SpacesInParentheses: false
|
SpacesInParentheses: false
|
||||||
SpacesInSquareBrackets: false
|
SpacesInSquareBrackets: false
|
||||||
Standard: c++20
|
Standard: c++17
|
||||||
TabWidth: 4
|
TabWidth: 4
|
||||||
UseCRLF: false
|
UseCRLF: false
|
||||||
UseTab: AlignWithSpaces
|
UseTab: ForIndentation
|
||||||
|
|||||||
56
.github/actions/doc_postproc.awk
vendored
56
.github/actions/doc_postproc.awk
vendored
@@ -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
|
|
||||||
}
|
|
||||||
113
.github/actions/get-pages.sh
vendored
113
.github/actions/get-pages.sh
vendored
@@ -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[@]##*/}"
|
|
||||||
10
.github/workflows/create-release-docs.yml
vendored
10
.github/workflows/create-release-docs.yml
vendored
@@ -23,16 +23,16 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get -qq update
|
sudo apt-get -qq update
|
||||||
sudo apt-get install -yq groff zlib1g-dev
|
sudo apt-get install -yq groff zlib1g-dev
|
||||||
wget 'http://mandoc.bsd.lv/snapshots/mandoc-1.14.5.tar.gz'
|
wget 'http://mandoc.bsd.lv/snapshots/mandoc-1.14.6.tar.gz'
|
||||||
tar xf mandoc-1.14.5.tar.gz
|
tar xf mandoc-1.14.6.tar.gz
|
||||||
cd mandoc-1.14.5
|
cd mandoc-1.14.6
|
||||||
./configure
|
./configure
|
||||||
make
|
make
|
||||||
sudo make install
|
sudo make install
|
||||||
- name: Update pages
|
- 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
|
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
|
- name: Push new pages
|
||||||
working-directory: rgbds-www
|
working-directory: rgbds-www
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
70
.github/workflows/testing.yml
vendored
70
.github/workflows/testing.yml
vendored
@@ -16,6 +16,11 @@ jobs:
|
|||||||
cc: gcc
|
cc: gcc
|
||||||
- os: macos-11.0
|
- os: macos-11.0
|
||||||
cc: gcc
|
cc: gcc
|
||||||
|
include:
|
||||||
|
- cc: gcc
|
||||||
|
cxx: g++
|
||||||
|
- cc: clang
|
||||||
|
cxx: clang++
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
@@ -28,25 +33,25 @@ jobs:
|
|||||||
# Apple's base version is severely outdated, not even supporting -Wall,
|
# Apple's base version is severely outdated, not even supporting -Wall,
|
||||||
# but it overrides Homebrew's version nonetheless...
|
# but it overrides Homebrew's version nonetheless...
|
||||||
- name: Build & install using Make
|
- 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'
|
if: matrix.buildsys == 'make'
|
||||||
- name: Build & install using CMake
|
|
||||||
run: |
|
run: |
|
||||||
export PATH="/usr/local/opt/bison/bin:$PATH"
|
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
|
make develop -j Q= CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
|
||||||
cmake --build build -j
|
sudo make install -j Q=
|
||||||
cp build/src/rgb{asm,link,fix,gfx} .
|
- name: Build & install using CMake
|
||||||
sudo cmake --install build
|
|
||||||
if: matrix.buildsys == '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
|
- name: Package binaries
|
||||||
run: |
|
run: |
|
||||||
mkdir bins
|
mkdir bins
|
||||||
cp rgb{asm,link,fix,gfx} bins
|
cp rgb{asm,link,fix,gfx} bins
|
||||||
- name: Upload binaries
|
- name: Upload binaries
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: rgbds-canary-${{ matrix.os }}-${{ matrix.cc }}-${{ matrix.buildsys }}
|
name: rgbds-canary-${{ matrix.os }}-${{ matrix.cc }}-${{ matrix.buildsys }}
|
||||||
path: bins
|
path: bins
|
||||||
@@ -71,7 +76,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Get zlib, libpng and bison
|
- 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 = New-Object System.Net.WebClient
|
||||||
$wc.DownloadFile('https://www.zlib.net/zlib1212.zip', 'zlib.zip')
|
$wc.DownloadFile('https://www.zlib.net/zlib1212.zip', 'zlib.zip')
|
||||||
$hash = (Get-FileHash "zlib.zip" -Algorithm SHA256).Hash
|
$hash = (Get-FileHash "zlib.zip" -Algorithm SHA256).Hash
|
||||||
@@ -95,28 +100,41 @@ jobs:
|
|||||||
Expand-Archive -DestinationPath install_dir "winflexbison.zip"
|
Expand-Archive -DestinationPath install_dir "winflexbison.zip"
|
||||||
Move-Item zlib-1.2.12 zlib
|
Move-Item zlib-1.2.12 zlib
|
||||||
Move-Item lpng1637 libpng
|
Move-Item lpng1637 libpng
|
||||||
|
- uses: actions/cache@v3
|
||||||
|
id: cache
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
zbuild
|
||||||
|
pngbuild
|
||||||
|
key: ${{ matrix.arch }}-${{ hashFiles('zlib/**', 'libpng/**') }}
|
||||||
- name: Build zlib
|
- name: Build zlib
|
||||||
run: | # BUILD_SHARED_LIBS causes the output DLL to be correctly called `zlib1.dll`
|
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 -S zlib -B zbuild -A ${{ matrix.platform }} -DCMAKE_INSTALL_PREFIX=install_dir -DBUILD_SHARED_LIBS=ON
|
||||||
cmake --build zbuild --config Release -j
|
cmake --build zbuild --config Release -j
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
- name: Install zlib
|
||||||
|
run: |
|
||||||
cmake --install zbuild
|
cmake --install zbuild
|
||||||
- name: Build libpng
|
- name: Build libpng
|
||||||
run: |
|
run: |
|
||||||
cmake -S libpng -B pngbuild -A ${{ matrix.platform }} -DCMAKE_INSTALL_PREFIX=install_dir -DPNG_SHARED=ON -DPNG_STATIC=ON -DPNG_TESTS=OFF
|
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
|
cmake --build pngbuild --config Release -j
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
- name: Install libpng
|
||||||
|
run: |
|
||||||
cmake --install pngbuild
|
cmake --install pngbuild
|
||||||
- name: Build Windows binaries
|
- name: Build Windows binaries
|
||||||
run: |
|
run: |
|
||||||
cmake -S . -B build -A ${{ matrix.platform }} -DCMAKE_INSTALL_PREFIX=install_dir -DCMAKE_BUILD_TYPE=Release
|
cmake -S . -B build -A ${{ matrix.platform }} -DCMAKE_INSTALL_PREFIX=install_dir -DCMAKE_BUILD_TYPE=Release
|
||||||
cmake --build build --config Release -j
|
cmake --build build --config Release -j --verbose
|
||||||
cmake --install build
|
cmake --install build --verbose --prefix install_dir
|
||||||
- name: Package binaries
|
- name: Package binaries
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir bins
|
mkdir bins
|
||||||
cp install_dir/bin/{rgbasm.exe,rgblink.exe,rgbfix.exe,rgbgfx.exe,zlib1.dll,libpng16.dll} bins
|
cp install_dir/bin/{rgbasm.exe,rgblink.exe,rgbfix.exe,rgbgfx.exe,zlib1.dll,libpng16.dll} bins
|
||||||
- name: Upload Windows binaries
|
- name: Upload Windows binaries
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: rgbds-canary-win${{ matrix.bits }}
|
name: rgbds-canary-win${{ matrix.bits }}
|
||||||
path: bins
|
path: bins
|
||||||
@@ -124,6 +142,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cp bins/* .
|
cp bins/* .
|
||||||
|
cp bins/*.dll test/gfx
|
||||||
test/run-tests.sh
|
test/run-tests.sh
|
||||||
|
|
||||||
windows-xbuild:
|
windows-xbuild:
|
||||||
@@ -150,7 +169,7 @@ jobs:
|
|||||||
./.github/actions/install_deps.sh ${{ matrix.os }}
|
./.github/actions/install_deps.sh ${{ matrix.os }}
|
||||||
- name: Install MinGW
|
- name: Install MinGW
|
||||||
run: |
|
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
|
- name: Install libpng dev headers for MinGW
|
||||||
run: |
|
run: |
|
||||||
sudo ./.github/actions/mingw-w64-libpng-dev.sh ${{ matrix.triplet }}
|
sudo ./.github/actions/mingw-w64-libpng-dev.sh ${{ matrix.triplet }}
|
||||||
@@ -166,12 +185,21 @@ jobs:
|
|||||||
mv rgbgfx bins/rgbgfx.exe
|
mv rgbgfx bins/rgbgfx.exe
|
||||||
cp /usr/${{ matrix.triplet }}/lib/zlib1.dll bins
|
cp /usr/${{ matrix.triplet }}/lib/zlib1.dll bins
|
||||||
cp /usr/${{ matrix.triplet }}/bin/libpng16-16.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
|
- name: Upload Windows binaries
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: rgbds-canary-mingw-win${{ matrix.bits }}
|
name: rgbds-canary-mingw-win${{ matrix.bits }}
|
||||||
path: bins
|
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:
|
windows-xtesting:
|
||||||
needs: windows-xbuild
|
needs: windows-xbuild
|
||||||
@@ -183,14 +211,20 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Retrieve binaries
|
- name: Retrieve binaries
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: rgbds-canary-mingw-win${{ matrix.bits }}
|
name: rgbds-canary-mingw-win${{ matrix.bits }}
|
||||||
path: bins
|
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
|
- name: Extract binaries
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cp bins/* .
|
cp bins/* .
|
||||||
|
cp bins/*.dll test/gfx
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
31
.github/workflows/update-master-docs.yml
vendored
31
.github/workflows/update-master-docs.yml
vendored
@@ -4,16 +4,15 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
paths:
|
paths:
|
||||||
- .github/actions/get-pages.sh
|
- man/gbz80.7
|
||||||
- src/gbz80.7
|
- man/rgbds.5
|
||||||
- src/rgbds.5
|
- man/rgbds.7
|
||||||
- src/rgbds.7
|
- man/rgbasm.1
|
||||||
- src/asm/rgbasm.1
|
- man/rgbasm.5
|
||||||
- src/asm/rgbasm.5
|
- man/rgblink.1
|
||||||
- src/link/rgblink.1
|
- man/rgblink.5
|
||||||
- src/link/rgblink.5
|
- man/rgbfix.1
|
||||||
- src/fix/rgbfix.1
|
- man/rgbgfx.1
|
||||||
- src/gfx/rgbgfx.1
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -36,16 +35,16 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get -qq update
|
sudo apt-get -qq update
|
||||||
sudo apt-get install -yq groff zlib1g-dev
|
sudo apt-get install -yq groff zlib1g-dev
|
||||||
wget 'http://mandoc.bsd.lv/snapshots/mandoc-1.14.5.tar.gz'
|
wget 'http://mandoc.bsd.lv/snapshots/mandoc-1.14.6.tar.gz'
|
||||||
tar xf mandoc-1.14.5.tar.gz
|
tar xf mandoc-1.14.6.tar.gz
|
||||||
cd mandoc-1.14.5
|
cd mandoc-1.14.6
|
||||||
./configure
|
./configure
|
||||||
make
|
make
|
||||||
sudo make install
|
sudo make install
|
||||||
- name: Update pages
|
- name: Update pages
|
||||||
working-directory: rgbds
|
working-directory: rgbds/man
|
||||||
run: |
|
run: |
|
||||||
./.github/actions/get-pages.sh ../rgbds-www master
|
../../rgbds-www/maintainer/man_to_html.sh master *
|
||||||
- name: Push new pages
|
- name: Push new pages
|
||||||
working-directory: rgbds-www
|
working-directory: rgbds-www
|
||||||
run: |
|
run: |
|
||||||
@@ -56,7 +55,7 @@ jobs:
|
|||||||
ssh-add ~/.ssh/id_ed25519
|
ssh-add ~/.ssh/id_ed25519
|
||||||
git config --global user.name "GitHub Action"
|
git config --global user.name "GitHub Action"
|
||||||
git config --global user.email "community@gbdev.io"
|
git config --global user.email "community@gbdev.io"
|
||||||
git add .
|
git add -A
|
||||||
git commit -m "Update RGBDS master documentation"
|
git commit -m "Update RGBDS master documentation"
|
||||||
if git remote | grep -q origin; then
|
if git remote | grep -q origin; then
|
||||||
git remote set-url origin git@github.com:gbdev/rgbds-www.git
|
git remote set-url origin git@github.com:gbdev/rgbds-www.git
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
|
cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
|
||||||
|
|
||||||
project(rgbds
|
project(rgbds
|
||||||
LANGUAGES C)
|
LANGUAGES C CXX)
|
||||||
|
|
||||||
# get real path of source and binary directories
|
# get real path of source and binary directories
|
||||||
get_filename_component(srcdir "${CMAKE_SOURCE_DIR}" REALPATH)
|
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)
|
if(MSVC)
|
||||||
# MSVC's standard library triggers warning C5105,
|
# MSVC's standard library triggers warning C5105,
|
||||||
# "macro expansion producing 'defined' has undefined behavior"
|
# "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)
|
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()
|
else()
|
||||||
add_compile_options(-Wall -pedantic)
|
add_compile_options(-Wall -pedantic)
|
||||||
add_definitions(-D_POSIX_C_SOURCE=200809L -D_ISOC11_SOURCE)
|
add_definitions(-D_POSIX_C_SOURCE=200809L -D_ISOC11_SOURCE)
|
||||||
@@ -41,18 +51,18 @@ else()
|
|||||||
-fsanitize=object-size -fsanitize=bool -fsanitize=enum
|
-fsanitize=object-size -fsanitize=bool -fsanitize=enum
|
||||||
-fsanitize=alignment -fsanitize=null -fsanitize=address)
|
-fsanitize=alignment -fsanitize=null -fsanitize=address)
|
||||||
add_compile_options(${SAN_FLAGS})
|
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
|
# 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!
|
# 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_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()
|
endif()
|
||||||
|
|
||||||
if(MORE_WARNINGS)
|
if(MORE_WARNINGS)
|
||||||
add_compile_options(-Werror -Wextra
|
add_compile_options(-Werror -Wextra
|
||||||
-Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond
|
-Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond
|
||||||
-Wfloat-equal -Winline -Wlogical-op -Wnested-externs -Wnull-dereference
|
-Wfloat-equal -Wlogical-op -Wnull-dereference -Wshift-overflow=2
|
||||||
-Wold-style-definition -Wshift-overflow=2 -Wstrict-overflow=5
|
-Wstringop-overflow=4 -Wstrict-overflow=5 -Wundef -Wuninitialized -Wunused
|
||||||
-Wstrict-prototypes -Wstringop-overflow=4 -Wundef -Wuninitialized -Wunused
|
|
||||||
-Wshadow # TODO: -Wshadow=compatible-local ?
|
-Wshadow # TODO: -Wshadow=compatible-local ?
|
||||||
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1
|
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1
|
||||||
-Wno-format-nonliteral # We have a couple of "dynamic" prints
|
-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")
|
message(STATUS "Cannot determine RGBDS version (Git not installed), falling back")
|
||||||
endif(GIT)
|
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")
|
include_directories("${PROJECT_SOURCE_DIR}/include")
|
||||||
|
|
||||||
set(CMAKE_C_STANDARD 11)
|
set(CMAKE_C_STANDARD 11)
|
||||||
set(CMAKE_C_STANDARD_REQUIRED True)
|
set(CMAKE_C_STANDARD_REQUIRED True)
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||||
|
|
||||||
add_subdirectory(src)
|
add_subdirectory(src)
|
||||||
|
add_subdirectory(test)
|
||||||
|
|
||||||
# By default, build in Release mode; Debug mode must be explicitly requested
|
# By default, build in Release mode; Debug mode must be explicitly requested
|
||||||
# (You may want to augment it with the options above)
|
# (You may want to augment it with the options above)
|
||||||
@@ -97,3 +118,19 @@ if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
|||||||
message(CHECK_FAIL "no")
|
message(CHECK_FAIL "no")
|
||||||
endif()
|
endif()
|
||||||
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()
|
||||||
|
|||||||
65
Makefile
65
Makefile
@@ -7,7 +7,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
.SUFFIXES:
|
.SUFFIXES:
|
||||||
.SUFFIXES: .h .y .c .o
|
.SUFFIXES: .h .y .c .cpp .o
|
||||||
|
|
||||||
# User-defined variables
|
# User-defined variables
|
||||||
|
|
||||||
@@ -34,10 +34,13 @@ WARNFLAGS := -Wall -pedantic
|
|||||||
|
|
||||||
# Overridable CFLAGS
|
# Overridable CFLAGS
|
||||||
CFLAGS ?= -O3 -flto -DNDEBUG
|
CFLAGS ?= -O3 -flto -DNDEBUG
|
||||||
|
CXXFLAGS ?= -O3 -flto -DNDEBUG
|
||||||
# Non-overridable CFLAGS
|
# Non-overridable CFLAGS
|
||||||
# _ISOC11_SOURCE is required on certain platforms to get C11 on top of the C99-based POSIX 2008
|
# _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 \
|
REALCFLAGS := ${CFLAGS} ${WARNFLAGS} -std=gnu11 -I include \
|
||||||
-D_POSIX_C_SOURCE=200809L -D_ISOC11_SOURCE
|
-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
|
# Overridable LDFLAGS
|
||||||
LDFLAGS ?=
|
LDFLAGS ?=
|
||||||
# Non-overridable LDFLAGS
|
# Non-overridable LDFLAGS
|
||||||
@@ -102,9 +105,14 @@ rgbfix_obj := \
|
|||||||
src/error.o
|
src/error.o
|
||||||
|
|
||||||
rgbgfx_obj := \
|
rgbgfx_obj := \
|
||||||
src/gfx/gb.o \
|
|
||||||
src/gfx/main.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/extern/getopt.o \
|
||||||
src/error.o
|
src/error.o
|
||||||
|
|
||||||
@@ -118,7 +126,13 @@ rgbfix: ${rgbfix_obj}
|
|||||||
$Q${CC} ${REALLDFLAGS} -o $@ ${rgbfix_obj} ${REALCFLAGS} src/version.c
|
$Q${CC} ${REALLDFLAGS} -o $@ ${rgbfix_obj} ${REALCFLAGS} src/version.c
|
||||||
|
|
||||||
rgbgfx: ${rgbgfx_obj}
|
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
|
# Rules to process files
|
||||||
|
|
||||||
@@ -145,7 +159,10 @@ src/asm/parser.c: src/asm/parser.y
|
|||||||
${BISON} $$DEFS -d ${YFLAGS} -o $@ $<
|
${BISON} $$DEFS -d ${YFLAGS} -o $@ $<
|
||||||
|
|
||||||
.c.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
|
# Target used to remove all files generated by other Makefile targets
|
||||||
|
|
||||||
@@ -157,6 +174,7 @@ clean:
|
|||||||
$Qfind src/ -name "*.o" -exec rm {} \;
|
$Qfind src/ -name "*.o" -exec rm {} \;
|
||||||
$Q${RM} rgbshim.sh
|
$Q${RM} rgbshim.sh
|
||||||
$Q${RM} src/asm/parser.c src/asm/parser.h
|
$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.
|
# 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} rgblink ${DESTDIR}${bindir}/rgblink
|
||||||
$Qinstall ${STRIP} -m ${BINMODE} rgbgfx ${DESTDIR}${bindir}/rgbgfx
|
$Qinstall ${STRIP} -m ${BINMODE} rgbgfx ${DESTDIR}${bindir}/rgbgfx
|
||||||
$Qmkdir -p ${DESTDIR}${mandir}/man1 ${DESTDIR}${mandir}/man5 ${DESTDIR}${mandir}/man7
|
$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} man/rgbds.7 ${DESTDIR}${mandir}/man7/rgbds.7
|
||||||
$Qinstall -m ${MANMODE} src/gbz80.7 ${DESTDIR}${mandir}/man7/gbz80.7
|
$Qinstall -m ${MANMODE} man/gbz80.7 ${DESTDIR}${mandir}/man7/gbz80.7
|
||||||
$Qinstall -m ${MANMODE} src/rgbds.5 ${DESTDIR}${mandir}/man5/rgbds.5
|
$Qinstall -m ${MANMODE} man/rgbds.5 ${DESTDIR}${mandir}/man5/rgbds.5
|
||||||
$Qinstall -m ${MANMODE} src/asm/rgbasm.1 ${DESTDIR}${mandir}/man1/rgbasm.1
|
$Qinstall -m ${MANMODE} man/rgbasm.1 ${DESTDIR}${mandir}/man1/rgbasm.1
|
||||||
$Qinstall -m ${MANMODE} src/asm/rgbasm.5 ${DESTDIR}${mandir}/man5/rgbasm.5
|
$Qinstall -m ${MANMODE} man/rgbasm.5 ${DESTDIR}${mandir}/man5/rgbasm.5
|
||||||
$Qinstall -m ${MANMODE} src/fix/rgbfix.1 ${DESTDIR}${mandir}/man1/rgbfix.1
|
$Qinstall -m ${MANMODE} man/rgbfix.1 ${DESTDIR}${mandir}/man1/rgbfix.1
|
||||||
$Qinstall -m ${MANMODE} src/link/rgblink.1 ${DESTDIR}${mandir}/man1/rgblink.1
|
$Qinstall -m ${MANMODE} man/rgblink.1 ${DESTDIR}${mandir}/man1/rgblink.1
|
||||||
$Qinstall -m ${MANMODE} src/link/rgblink.5 ${DESTDIR}${mandir}/man5/rgblink.5
|
$Qinstall -m ${MANMODE} man/rgblink.5 ${DESTDIR}${mandir}/man5/rgblink.5
|
||||||
$Qinstall -m ${MANMODE} src/gfx/rgbgfx.1 ${DESTDIR}${mandir}/man1/rgbgfx.1
|
$Qinstall -m ${MANMODE} man/rgbgfx.1 ${DESTDIR}${mandir}/man1/rgbgfx.1
|
||||||
|
|
||||||
# Target used to check the coding style of the whole codebase.
|
# Target used to check the coding style of the whole codebase.
|
||||||
# `extern/` is excluded, as it contains external code that should not be patched
|
# `extern/` is excluded, as it contains external code that should not be patched
|
||||||
@@ -214,11 +232,9 @@ checkdiff:
|
|||||||
develop:
|
develop:
|
||||||
$Qenv ${MAKE} WARNFLAGS="-Werror -Wextra \
|
$Qenv ${MAKE} WARNFLAGS="-Werror -Wextra \
|
||||||
-Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond \
|
-Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond \
|
||||||
-Wfloat-equal -Winline -Wlogical-op -Wnested-externs -Wold-style-definition \
|
-Wfloat-equal -Wlogical-op -Wnull-dereference -Wshift-overflow=2 \
|
||||||
-Wshift-overflow=2 \
|
-Wstringop-overflow=4 -Wstrict-overflow=5 -Wundef -Wuninitialized -Wunused \
|
||||||
-Wstrict-overflow=5 -Wstrict-prototypes -Wundef -Wuninitialized -Wunused \
|
|
||||||
-Wshadow \
|
-Wshadow \
|
||||||
-Wnull-dereference -Wstringop-overflow=4 \
|
|
||||||
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1 \
|
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1 \
|
||||||
-Wno-format-nonliteral \
|
-Wno-format-nonliteral \
|
||||||
-Wno-type-limits -Wno-tautological-constant-out-of-range-compare \
|
-Wno-type-limits -Wno-tautological-constant-out-of-range-compare \
|
||||||
@@ -229,7 +245,8 @@ develop:
|
|||||||
-fsanitize=signed-integer-overflow -fsanitize=bounds \
|
-fsanitize=signed-integer-overflow -fsanitize=bounds \
|
||||||
-fsanitize=object-size -fsanitize=bool -fsanitize=enum \
|
-fsanitize=object-size -fsanitize=bool -fsanitize=enum \
|
||||||
-fsanitize=alignment -fsanitize=null -fsanitize=address" \
|
-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.
|
# Targets for the project maintainer to easily create Windows exes.
|
||||||
# This is not for Windows users!
|
# This is not for Windows users!
|
||||||
@@ -237,12 +254,14 @@ develop:
|
|||||||
# install instructions instead.
|
# install instructions instead.
|
||||||
|
|
||||||
mingw32:
|
mingw32:
|
||||||
$Q${MAKE} CC=i686-w64-mingw32-gcc BISON=bison \
|
$Q${MAKE} all test/gfx/randtilegen test/gfx/rgbgfx_test \
|
||||||
PKG_CONFIG=i686-w64-mingw32-pkg-config -j
|
CC=i686-w64-mingw32-gcc CXX=i686-w64-mingw32-g++ \
|
||||||
|
BISON=bison PKG_CONFIG=i686-w64-mingw32-pkg-config -j
|
||||||
|
|
||||||
mingw64:
|
mingw64:
|
||||||
$Q${MAKE} CC=x86_64-w64-mingw32-gcc BISON=bison \
|
$Q${MAKE} all test/gfx/randtilegen test/gfx/rgbgfx_test \
|
||||||
PKG_CONFIG=x86_64-w64-mingw32-pkg-config -j
|
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:
|
wine-shim:
|
||||||
$Qecho '#!/bin/bash' > rgbshim.sh
|
$Qecho '#!/bin/bash' > rgbshim.sh
|
||||||
|
|||||||
23
README.rst
23
README.rst
@@ -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
|
This is a fork of the original RGBDS which aims to make the programs more like
|
||||||
other UNIX tools.
|
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
|
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.
|
||||||
`here <https://rgbds.gbdev.io/docs/>`__, it is generated from the man pages
|
The source code of the website itself is on GitHub as well under the repo `rgbds-www <https://github.com/gbdev/rgbds-www>`__.
|
||||||
found in this repository.
|
|
||||||
|
|
||||||
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``.
|
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
|
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.
|
is possible using ``make`` or ``cmake``; follow the link for more detailed instructions.
|
||||||
|
|
||||||
.. code:: sh
|
.. code:: sh
|
||||||
@@ -57,6 +56,8 @@ The RGBDS source code file structure somewhat resembles the following:
|
|||||||
│ └── ...
|
│ └── ...
|
||||||
├── include/
|
├── include/
|
||||||
│ └── ...
|
│ └── ...
|
||||||
|
├── man/
|
||||||
|
│ └── ...
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── asm/
|
│ ├── 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`.
|
- ``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
|
* 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.
|
(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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -11,19 +11,24 @@ _rgbgfx_completions() {
|
|||||||
declare -A opts=(
|
declare -A opts=(
|
||||||
[V]="version:normal"
|
[V]="version:normal"
|
||||||
[C]="color-curve:normal"
|
[C]="color-curve:normal"
|
||||||
[D]="debug:normal"
|
|
||||||
[h]="horizontal:normal"
|
|
||||||
[m]="mirror-tiles:normal"
|
[m]="mirror-tiles:normal"
|
||||||
[u]="unique-tiles:normal"
|
[u]="unique-tiles:normal"
|
||||||
[v]="verbose:normal"
|
[v]="verbose:normal"
|
||||||
[f]="fix:normal"
|
[Z]="columns:normal"
|
||||||
[F]="fix-and-save:normal"
|
|
||||||
[a]="attr-map:*.attrmap"
|
[a]="attr-map:*.attrmap"
|
||||||
[A]="output-attr-map:normal"
|
[A]="output-attr-map:normal"
|
||||||
|
[b]="base-tiles:unk"
|
||||||
[d]="depth:unk"
|
[d]="depth:unk"
|
||||||
|
[L]="slice:unk"
|
||||||
|
[N]="nb-tiles:unk"
|
||||||
|
[n]="nb-palettes:unk"
|
||||||
[o]="output:glob *.2bpp"
|
[o]="output:glob *.2bpp"
|
||||||
[p]="palette:glob *.pal"
|
[p]="palette:glob *.pal"
|
||||||
[P]="output-palette:normal"
|
[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]="tilemap:glob *.tilemap"
|
||||||
[T]="output-tilemap:normal"
|
[T]="output-tilemap:normal"
|
||||||
[x]="trim-end:unk"
|
[x]="trim-end:unk"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
declare -A FILES
|
declare -A FILES
|
||||||
while read -r -d '' file; do
|
while read -r -d '' file; do
|
||||||
FILES["$file"]="true"
|
FILES["$file"]="true"
|
||||||
done < <(git diff --name-only -z $1 HEAD)
|
done < <(git diff --name-only -z "$1" HEAD)
|
||||||
|
|
||||||
edited () {
|
edited () {
|
||||||
${FILES["$1"]:-"false"}
|
${FILES["$1"]:-"false"}
|
||||||
@@ -40,13 +40,13 @@ dependency () {
|
|||||||
# Pull requests that edit the first file without the second may be correct,
|
# Pull requests that edit the first file without the second may be correct,
|
||||||
# but are suspicious enough to require review.
|
# 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?"
|
"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?"
|
"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?"
|
"Were the rgbasm warnings changed?"
|
||||||
|
|
||||||
dependency src/asm/object.c include/linkdefs.h \
|
dependency src/asm/object.c include/linkdefs.h \
|
||||||
@@ -59,27 +59,27 @@ dependency Makefile CMakeLists.txt \
|
|||||||
dependency Makefile src/CMakeLists.txt \
|
dependency Makefile src/CMakeLists.txt \
|
||||||
"Did the build process change?"
|
"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?"
|
"Did the rgbasm CLI change?"
|
||||||
dependency src/asm/main.c contrib/zsh_compl/_rgbasm \
|
dependency src/asm/main.c contrib/zsh_compl/_rgbasm \
|
||||||
"Did the rgbasm CLI change?"
|
"Did the rgbasm CLI change?"
|
||||||
dependency src/asm/main.c contrib/bash_compl/_rgbasm.bash \
|
dependency src/asm/main.c contrib/bash_compl/_rgbasm.bash \
|
||||||
"Did the rgbasm CLI change?"
|
"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?"
|
"Did the rgblink CLI change?"
|
||||||
dependency src/link/main.c contrib/zsh_compl/_rgblink \
|
dependency src/link/main.c contrib/zsh_compl/_rgblink \
|
||||||
"Did the rgblink CLI change?"
|
"Did the rgblink CLI change?"
|
||||||
dependency src/link/main.c contrib/bash_compl/_rgblink.bash \
|
dependency src/link/main.c contrib/bash_compl/_rgblink.bash \
|
||||||
"Did the rgblink CLI change?"
|
"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?"
|
"Did the rgbfix CLI change?"
|
||||||
dependency src/fix/main.c contrib/zsh_compl/_rgbfix \
|
dependency src/fix/main.c contrib/zsh_compl/_rgbfix \
|
||||||
"Did the rgbfix CLI change?"
|
"Did the rgbfix CLI change?"
|
||||||
dependency src/fix/main.c contrib/bash_compl/_rgbfix.bash \
|
dependency src/fix/main.c contrib/bash_compl/_rgbfix.bash \
|
||||||
"Did the rgbfix CLI change?"
|
"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?"
|
"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?"
|
"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?"
|
"Did the rgbgfx CLI change?"
|
||||||
|
|||||||
@@ -23,51 +23,51 @@
|
|||||||
# SOFTWARE.
|
# SOFTWARE.
|
||||||
|
|
||||||
STATE=0
|
STATE=0
|
||||||
diff <(xxd $1) <(xxd $2) | while read -r LINE; do
|
diff <(xxd "$1") <(xxd "$2") | while read -r LINE; do
|
||||||
if [ $STATE -eq 0 ]; then
|
if [[ $STATE -eq 0 ]]; then
|
||||||
# Discard first line (line info)
|
# Discard first line (line info)
|
||||||
STATE=1
|
STATE=1
|
||||||
elif [ "$LINE" = '---' ]; then
|
elif [[ "$LINE" = '---' ]]; then
|
||||||
# Separator between files switches states
|
# Separator between files switches states
|
||||||
echo $LINE
|
echo "$LINE"
|
||||||
STATE=3
|
STATE=3
|
||||||
elif grep -Eq '^[0-9]+(,[0-9]+)?[cd][0-9]+(,[0-9]+)?' <<< "$LINE"; then
|
elif grep -Eq '^[0-9]+(,[0-9]+)?[cd][0-9]+(,[0-9]+)?' <<< "$LINE"; then
|
||||||
# Line info resets the whole thing
|
# Line info resets the whole thing
|
||||||
STATE=1
|
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
|
# Compute the GB address from the ROM offset
|
||||||
OFS=$(cut -d ' ' -f 2 <<< "$LINE" | tr -d ':')
|
OFS=$(cut -d ' ' -f 2 <<< "$LINE" | tr -d ':')
|
||||||
BANK=$((0x$OFS / 0x4000))
|
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
|
# Try finding the preceding symbol closest to the diff
|
||||||
if [ $STATE -eq 1 ]; then
|
if [[ $STATE -eq 1 ]]; then
|
||||||
STATE=2
|
STATE=2
|
||||||
SYMFILE=${1%.*}.sym
|
SYMFILE=${1%.*}.sym
|
||||||
else
|
else
|
||||||
STATE=4
|
STATE=4
|
||||||
SYMFILE=${2%.*}.sym
|
SYMFILE=${2%.*}.sym
|
||||||
fi
|
fi
|
||||||
EXTRA=$(if [ -f "$SYMFILE" ]; then
|
EXTRA=$(if [[ -f "$SYMFILE" ]]; then
|
||||||
# Read the sym file for such a symbol
|
# Read the sym file for such a symbol
|
||||||
# Ignore comment lines, only pick matching bank
|
# Ignore comment lines, only pick matching bank
|
||||||
# (The bank regex ignores comments already, make `cut` and `tr` process less lines)
|
# (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 |
|
cut -d ';' -f 1 |
|
||||||
tr -d "\r" |
|
tr -d "\r" |
|
||||||
while read -r SYMADDR SYM; do
|
while read -r SYMADDR SYM; do
|
||||||
SYMADDR=$((0x${SYMADDR#*:}))
|
SYMADDR=$((0x${SYMADDR#*:}))
|
||||||
if [ $SYMADDR -le $ADDR ]; then
|
if [[ $SYMADDR -le $ADDR ]]; then
|
||||||
printf " (%s+%#x)\n" "$SYM" $(($ADDR - $SYMADDR))
|
printf " (%s+%#x)\n" "$SYM" $((ADDR - SYMADDR))
|
||||||
fi
|
fi
|
||||||
# TODO: assumes sorted sym files
|
# TODO: assumes sorted sym files
|
||||||
done | tail -n 1
|
done | tail -n 1
|
||||||
fi)
|
fi)
|
||||||
printf "%02x:%04x %s\n" $BANK $ADDR $EXTRA
|
printf "%02x:%04x %s\n" $BANK $ADDR "$EXTRA"
|
||||||
fi
|
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 ':')
|
OFS=$(cut -d ' ' -f 2 <<< "$LINE" | tr -d ':')
|
||||||
BANK=$((0x$OFS / 0x4000))
|
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#*: }"
|
printf "%s %02x:%04x: %s\n" "${LINE:0:1}" $BANK $ADDR "${LINE#*: }"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -15,20 +15,25 @@ local args=(
|
|||||||
|
|
||||||
'(-a --attr-map -A --output-attr-map)'{-A,--output-attr-map}'[Shortcut for -a <file>.attrmap]'
|
'(-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]'
|
'(-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]'
|
'(-m --mirror-tiles)'{-m,--mirror-tiles}'[Eliminate mirrored tiles from output]'
|
||||||
'(-p --palette -P --output-palette)'{-P,--output-palette}'[Shortcut for -p <file>.pal]'
|
'(-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]'
|
'(-t --tilemap -T --output-tilemap)'{-T,--output-tilemap}'[Shortcut for -t <file>.tilemap]'
|
||||||
'(-u --unique-tiles)'{-u,--unique-tiles}'[Eliminate redundant tiles]'
|
'(-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'
|
'(-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'
|
'(-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'
|
'(-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"
|
'(-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'
|
'(-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:'
|
'(-x --trim-end)'{-x,--trim-end}'+[Trim end of output by this many tiles]:tile count:'
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
int32_t fix_Callback_PI(void);
|
|
||||||
void fix_Print(int32_t i);
|
void fix_Print(int32_t i);
|
||||||
int32_t fix_Sin(int32_t i);
|
int32_t fix_Sin(int32_t i);
|
||||||
int32_t fix_Cos(int32_t i);
|
int32_t fix_Cos(int32_t i);
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
#include "helpers.h"
|
#include "helpers.h"
|
||||||
|
|
||||||
extern bool haltnop;
|
extern bool haltnop;
|
||||||
|
extern bool warnOnHaltNop;
|
||||||
extern bool optimizeLoads;
|
extern bool optimizeLoads;
|
||||||
|
extern bool warnOnLdOpt;
|
||||||
extern bool verbose;
|
extern bool verbose;
|
||||||
extern bool warnings; /* True to enable warnings, false to disable them. */
|
extern bool warnings; /* True to enable warnings, false to disable them. */
|
||||||
|
|
||||||
|
|||||||
39
include/defaultinitalloc.hpp
Normal file
39
include/defaultinitalloc.hpp
Normal 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
|
||||||
@@ -12,10 +12,18 @@
|
|||||||
#include "helpers.h"
|
#include "helpers.h"
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
void warn(char const NONNULL(fmt), ...) format_(printf, 1, 2);
|
void warn(char const NONNULL(fmt), ...) format_(printf, 1, 2);
|
||||||
void warnx(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 err(char const NONNULL(fmt), ...) format_(printf, 1, 2);
|
||||||
_Noreturn void errx(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 */
|
#endif /* RGBDS_ERROR_H */
|
||||||
|
|||||||
8
include/extern/getopt.h
vendored
8
include/extern/getopt.h
vendored
@@ -26,6 +26,10 @@
|
|||||||
#ifndef RGBDS_EXTERN_GETOPT_H
|
#ifndef RGBDS_EXTERN_GETOPT_H
|
||||||
#define RGBDS_EXTERN_GETOPT_H
|
#define RGBDS_EXTERN_GETOPT_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
extern char *musl_optarg;
|
extern char *musl_optarg;
|
||||||
extern int musl_optind, musl_opterr, musl_optopt, musl_optreset;
|
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 required_argument 1
|
||||||
#define optional_argument 2
|
#define optional_argument 2
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
} // extern "C"
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
123
include/gfx/main.hpp
Normal 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 */
|
||||||
@@ -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 */
|
|
||||||
32
include/gfx/pal_packing.hpp
Normal file
32
include/gfx/pal_packing.hpp
Normal 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 */
|
||||||
32
include/gfx/pal_sorting.hpp
Normal file
32
include/gfx/pal_sorting.hpp
Normal 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
15
include/gfx/pal_spec.hpp
Normal 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
14
include/gfx/process.hpp
Normal 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 */
|
||||||
44
include/gfx/proto_palette.hpp
Normal file
44
include/gfx/proto_palette.hpp
Normal 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
14
include/gfx/reverse.hpp
Normal 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
67
include/gfx/rgba.hpp
Normal 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 */
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
#define attr_(...)
|
#define attr_(...)
|
||||||
// This seems to generate similar code to __builtin_unreachable, despite different semantics
|
// This seems to generate similar code to __builtin_unreachable, despite different semantics
|
||||||
// Note that executing this is undefined behavior (declared _Noreturn, but does return)
|
// Note that executing this is undefined behavior (declared _Noreturn, but does return)
|
||||||
static inline _Noreturn unreachable_(void) {}
|
static inline _Noreturn void unreachable_(void) {}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Use builtins whenever possible, and shim them otherwise
|
// Use builtins whenever possible, and shim them otherwise
|
||||||
|
|||||||
90
include/itertools.hpp
Normal file
90
include/itertools.hpp
Normal 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 */
|
||||||
@@ -46,12 +46,14 @@
|
|||||||
# include <unistd.h>
|
# include <unistd.h>
|
||||||
#endif
|
#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
|
#ifdef _MSC_VER
|
||||||
# define MIN_NB_ELMS(N)
|
# define MIN_NB_ELMS(N)
|
||||||
|
# define ARR_QUALS(...)
|
||||||
# define NONNULL(ptr) *ptr
|
# define NONNULL(ptr) *ptr
|
||||||
#else
|
#else
|
||||||
# define MIN_NB_ELMS(N) static (N)
|
# define MIN_NB_ELMS(N) static (N)
|
||||||
|
# define ARR_QUALS(...) __VA_ARGS__
|
||||||
# define NONNULL(ptr) ptr[static 1]
|
# define NONNULL(ptr) ptr[static 1]
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,19 @@
|
|||||||
#ifndef EXTERN_VERSION_H
|
#ifndef EXTERN_VERSION_H
|
||||||
#define EXTERN_VERSION_H
|
#define EXTERN_VERSION_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
#define PACKAGE_VERSION_MAJOR 0
|
#define PACKAGE_VERSION_MAJOR 0
|
||||||
#define PACKAGE_VERSION_MINOR 5
|
#define PACKAGE_VERSION_MINOR 6
|
||||||
#define PACKAGE_VERSION_PATCH 2
|
#define PACKAGE_VERSION_PATCH 0
|
||||||
|
#define PACKAGE_VERSION_RC 1
|
||||||
|
|
||||||
char const *get_package_version_string(void);
|
char const *get_package_version_string(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif /* EXTERN_VERSION_H */
|
#endif /* EXTERN_VERSION_H */
|
||||||
|
|||||||
@@ -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
|
.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.
|
Add a string symbol to the compiled source code.
|
||||||
This is equivalent to
|
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
|
in code, or
|
||||||
.Ql Ar name Ic EQUS \(dq1\(dq
|
.Ql Ar name Ic EQUS No \(dq1\(dq
|
||||||
if
|
if
|
||||||
.Ar value
|
.Ar value
|
||||||
is not specified.
|
is not specified.
|
||||||
@@ -192,7 +192,7 @@ SurvivalKids.gbc
|
|||||||
.Sh TPP1
|
.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.
|
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
|
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
|
.Ss MBC name
|
||||||
The MBC name for TPP1 is more complex than standard mappers.
|
The MBC name for TPP1 is more complex than standard mappers.
|
||||||
It must be followed with the revision number, of the form
|
It must be followed with the revision number, of the form
|
||||||
591
man/rgbgfx.1
Normal file
591
man/rgbgfx.1
Normal 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 .
|
||||||
@@ -14,14 +14,6 @@ set(common_src
|
|||||||
"_version.c"
|
"_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)
|
find_package(BISON REQUIRED)
|
||||||
set(BISON_FLAGS "-Wall")
|
set(BISON_FLAGS "-Wall")
|
||||||
# Set sompe optimization flags on versions that support them
|
# Set sompe optimization flags on versions that support them
|
||||||
@@ -70,9 +62,16 @@ set(rgbfix_src
|
|||||||
)
|
)
|
||||||
|
|
||||||
set(rgbgfx_src
|
set(rgbgfx_src
|
||||||
"gfx/gb.c"
|
"gfx/main.cpp"
|
||||||
"gfx/main.c"
|
"gfx/pal_packing.cpp"
|
||||||
"gfx/makepng.c"
|
"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
|
set(rgblink_src
|
||||||
@@ -97,22 +96,6 @@ foreach(PROG "asm" "fix" "gfx" "link")
|
|||||||
install(TARGETS rgb${PROG} RUNTIME DESTINATION bin)
|
install(TARGETS rgb${PROG} RUNTIME DESTINATION bin)
|
||||||
endforeach()
|
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
|
if(LIBPNG_FOUND) # pkg-config
|
||||||
target_include_directories(rgbgfx PRIVATE ${LIBPNG_INCLUDE_DIRS})
|
target_include_directories(rgbgfx PRIVATE ${LIBPNG_INCLUDE_DIRS})
|
||||||
target_link_directories(rgbgfx PRIVATE ${LIBPNG_LIBRARY_DIRS})
|
target_link_directories(rgbgfx PRIVATE ${LIBPNG_LIBRARY_DIRS})
|
||||||
|
|||||||
@@ -30,16 +30,6 @@
|
|||||||
#define M_PI 3.14159265358979323846
|
#define M_PI 3.14159265358979323846
|
||||||
#endif
|
#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
|
* Print a fixed point value
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -212,10 +212,6 @@ static struct KeywordMapping {
|
|||||||
{"INCLUDE", T_POP_INCLUDE},
|
{"INCLUDE", T_POP_INCLUDE},
|
||||||
{"PRINT", T_POP_PRINT},
|
{"PRINT", T_POP_PRINT},
|
||||||
{"PRINTLN", T_POP_PRINTLN},
|
{"PRINTLN", T_POP_PRINTLN},
|
||||||
{"PRINTT", T_POP_PRINTT},
|
|
||||||
{"PRINTI", T_POP_PRINTI},
|
|
||||||
{"PRINTV", T_POP_PRINTV},
|
|
||||||
{"PRINTF", T_POP_PRINTF},
|
|
||||||
{"EXPORT", T_POP_EXPORT},
|
{"EXPORT", T_POP_EXPORT},
|
||||||
{"DS", T_POP_DS},
|
{"DS", T_POP_DS},
|
||||||
{"DB", T_POP_DB},
|
{"DB", T_POP_DB},
|
||||||
@@ -2056,9 +2052,23 @@ static int yylex_RAW(void)
|
|||||||
size_t i = 0;
|
size_t i = 0;
|
||||||
int c;
|
int c;
|
||||||
|
|
||||||
/* Trim left whitespace (stops at a block comment or line continuation) */
|
/* Trim left whitespace (stops at a block comment) */
|
||||||
while (isWhitespace(peek()))
|
for (;;) {
|
||||||
shiftChar();
|
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 (;;) {
|
for (;;) {
|
||||||
c = peek();
|
c = peek();
|
||||||
@@ -2107,6 +2117,7 @@ static int yylex_RAW(void)
|
|||||||
shiftChar();
|
shiftChar();
|
||||||
c = peek();
|
c = peek();
|
||||||
|
|
||||||
|
backslash:
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case ',': /* Escapes only valid inside a macro arg */
|
case ',': /* Escapes only valid inside a macro arg */
|
||||||
case '(':
|
case '(':
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ bool generatePhonyDeps;
|
|||||||
char *targetFileName;
|
char *targetFileName;
|
||||||
|
|
||||||
bool haltnop;
|
bool haltnop;
|
||||||
|
bool warnOnHaltNop;
|
||||||
bool optimizeLoads;
|
bool optimizeLoads;
|
||||||
|
bool warnOnLdOpt;
|
||||||
bool verbose;
|
bool verbose;
|
||||||
bool warnings; /* True to enable warnings, false to disable them. */
|
bool warnings; /* True to enable warnings, false to disable them. */
|
||||||
|
|
||||||
@@ -84,7 +86,7 @@ static char *make_escape(char const *str)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Short options */
|
/* 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 */
|
/* Variables for the long-only options */
|
||||||
static int depType; /* Variants of `-M` */
|
static int depType; /* Variants of `-M` */
|
||||||
@@ -104,9 +106,11 @@ static struct option const longopts[] = {
|
|||||||
{ "define", required_argument, NULL, 'D' },
|
{ "define", required_argument, NULL, 'D' },
|
||||||
{ "export-all", no_argument, NULL, 'E' },
|
{ "export-all", no_argument, NULL, 'E' },
|
||||||
{ "gfx-chars", required_argument, NULL, 'g' },
|
{ "gfx-chars", required_argument, NULL, 'g' },
|
||||||
|
{ "nop-after-halt", no_argument, NULL, 'H' },
|
||||||
{ "halt-without-nop", no_argument, NULL, 'h' },
|
{ "halt-without-nop", no_argument, NULL, 'h' },
|
||||||
{ "include", required_argument, NULL, 'i' },
|
{ "include", required_argument, NULL, 'i' },
|
||||||
{ "preserve-ld", no_argument, NULL, 'L' },
|
{ "preserve-ld", no_argument, NULL, 'L' },
|
||||||
|
{ "auto-ldh", no_argument, NULL, 'l' },
|
||||||
{ "dependfile", required_argument, NULL, 'M' },
|
{ "dependfile", required_argument, NULL, 'M' },
|
||||||
{ "MG", no_argument, &depType, 'G' },
|
{ "MG", no_argument, &depType, 'G' },
|
||||||
{ "MP", no_argument, &depType, 'P' },
|
{ "MP", no_argument, &depType, 'P' },
|
||||||
@@ -170,8 +174,10 @@ int main(int argc, char *argv[])
|
|||||||
opt_B("01");
|
opt_B("01");
|
||||||
opt_G("0123");
|
opt_G("0123");
|
||||||
opt_P(0);
|
opt_P(0);
|
||||||
optimizeLoads = true;
|
|
||||||
haltnop = true;
|
haltnop = true;
|
||||||
|
warnOnHaltNop = true;
|
||||||
|
optimizeLoads = true;
|
||||||
|
warnOnLdOpt = true;
|
||||||
verbose = false;
|
verbose = false;
|
||||||
warnings = true;
|
warnings = true;
|
||||||
sym_SetExportAll(false);
|
sym_SetExportAll(false);
|
||||||
@@ -209,7 +215,14 @@ int main(int argc, char *argv[])
|
|||||||
errx("Must specify exactly 4 characters for option 'g'");
|
errx("Must specify exactly 4 characters for option 'g'");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'H':
|
||||||
|
if (!haltnop)
|
||||||
|
errx("`-H` and `-h` don't make sense together");
|
||||||
|
warnOnHaltNop = false;
|
||||||
|
break;
|
||||||
case 'h':
|
case 'h':
|
||||||
|
if (!warnOnHaltNop)
|
||||||
|
errx("`-H` and `-h` don't make sense together");
|
||||||
haltnop = false;
|
haltnop = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -218,8 +231,15 @@ int main(int argc, char *argv[])
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'L':
|
case 'L':
|
||||||
|
if (!warnOnLdOpt)
|
||||||
|
errx("`-L` and `-l` don't make sense together");
|
||||||
optimizeLoads = false;
|
optimizeLoads = false;
|
||||||
break;
|
break;
|
||||||
|
case 'l':
|
||||||
|
if (!optimizeLoads)
|
||||||
|
errx("`-L` and `-l` don't make sense together");
|
||||||
|
warnOnLdOpt = false;
|
||||||
|
break;
|
||||||
|
|
||||||
case 'M':
|
case 'M':
|
||||||
if (!strcmp("-", musl_optarg))
|
if (!strcmp("-", musl_optarg))
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ struct OptStackEntry {
|
|||||||
char gbgfx[4];
|
char gbgfx[4];
|
||||||
int32_t fillByte;
|
int32_t fillByte;
|
||||||
bool haltnop;
|
bool haltnop;
|
||||||
|
bool warnOnHaltNop;
|
||||||
bool optimizeLoads;
|
bool optimizeLoads;
|
||||||
|
bool warnOnLdOpt;
|
||||||
bool warningsAreErrors;
|
bool warningsAreErrors;
|
||||||
size_t maxRecursionDepth;
|
size_t maxRecursionDepth;
|
||||||
// Don't be confused: we use the size of the **global variable** `warningStates`!
|
// 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();
|
lexer_CheckRecursionDepth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void opt_H(bool warn)
|
||||||
|
{
|
||||||
|
warnOnHaltNop = warn;
|
||||||
|
}
|
||||||
|
|
||||||
void opt_h(bool halt)
|
void opt_h(bool halt)
|
||||||
{
|
{
|
||||||
haltnop = halt;
|
haltnop = halt;
|
||||||
@@ -58,6 +65,11 @@ void opt_L(bool optimize)
|
|||||||
optimizeLoads = optimize;
|
optimizeLoads = optimize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void opt_l(bool warn)
|
||||||
|
{
|
||||||
|
warnOnLdOpt = warn;
|
||||||
|
}
|
||||||
|
|
||||||
void opt_W(char *flag)
|
void opt_W(char *flag)
|
||||||
{
|
{
|
||||||
processWarningFlag(flag);
|
processWarningFlag(flag);
|
||||||
@@ -118,6 +130,13 @@ void opt_Parse(char *s)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'H':
|
||||||
|
if (s[1] == '\0')
|
||||||
|
opt_H(false);
|
||||||
|
else
|
||||||
|
error("Option 'H' does not take an argument\n");
|
||||||
|
break;
|
||||||
|
|
||||||
case 'h':
|
case 'h':
|
||||||
if (s[1] == '\0')
|
if (s[1] == '\0')
|
||||||
opt_h(false);
|
opt_h(false);
|
||||||
@@ -132,6 +151,13 @@ void opt_Parse(char *s)
|
|||||||
error("Option 'L' does not take an argument\n");
|
error("Option 'L' does not take an argument\n");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'l':
|
||||||
|
if (s[1] == '\0')
|
||||||
|
opt_l(false);
|
||||||
|
else
|
||||||
|
error("Option 'l' does not take an argument\n");
|
||||||
|
break;
|
||||||
|
|
||||||
case 'W':
|
case 'W':
|
||||||
if (strlen(&s[1]) > 0)
|
if (strlen(&s[1]) > 0)
|
||||||
opt_W(&s[1]);
|
opt_W(&s[1]);
|
||||||
@@ -186,8 +212,10 @@ void opt_Push(void)
|
|||||||
entry->fillByte = fillByte; // Pulled from section.h
|
entry->fillByte = fillByte; // Pulled from section.h
|
||||||
|
|
||||||
entry->haltnop = haltnop; // Pulled from main.h
|
entry->haltnop = haltnop; // Pulled from main.h
|
||||||
|
entry->warnOnHaltNop = warnOnHaltNop;
|
||||||
|
|
||||||
entry->optimizeLoads = optimizeLoads; // Pulled from main.h
|
entry->optimizeLoads = optimizeLoads; // Pulled from main.h
|
||||||
|
entry->warnOnLdOpt = warnOnLdOpt;
|
||||||
|
|
||||||
// Both of these pulled from warning.h
|
// Both of these pulled from warning.h
|
||||||
entry->warningsAreErrors = warningsAreErrors;
|
entry->warningsAreErrors = warningsAreErrors;
|
||||||
@@ -209,8 +237,10 @@ void opt_Pop(void)
|
|||||||
opt_B(entry->binary);
|
opt_B(entry->binary);
|
||||||
opt_G(entry->gbgfx);
|
opt_G(entry->gbgfx);
|
||||||
opt_P(entry->fillByte);
|
opt_P(entry->fillByte);
|
||||||
|
opt_H(entry->warnOnHaltNop);
|
||||||
opt_h(entry->haltnop);
|
opt_h(entry->haltnop);
|
||||||
opt_L(entry->optimizeLoads);
|
opt_L(entry->optimizeLoads);
|
||||||
|
opt_l(entry->warnOnLdOpt);
|
||||||
|
|
||||||
// opt_W does not apply a whole warning state; it processes one flag string
|
// opt_W does not apply a whole warning state; it processes one flag string
|
||||||
warningsAreErrors = entry->warningsAreErrors;
|
warningsAreErrors = entry->warningsAreErrors;
|
||||||
|
|||||||
@@ -599,7 +599,6 @@ enum {
|
|||||||
|
|
||||||
%token T_POP_INCLUDE "INCLUDE"
|
%token T_POP_INCLUDE "INCLUDE"
|
||||||
%token T_POP_PRINT "PRINT" T_POP_PRINTLN "PRINTLN"
|
%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_IF "IF" T_POP_ELIF "ELIF" T_POP_ELSE "ELSE" T_POP_ENDC "ENDC"
|
||||||
%token T_POP_EXPORT "EXPORT"
|
%token T_POP_EXPORT "EXPORT"
|
||||||
%token T_POP_DB "DB" T_POP_DS "DS" T_POP_DW "DW" T_POP_DL "DL"
|
%token T_POP_DB "DB" T_POP_DS "DS" T_POP_DW "DW" T_POP_DL "DL"
|
||||||
@@ -856,7 +855,7 @@ macroargs : %empty {
|
|||||||
|
|
||||||
/* These commands start with a T_LABEL. */
|
/* These commands start with a T_LABEL. */
|
||||||
assignment_directive : equ
|
assignment_directive : equ
|
||||||
| set
|
| assignment
|
||||||
| rb
|
| rb
|
||||||
| rw
|
| rw
|
||||||
| rl
|
| rl
|
||||||
@@ -866,10 +865,6 @@ assignment_directive : equ
|
|||||||
directive : endc
|
directive : endc
|
||||||
| print
|
| print
|
||||||
| println
|
| println
|
||||||
| printf
|
|
||||||
| printt
|
|
||||||
| printv
|
|
||||||
| printi
|
|
||||||
| export
|
| export
|
||||||
| db
|
| db
|
||||||
| dw
|
| dw
|
||||||
@@ -927,12 +922,8 @@ compoundeq : T_POP_ADDEQ { $$ = RPN_ADD; }
|
|||||||
equ : T_LABEL T_POP_EQU const { sym_AddEqu($1, $3); }
|
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 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); }
|
equs : T_LABEL T_POP_EQUS string { sym_AddString($1, $3); }
|
||||||
@@ -1174,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); }
|
| redef_id T_POP_EQUAL const { sym_AddVar($1, $3); }
|
||||||
| def_id compoundeq const { compoundAssignment($1, $2, $3); }
|
| def_id compoundeq const { compoundAssignment($1, $2, $3); }
|
||||||
| redef_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 {
|
def_rb : def_id T_POP_RB rs_uconst {
|
||||||
@@ -1295,30 +1278,6 @@ print_expr : const_no_str { printf("$%" PRIX32, $1); }
|
|||||||
| string { printf("%s", $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 {
|
const_3bit : const {
|
||||||
int32_t value = $1;
|
int32_t value = $1;
|
||||||
|
|
||||||
@@ -1842,8 +1801,13 @@ z80_ei : T_Z80_EI { sect_AbsByte(0xFB); }
|
|||||||
|
|
||||||
z80_halt : T_Z80_HALT {
|
z80_halt : T_Z80_HALT {
|
||||||
sect_AbsByte(0x76);
|
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);
|
sect_AbsByte(0x00);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -1951,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 {
|
| T_Z80_LD op_mem_ind T_COMMA T_MODE_A {
|
||||||
if (optimizeLoads && rpn_isKnown(&$2)
|
if (optimizeLoads && rpn_isKnown(&$2)
|
||||||
&& $2.val >= 0xFF00) {
|
&& $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(0xE0);
|
||||||
sect_AbsByte($2.val & 0xFF);
|
sect_AbsByte($2.val & 0xFF);
|
||||||
rpn_Free(&$2);
|
rpn_Free(&$2);
|
||||||
@@ -1999,6 +1967,10 @@ z80_ld_a : T_Z80_LD reg_r T_COMMA c_ind {
|
|||||||
if ($2 == REG_A) {
|
if ($2 == REG_A) {
|
||||||
if (optimizeLoads && rpn_isKnown(&$4)
|
if (optimizeLoads && rpn_isKnown(&$4)
|
||||||
&& $4.val >= 0xFF00) {
|
&& $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(0xF0);
|
||||||
sect_AbsByte($4.val & 0xFF);
|
sect_AbsByte($4.val & 0xFF);
|
||||||
rpn_Free(&$4);
|
rpn_Free(&$4);
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
int size = snprintf(_expr->reason, 128, __VA_ARGS__); \
|
int size = snprintf(_expr->reason, 128, __VA_ARGS__); \
|
||||||
if (size >= 128) { /* If this wasn't enough, try again */ \
|
if (size >= 128) { /* If this wasn't enough, try again */ \
|
||||||
_expr->reason = realloc(_expr->reason, size + 1); \
|
_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__); \
|
sprintf(_expr->reason, __VA_ARGS__); \
|
||||||
} \
|
} \
|
||||||
} while (0)
|
} while (0)
|
||||||
|
|||||||
@@ -743,8 +743,13 @@ void sym_Init(time_t now)
|
|||||||
|
|
||||||
sym_AddVar("_RS", 0)->isBuiltin = true;
|
sym_AddVar("_RS", 0)->isBuiltin = true;
|
||||||
|
|
||||||
#define addNumber(name, val) sym_AddEqu(name, val)->isBuiltin = true
|
#define addSym(fn, name, val) do { \
|
||||||
#define addString(name, val) sym_AddString(name, val)->isBuiltin = true
|
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());
|
addString("__RGBDS_VERSION__", get_package_version_string());
|
||||||
addNumber("__RGBDS_MAJOR__", PACKAGE_VERSION_MAJOR);
|
addNumber("__RGBDS_MAJOR__", PACKAGE_VERSION_MAJOR);
|
||||||
@@ -788,16 +793,7 @@ void sym_Init(time_t now)
|
|||||||
|
|
||||||
#undef addNumber
|
#undef addNumber
|
||||||
#undef addString
|
#undef addString
|
||||||
|
#undef addSym
|
||||||
|
|
||||||
sym_SetCurrentSymbolScope(NULL);
|
sym_SetCurrentSymbolScope(NULL);
|
||||||
anonLabelID = 0;
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env bash
|
||||||
bison -V | awk -v major="$1" -v minor="$2" '
|
bison -V | awk -v major="$1" -v minor="$2" '
|
||||||
/^bison.*[0-9]+(\.[0-9]+)(\.[0-9]+)?$/ {
|
/^bison.*[0-9]+(\.[0-9]+)(\.[0-9]+)?$/ {
|
||||||
match($0, /[0-9]+(\.[0-9]+)(\.[0-9]+)?$/);
|
match($0, /[0-9]+(\.[0-9]+)(\.[0-9]+)?$/);
|
||||||
|
|||||||
385
src/gfx/gb.c
385
src/gfx/gb.c
@@ -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);
|
|
||||||
}
|
|
||||||
358
src/gfx/main.c
358
src/gfx/main.c
@@ -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
813
src/gfx/main.cpp
Normal 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);
|
||||||
|
}
|
||||||
@@ -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
512
src/gfx/pal_packing.cpp
Normal 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
105
src/gfx/pal_sorting.cpp
Normal 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
451
src/gfx/pal_spec.cpp
Normal 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
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
83
src/gfx/proto_palette.cpp
Normal 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
324
src/gfx/reverse.cpp
Normal 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
52
src/gfx/rgba.cpp
Normal 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;
|
||||||
|
}
|
||||||
160
src/gfx/rgbgfx.1
160
src/gfx/rgbgfx.1
@@ -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 .
|
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
@@ -446,9 +447,27 @@ void assign_AssignSections(void)
|
|||||||
|
|
||||||
/* Overlaying requires only fully-constrained sections */
|
/* Overlaying requires only fully-constrained sections */
|
||||||
verbosePrint("Assigning other sections...\n");
|
verbosePrint("Assigning other sections...\n");
|
||||||
if (overlayFileName)
|
if (overlayFileName) {
|
||||||
errx("All sections must be fixed when using an overlay file; %" PRIu64 " %sn't",
|
fprintf(stderr, "FATAL: All sections must be fixed when using an overlay file");
|
||||||
nbSectionsToAssign, nbSectionsToAssign == 1 ? "is" : "are");
|
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 */
|
/* Assign all remaining sections by decreasing constraint order */
|
||||||
for (int8_t constraints = BANK_CONSTRAINED | ALIGN_CONSTRAINED;
|
for (int8_t constraints = BANK_CONSTRAINED | ALIGN_CONSTRAINED;
|
||||||
|
|||||||
@@ -151,8 +151,10 @@ static char *readstr(FILE *file)
|
|||||||
/* Read char */
|
/* Read char */
|
||||||
int byte = getc(file);
|
int byte = getc(file);
|
||||||
|
|
||||||
if (byte == EOF)
|
if (byte == EOF) {
|
||||||
|
free(str);
|
||||||
return NULL;
|
return NULL;
|
||||||
|
}
|
||||||
str[index] = byte;
|
str[index] = byte;
|
||||||
} while (str[index]);
|
} while (str[index]);
|
||||||
return str;
|
return str;
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ struct SortedSymbol {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static struct {
|
static struct {
|
||||||
uint32_t nbBanks;
|
uint32_t nbBanks; // Size of the array below (which may be NULL if this is 0)
|
||||||
struct SortedSections {
|
struct SortedSections {
|
||||||
struct SortedSection *sections;
|
struct SortedSection *sections;
|
||||||
struct SortedSection *zeroLenSections;
|
struct SortedSection *zeroLenSections;
|
||||||
@@ -243,9 +243,9 @@ static void writeROM(void)
|
|||||||
coverOverlayBanks(nbOverlayBanks);
|
coverOverlayBanks(nbOverlayBanks);
|
||||||
|
|
||||||
if (outputFile) {
|
if (outputFile) {
|
||||||
if (sections[SECTTYPE_ROM0].nbBanks > 0)
|
writeBank(sections[SECTTYPE_ROM0].banks ? sections[SECTTYPE_ROM0].banks[0].sections
|
||||||
writeBank(sections[SECTTYPE_ROM0].banks[0].sections,
|
: NULL,
|
||||||
startaddr[SECTTYPE_ROM0], maxsize[SECTTYPE_ROM0]);
|
startaddr[SECTTYPE_ROM0], maxsize[SECTTYPE_ROM0]);
|
||||||
|
|
||||||
for (uint32_t i = 0 ; i < sections[SECTTYPE_ROMX].nbBanks; i++)
|
for (uint32_t i = 0 ; i < sections[SECTTYPE_ROMX].nbBanks; i++)
|
||||||
writeBank(sections[SECTTYPE_ROMX].banks[i].sections,
|
writeBank(sections[SECTTYPE_ROMX].banks[i].sections,
|
||||||
|
|||||||
18
test/CMakeLists.txt
Normal file
18
test/CMakeLists.txt
Normal 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()
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
; Remove this test case when _PI is removed.
|
|
||||||
PRINTLN "{f:_PI}"
|
|
||||||
PURGE _PI
|
|
||||||
@@ -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)!
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
3.14159
|
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
\1: < 1>
|
\1: < 1>
|
||||||
\2: <2>
|
\2: <2>
|
||||||
|
|
||||||
'mac c,d':
|
'mac c,d':
|
||||||
\1: < c>
|
\1: <c>
|
||||||
\2: <d>
|
\2: <d>
|
||||||
|
|
||||||
'mac 1,2 + 2,3':
|
'mac 1,2 + 2,3':
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ pusho
|
|||||||
println $8000_0000 / -1
|
println $8000_0000 / -1
|
||||||
popo
|
popo
|
||||||
|
|
||||||
|
opt H, l
|
||||||
|
|
||||||
ds 1
|
ds 1
|
||||||
ld [$ff88], a
|
ld [$ff88], a
|
||||||
halt
|
halt
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
warning: opt.asm(16): [-Wdiv]
|
warning: opt.asm(18): [-Wdiv]
|
||||||
Division of -2147483648 by -1 yields -2147483648
|
Division of -2147483648 by -1 yields -2147483648
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
export LC_ALL=C
|
export LC_ALL=C
|
||||||
|
|
||||||
|
|||||||
19
test/asm/trimmed-macro-args.asm
Normal file
19
test/asm/trimmed-macro-args.asm
Normal 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
|
||||||
0
test/asm/trimmed-macro-args.err
Normal file
0
test/asm/trimmed-macro-args.err
Normal file
9
test/asm/trimmed-macro-args.out
Normal file
9
test/asm/trimmed-macro-args.out
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
3: "a"
|
||||||
|
2: "b"
|
||||||
|
1: "c"
|
||||||
|
3: "a"
|
||||||
|
2: "b"
|
||||||
|
1: "c"
|
||||||
|
3: "a"
|
||||||
|
2: " b"
|
||||||
|
1: "c"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env bash
|
||||||
fname=$(mktemp)
|
fname=$(mktemp)
|
||||||
|
|
||||||
for i in *.asm; do
|
for i in *.asm; do
|
||||||
|
|||||||
@@ -80,22 +80,23 @@ echo "${bold}Checking padding...${resbold}"
|
|||||||
cp "$src"/padding{,-large,-larger}.bin .
|
cp "$src"/padding{,-large,-larger}.bin .
|
||||||
touch padding{,-large,-larger}.err
|
touch padding{,-large,-larger}.err
|
||||||
progress=0
|
progress=0
|
||||||
for b in {0..254}; do
|
for (( i=0; i < 10; ++i )); do
|
||||||
printf "\r$b..."
|
(( padding = RANDOM % 256 ))
|
||||||
|
echo "$padding..."
|
||||||
for suffix in '' -large -larger; do
|
for suffix in '' -large -larger; do
|
||||||
cat <<<'-p $b' >padding$suffix.flags
|
cat <<<"-p $padding" >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
|
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} .
|
runTest padding${suffix} .
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
printf "\rDone! \n"
|
echo "Done!"
|
||||||
|
|
||||||
# TODO: check MBC names
|
# TODO: check MBC names
|
||||||
|
|
||||||
# Check that RGBFIX errors out when inputting a non-existent file...
|
# Check that RGBFIX errors out when inputting a non-existent file...
|
||||||
$RGBFIX noexist 2>out.err
|
$RGBFIX noexist 2>out.err
|
||||||
rc=$(($rc || $? != 1))
|
rc=$((rc || $? != 1))
|
||||||
tryDiff "$src/noexist.err" out.err noexist.err
|
tryDiff "$src/noexist.err" out.err noexist.err
|
||||||
rc=$(($rc || $?))
|
rc=$((rc || $?))
|
||||||
|
|
||||||
exit $rc
|
exit $rc
|
||||||
|
|||||||
11
test/gfx/.gitignore
vendored
Normal file
11
test/gfx/.gitignore
vendored
Normal 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
|
||||||
6
test/gfx/bad_manual_pals.err
Normal file
6
test/gfx/bad_manual_pals.err
Normal 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
|
||||||
1
test/gfx/bad_manual_pals.flags
Normal file
1
test/gfx/bad_manual_pals.flags
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-c #fff,#a9d4fe:#fff,#5721d9
|
||||||
BIN
test/gfx/bad_manual_pals.png
Normal file
BIN
test/gfx/bad_manual_pals.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 712 B |
1
test/gfx/crop.flags
Normal file
1
test/gfx/crop.flags
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-L 2,1:1,1
|
||||||
BIN
test/gfx/crop.png
Normal file
BIN
test/gfx/crop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 671 B |
2
test/gfx/damaged1.err
Normal file
2
test/gfx/damaged1.err
Normal 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
BIN
test/gfx/damaged1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 433 B |
2
test/gfx/damaged2.err
Normal file
2
test/gfx/damaged2.err
Normal 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
BIN
test/gfx/damaged2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 433 B |
2
test/gfx/damaged9.err
Normal file
2
test/gfx/damaged9.err
Normal 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
BIN
test/gfx/damaged9.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 433 B |
320
test/gfx/randtilegen.c
Normal file
320
test/gfx/randtilegen.c
Normal 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
457
test/gfx/rgbgfx_test.cpp
Normal 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
BIN
test/gfx/seed0.bin
Normal file
Binary file not shown.
BIN
test/gfx/seed1.bin
Normal file
BIN
test/gfx/seed1.bin
Normal file
Binary file not shown.
2
test/gfx/seed10.bin
Normal file
2
test/gfx/seed10.bin
Normal 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<>TЎ{Z<><5A><EFBFBD><EFBFBD>AR<41><52>W˱<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><>Mߏ<4D>]/<2F>hX<68>+6fq<13><><EFBFBD>0Go<47><6F>BE<42><45><EFBFBD>R<EFBFBD>D<><44>b<EFBFBD><62>Am5İ<35><C4B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD>g<EFBFBD><67>l<EFBFBD><1F><>:H<18><>
|
||||||
BIN
test/gfx/seed11.bin
Normal file
BIN
test/gfx/seed11.bin
Normal file
Binary file not shown.
BIN
test/gfx/seed12.bin
Normal file
BIN
test/gfx/seed12.bin
Normal file
Binary file not shown.
1
test/gfx/seed13.bin
Normal file
1
test/gfx/seed13.bin
Normal 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>Iٽ<49>N<EFBFBD>
|
||||||
BIN
test/gfx/seed14.bin
Normal file
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
Reference in New Issue
Block a user