mirror of
https://github.com/gbdev/rgbds.git
synced 2025-11-20 18:22:07 +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
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: c++20
|
||||
Standard: c++17
|
||||
TabWidth: 4
|
||||
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: |
|
||||
sudo apt-get -qq update
|
||||
sudo apt-get install -yq groff zlib1g-dev
|
||||
wget 'http://mandoc.bsd.lv/snapshots/mandoc-1.14.5.tar.gz'
|
||||
tar xf mandoc-1.14.5.tar.gz
|
||||
cd mandoc-1.14.5
|
||||
wget 'http://mandoc.bsd.lv/snapshots/mandoc-1.14.6.tar.gz'
|
||||
tar xf mandoc-1.14.6.tar.gz
|
||||
cd mandoc-1.14.6
|
||||
./configure
|
||||
make
|
||||
sudo make install
|
||||
- name: Update pages
|
||||
working-directory: rgbds
|
||||
working-directory: rgbds/man
|
||||
run: | # The ref appears to be in the format "refs/tags/<version>", so strip that
|
||||
./.github/actions/get-pages.sh -r ../rgbds-www ${GITHUB_REF##*/}
|
||||
../../rgbds-www/.github/actions/get-pages.sh ${GITHUB_REF##*/} *
|
||||
- name: Push new pages
|
||||
working-directory: rgbds-www
|
||||
run: |
|
||||
|
||||
70
.github/workflows/testing.yml
vendored
70
.github/workflows/testing.yml
vendored
@@ -16,6 +16,11 @@ jobs:
|
||||
cc: gcc
|
||||
- os: macos-11.0
|
||||
cc: gcc
|
||||
include:
|
||||
- cc: gcc
|
||||
cxx: g++
|
||||
- cc: clang
|
||||
cxx: clang++
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -28,25 +33,25 @@ jobs:
|
||||
# Apple's base version is severely outdated, not even supporting -Wall,
|
||||
# but it overrides Homebrew's version nonetheless...
|
||||
- name: Build & install using Make
|
||||
run: |
|
||||
export PATH="/usr/local/opt/bison/bin:$PATH"
|
||||
make develop -j Q= CC=${{ matrix.cc }}
|
||||
sudo make install -j Q=
|
||||
if: matrix.buildsys == 'make'
|
||||
- name: Build & install using CMake
|
||||
run: |
|
||||
export PATH="/usr/local/opt/bison/bin:$PATH"
|
||||
cmake -S . -B build -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=${{ matrix.cc }} -DSANITIZERS=ON -DMORE_WARNINGS=ON
|
||||
cmake --build build -j
|
||||
cp build/src/rgb{asm,link,fix,gfx} .
|
||||
sudo cmake --install build
|
||||
make develop -j Q= CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
|
||||
sudo make install -j Q=
|
||||
- name: Build & install using CMake
|
||||
if: matrix.buildsys == 'cmake'
|
||||
run: |
|
||||
export PATH="/usr/local/opt/bison/bin:$PATH"
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=${{ matrix.cc }} -DCMAKE_CXX_COMPILER=${{ matrix.cxx }} -DSANITIZERS=ON -DMORE_WARNINGS=ON
|
||||
cmake --build build -j --verbose
|
||||
cp build/src/rgb{asm,link,fix,gfx} .
|
||||
sudo cmake --install build --verbose
|
||||
- name: Package binaries
|
||||
run: |
|
||||
mkdir bins
|
||||
cp rgb{asm,link,fix,gfx} bins
|
||||
- name: Upload binaries
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: rgbds-canary-${{ matrix.os }}-${{ matrix.cc }}-${{ matrix.buildsys }}
|
||||
path: bins
|
||||
@@ -71,7 +76,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Get zlib, libpng and bison
|
||||
run: | # TODO: use an array
|
||||
run: | # TODO: use an array; remember to update the versions being downloaded, *and* the paths being extracted! (`Move-Item`)
|
||||
$wc = New-Object System.Net.WebClient
|
||||
$wc.DownloadFile('https://www.zlib.net/zlib1212.zip', 'zlib.zip')
|
||||
$hash = (Get-FileHash "zlib.zip" -Algorithm SHA256).Hash
|
||||
@@ -95,28 +100,41 @@ jobs:
|
||||
Expand-Archive -DestinationPath install_dir "winflexbison.zip"
|
||||
Move-Item zlib-1.2.12 zlib
|
||||
Move-Item lpng1637 libpng
|
||||
- uses: actions/cache@v3
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
zbuild
|
||||
pngbuild
|
||||
key: ${{ matrix.arch }}-${{ hashFiles('zlib/**', 'libpng/**') }}
|
||||
- name: Build zlib
|
||||
run: | # BUILD_SHARED_LIBS causes the output DLL to be correctly called `zlib1.dll`
|
||||
cmake -S zlib -B zbuild -A ${{ matrix.platform }} -DCMAKE_INSTALL_PREFIX=install_dir -DBUILD_SHARED_LIBS=ON
|
||||
cmake --build zbuild --config Release -j
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
- name: Install zlib
|
||||
run: |
|
||||
cmake --install zbuild
|
||||
- name: Build libpng
|
||||
run: |
|
||||
cmake -S libpng -B pngbuild -A ${{ matrix.platform }} -DCMAKE_INSTALL_PREFIX=install_dir -DPNG_SHARED=ON -DPNG_STATIC=ON -DPNG_TESTS=OFF
|
||||
cmake --build pngbuild --config Release -j
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
- name: Install libpng
|
||||
run: |
|
||||
cmake --install pngbuild
|
||||
- name: Build Windows binaries
|
||||
run: |
|
||||
cmake -S . -B build -A ${{ matrix.platform }} -DCMAKE_INSTALL_PREFIX=install_dir -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build --config Release -j
|
||||
cmake --install build
|
||||
cmake --build build --config Release -j --verbose
|
||||
cmake --install build --verbose --prefix install_dir
|
||||
- name: Package binaries
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir bins
|
||||
cp install_dir/bin/{rgbasm.exe,rgblink.exe,rgbfix.exe,rgbgfx.exe,zlib1.dll,libpng16.dll} bins
|
||||
- name: Upload Windows binaries
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: rgbds-canary-win${{ matrix.bits }}
|
||||
path: bins
|
||||
@@ -124,6 +142,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
cp bins/* .
|
||||
cp bins/*.dll test/gfx
|
||||
test/run-tests.sh
|
||||
|
||||
windows-xbuild:
|
||||
@@ -150,7 +169,7 @@ jobs:
|
||||
./.github/actions/install_deps.sh ${{ matrix.os }}
|
||||
- name: Install MinGW
|
||||
run: |
|
||||
sudo apt-get install gcc-mingw-w64-${{ matrix.arch }} mingw-w64-tools libz-mingw-w64-dev
|
||||
sudo apt-get install {gcc,g++}-mingw-w64-${{ matrix.arch }} mingw-w64-tools libz-mingw-w64-dev
|
||||
- name: Install libpng dev headers for MinGW
|
||||
run: |
|
||||
sudo ./.github/actions/mingw-w64-libpng-dev.sh ${{ matrix.triplet }}
|
||||
@@ -166,12 +185,21 @@ jobs:
|
||||
mv rgbgfx bins/rgbgfx.exe
|
||||
cp /usr/${{ matrix.triplet }}/lib/zlib1.dll bins
|
||||
cp /usr/${{ matrix.triplet }}/bin/libpng16-16.dll bins
|
||||
if [ ${{ matrix.bits }} -eq 32 ]; then cp /usr/lib/gcc/${{ matrix.triplet }}/7.3-win32/libgcc_s_sjlj-1.dll bins; fi
|
||||
if [ ${{ matrix.bits }} -eq 32 ]; then cp /usr/lib/gcc/${{ matrix.triplet }}/7.3-win32/lib{gcc_s_sjlj-1,stdc++-6}.dll bins; fi
|
||||
mv test/gfx/randtilegen{,.exe}
|
||||
mv test/gfx/rgbgfx_test{,.exe}
|
||||
- name: Upload Windows binaries
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: rgbds-canary-mingw-win${{ matrix.bits }}
|
||||
path: bins
|
||||
- name: Upload Windows test binaries
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: testing-programs-mingw-win${{ matrix.bits }}
|
||||
path: |
|
||||
test/gfx/randtilegen.exe
|
||||
test/gfx/rgbgfx_test.exe
|
||||
|
||||
windows-xtesting:
|
||||
needs: windows-xbuild
|
||||
@@ -183,14 +211,20 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Retrieve binaries
|
||||
uses: actions/download-artifact@v1
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: rgbds-canary-mingw-win${{ matrix.bits }}
|
||||
path: bins
|
||||
- name: Retrieve test binaries
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: testing-programs-mingw-win${{ matrix.bits }}
|
||||
path: test/gfx
|
||||
- name: Extract binaries
|
||||
shell: bash
|
||||
run: |
|
||||
cp bins/* .
|
||||
cp bins/*.dll test/gfx
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
31
.github/workflows/update-master-docs.yml
vendored
31
.github/workflows/update-master-docs.yml
vendored
@@ -4,16 +4,15 @@ on:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- .github/actions/get-pages.sh
|
||||
- src/gbz80.7
|
||||
- src/rgbds.5
|
||||
- src/rgbds.7
|
||||
- src/asm/rgbasm.1
|
||||
- src/asm/rgbasm.5
|
||||
- src/link/rgblink.1
|
||||
- src/link/rgblink.5
|
||||
- src/fix/rgbfix.1
|
||||
- src/gfx/rgbgfx.1
|
||||
- man/gbz80.7
|
||||
- man/rgbds.5
|
||||
- man/rgbds.7
|
||||
- man/rgbasm.1
|
||||
- man/rgbasm.5
|
||||
- man/rgblink.1
|
||||
- man/rgblink.5
|
||||
- man/rgbfix.1
|
||||
- man/rgbgfx.1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -36,16 +35,16 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get -qq update
|
||||
sudo apt-get install -yq groff zlib1g-dev
|
||||
wget 'http://mandoc.bsd.lv/snapshots/mandoc-1.14.5.tar.gz'
|
||||
tar xf mandoc-1.14.5.tar.gz
|
||||
cd mandoc-1.14.5
|
||||
wget 'http://mandoc.bsd.lv/snapshots/mandoc-1.14.6.tar.gz'
|
||||
tar xf mandoc-1.14.6.tar.gz
|
||||
cd mandoc-1.14.6
|
||||
./configure
|
||||
make
|
||||
sudo make install
|
||||
- name: Update pages
|
||||
working-directory: rgbds
|
||||
working-directory: rgbds/man
|
||||
run: |
|
||||
./.github/actions/get-pages.sh ../rgbds-www master
|
||||
../../rgbds-www/maintainer/man_to_html.sh master *
|
||||
- name: Push new pages
|
||||
working-directory: rgbds-www
|
||||
run: |
|
||||
@@ -56,7 +55,7 @@ jobs:
|
||||
ssh-add ~/.ssh/id_ed25519
|
||||
git config --global user.name "GitHub Action"
|
||||
git config --global user.email "community@gbdev.io"
|
||||
git add .
|
||||
git add -A
|
||||
git commit -m "Update RGBDS master documentation"
|
||||
if git remote | grep -q origin; then
|
||||
git remote set-url origin git@github.com:gbdev/rgbds-www.git
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
|
||||
|
||||
project(rgbds
|
||||
LANGUAGES C)
|
||||
LANGUAGES C CXX)
|
||||
|
||||
# get real path of source and binary directories
|
||||
get_filename_component(srcdir "${CMAKE_SOURCE_DIR}" REALPATH)
|
||||
@@ -29,8 +29,18 @@ option(MORE_WARNINGS "Turn on more warnings" OFF) # Ignored on MSVC
|
||||
if(MSVC)
|
||||
# MSVC's standard library triggers warning C5105,
|
||||
# "macro expansion producing 'defined' has undefined behavior"
|
||||
add_compile_options(/std:c11 /W1 /MP /wd5105)
|
||||
add_compile_options(/MP /wd5105)
|
||||
add_definitions(/D_CRT_SECURE_NO_WARNINGS)
|
||||
# Also, CMake appears not to pass the C11-enabling flag, so we must add it manually... but only for C!
|
||||
if(NOT CMAKE_C_FLAGS MATCHES "std:c11") # The flag may already have been injected by an earlier CMake invocation, so don't add it twice
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std:c11" CACHE STRING "Flags used by the C compiler during all build types." FORCE)
|
||||
endif()
|
||||
|
||||
if(SANITIZERS)
|
||||
set(SAN_FLAGS /fsanitize=address)
|
||||
add_compile_options(${SAN_FLAGS})
|
||||
add_link_options(${SAN_FLAGS})
|
||||
endif()
|
||||
else()
|
||||
add_compile_options(-Wall -pedantic)
|
||||
add_definitions(-D_POSIX_C_SOURCE=200809L -D_ISOC11_SOURCE)
|
||||
@@ -41,18 +51,18 @@ else()
|
||||
-fsanitize=object-size -fsanitize=bool -fsanitize=enum
|
||||
-fsanitize=alignment -fsanitize=null -fsanitize=address)
|
||||
add_compile_options(${SAN_FLAGS})
|
||||
link_libraries(${SAN_FLAGS})
|
||||
add_link_options(${SAN_FLAGS})
|
||||
# A non-zero optimization level is desired in debug mode, but allow overriding it nonetheless
|
||||
# TODO: this overrides anything previously set... that's a bit sloppy!
|
||||
set(CMAKE_C_FLAGS_DEBUG "-g -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE STRING "" FORCE)
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "-g -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE STRING "" FORCE)
|
||||
endif()
|
||||
|
||||
if(MORE_WARNINGS)
|
||||
add_compile_options(-Werror -Wextra
|
||||
-Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond
|
||||
-Wfloat-equal -Winline -Wlogical-op -Wnested-externs -Wnull-dereference
|
||||
-Wold-style-definition -Wshift-overflow=2 -Wstrict-overflow=5
|
||||
-Wstrict-prototypes -Wstringop-overflow=4 -Wundef -Wuninitialized -Wunused
|
||||
-Wfloat-equal -Wlogical-op -Wnull-dereference -Wshift-overflow=2
|
||||
-Wstringop-overflow=4 -Wstrict-overflow=5 -Wundef -Wuninitialized -Wunused
|
||||
-Wshadow # TODO: -Wshadow=compatible-local ?
|
||||
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1
|
||||
-Wno-format-nonliteral # We have a couple of "dynamic" prints
|
||||
@@ -77,12 +87,23 @@ else(GIT)
|
||||
message(STATUS "Cannot determine RGBDS version (Git not installed), falling back")
|
||||
endif(GIT)
|
||||
|
||||
find_package(PkgConfig)
|
||||
if(MSVC OR NOT PKG_CONFIG_FOUND)
|
||||
# fallback to find_package
|
||||
find_package(PNG REQUIRED)
|
||||
else()
|
||||
pkg_check_modules(LIBPNG REQUIRED libpng)
|
||||
endif()
|
||||
|
||||
include_directories("${PROJECT_SOURCE_DIR}/include")
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED True)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
add_subdirectory(src)
|
||||
add_subdirectory(test)
|
||||
|
||||
# By default, build in Release mode; Debug mode must be explicitly requested
|
||||
# (You may want to augment it with the options above)
|
||||
@@ -97,3 +118,19 @@ if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
message(CHECK_FAIL "no")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(MANDIR "share/man")
|
||||
set(man1 "man/rgbasm.1"
|
||||
"man/rgbfix.1"
|
||||
"man/rgbgfx.1"
|
||||
"man/rgblink.1")
|
||||
set(man5 "man/rgbasm.5"
|
||||
"man/rgblink.5"
|
||||
"man/rgbds.5")
|
||||
set(man7 "man/gbz80.7"
|
||||
"man/rgbds.7")
|
||||
|
||||
foreach(SECTION "man1" "man5" "man7")
|
||||
set(DEST "${MANDIR}/${SECTION}")
|
||||
install(FILES ${${SECTION}} DESTINATION ${DEST})
|
||||
endforeach()
|
||||
|
||||
65
Makefile
65
Makefile
@@ -7,7 +7,7 @@
|
||||
#
|
||||
|
||||
.SUFFIXES:
|
||||
.SUFFIXES: .h .y .c .o
|
||||
.SUFFIXES: .h .y .c .cpp .o
|
||||
|
||||
# User-defined variables
|
||||
|
||||
@@ -34,10 +34,13 @@ WARNFLAGS := -Wall -pedantic
|
||||
|
||||
# Overridable CFLAGS
|
||||
CFLAGS ?= -O3 -flto -DNDEBUG
|
||||
CXXFLAGS ?= -O3 -flto -DNDEBUG
|
||||
# Non-overridable CFLAGS
|
||||
# _ISOC11_SOURCE is required on certain platforms to get C11 on top of the C99-based POSIX 2008
|
||||
REALCFLAGS := ${CFLAGS} ${WARNFLAGS} -std=gnu11 -I include \
|
||||
-D_POSIX_C_SOURCE=200809L -D_ISOC11_SOURCE
|
||||
REALCXXFLAGS := ${CXXFLAGS} ${WARNFLAGS} -std=c++17 -I include \
|
||||
-D_POSIX_C_SOURCE=200809L -fno-exceptions -fno-rtti
|
||||
# Overridable LDFLAGS
|
||||
LDFLAGS ?=
|
||||
# Non-overridable LDFLAGS
|
||||
@@ -102,9 +105,14 @@ rgbfix_obj := \
|
||||
src/error.o
|
||||
|
||||
rgbgfx_obj := \
|
||||
src/gfx/gb.o \
|
||||
src/gfx/main.o \
|
||||
src/gfx/makepng.o \
|
||||
src/gfx/pal_packing.o \
|
||||
src/gfx/pal_sorting.o \
|
||||
src/gfx/pal_spec.o \
|
||||
src/gfx/process.o \
|
||||
src/gfx/proto_palette.o \
|
||||
src/gfx/reverse.o \
|
||||
src/gfx/rgba.o \
|
||||
src/extern/getopt.o \
|
||||
src/error.o
|
||||
|
||||
@@ -118,7 +126,13 @@ rgbfix: ${rgbfix_obj}
|
||||
$Q${CC} ${REALLDFLAGS} -o $@ ${rgbfix_obj} ${REALCFLAGS} src/version.c
|
||||
|
||||
rgbgfx: ${rgbgfx_obj}
|
||||
$Q${CC} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ ${rgbgfx_obj} ${REALCFLAGS} src/version.c ${PNGLDLIBS}
|
||||
$Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ ${rgbgfx_obj} ${REALCXXFLAGS} -x c++ src/version.c ${PNGLDLIBS}
|
||||
|
||||
test/gfx/randtilegen: test/gfx/randtilegen.c
|
||||
$Q${CC} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCFLAGS} ${PNGCFLAGS} ${PNGLDLIBS}
|
||||
|
||||
test/gfx/rgbgfx_test: test/gfx/rgbgfx_test.cpp
|
||||
$Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ $^ ${REALCXXFLAGS} ${PNGLDLIBS}
|
||||
|
||||
# Rules to process files
|
||||
|
||||
@@ -145,7 +159,10 @@ src/asm/parser.c: src/asm/parser.y
|
||||
${BISON} $$DEFS -d ${YFLAGS} -o $@ $<
|
||||
|
||||
.c.o:
|
||||
$Q${CC} ${REALCFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
$Q${CC} ${REALCFLAGS} -c -o $@ $<
|
||||
|
||||
.cpp.o:
|
||||
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
|
||||
# Target used to remove all files generated by other Makefile targets
|
||||
|
||||
@@ -157,6 +174,7 @@ clean:
|
||||
$Qfind src/ -name "*.o" -exec rm {} \;
|
||||
$Q${RM} rgbshim.sh
|
||||
$Q${RM} src/asm/parser.c src/asm/parser.h
|
||||
$Q${RM} test/gfx/randtilegen test/gfx/rgbgfx_test
|
||||
|
||||
# Target used to install the binaries and man pages.
|
||||
|
||||
@@ -167,15 +185,15 @@ install: all
|
||||
$Qinstall ${STRIP} -m ${BINMODE} rgblink ${DESTDIR}${bindir}/rgblink
|
||||
$Qinstall ${STRIP} -m ${BINMODE} rgbgfx ${DESTDIR}${bindir}/rgbgfx
|
||||
$Qmkdir -p ${DESTDIR}${mandir}/man1 ${DESTDIR}${mandir}/man5 ${DESTDIR}${mandir}/man7
|
||||
$Qinstall -m ${MANMODE} src/rgbds.7 ${DESTDIR}${mandir}/man7/rgbds.7
|
||||
$Qinstall -m ${MANMODE} src/gbz80.7 ${DESTDIR}${mandir}/man7/gbz80.7
|
||||
$Qinstall -m ${MANMODE} src/rgbds.5 ${DESTDIR}${mandir}/man5/rgbds.5
|
||||
$Qinstall -m ${MANMODE} src/asm/rgbasm.1 ${DESTDIR}${mandir}/man1/rgbasm.1
|
||||
$Qinstall -m ${MANMODE} src/asm/rgbasm.5 ${DESTDIR}${mandir}/man5/rgbasm.5
|
||||
$Qinstall -m ${MANMODE} src/fix/rgbfix.1 ${DESTDIR}${mandir}/man1/rgbfix.1
|
||||
$Qinstall -m ${MANMODE} src/link/rgblink.1 ${DESTDIR}${mandir}/man1/rgblink.1
|
||||
$Qinstall -m ${MANMODE} src/link/rgblink.5 ${DESTDIR}${mandir}/man5/rgblink.5
|
||||
$Qinstall -m ${MANMODE} src/gfx/rgbgfx.1 ${DESTDIR}${mandir}/man1/rgbgfx.1
|
||||
$Qinstall -m ${MANMODE} man/rgbds.7 ${DESTDIR}${mandir}/man7/rgbds.7
|
||||
$Qinstall -m ${MANMODE} man/gbz80.7 ${DESTDIR}${mandir}/man7/gbz80.7
|
||||
$Qinstall -m ${MANMODE} man/rgbds.5 ${DESTDIR}${mandir}/man5/rgbds.5
|
||||
$Qinstall -m ${MANMODE} man/rgbasm.1 ${DESTDIR}${mandir}/man1/rgbasm.1
|
||||
$Qinstall -m ${MANMODE} man/rgbasm.5 ${DESTDIR}${mandir}/man5/rgbasm.5
|
||||
$Qinstall -m ${MANMODE} man/rgbfix.1 ${DESTDIR}${mandir}/man1/rgbfix.1
|
||||
$Qinstall -m ${MANMODE} man/rgblink.1 ${DESTDIR}${mandir}/man1/rgblink.1
|
||||
$Qinstall -m ${MANMODE} man/rgblink.5 ${DESTDIR}${mandir}/man5/rgblink.5
|
||||
$Qinstall -m ${MANMODE} man/rgbgfx.1 ${DESTDIR}${mandir}/man1/rgbgfx.1
|
||||
|
||||
# Target used to check the coding style of the whole codebase.
|
||||
# `extern/` is excluded, as it contains external code that should not be patched
|
||||
@@ -214,11 +232,9 @@ checkdiff:
|
||||
develop:
|
||||
$Qenv ${MAKE} WARNFLAGS="-Werror -Wextra \
|
||||
-Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond \
|
||||
-Wfloat-equal -Winline -Wlogical-op -Wnested-externs -Wold-style-definition \
|
||||
-Wshift-overflow=2 \
|
||||
-Wstrict-overflow=5 -Wstrict-prototypes -Wundef -Wuninitialized -Wunused \
|
||||
-Wfloat-equal -Wlogical-op -Wnull-dereference -Wshift-overflow=2 \
|
||||
-Wstringop-overflow=4 -Wstrict-overflow=5 -Wundef -Wuninitialized -Wunused \
|
||||
-Wshadow \
|
||||
-Wnull-dereference -Wstringop-overflow=4 \
|
||||
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1 \
|
||||
-Wno-format-nonliteral \
|
||||
-Wno-type-limits -Wno-tautological-constant-out-of-range-compare \
|
||||
@@ -229,7 +245,8 @@ develop:
|
||||
-fsanitize=signed-integer-overflow -fsanitize=bounds \
|
||||
-fsanitize=object-size -fsanitize=bool -fsanitize=enum \
|
||||
-fsanitize=alignment -fsanitize=null -fsanitize=address" \
|
||||
CFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls"
|
||||
CFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" \
|
||||
CXXFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls"
|
||||
|
||||
# Targets for the project maintainer to easily create Windows exes.
|
||||
# This is not for Windows users!
|
||||
@@ -237,12 +254,14 @@ develop:
|
||||
# install instructions instead.
|
||||
|
||||
mingw32:
|
||||
$Q${MAKE} CC=i686-w64-mingw32-gcc BISON=bison \
|
||||
PKG_CONFIG=i686-w64-mingw32-pkg-config -j
|
||||
$Q${MAKE} all test/gfx/randtilegen test/gfx/rgbgfx_test \
|
||||
CC=i686-w64-mingw32-gcc CXX=i686-w64-mingw32-g++ \
|
||||
BISON=bison PKG_CONFIG=i686-w64-mingw32-pkg-config -j
|
||||
|
||||
mingw64:
|
||||
$Q${MAKE} CC=x86_64-w64-mingw32-gcc BISON=bison \
|
||||
PKG_CONFIG=x86_64-w64-mingw32-pkg-config -j
|
||||
$Q${MAKE} all test/gfx/randtilegen test/gfx/rgbgfx_test \
|
||||
CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ \
|
||||
BISON=bison PKG_CONFIG=x86_64-w64-mingw32-pkg-config -j
|
||||
|
||||
wine-shim:
|
||||
$Qecho '#!/bin/bash' > rgbshim.sh
|
||||
|
||||
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
|
||||
other UNIX tools.
|
||||
|
||||
This toolchain is maintained on `GitHub <https://github.com/rednex/rgbds>`__.
|
||||
This toolchain is maintained `on GitHub <https://github.com/gbdev/rgbds>`__.
|
||||
|
||||
The documentation of this toolchain can be viewed online
|
||||
`here <https://rgbds.gbdev.io/docs/>`__, it is generated from the man pages
|
||||
found in this repository.
|
||||
The documentation of this toolchain can be viewed online `here <https://rgbds.gbdev.io/docs/>`__, it is generated from the man pages found in this repository.
|
||||
The source code of the website itself is on GitHub as well under the repo `rgbds-www <https://github.com/gbdev/rgbds-www>`__.
|
||||
|
||||
If you want to contribute or maintain RGBDS, and have questions regarding the code, its organisation, etc. you can find me `on GBDev <https://gbdev.io/chat>`__ or via mail at ``rgbds at eldred dot fr``.
|
||||
|
||||
@@ -24,7 +23,7 @@ If you want to contribute or maintain RGBDS, and have questions regarding the co
|
||||
-------------------
|
||||
|
||||
The `installation procedure <https://rgbds.gbdev.io/install>`__ is available
|
||||
online for various platforms. `Building from source <https://rgbds.gbdev.io/install/source>`__
|
||||
online for various platforms. `Building from source <https://rgbds.gbdev.io/install/#building-from-source>`__
|
||||
is possible using ``make`` or ``cmake``; follow the link for more detailed instructions.
|
||||
|
||||
.. code:: sh
|
||||
@@ -57,6 +56,8 @@ The RGBDS source code file structure somewhat resembles the following:
|
||||
│ └── ...
|
||||
├── include/
|
||||
│ └── ...
|
||||
├── man/
|
||||
│ └── ...
|
||||
├── src/
|
||||
│ ├── asm/
|
||||
│ │ └── ...
|
||||
@@ -96,7 +97,9 @@ The RGBDS source code file structure somewhat resembles the following:
|
||||
|
||||
- ``include/`` - header files for each respective C files in `src`.
|
||||
|
||||
- ``src/`` - source code and manual pages for RGBDS.
|
||||
- ``man/`` - manual pages.
|
||||
|
||||
- ``src/`` - source code of RGBDS.
|
||||
|
||||
* Note that the code unique to each RGBDS tool is stored in its respective subdirectory
|
||||
(rgbasm -> ``src/asm/``, for example). ``src/extern/`` contains code imported from external sources.
|
||||
@@ -126,3 +129,11 @@ The RGBDS source code file structure somewhat resembles the following:
|
||||
- 2018, codebase relicensed under the MIT license.
|
||||
|
||||
- 2020, repository is moved to the `gbdev <https://github.com/gbdev>`__ organisation. The `rgbds.gbdev.io <https://rgbds.gbdev.io>`__ website serving documentation and downloads is created.
|
||||
|
||||
4. Acknowledgements
|
||||
-------------------
|
||||
|
||||
RGBGFX generates palettes using algorithms found in the paper
|
||||
`"Algorithms for the Pagination Problem, a Bin Packing with Overlapping Items" <http://arxiv.org/abs/1605.00558>`__
|
||||
(`GitHub <https://github.com/pagination-problem/pagination>`__, MIT license),
|
||||
by Aristide Grange, Imed Kacem, and Sébastien Martin.
|
||||
|
||||
@@ -11,19 +11,24 @@ _rgbgfx_completions() {
|
||||
declare -A opts=(
|
||||
[V]="version:normal"
|
||||
[C]="color-curve:normal"
|
||||
[D]="debug:normal"
|
||||
[h]="horizontal:normal"
|
||||
[m]="mirror-tiles:normal"
|
||||
[u]="unique-tiles:normal"
|
||||
[v]="verbose:normal"
|
||||
[f]="fix:normal"
|
||||
[F]="fix-and-save:normal"
|
||||
[Z]="columns:normal"
|
||||
[a]="attr-map:*.attrmap"
|
||||
[A]="output-attr-map:normal"
|
||||
[b]="base-tiles:unk"
|
||||
[d]="depth:unk"
|
||||
[L]="slice:unk"
|
||||
[N]="nb-tiles:unk"
|
||||
[n]="nb-palettes:unk"
|
||||
[o]="output:glob *.2bpp"
|
||||
[p]="palette:glob *.pal"
|
||||
[P]="output-palette:normal"
|
||||
[q]="palette-map:glob *.palmap"
|
||||
[Q]="output-palette-map:normal"
|
||||
[r]="reverse:unk"
|
||||
[s]="palette-size:unk"
|
||||
[t]="tilemap:glob *.tilemap"
|
||||
[T]="output-tilemap:normal"
|
||||
[x]="trim-end:unk"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
declare -A FILES
|
||||
while read -r -d '' file; do
|
||||
FILES["$file"]="true"
|
||||
done < <(git diff --name-only -z $1 HEAD)
|
||||
done < <(git diff --name-only -z "$1" HEAD)
|
||||
|
||||
edited () {
|
||||
${FILES["$1"]:-"false"}
|
||||
@@ -40,13 +40,13 @@ dependency () {
|
||||
# Pull requests that edit the first file without the second may be correct,
|
||||
# but are suspicious enough to require review.
|
||||
|
||||
dependency include/linkdefs.h src/rgbds.5 \
|
||||
dependency include/linkdefs.h man/rgbds.5 \
|
||||
"Was the object file format changed?"
|
||||
|
||||
dependency src/asm/parser.y src/asm/rgbasm.5 \
|
||||
dependency src/asm/parser.y man/rgbasm.5 \
|
||||
"Was the rgbasm grammar changed?"
|
||||
|
||||
dependency include/asm/warning.h src/asm/rgbasm.1 \
|
||||
dependency include/asm/warning.h man/rgbasm.1 \
|
||||
"Were the rgbasm warnings changed?"
|
||||
|
||||
dependency src/asm/object.c include/linkdefs.h \
|
||||
@@ -59,27 +59,27 @@ dependency Makefile CMakeLists.txt \
|
||||
dependency Makefile src/CMakeLists.txt \
|
||||
"Did the build process change?"
|
||||
|
||||
dependency src/asm/main.c src/asm/rgbasm.1 \
|
||||
dependency src/asm/main.c man/rgbasm.1 \
|
||||
"Did the rgbasm CLI change?"
|
||||
dependency src/asm/main.c contrib/zsh_compl/_rgbasm \
|
||||
"Did the rgbasm CLI change?"
|
||||
dependency src/asm/main.c contrib/bash_compl/_rgbasm.bash \
|
||||
"Did the rgbasm CLI change?"
|
||||
dependency src/link/main.c src/link/rgblink.1 \
|
||||
dependency src/link/main.c man/rgblink.1 \
|
||||
"Did the rgblink CLI change?"
|
||||
dependency src/link/main.c contrib/zsh_compl/_rgblink \
|
||||
"Did the rgblink CLI change?"
|
||||
dependency src/link/main.c contrib/bash_compl/_rgblink.bash \
|
||||
"Did the rgblink CLI change?"
|
||||
dependency src/fix/main.c src/fix/rgbfix.1 \
|
||||
dependency src/fix/main.c man/rgbfix.1 \
|
||||
"Did the rgbfix CLI change?"
|
||||
dependency src/fix/main.c contrib/zsh_compl/_rgbfix \
|
||||
"Did the rgbfix CLI change?"
|
||||
dependency src/fix/main.c contrib/bash_compl/_rgbfix.bash \
|
||||
"Did the rgbfix CLI change?"
|
||||
dependency src/gfx/main.c src/gfx/rgbgfx.1 \
|
||||
dependency src/gfx/main.cpp man/rgbgfx.1 \
|
||||
"Did the rgbgfx CLI change?"
|
||||
dependency src/gfx/main.c contrib/zsh_compl/_rgbgfx \
|
||||
dependency src/gfx/main.cpp contrib/zsh_compl/_rgbgfx \
|
||||
"Did the rgbgfx CLI change?"
|
||||
dependency src/gfx/main.c contrib/bash_compl/_rgbgfx.bash \
|
||||
dependency src/gfx/main.cpp contrib/bash_compl/_rgbgfx.bash \
|
||||
"Did the rgbgfx CLI change?"
|
||||
|
||||
@@ -23,51 +23,51 @@
|
||||
# SOFTWARE.
|
||||
|
||||
STATE=0
|
||||
diff <(xxd $1) <(xxd $2) | while read -r LINE; do
|
||||
if [ $STATE -eq 0 ]; then
|
||||
diff <(xxd "$1") <(xxd "$2") | while read -r LINE; do
|
||||
if [[ $STATE -eq 0 ]]; then
|
||||
# Discard first line (line info)
|
||||
STATE=1
|
||||
elif [ "$LINE" = '---' ]; then
|
||||
elif [[ "$LINE" = '---' ]]; then
|
||||
# Separator between files switches states
|
||||
echo $LINE
|
||||
echo "$LINE"
|
||||
STATE=3
|
||||
elif grep -Eq '^[0-9]+(,[0-9]+)?[cd][0-9]+(,[0-9]+)?' <<< "$LINE"; then
|
||||
# Line info resets the whole thing
|
||||
STATE=1
|
||||
elif [ $STATE -eq 1 -o $STATE -eq 3 ]; then
|
||||
elif [[ $STATE -eq 1 || $STATE -eq 3 ]]; then
|
||||
# Compute the GB address from the ROM offset
|
||||
OFS=$(cut -d ' ' -f 2 <<< "$LINE" | tr -d ':')
|
||||
BANK=$((0x$OFS / 0x4000))
|
||||
ADDR=$((0x$OFS % 0x4000 + ($BANK != 0) * 0x4000))
|
||||
ADDR=$((0x$OFS % 0x4000 + (BANK != 0) * 0x4000))
|
||||
# Try finding the preceding symbol closest to the diff
|
||||
if [ $STATE -eq 1 ]; then
|
||||
if [[ $STATE -eq 1 ]]; then
|
||||
STATE=2
|
||||
SYMFILE=${1%.*}.sym
|
||||
else
|
||||
STATE=4
|
||||
SYMFILE=${2%.*}.sym
|
||||
fi
|
||||
EXTRA=$(if [ -f "$SYMFILE" ]; then
|
||||
EXTRA=$(if [[ -f "$SYMFILE" ]]; then
|
||||
# Read the sym file for such a symbol
|
||||
# Ignore comment lines, only pick matching bank
|
||||
# (The bank regex ignores comments already, make `cut` and `tr` process less lines)
|
||||
grep -Ei $(printf "^%02x:" $BANK) "$SYMFILE" |
|
||||
grep -Ei "$(printf "^%02x:" $BANK)" "$SYMFILE" |
|
||||
cut -d ';' -f 1 |
|
||||
tr -d "\r" |
|
||||
while read -r SYMADDR SYM; do
|
||||
SYMADDR=$((0x${SYMADDR#*:}))
|
||||
if [ $SYMADDR -le $ADDR ]; then
|
||||
printf " (%s+%#x)\n" "$SYM" $(($ADDR - $SYMADDR))
|
||||
if [[ $SYMADDR -le $ADDR ]]; then
|
||||
printf " (%s+%#x)\n" "$SYM" $((ADDR - SYMADDR))
|
||||
fi
|
||||
# TODO: assumes sorted sym files
|
||||
done | tail -n 1
|
||||
fi)
|
||||
printf "%02x:%04x %s\n" $BANK $ADDR $EXTRA
|
||||
printf "%02x:%04x %s\n" $BANK $ADDR "$EXTRA"
|
||||
fi
|
||||
if [ $STATE -eq 2 -o $STATE -eq 4 ]; then
|
||||
if [[ $STATE -eq 2 || $STATE -eq 4 ]]; then
|
||||
OFS=$(cut -d ' ' -f 2 <<< "$LINE" | tr -d ':')
|
||||
BANK=$((0x$OFS / 0x4000))
|
||||
ADDR=$((0x$OFS % 0x4000 + ($BANK != 0) * 0x4000))
|
||||
ADDR=$((0x$OFS % 0x4000 + (BANK != 0) * 0x4000))
|
||||
printf "%s %02x:%04x: %s\n" "${LINE:0:1}" $BANK $ADDR "${LINE#*: }"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -15,20 +15,25 @@ local args=(
|
||||
|
||||
'(-a --attr-map -A --output-attr-map)'{-A,--output-attr-map}'[Shortcut for -a <file>.attrmap]'
|
||||
'(-C --color-curve)'{-C,--color-curve}'[Generate palettes using GBC color curve]'
|
||||
'(-D --debug)'{-D,--debug}'[Enable debug features]'
|
||||
'(-f --fix -F --fix-and-save)'{-f,--fix}'[Fix input PNG into an indexed image]'
|
||||
'(-f --fix -F --fix-and-save)'{-F,--fix-and-save}'[Like -f but also save CLI params within the PNG]'
|
||||
'(-h --horizontal)'{-h,--horizontal}'[Lay out tiles horizontally instead of vertically]'
|
||||
'(-m --mirror-tiles)'{-m,--mirror-tiles}'[Eliminate mirrored tiles from output]'
|
||||
'(-p --palette -P --output-palette)'{-P,--output-palette}'[Shortcut for -p <file>.pal]'
|
||||
'(-q --palette-map -Q --output-palette-map)'{-Q,--output-palette-map}'[Shortcut for -p <file>.palmap]'
|
||||
'(-t --tilemap -T --output-tilemap)'{-T,--output-tilemap}'[Shortcut for -t <file>.tilemap]'
|
||||
'(-u --unique-tiles)'{-u,--unique-tiles}'[Eliminate redundant tiles]'
|
||||
'(-v --verbose)'{-v,--verbose}'[Enable verbose output]'
|
||||
{-v,--verbose}'[Enable verbose output]'
|
||||
'(-h --horizontal -Z --columns)'{-Z,--columns}'[Read the image in column-major order]'
|
||||
|
||||
'(-a --attr-map -A --output-attr-map)'{-a,--attr-map}'+[Generate a map of tile attributes (mirroring)]:attrmap file:_files'
|
||||
'(-b --base-tiles)'{-b,--base-tiles}'+[Base tile IDs for tile map output]:base tile IDs:'
|
||||
'(-d --depth)'{-d,--depth}'+[Set bit depth]:bit depth:_depths'
|
||||
'(-L --slice)'{-L,--slice}'+[Only process a portion of the image]:input slice:'
|
||||
'(-N --nb-tiles)'{-n,--nb-tiles}'+[Limit number of tiles]:tile count:'
|
||||
'(-n --nb-palettes)'{-n,--nb-palettes}'+[Limit number of palettes]:palette count:'
|
||||
'(-o --output)'{-o,--output}'+[Set output file]:output file:_files'
|
||||
'(-p --palette -P --output-palette)'{-p,--palette}"+[Output the image's palette in little-endian native RGB555 format]:palette file:_files"
|
||||
'(-q --palette-map -Q --output-palette-map)'{-p,--palette-map}"+[Output the image's palette map]:palette map file:_files"
|
||||
'(-r --reverse)'{-r,--reverse}'+[Yield an image from binary data]:image width (in tiles):'
|
||||
'(-s --palette-size)'{-s,--palette-size}'+[Limit palette size]:palette size:'
|
||||
'(-t --tilemap -T --output-tilemap)'{-t,--tilemap}'+[Generate a map of tile indices]:tilemap file:_files'
|
||||
'(-x --trim-end)'{-x,--trim-end}'+[Trim end of output by this many tiles]:tile count:'
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
int32_t fix_Callback_PI(void);
|
||||
void fix_Print(int32_t i);
|
||||
int32_t fix_Sin(int32_t i);
|
||||
int32_t fix_Cos(int32_t i);
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
#include "helpers.h"
|
||||
|
||||
extern bool haltnop;
|
||||
extern bool warnOnHaltNop;
|
||||
extern bool optimizeLoads;
|
||||
extern bool warnOnLdOpt;
|
||||
extern bool verbose;
|
||||
extern bool warnings; /* True to enable warnings, false to disable them. */
|
||||
|
||||
|
||||
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 "platform.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void warn(char const NONNULL(fmt), ...) format_(printf, 1, 2);
|
||||
void warnx(char const NONNULL(fmt), ...) format_(printf, 1, 2);
|
||||
|
||||
_Noreturn void err(char const NONNULL(fmt), ...) format_(printf, 1, 2);
|
||||
_Noreturn void errx(char const NONNULL(fmt), ...) format_(printf, 1, 2);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* RGBDS_ERROR_H */
|
||||
|
||||
8
include/extern/getopt.h
vendored
8
include/extern/getopt.h
vendored
@@ -26,6 +26,10 @@
|
||||
#ifndef RGBDS_EXTERN_GETOPT_H
|
||||
#define RGBDS_EXTERN_GETOPT_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern char *musl_optarg;
|
||||
extern int musl_optind, musl_opterr, musl_optopt, musl_optreset;
|
||||
|
||||
@@ -43,4 +47,8 @@ int musl_getopt_long_only(int argc, char **argv, char const *optstring,
|
||||
#define required_argument 1
|
||||
#define optional_argument 2
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -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_(...)
|
||||
// This seems to generate similar code to __builtin_unreachable, despite different semantics
|
||||
// Note that executing this is undefined behavior (declared _Noreturn, but does return)
|
||||
static inline _Noreturn unreachable_(void) {}
|
||||
static inline _Noreturn void unreachable_(void) {}
|
||||
#endif
|
||||
|
||||
// Use builtins whenever possible, and shim them otherwise
|
||||
|
||||
90
include/itertools.hpp
Normal file
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>
|
||||
#endif
|
||||
|
||||
/* MSVC doesn't support `[static N]` for array arguments from C99 */
|
||||
/* MSVC doesn't support `[static N]` for array arguments from C99 or C11 */
|
||||
#ifdef _MSC_VER
|
||||
# define MIN_NB_ELMS(N)
|
||||
# define ARR_QUALS(...)
|
||||
# define NONNULL(ptr) *ptr
|
||||
#else
|
||||
# define MIN_NB_ELMS(N) static (N)
|
||||
# define ARR_QUALS(...) __VA_ARGS__
|
||||
# define NONNULL(ptr) ptr[static 1]
|
||||
#endif
|
||||
|
||||
|
||||
@@ -9,10 +9,19 @@
|
||||
#ifndef EXTERN_VERSION_H
|
||||
#define EXTERN_VERSION_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define PACKAGE_VERSION_MAJOR 0
|
||||
#define PACKAGE_VERSION_MINOR 5
|
||||
#define PACKAGE_VERSION_PATCH 2
|
||||
#define PACKAGE_VERSION_MINOR 6
|
||||
#define PACKAGE_VERSION_PATCH 0
|
||||
#define PACKAGE_VERSION_RC 1
|
||||
|
||||
char const *get_package_version_string(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* EXTERN_VERSION_H */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -55,9 +55,9 @@ The defaults are 01.
|
||||
.It Fl D Ar name Ns Oo = Ns Ar value Oc , Fl Fl define Ar name Ns Oo = Ns Ar value Oc
|
||||
Add a string symbol to the compiled source code.
|
||||
This is equivalent to
|
||||
.Ql Ar name Ic EQUS \(dq Ns Ar value Ns \(dq
|
||||
.Ql Ar name Ic EQUS No \(dq Ns Ar value Ns \(dq
|
||||
in code, or
|
||||
.Ql Ar name Ic EQUS \(dq1\(dq
|
||||
.Ql Ar name Ic EQUS No \(dq1\(dq
|
||||
if
|
||||
.Ar value
|
||||
is not specified.
|
||||
@@ -192,7 +192,7 @@ SurvivalKids.gbc
|
||||
.Sh TPP1
|
||||
TPP1 is a homebrew mapper designed as a functional superset of the common traditional MBCs, allowing larger ROM and RAM sizes combined with other hardware features.
|
||||
Its specification, as well as more resources, can be found online at
|
||||
.Lk https://github.com/TwitchPlaysPokemon/tpp1 .
|
||||
.Lk https://github.com/aaaaaa123456789/tpp1 .
|
||||
.Ss MBC name
|
||||
The MBC name for TPP1 is more complex than standard mappers.
|
||||
It must be followed with the revision number, of the form
|
||||
591
man/rgbgfx.1
Normal file
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"
|
||||
)
|
||||
|
||||
find_package(PkgConfig)
|
||||
if(MSVC OR NOT PKG_CONFIG_FOUND)
|
||||
# fallback to find_package
|
||||
find_package(PNG REQUIRED)
|
||||
else()
|
||||
pkg_check_modules(LIBPNG REQUIRED libpng)
|
||||
endif()
|
||||
|
||||
find_package(BISON REQUIRED)
|
||||
set(BISON_FLAGS "-Wall")
|
||||
# Set sompe optimization flags on versions that support them
|
||||
@@ -70,9 +62,16 @@ set(rgbfix_src
|
||||
)
|
||||
|
||||
set(rgbgfx_src
|
||||
"gfx/gb.c"
|
||||
"gfx/main.c"
|
||||
"gfx/makepng.c"
|
||||
"gfx/main.cpp"
|
||||
"gfx/pal_packing.cpp"
|
||||
"gfx/pal_sorting.cpp"
|
||||
"gfx/pal_spec.cpp"
|
||||
"gfx/process.cpp"
|
||||
"gfx/proto_palette.cpp"
|
||||
"gfx/reverse.cpp"
|
||||
"gfx/rgba.cpp"
|
||||
"extern/getopt.c"
|
||||
"error.c"
|
||||
)
|
||||
|
||||
set(rgblink_src
|
||||
@@ -97,22 +96,6 @@ foreach(PROG "asm" "fix" "gfx" "link")
|
||||
install(TARGETS rgb${PROG} RUNTIME DESTINATION bin)
|
||||
endforeach()
|
||||
|
||||
set(MANDIR "share/man")
|
||||
set(man1 "asm/rgbasm.1"
|
||||
"fix/rgbfix.1"
|
||||
"gfx/rgbgfx.1"
|
||||
"link/rgblink.1")
|
||||
set(man5 "asm/rgbasm.5"
|
||||
"link/rgblink.5"
|
||||
"rgbds.5")
|
||||
set(man7 "gbz80.7"
|
||||
"rgbds.7")
|
||||
|
||||
foreach(SECTION "man1" "man5" "man7")
|
||||
set(DEST "${MANDIR}/${SECTION}")
|
||||
install(FILES ${${SECTION}} DESTINATION ${DEST})
|
||||
endforeach()
|
||||
|
||||
if(LIBPNG_FOUND) # pkg-config
|
||||
target_include_directories(rgbgfx PRIVATE ${LIBPNG_INCLUDE_DIRS})
|
||||
target_link_directories(rgbgfx PRIVATE ${LIBPNG_LIBRARY_DIRS})
|
||||
|
||||
@@ -30,16 +30,6 @@
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Return the _PI symbol value
|
||||
*/
|
||||
int32_t fix_Callback_PI(void)
|
||||
{
|
||||
warning(WARNING_OBSOLETE, "`_PI` is deprecated; use 3.14159\n");
|
||||
|
||||
return double2fix(M_PI);
|
||||
}
|
||||
|
||||
/*
|
||||
* Print a fixed point value
|
||||
*/
|
||||
|
||||
105
src/asm/lexer.c
105
src/asm/lexer.c
@@ -109,7 +109,7 @@ static struct KeywordMapping {
|
||||
{"DEC", T_Z80_DEC},
|
||||
{"DI", T_Z80_DI},
|
||||
{"EI", T_Z80_EI},
|
||||
{"HALT✋", T_Z80_HALT},
|
||||
{"HALT", T_Z80_HALT},
|
||||
{"INC", T_Z80_INC},
|
||||
{"JP", T_Z80_JP},
|
||||
{"JR", T_Z80_JR},
|
||||
@@ -118,9 +118,8 @@ static struct KeywordMapping {
|
||||
{"LDD", T_Z80_LDD},
|
||||
{"LDIO", T_Z80_LDH},
|
||||
{"LDH", T_Z80_LDH},
|
||||
{"NOPE", T_Z80_NOP},
|
||||
{"NOP", T_Z80_NOP},
|
||||
{"OR", T_Z80_OR},
|
||||
{"OWO", T_OWO},
|
||||
{"POP", T_Z80_POP},
|
||||
{"PUSH", T_Z80_PUSH},
|
||||
{"RES", T_Z80_RES},
|
||||
@@ -141,7 +140,7 @@ static struct KeywordMapping {
|
||||
{"SLA", T_Z80_SLA},
|
||||
{"SRA", T_Z80_SRA},
|
||||
{"SRL", T_Z80_SRL},
|
||||
{"STOP!!🛑", T_Z80_STOP},
|
||||
{"STOP", T_Z80_STOP},
|
||||
{"SUB", T_Z80_SUB},
|
||||
{"SWAP", T_Z80_SWAP},
|
||||
{"XOR", T_Z80_XOR},
|
||||
@@ -149,29 +148,24 @@ static struct KeywordMapping {
|
||||
{"NZ", T_CC_NZ},
|
||||
{"Z", T_CC_Z},
|
||||
{"NC", T_CC_NC},
|
||||
{"C", T_CC_C},
|
||||
/* Handled after as T_TOKEN_C */
|
||||
/* { "C", T_CC_C }, */
|
||||
|
||||
{"•̀A•́)𝓕𝓾𝓬𝓴", T_MODE_AF},
|
||||
// {"BC", T_MODE_BC},
|
||||
// {"DE", T_MODE_DE},
|
||||
{"н∠(", T_MODE_HL_START},
|
||||
{"AF", T_MODE_AF},
|
||||
{"BC", T_MODE_BC},
|
||||
{"DE", T_MODE_DE},
|
||||
{"HL", T_MODE_HL},
|
||||
{"SP", T_MODE_SP},
|
||||
{"н∠( ᐛ 」∠)_👁", T_MODE_HL_DEC},
|
||||
{"н∠( ᐛ 」∠)_👎", T_MODE_HL_INC},
|
||||
{"HLD", T_MODE_HL_DEC},
|
||||
{"HLI", T_MODE_HL_INC},
|
||||
|
||||
// HACK: normally this is surrounded by parens, but this is annoying to special-case,
|
||||
// so we use cooperation from the parser.
|
||||
{"•̀A•́", T_TOKEN_A},
|
||||
// {"=B", T_TOKEN_B}, HACK: This begins with a non-identifier character, so we'll cheat
|
||||
{"♥(˘⌣˘", T_TOKEN_C}, // HACK: same for "C" after the space & closing paren
|
||||
// {";D", T_TOKEN_D}, HACK: also needs to be special-cased. God I feel dirty.
|
||||
{"(´ε`", T_TOKEN_E},
|
||||
{"♡", T_TOKEN_E_HEART},
|
||||
{"н", T_TOKEN_H},
|
||||
{"∠(", T_TOKEN_L_ARM},
|
||||
{"ᐛ", T_TOKEN_L_FACE},
|
||||
{"」∠", T_TOKEN_L_BODY},
|
||||
{"_", T_TOKEN_L_LEG},
|
||||
{"A", T_TOKEN_A},
|
||||
{"B", T_TOKEN_B},
|
||||
{"C", T_TOKEN_C},
|
||||
{"D", T_TOKEN_D},
|
||||
{"E", T_TOKEN_E},
|
||||
{"H", T_TOKEN_H},
|
||||
{"L", T_TOKEN_L},
|
||||
|
||||
{"DEF", T_OP_DEF},
|
||||
|
||||
@@ -218,10 +212,6 @@ static struct KeywordMapping {
|
||||
{"INCLUDE", T_POP_INCLUDE},
|
||||
{"PRINT", T_POP_PRINT},
|
||||
{"PRINTLN", T_POP_PRINTLN},
|
||||
{"PRINTT", T_POP_PRINTT},
|
||||
{"PRINTI", T_POP_PRINTI},
|
||||
{"PRINTV", T_POP_PRINTV},
|
||||
{"PRINTF", T_POP_PRINTF},
|
||||
{"EXPORT", T_POP_EXPORT},
|
||||
{"DS", T_POP_DS},
|
||||
{"DB", T_POP_DB},
|
||||
@@ -584,16 +574,16 @@ struct KeywordDictNode {
|
||||
* In turn, this allows greatly simplifying checking an index into this array,
|
||||
* which should help speed up the lexer.
|
||||
*/
|
||||
uint16_t children[256]; // HACK: we "support" UTF-8 as input now
|
||||
uint16_t children[0x60 - ' '];
|
||||
struct KeywordMapping const *keyword;
|
||||
/* Since the keyword structure is invariant, the min number of nodes is known at compile time */
|
||||
} keywordDict[690] = {0}; /* Nice */
|
||||
} keywordDict[365] = {0}; /* Make sure to keep this correct when adding keywords! */
|
||||
|
||||
/* Convert a char into its index into the dict */
|
||||
static uint8_t dictIndex(char c)
|
||||
{
|
||||
/* Translate uppercase to lowercase (roughly) */
|
||||
if (c > 0x60 && c < 0x80)
|
||||
if (c > 0x60)
|
||||
c = c - ('a' - 'A');
|
||||
return c - ' ';
|
||||
}
|
||||
@@ -615,9 +605,8 @@ void lexer_Init(void)
|
||||
|
||||
/* Walk the dictionary, creating intermediate nodes for the keyword */
|
||||
for (char const *ptr = keywords[i].name; *ptr; ptr++) {
|
||||
unsigned char index = (unsigned char)*ptr - ' ';
|
||||
/* We should be able to assume all entries are well-formed */
|
||||
if (keywordDict[nodeID].children[index] == 0) {
|
||||
if (keywordDict[nodeID].children[*ptr - ' '] == 0) {
|
||||
/*
|
||||
* If this gets tripped up, set the size of keywordDict to
|
||||
* something high, compile with `-DPRINT_NODE_COUNT` (see below),
|
||||
@@ -626,10 +615,10 @@ void lexer_Init(void)
|
||||
assert(usedNodes < sizeof(keywordDict) / sizeof(*keywordDict));
|
||||
|
||||
/* There is no node at that location, grab one from the pool */
|
||||
keywordDict[nodeID].children[index] = usedNodes;
|
||||
keywordDict[nodeID].children[*ptr - ' '] = usedNodes;
|
||||
usedNodes++;
|
||||
}
|
||||
nodeID = keywordDict[nodeID].children[index];
|
||||
nodeID = keywordDict[nodeID].children[*ptr - ' '];
|
||||
}
|
||||
|
||||
/* This assumes that no two keywords have the same name */
|
||||
@@ -1296,16 +1285,12 @@ static uint32_t readGfxConstant(void)
|
||||
static bool startsIdentifier(int c)
|
||||
{
|
||||
// Anonymous labels internally start with '!'
|
||||
return (c <= 'Z' && c >= 'A') || (c <= 'z' && c >= 'a') || c == '.' || c == '_' || c >= 0x80 || c == '(';
|
||||
return (c <= 'Z' && c >= 'A') || (c <= 'z' && c >= 'a') || c == '.' || c == '_';
|
||||
}
|
||||
|
||||
static bool continuesIdentifier(int c)
|
||||
{
|
||||
// April Fools HACK: allow UTF-8 :D
|
||||
// This would normally be quite unsafe (hello, RTL control codes?),
|
||||
// but since this is for a joke I'll also make the code a joke
|
||||
// Also, hi if you're reading this!
|
||||
return startsIdentifier(c) || (c <= '9' && c >= '0') || c == '#' || c == '@' || c == '!';
|
||||
return startsIdentifier(c) || (c <= '9' && c >= '0') || c == '#' || c == '@';
|
||||
}
|
||||
|
||||
static int readIdentifier(char firstChar)
|
||||
@@ -1785,10 +1770,6 @@ static int yylex_NORMAL(void)
|
||||
/* Ignore whitespace and comments */
|
||||
|
||||
case ';':
|
||||
if (peek() == 'D') {
|
||||
shiftChar();
|
||||
return T_TOKEN_D;
|
||||
}
|
||||
discardComment();
|
||||
/* fallthrough */
|
||||
case ' ':
|
||||
@@ -1809,6 +1790,8 @@ static int yylex_NORMAL(void)
|
||||
return T_LBRACK;
|
||||
case ']':
|
||||
return T_RBRACK;
|
||||
case '(':
|
||||
return T_LPAREN;
|
||||
case ')':
|
||||
return T_RPAREN;
|
||||
case ',':
|
||||
@@ -1876,14 +1859,9 @@ static int yylex_NORMAL(void)
|
||||
return T_OP_XOR;
|
||||
|
||||
case '=': /* Either assignment or EQ */
|
||||
switch (peek()) {
|
||||
case '=':
|
||||
if (peek() == '=') {
|
||||
shiftChar();
|
||||
return T_OP_LOGICEQU;
|
||||
case 'b':
|
||||
case 'B':
|
||||
shiftChar();
|
||||
return T_TOKEN_B;
|
||||
}
|
||||
return T_POP_EQUAL;
|
||||
|
||||
@@ -2022,12 +2000,6 @@ static int yylex_NORMAL(void)
|
||||
|
||||
/* Handle identifiers... or report garbage characters */
|
||||
|
||||
case '(':
|
||||
if (peek() != (unsigned char)"´"[0]) {
|
||||
return T_LPAREN;
|
||||
}
|
||||
// fallthrough
|
||||
|
||||
default:
|
||||
if (startsIdentifier(c)) {
|
||||
int tokenType = readIdentifier(c);
|
||||
@@ -2080,9 +2052,23 @@ static int yylex_RAW(void)
|
||||
size_t i = 0;
|
||||
int c;
|
||||
|
||||
/* Trim left whitespace (stops at a block comment or line continuation) */
|
||||
while (isWhitespace(peek()))
|
||||
/* Trim left whitespace (stops at a block comment) */
|
||||
for (;;) {
|
||||
c = peek();
|
||||
if (isWhitespace(c)) {
|
||||
shiftChar();
|
||||
} else if (c == '\\') {
|
||||
shiftChar();
|
||||
c = peek();
|
||||
// If not a line continuation, handle as a normal char
|
||||
if (!isWhitespace(c) && c != '\n' && c != '\r')
|
||||
goto backslash;
|
||||
// Line continuations count as "whitespace"
|
||||
readLineContinuation();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
c = peek();
|
||||
@@ -2131,6 +2117,7 @@ static int yylex_RAW(void)
|
||||
shiftChar();
|
||||
c = peek();
|
||||
|
||||
backslash:
|
||||
switch (c) {
|
||||
case ',': /* Escapes only valid inside a macro arg */
|
||||
case '(':
|
||||
|
||||
@@ -59,7 +59,9 @@ bool generatePhonyDeps;
|
||||
char *targetFileName;
|
||||
|
||||
bool haltnop;
|
||||
bool warnOnHaltNop;
|
||||
bool optimizeLoads;
|
||||
bool warnOnLdOpt;
|
||||
bool verbose;
|
||||
bool warnings; /* True to enable warnings, false to disable them. */
|
||||
|
||||
@@ -84,7 +86,7 @@ static char *make_escape(char const *str)
|
||||
}
|
||||
|
||||
/* Short options */
|
||||
static const char *optstring = "b:D:Eg:hi:LM:o:p:r:VvW:w";
|
||||
static const char *optstring = "b:D:Eg:Hhi:LlM:o:p:r:VvW:w";
|
||||
|
||||
/* Variables for the long-only options */
|
||||
static int depType; /* Variants of `-M` */
|
||||
@@ -104,9 +106,11 @@ static struct option const longopts[] = {
|
||||
{ "define", required_argument, NULL, 'D' },
|
||||
{ "export-all", no_argument, NULL, 'E' },
|
||||
{ "gfx-chars", required_argument, NULL, 'g' },
|
||||
{ "nop-after-halt", no_argument, NULL, 'H' },
|
||||
{ "halt-without-nop", no_argument, NULL, 'h' },
|
||||
{ "include", required_argument, NULL, 'i' },
|
||||
{ "preserve-ld", no_argument, NULL, 'L' },
|
||||
{ "auto-ldh", no_argument, NULL, 'l' },
|
||||
{ "dependfile", required_argument, NULL, 'M' },
|
||||
{ "MG", no_argument, &depType, 'G' },
|
||||
{ "MP", no_argument, &depType, 'P' },
|
||||
@@ -142,9 +146,6 @@ static void print_usage(void)
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
#if YYDEBUG
|
||||
yydebug = 1;
|
||||
#endif
|
||||
int ch;
|
||||
char *ep;
|
||||
|
||||
@@ -173,8 +174,10 @@ int main(int argc, char *argv[])
|
||||
opt_B("01");
|
||||
opt_G("0123");
|
||||
opt_P(0);
|
||||
optimizeLoads = true;
|
||||
haltnop = true;
|
||||
warnOnHaltNop = true;
|
||||
optimizeLoads = true;
|
||||
warnOnLdOpt = true;
|
||||
verbose = false;
|
||||
warnings = true;
|
||||
sym_SetExportAll(false);
|
||||
@@ -212,7 +215,14 @@ int main(int argc, char *argv[])
|
||||
errx("Must specify exactly 4 characters for option 'g'");
|
||||
break;
|
||||
|
||||
case 'H':
|
||||
if (!haltnop)
|
||||
errx("`-H` and `-h` don't make sense together");
|
||||
warnOnHaltNop = false;
|
||||
break;
|
||||
case 'h':
|
||||
if (!warnOnHaltNop)
|
||||
errx("`-H` and `-h` don't make sense together");
|
||||
haltnop = false;
|
||||
break;
|
||||
|
||||
@@ -221,8 +231,15 @@ int main(int argc, char *argv[])
|
||||
break;
|
||||
|
||||
case 'L':
|
||||
if (!warnOnLdOpt)
|
||||
errx("`-L` and `-l` don't make sense together");
|
||||
optimizeLoads = false;
|
||||
break;
|
||||
case 'l':
|
||||
if (!optimizeLoads)
|
||||
errx("`-L` and `-l` don't make sense together");
|
||||
warnOnLdOpt = false;
|
||||
break;
|
||||
|
||||
case 'M':
|
||||
if (!strcmp("-", musl_optarg))
|
||||
|
||||
@@ -17,7 +17,9 @@ struct OptStackEntry {
|
||||
char gbgfx[4];
|
||||
int32_t fillByte;
|
||||
bool haltnop;
|
||||
bool warnOnHaltNop;
|
||||
bool optimizeLoads;
|
||||
bool warnOnLdOpt;
|
||||
bool warningsAreErrors;
|
||||
size_t maxRecursionDepth;
|
||||
// Don't be confused: we use the size of the **global variable** `warningStates`!
|
||||
@@ -48,6 +50,11 @@ void opt_R(size_t newDepth)
|
||||
lexer_CheckRecursionDepth();
|
||||
}
|
||||
|
||||
void opt_H(bool warn)
|
||||
{
|
||||
warnOnHaltNop = warn;
|
||||
}
|
||||
|
||||
void opt_h(bool halt)
|
||||
{
|
||||
haltnop = halt;
|
||||
@@ -58,6 +65,11 @@ void opt_L(bool optimize)
|
||||
optimizeLoads = optimize;
|
||||
}
|
||||
|
||||
void opt_l(bool warn)
|
||||
{
|
||||
warnOnLdOpt = warn;
|
||||
}
|
||||
|
||||
void opt_W(char *flag)
|
||||
{
|
||||
processWarningFlag(flag);
|
||||
@@ -118,6 +130,13 @@ void opt_Parse(char *s)
|
||||
break;
|
||||
}
|
||||
|
||||
case 'H':
|
||||
if (s[1] == '\0')
|
||||
opt_H(false);
|
||||
else
|
||||
error("Option 'H' does not take an argument\n");
|
||||
break;
|
||||
|
||||
case 'h':
|
||||
if (s[1] == '\0')
|
||||
opt_h(false);
|
||||
@@ -132,6 +151,13 @@ void opt_Parse(char *s)
|
||||
error("Option 'L' does not take an argument\n");
|
||||
break;
|
||||
|
||||
case 'l':
|
||||
if (s[1] == '\0')
|
||||
opt_l(false);
|
||||
else
|
||||
error("Option 'l' does not take an argument\n");
|
||||
break;
|
||||
|
||||
case 'W':
|
||||
if (strlen(&s[1]) > 0)
|
||||
opt_W(&s[1]);
|
||||
@@ -186,8 +212,10 @@ void opt_Push(void)
|
||||
entry->fillByte = fillByte; // Pulled from section.h
|
||||
|
||||
entry->haltnop = haltnop; // Pulled from main.h
|
||||
entry->warnOnHaltNop = warnOnHaltNop;
|
||||
|
||||
entry->optimizeLoads = optimizeLoads; // Pulled from main.h
|
||||
entry->warnOnLdOpt = warnOnLdOpt;
|
||||
|
||||
// Both of these pulled from warning.h
|
||||
entry->warningsAreErrors = warningsAreErrors;
|
||||
@@ -209,8 +237,10 @@ void opt_Pop(void)
|
||||
opt_B(entry->binary);
|
||||
opt_G(entry->gbgfx);
|
||||
opt_P(entry->fillByte);
|
||||
opt_H(entry->warnOnHaltNop);
|
||||
opt_h(entry->haltnop);
|
||||
opt_L(entry->optimizeLoads);
|
||||
opt_l(entry->warnOnLdOpt);
|
||||
|
||||
// opt_W does not apply a whole warning state; it processes one flag string
|
||||
warningsAreErrors = entry->warningsAreErrors;
|
||||
|
||||
103
src/asm/parser.y
103
src/asm/parser.y
@@ -599,7 +599,6 @@ enum {
|
||||
|
||||
%token T_POP_INCLUDE "INCLUDE"
|
||||
%token T_POP_PRINT "PRINT" T_POP_PRINTLN "PRINTLN"
|
||||
%token T_POP_PRINTF "PRINTF" T_POP_PRINTT "PRINTT" T_POP_PRINTV "PRINTV" T_POP_PRINTI "PRINTI"
|
||||
%token T_POP_IF "IF" T_POP_ELIF "ELIF" T_POP_ELSE "ELSE" T_POP_ENDC "ENDC"
|
||||
%token T_POP_EXPORT "EXPORT"
|
||||
%token T_POP_DB "DB" T_POP_DS "DS" T_POP_DW "DW" T_POP_DL "DL"
|
||||
@@ -646,32 +645,31 @@ enum {
|
||||
%token T_Z80_CALL "call" T_Z80_CCF "ccf" T_Z80_CP "cp" T_Z80_CPL "cpl"
|
||||
%token T_Z80_DAA "daa" T_Z80_DEC "dec" T_Z80_DI "di"
|
||||
%token T_Z80_EI "ei"
|
||||
%token T_Z80_HALT "halt✋"
|
||||
%token T_Z80_HALT "halt"
|
||||
%token T_Z80_INC "inc"
|
||||
%token T_Z80_JP "jp" T_Z80_JR "jr"
|
||||
%token T_Z80_LD "ld"
|
||||
%token T_Z80_LDI "ldi"
|
||||
%token T_Z80_LDD "ldd"
|
||||
%token T_Z80_LDH "ldh"
|
||||
%token T_Z80_NOP "nope"
|
||||
%token T_Z80_NOP "nop"
|
||||
%token T_Z80_OR "or"
|
||||
%token T_OWO "owo"
|
||||
%token T_Z80_POP "pop" T_Z80_PUSH "push"
|
||||
%token T_Z80_RES "res" T_Z80_RET "ret" T_Z80_RETI "reti" T_Z80_RST "rst"
|
||||
%token T_Z80_RL "rl" T_Z80_RLA "rla" T_Z80_RLC "rlc" T_Z80_RLCA "rlca"
|
||||
%token T_Z80_RR "rr" T_Z80_RRA "rra" T_Z80_RRC "rrc" T_Z80_RRCA "rrca"
|
||||
%token T_Z80_SBC "sbc" T_Z80_SCF "scf" T_Z80_STOP "stop!!🛑"
|
||||
%token T_Z80_SBC "sbc" T_Z80_SCF "scf" T_Z80_STOP "stop"
|
||||
%token T_Z80_SLA "sla" T_Z80_SRA "sra" T_Z80_SRL "srl" T_Z80_SUB "sub"
|
||||
%token T_Z80_SWAP "swap"
|
||||
%token T_Z80_XOR "xor"
|
||||
|
||||
%token T_TOKEN_A "( •̀A•́)" T_TOKEN_F "𝓕𝓾𝓬𝓴"
|
||||
%token T_TOKEN_B "=B" T_TOKEN_C "♥(˘⌣˘ C)"
|
||||
%token T_TOKEN_D ";D" T_TOKEN_E "(´ε` )♡" T_TOKEN_E_HEART "(´ε` )♡"
|
||||
%token T_TOKEN_H "н" T_TOKEN_L_ARM "∠( ᐛ 」∠)_" T_TOKEN_L_FACE "∠( ᐛ 」∠)_" T_TOKEN_L_BODY "∠( ᐛ 」∠)_" T_TOKEN_L_LEG "∠( ᐛ 」∠)_"
|
||||
%token T_MODE_AF "af" /* T_MODE_BC "bc" T_MODE_DE "de" */ T_MODE_SP "sp"
|
||||
%token T_MODE_HL_START "н∠( ᐛ 」∠)_" T_MODE_HL_DEC "hld/hl-" T_MODE_HL_INC "hli/hl+"
|
||||
%token T_CC_NZ "nz" T_CC_Z "z" T_CC_NC "nc" T_CC_C "c"
|
||||
%token T_TOKEN_A "a"
|
||||
%token T_TOKEN_B "b" T_TOKEN_C "c"
|
||||
%token T_TOKEN_D "d" T_TOKEN_E "e"
|
||||
%token T_TOKEN_H "h" T_TOKEN_L "l"
|
||||
%token T_MODE_AF "af" T_MODE_BC "bc" T_MODE_DE "de" T_MODE_SP "sp"
|
||||
%token T_MODE_HL "hl" T_MODE_HL_DEC "hld/hl-" T_MODE_HL_INC "hli/hl+"
|
||||
%token T_CC_NZ "nz" T_CC_Z "z" T_CC_NC "nc" // There is no T_CC_C, only T_TOKEN_C
|
||||
|
||||
%type <constValue> reg_r
|
||||
%type <constValue> reg_ss
|
||||
@@ -857,7 +855,7 @@ macroargs : %empty {
|
||||
|
||||
/* These commands start with a T_LABEL. */
|
||||
assignment_directive : equ
|
||||
| set
|
||||
| assignment
|
||||
| rb
|
||||
| rw
|
||||
| rl
|
||||
@@ -867,10 +865,6 @@ assignment_directive : equ
|
||||
directive : endc
|
||||
| print
|
||||
| println
|
||||
| printf
|
||||
| printt
|
||||
| printv
|
||||
| printi
|
||||
| export
|
||||
| db
|
||||
| dw
|
||||
@@ -928,12 +922,8 @@ compoundeq : T_POP_ADDEQ { $$ = RPN_ADD; }
|
||||
equ : T_LABEL T_POP_EQU const { sym_AddEqu($1, $3); }
|
||||
;
|
||||
|
||||
set : T_LABEL T_POP_EQUAL const { sym_AddVar($1, $3); }
|
||||
assignment : T_LABEL T_POP_EQUAL const { sym_AddVar($1, $3); }
|
||||
| T_LABEL compoundeq const { compoundAssignment($1, $2, $3); }
|
||||
| T_LABEL T_POP_SET const {
|
||||
warning(WARNING_OBSOLETE, "`SET` for variables is deprecated; use `=`\n");
|
||||
sym_AddVar($1, $3);
|
||||
}
|
||||
;
|
||||
|
||||
equs : T_LABEL T_POP_EQUS string { sym_AddString($1, $3); }
|
||||
@@ -1175,14 +1165,6 @@ def_set : def_id T_POP_EQUAL const { sym_AddVar($1, $3); }
|
||||
| redef_id T_POP_EQUAL const { sym_AddVar($1, $3); }
|
||||
| def_id compoundeq const { compoundAssignment($1, $2, $3); }
|
||||
| redef_id compoundeq const { compoundAssignment($1, $2, $3); }
|
||||
| def_id T_POP_SET const {
|
||||
warning(WARNING_OBSOLETE, "`SET` for variables is deprecated; use `=`\n");
|
||||
sym_AddVar($1, $3);
|
||||
}
|
||||
| redef_id T_POP_SET const {
|
||||
warning(WARNING_OBSOLETE, "`SET` for variables is deprecated; use `=`\n");
|
||||
sym_AddVar($1, $3);
|
||||
}
|
||||
;
|
||||
|
||||
def_rb : def_id T_POP_RB rs_uconst {
|
||||
@@ -1296,30 +1278,6 @@ print_expr : const_no_str { printf("$%" PRIX32, $1); }
|
||||
| string { printf("%s", $1); }
|
||||
;
|
||||
|
||||
printt : T_POP_PRINTT string {
|
||||
warning(WARNING_OBSOLETE, "`PRINTT` is deprecated; use `PRINT`\n");
|
||||
printf("%s", $2);
|
||||
}
|
||||
;
|
||||
|
||||
printv : T_POP_PRINTV const {
|
||||
warning(WARNING_OBSOLETE, "`PRINTV` is deprecated; use `PRINT`\n");
|
||||
printf("$%" PRIX32, $2);
|
||||
}
|
||||
;
|
||||
|
||||
printi : T_POP_PRINTI const {
|
||||
warning(WARNING_OBSOLETE, "`PRINTI` is deprecated; use `PRINT` with `STRFMT` \"%%d\"\n");
|
||||
printf("%" PRId32, $2);
|
||||
}
|
||||
;
|
||||
|
||||
printf : T_POP_PRINTF const {
|
||||
warning(WARNING_OBSOLETE, "`PRINTF` is deprecated; use `PRINT` with `STRFMT` \"%%f\"\n");
|
||||
fix_Print($2);
|
||||
}
|
||||
;
|
||||
|
||||
const_3bit : const {
|
||||
int32_t value = $1;
|
||||
|
||||
@@ -1770,7 +1728,6 @@ cpu_command : z80_adc
|
||||
| z80_sub
|
||||
| z80_swap
|
||||
| z80_xor
|
||||
| T_OWO { fatalerror("*BONK* go to horny jail\n"); }
|
||||
;
|
||||
|
||||
z80_adc : T_Z80_ADC op_a_n {
|
||||
@@ -1844,9 +1801,14 @@ z80_ei : T_Z80_EI { sect_AbsByte(0xFB); }
|
||||
|
||||
z80_halt : T_Z80_HALT {
|
||||
sect_AbsByte(0x76);
|
||||
if (haltnop)
|
||||
if (haltnop) {
|
||||
if (warnOnHaltNop) {
|
||||
warnOnHaltNop = false;
|
||||
warning(WARNING_OBSOLETE, "`nop` after `halt` will stop being the default; pass `-H` to opt into it\n");
|
||||
}
|
||||
sect_AbsByte(0x00);
|
||||
}
|
||||
}
|
||||
;
|
||||
|
||||
z80_inc : T_Z80_INC reg_r { sect_AbsByte(0x04 | ($2 << 3)); }
|
||||
@@ -1953,6 +1915,10 @@ z80_ld_mem : T_Z80_LD op_mem_ind T_COMMA T_MODE_SP {
|
||||
| T_Z80_LD op_mem_ind T_COMMA T_MODE_A {
|
||||
if (optimizeLoads && rpn_isKnown(&$2)
|
||||
&& $2.val >= 0xFF00) {
|
||||
if (warnOnLdOpt) {
|
||||
warnOnLdOpt = false;
|
||||
warning(WARNING_OBSOLETE, "ld optimization will stop being the default; pass `-l` to opt into it\n");
|
||||
}
|
||||
sect_AbsByte(0xE0);
|
||||
sect_AbsByte($2.val & 0xFF);
|
||||
rpn_Free(&$2);
|
||||
@@ -2001,6 +1967,10 @@ z80_ld_a : T_Z80_LD reg_r T_COMMA c_ind {
|
||||
if ($2 == REG_A) {
|
||||
if (optimizeLoads && rpn_isKnown(&$4)
|
||||
&& $4.val >= 0xFF00) {
|
||||
if (warnOnLdOpt) {
|
||||
warnOnLdOpt = false;
|
||||
warning(WARNING_OBSOLETE, "ld optimization will stop being the default; pass `-l` to opt into it\n");
|
||||
}
|
||||
sect_AbsByte(0xF0);
|
||||
sect_AbsByte($4.val & 0xFF);
|
||||
rpn_Free(&$4);
|
||||
@@ -2179,7 +2149,7 @@ op_a_n : reloc_8bit
|
||||
| T_MODE_A T_COMMA reloc_8bit { $$ = $3; }
|
||||
;
|
||||
|
||||
T_MODE_A : T_LPAREN T_TOKEN_A T_RPAREN
|
||||
T_MODE_A : T_TOKEN_A
|
||||
| T_OP_HIGH T_LPAREN T_MODE_AF T_RPAREN
|
||||
;
|
||||
|
||||
@@ -2187,7 +2157,7 @@ T_MODE_B : T_TOKEN_B
|
||||
| T_OP_HIGH T_LPAREN T_MODE_BC T_RPAREN
|
||||
;
|
||||
|
||||
T_MODE_C : T_TOKEN_C T_CC_C T_RPAREN
|
||||
T_MODE_C : T_TOKEN_C
|
||||
| T_OP_LOW T_LPAREN T_MODE_BC T_RPAREN
|
||||
;
|
||||
|
||||
@@ -2195,7 +2165,7 @@ T_MODE_D : T_TOKEN_D
|
||||
| T_OP_HIGH T_LPAREN T_MODE_DE T_RPAREN
|
||||
;
|
||||
|
||||
T_MODE_E : T_TOKEN_E T_RPAREN T_TOKEN_E_HEART
|
||||
T_MODE_E : T_TOKEN_E
|
||||
| T_OP_LOW T_LPAREN T_MODE_DE T_RPAREN
|
||||
;
|
||||
|
||||
@@ -2203,19 +2173,10 @@ T_MODE_H : T_TOKEN_H
|
||||
| T_OP_HIGH T_LPAREN T_MODE_HL T_RPAREN
|
||||
;
|
||||
|
||||
T_MODE_L : T_TOKEN_L_ARM T_TOKEN_L_FACE T_TOKEN_L_BODY T_RPAREN T_TOKEN_L_LEG
|
||||
T_MODE_L : T_TOKEN_L
|
||||
| T_OP_LOW T_LPAREN T_MODE_HL T_RPAREN
|
||||
;
|
||||
|
||||
T_MODE_BC : T_TOKEN_B T_TOKEN_C T_CC_C T_RPAREN
|
||||
;
|
||||
|
||||
T_MODE_DE : T_TOKEN_D T_TOKEN_E T_RPAREN T_TOKEN_E_HEART
|
||||
;
|
||||
|
||||
T_MODE_HL : T_MODE_HL_START T_TOKEN_L_FACE T_TOKEN_L_BODY T_RPAREN T_TOKEN_L_LEG
|
||||
;
|
||||
|
||||
ccode_expr : ccode
|
||||
| T_OP_LOGICNOT ccode_expr {
|
||||
$$ = $2 ^ 1;
|
||||
@@ -2225,7 +2186,7 @@ ccode_expr : ccode
|
||||
ccode : T_CC_NZ { $$ = CC_NZ; }
|
||||
| T_CC_Z { $$ = CC_Z; }
|
||||
| T_CC_NC { $$ = CC_NC; }
|
||||
| T_CC_C { $$ = CC_C; }
|
||||
| T_TOKEN_C { $$ = CC_C; }
|
||||
;
|
||||
|
||||
reg_r : T_MODE_B { $$ = REG_B; }
|
||||
@@ -2241,7 +2202,7 @@ reg_r : T_MODE_B { $$ = REG_B; }
|
||||
reg_tt : T_MODE_BC { $$ = REG_BC; }
|
||||
| T_MODE_DE { $$ = REG_DE; }
|
||||
| T_MODE_HL { $$ = REG_HL; }
|
||||
| T_LPAREN T_TOKEN_A T_RPAREN T_TOKEN_F { $$ = REG_AF; }
|
||||
| T_MODE_AF { $$ = REG_AF; }
|
||||
;
|
||||
|
||||
reg_ss : T_MODE_BC { $$ = REG_BC; }
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
int size = snprintf(_expr->reason, 128, __VA_ARGS__); \
|
||||
if (size >= 128) { /* If this wasn't enough, try again */ \
|
||||
_expr->reason = realloc(_expr->reason, size + 1); \
|
||||
if (!_expr->reason) \
|
||||
fatalerror("Can't allocate err string: %s\n", strerror(errno)); \
|
||||
sprintf(_expr->reason, __VA_ARGS__); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
@@ -743,8 +743,13 @@ void sym_Init(time_t now)
|
||||
|
||||
sym_AddVar("_RS", 0)->isBuiltin = true;
|
||||
|
||||
#define addNumber(name, val) sym_AddEqu(name, val)->isBuiltin = true
|
||||
#define addString(name, val) sym_AddString(name, val)->isBuiltin = true
|
||||
#define addSym(fn, name, val) do { \
|
||||
struct Symbol *sym = fn(name, val); \
|
||||
assert(sym); \
|
||||
sym->isBuiltin = true; \
|
||||
} while (0)
|
||||
#define addNumber(name, val) addSym(sym_AddEqu, name, val)
|
||||
#define addString(name, val) addSym(sym_AddString, name, val)
|
||||
|
||||
addString("__RGBDS_VERSION__", get_package_version_string());
|
||||
addNumber("__RGBDS_MAJOR__", PACKAGE_VERSION_MAJOR);
|
||||
@@ -788,16 +793,7 @@ void sym_Init(time_t now)
|
||||
|
||||
#undef addNumber
|
||||
#undef addString
|
||||
#undef addSym
|
||||
|
||||
sym_SetCurrentSymbolScope(NULL);
|
||||
anonLabelID = 0;
|
||||
|
||||
/* _PI is deprecated */
|
||||
struct Symbol *_PISymbol = createBuiltinSymbol("_PI");
|
||||
|
||||
_PISymbol->type = SYM_EQU;
|
||||
_PISymbol->src = NULL;
|
||||
_PISymbol->fileLine = 0;
|
||||
_PISymbol->hasCallback = true;
|
||||
_PISymbol->numCallback = fix_Callback_PI;
|
||||
}
|
||||
anonLabelID = 0;}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env bash
|
||||
bison -V | awk -v major="$1" -v minor="$2" '
|
||||
/^bison.*[0-9]+(\.[0-9]+)(\.[0-9]+)?$/ {
|
||||
match($0, /[0-9]+(\.[0-9]+)(\.[0-9]+)?$/);
|
||||
|
||||
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 <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -446,9 +447,27 @@ void assign_AssignSections(void)
|
||||
|
||||
/* Overlaying requires only fully-constrained sections */
|
||||
verbosePrint("Assigning other sections...\n");
|
||||
if (overlayFileName)
|
||||
errx("All sections must be fixed when using an overlay file; %" PRIu64 " %sn't",
|
||||
nbSectionsToAssign, nbSectionsToAssign == 1 ? "is" : "are");
|
||||
if (overlayFileName) {
|
||||
fprintf(stderr, "FATAL: All sections must be fixed when using an overlay file");
|
||||
uint8_t nbSections = 0;
|
||||
for (int8_t constraints = BANK_CONSTRAINED | ALIGN_CONSTRAINED; constraints >= 0; constraints--) {
|
||||
for (sectionPtr = unassignedSections[constraints];
|
||||
sectionPtr;
|
||||
sectionPtr = sectionPtr->next) {
|
||||
fprintf(stderr, "%c \"%s\"",
|
||||
nbSections == 0 ? ';': ',', sectionPtr->section->name);
|
||||
nbSections++;
|
||||
if (nbSections == 10)
|
||||
goto max_out;
|
||||
}
|
||||
}
|
||||
|
||||
max_out:
|
||||
if (nbSectionsToAssign != nbSections)
|
||||
fprintf(stderr, " and %" PRIu64 " more", nbSectionsToAssign - nbSections);
|
||||
fprintf(stderr, " %sn't\n", nbSectionsToAssign == 1 ? "is" : "are");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/* Assign all remaining sections by decreasing constraint order */
|
||||
for (int8_t constraints = BANK_CONSTRAINED | ALIGN_CONSTRAINED;
|
||||
|
||||
@@ -151,8 +151,10 @@ static char *readstr(FILE *file)
|
||||
/* Read char */
|
||||
int byte = getc(file);
|
||||
|
||||
if (byte == EOF)
|
||||
if (byte == EOF) {
|
||||
free(str);
|
||||
return NULL;
|
||||
}
|
||||
str[index] = byte;
|
||||
} while (str[index]);
|
||||
return str;
|
||||
|
||||
@@ -39,7 +39,7 @@ struct SortedSymbol {
|
||||
};
|
||||
|
||||
static struct {
|
||||
uint32_t nbBanks;
|
||||
uint32_t nbBanks; // Size of the array below (which may be NULL if this is 0)
|
||||
struct SortedSections {
|
||||
struct SortedSection *sections;
|
||||
struct SortedSection *zeroLenSections;
|
||||
@@ -243,8 +243,8 @@ static void writeROM(void)
|
||||
coverOverlayBanks(nbOverlayBanks);
|
||||
|
||||
if (outputFile) {
|
||||
if (sections[SECTTYPE_ROM0].nbBanks > 0)
|
||||
writeBank(sections[SECTTYPE_ROM0].banks[0].sections,
|
||||
writeBank(sections[SECTTYPE_ROM0].banks ? sections[SECTTYPE_ROM0].banks[0].sections
|
||||
: NULL,
|
||||
startaddr[SECTTYPE_ROM0], maxsize[SECTTYPE_ROM0]);
|
||||
|
||||
for (uint32_t i = 0 ; i < sections[SECTTYPE_ROMX].nbBanks; i++)
|
||||
|
||||
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
|
||||
@@ -5,7 +5,7 @@
|
||||
\2: <2>
|
||||
|
||||
'mac c,d':
|
||||
\1: < c>
|
||||
\1: <c>
|
||||
\2: <d>
|
||||
|
||||
'mac 1,2 + 2,3':
|
||||
|
||||
@@ -10,6 +10,8 @@ pusho
|
||||
println $8000_0000 / -1
|
||||
popo
|
||||
|
||||
opt H, l
|
||||
|
||||
ds 1
|
||||
ld [$ff88], a
|
||||
halt
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
warning: opt.asm(16): [-Wdiv]
|
||||
warning: opt.asm(18): [-Wdiv]
|
||||
Division of -2147483648 by -1 yields -2147483648
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
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)
|
||||
|
||||
for i in *.asm; do
|
||||
|
||||
@@ -80,22 +80,23 @@ echo "${bold}Checking padding...${resbold}"
|
||||
cp "$src"/padding{,-large,-larger}.bin .
|
||||
touch padding{,-large,-larger}.err
|
||||
progress=0
|
||||
for b in {0..254}; do
|
||||
printf "\r$b..."
|
||||
for (( i=0; i < 10; ++i )); do
|
||||
(( padding = RANDOM % 256 ))
|
||||
echo "$padding..."
|
||||
for suffix in '' -large -larger; do
|
||||
cat <<<'-p $b' >padding$suffix.flags
|
||||
tr '\377' \\$(($b / 64))$((($b / 8) % 8))$(($b % 8)) <"$src/padding$suffix.gb" >padding$suffix.gb # OK because $FF bytes are only used for padding
|
||||
cat <<<"-p $padding" >padding$suffix.flags
|
||||
tr '\377' \\$((padding / 64))$(((padding / 8) % 8))$((padding % 8)) <"$src/padding$suffix.gb" >padding$suffix.gb # OK because $FF bytes are only used for padding
|
||||
runTest padding${suffix} .
|
||||
done
|
||||
done
|
||||
printf "\rDone! \n"
|
||||
echo "Done!"
|
||||
|
||||
# TODO: check MBC names
|
||||
|
||||
# Check that RGBFIX errors out when inputting a non-existent file...
|
||||
$RGBFIX noexist 2>out.err
|
||||
rc=$(($rc || $? != 1))
|
||||
rc=$((rc || $? != 1))
|
||||
tryDiff "$src/noexist.err" out.err noexist.err
|
||||
rc=$(($rc || $?))
|
||||
rc=$((rc || $?))
|
||||
|
||||
exit $rc
|
||||
|
||||
11
test/gfx/.gitignore
vendored
Normal file
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