ref: 8c62e80c18193bf086d7318d745fee8d9ccbb937
parent: 34bc650341ab2ab4f94cc25b506785d1531f59a8
author: ISSOtm <eldredhabert0@gmail.com>
date: Sun Feb 6 18:30:37 EST 2022
Reimplement basic RGBGFX features in C++ Currently missing from the old version: - `-f` ("fixing" the input image to be indexed) - `-m` (the code for detecting mirrored tiles is missing, but all of the "plumbing" is otherwise there) - `-C` - `-d` - `-x` (though I need to check the exact functionality the old one has) - Also the man page is still a draft and needs to be fleshed out More planned features are not implemented yet either: - Explicit palette spec - Better error messages, also error "images" - Better 8x16 support, as well as other "dedup unit" sizes - Support for arbitrary number of palettes & colors per palette - Other output formats (for example, a "full" palette map for "streaming" use cases like gb-open-world) - Quantization? Some things may also be bugged: - Transparency support - Tile offsets (not exposed yet) - Tile counts per bank (not exposed yet) ...and performance remains to be checked. We need to set up some tests, honestly.
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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)
@@ -45,14 +45,14 @@
# 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 -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
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,7 @@
#
.SUFFIXES:
-.SUFFIXES: .h .y .c .o
+.SUFFIXES: .h .y .c .cpp .o
# User-defined variables
@@ -34,10 +34,13 @@
# 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,11 @@
src/error.o
rgbgfx_obj := \
- src/gfx/gb.o \
+ src/gfx/convert.o \
src/gfx/main.o \
- src/gfx/makepng.o \
+ src/gfx/pal_packing.o \
+ src/gfx/pal_sorting.o \
+ src/gfx/proto_palette.o \
src/extern/getopt.o \
src/error.o
@@ -118,7 +123,7 @@
$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} src/version.c ${PNGLDLIBS}
# Rules to process files
@@ -145,8 +150,11 @@
${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
clean:
@@ -214,11 +222,9 @@
develop:
$Qenv ${MAKE} WARNFLAGS="-Werror -Wextra \
-Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond \
- -Wfloat-equal -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 +235,8 @@
-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!
--- a/README.rst
+++ b/README.rst
@@ -129,3 +129,11 @@
- 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.
--- /dev/null
+++ b/include/defaultinitalloc.hpp
@@ -1,0 +1,38 @@
+/**
+ * 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
--- a/include/error.h
+++ b/include/error.h
@@ -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 */
--- a/include/extern/getopt.h
+++ b/include/extern/getopt.h
@@ -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;
@@ -42,5 +46,9 @@
#define no_argument 0
#define required_argument 1
#define optional_argument 2
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
#endif
--- /dev/null
+++ b/include/gfx/convert.hpp
@@ -1,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 */
--- a/include/gfx/main.h
+++ /dev/null
@@ -1,91 +1,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 */
--- /dev/null
+++ b/include/gfx/main.hpp
@@ -1,0 +1,59 @@
+/*
+ * 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 <filesystem>
+#include <stdint.h>
+
+#include "helpers.h"
+
+struct Options {
+ bool beVerbose = false; // -v
+ bool fixInput = false; // -f
+ bool columnMajor = false; // -h; whether to output the tilemap in columns instead of rows
+ bool allowMirroring = false; // -m
+ bool allowDedup = false; // -u
+ bool useColorCurve = false; // -C
+ uint8_t bitDepth = 2; // -d
+ uint64_t trim = 0; // -x
+ uint8_t nbPalettes = 8; // TODO
+ uint8_t nbColorsPerPal = 0; // TODO; 0 means "auto" = 1 << bitDepth;
+ std::array<uint8_t, 2> baseTileIDs{0, 0}; // TODO
+ std::array<uint16_t, 2> maxNbTiles{384, 0}; // TODO
+ std::filesystem::path tilemap{}; // -t, -T
+ std::filesystem::path attrmap{}; // -a, -A
+ std::filesystem::path palettes{}; // -p, -P
+ std::filesystem::path output{}; // -o
+ std::filesystem::path input{}; // positional arg
+
+ format_(printf, 2, 3) void verbosePrint(char const *fmt, ...) const;
+};
+
+extern Options options;
+
+void warning(char const *fmt, ...);
+void error(char const *fmt, ...);
+[[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;
+
+ decltype(colors)::iterator begin();
+ decltype(colors)::iterator end();
+ decltype(colors)::const_iterator begin() const;
+ decltype(colors)::const_iterator end() const;
+};
+
+#endif /* RGBDS_GFX_MAIN_HPP */
--- /dev/null
+++ b/include/gfx/pal_packing.hpp
@@ -1,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"
+
+class 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 */
--- /dev/null
+++ b/include/gfx/pal_sorting.hpp
@@ -1,0 +1,26 @@
+/*
+ * 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 <png.h>
+#include <vector>
+
+class Palette;
+
+namespace sorting {
+
+void indexed(std::vector<Palette> &palettes, int palSize, png_color const *palRGB,
+ png_byte *palAlpha);
+void grayscale(std::vector<Palette> &palettes);
+void rgb(std::vector<Palette> &palettes);
+
+}
+
+#endif /* RGBDS_GFX_PAL_SORTING_HPP */
--- /dev/null
+++ b/include/gfx/proto_palette.hpp
@@ -1,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)
+ 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;
+
+ ProtoPalette &operator=(ProtoPalette const &other);
+
+ size_t size() const;
+
+ decltype(_colorIndices)::const_iterator begin() const;
+ decltype(_colorIndices)::const_iterator end() const;
+};
+
+#endif /* RGBDS_GFX_PROTO_PALETTE_HPP */
--- a/include/version.h
+++ b/include/version.h
@@ -9,10 +9,18 @@
#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
char const *get_package_version_string(void);
+
+#ifdef __cplusplus
+}
+#endif
#endif /* EXTERN_VERSION_H */
--- a/man/rgbgfx.1
+++ b/man/rgbgfx.1
@@ -25,23 +25,7 @@
.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.
+program converts PNG images into data suitable
.Sh ARGUMENTS
Note that options can be abbreviated as long as the abbreviation is unambiguous:
.Fl Fl verb
@@ -117,30 +101,60 @@
.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
+.It Fl x Ar amount , Fl Fl trim-end Ar amount
Trim the end of the output file by this many tiles.
+.Pq TODO: tiles, or tilemap?
.El
+.Sh PALETTE GENERATION
+.Nm
+must decide in which order to place colors in the palettes, but the input PNG usually lacks any indications.
+A lot of the time, the order does not matter; however, sometimes it does, and
+.Nm
+attempts to account for those cases.
+.Bl -bullet -offset indent
+.It
+First, if the image contains
+.Em any
+transparent pixel, color #0 of
+.Em all
+palettes will be allocated to it.
+If palettes were explicitly specified using
+.Fl TODO ,
+then they will specify color #1 onwards.
+.Pq If you do not want this, ask your image editor to remove the alpha channel.
+.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 any duplicates.
+.Pq If you want a given color to appear more than once in a palette, you should specify the palettes explicitly instead using Fl TODO .
+.It
+Otherwise, if the PNG only contains shades of gray, they will be categorized into 4
+.Dq bins
+.Pq white, light gray, dark gray, and black in this order ,
+and the palette is set to these four bins.
+(TODO: how does this interact with 1bpp? With more than 1 palette?)
+If more than one grey ends up in the same bin, the RGB method below is used instead.
+.It
+If none of the above apply, colors are sorted from lightest to darkest.
+(This is what the old documentation claimed, but the definition of luminance that was used wasn't quite right.
+It is kept for compatibility.)
+.El
+.Pp
+Note that the
+.Dq indexed
+behavior depends on an internal detail of how the PNG is saved.
+Since few image editors (such as GIMP) expose that detail, this behavior is only kept for compatibility and should be considered deprecated; if the order of colors in the palettes is important to you, please use
+.Fl TODO
+to specify the palette explicitly.
+.Sh OUTPUT FILES
+.Ss Palette data
+Palette data is output like a dump of GBC palette memory: the output is a binary file.
+Each color is written as GBC-native little-endian RGB555 (that is, the first byte contains the red and the lower 3 bits of green).
+There is no padding between colors, nor between palettes; however, empty colors in the palettes are filled as 0xFF bytes.
.Sh EXAMPLES
-The following will take a PNG file with a bit depth of 1, 2, or 8, and output planar 2bpp data:
+The following will only validate the PNG [...] but output nothing:
.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
@@ -153,8 +167,10 @@
.Xr gbz80 7
.Sh HISTORY
.Nm
-was created by
+was originally created by
.An stag019
to be included in RGBDS.
-It is now maintained by a number of contributors at
+It was later rewritten by
+.An ISSOtm ,
+and is now maintained by a number of contributors at
.Lk https://github.com/gbdev/rgbds .
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -70,9 +70,13 @@
)
set(rgbgfx_src
- "gfx/gb.c"
- "gfx/main.c"
- "gfx/makepng.c"
+ "gfx/convert.cpp"
+ "gfx/main.cpp"
+ "gfx/pal_packing.cpp"
+ "gfx/pal_sorting.cpp"
+ "gfx/proto_palette.cpp"
+ "extern/getopt.c"
+ "error.c"
)
set(rgblink_src
@@ -96,6 +100,8 @@
)
install(TARGETS rgb${PROG} RUNTIME DESTINATION bin)
endforeach()
+
+set_target_properties(rgbgfx PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED True)
if(LIBPNG_FOUND) # pkg-config
target_include_directories(rgbgfx PRIVATE ${LIBPNG_INCLUDE_DIRS})
--- /dev/null
+++ b/src/gfx/convert.cpp
@@ -1,0 +1,869 @@
+/*
+ * This file is part of RGBDS.
+ *
+ * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include "gfx/convert.hpp"
+
+#include <algorithm>
+#include <assert.h>
+#include <errno.h>
+#include <fstream>
+#include <inttypes.h>
+#include <memory>
+#include <optional>
+#include <png.h>
+#include <setjmp.h>
+#include <string.h>
+#include <tuple>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "defaultinitalloc.hpp"
+#include "helpers.h"
+
+#include "gfx/main.hpp"
+#include "gfx/pal_packing.hpp"
+#include "gfx/pal_sorting.hpp"
+#include "gfx/proto_palette.hpp"
+
+struct Rgba {
+ uint8_t red;
+ uint8_t green;
+ uint8_t blue;
+ uint8_t alpha;
+
+ Rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : red(r), green(g), blue(b), alpha(a) {}
+ Rgba(uint32_t rgba) : red(rgba), green(rgba >> 8), blue(rgba >> 16), alpha(rgba >> 24) {}
+
+ operator uint32_t() const {
+ auto shl = [](uint8_t val, unsigned shift) { return static_cast<uint32_t>(val) << shift; };
+ return shl(red, 0) | shl(green, 8) | shl(blue, 16) | shl(alpha, 24);
+ }
+ bool operator!=(Rgba const &other) const {
+ return static_cast<uint32_t>(*this) != static_cast<uint32_t>(other);
+ }
+
+ bool isGray() const { return red == green && green == blue; }
+
+ /**
+ * 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 = 5; // TODO: adjust this
+ /**
+ * Computes the equivalent CGB color, respects the color curve depending on options
+ */
+ uint16_t cgbColor() const {
+ if (alpha < transparency_threshold)
+ return transparent;
+ if (options.useColorCurve) {
+ assert(!"TODO");
+ } else {
+ return (red >> 3) | (green >> 3) << 5 | (blue >> 3) << 10;
+ }
+ }
+};
+
+class ImagePalette {
+ // Use as many slots as there are CGB colors (plus transparency)
+ std::array<std::optional<Rgba>, 0x8001> _colors;
+
+public:
+ ImagePalette() = default;
+
+ void registerColor(Rgba const &rgba) {
+ decltype(_colors)::value_type &slot = _colors[rgba.cgbColor()];
+
+ if (!slot.has_value()) {
+ slot.emplace(rgba);
+ } else if (*slot != rgba) {
+ warning("Different colors melded together"); // TODO: indicate position
+ }
+ }
+
+ size_t size() const { return _colors.size(); }
+
+ auto begin() const { return _colors.begin(); }
+
+ auto end() const { return _colors.end(); }
+};
+
+class Png {
+ std::filesystem::path const &path;
+ std::filebuf file{};
+ png_structp png = nullptr;
+ png_infop info = nullptr;
+
+ // These are cached for speed
+ uint32_t width, height;
+ DefaultInitVec<uint16_t> pixels;
+ ImagePalette colors;
+ int colorType;
+ int nbColors;
+ png_colorp embeddedPal = nullptr;
+ png_bytep transparencyPal = nullptr;
+ bool isGrayOnly = true;
+
+ [[noreturn]] static void handleError(png_structp png, char const *msg) {
+ struct 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) {
+ struct 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) {
+ struct 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:
+ ImagePalette const &getColors() const { return colors; }
+
+ int getColorType() const { return colorType; }
+
+ std::tuple<int, png_const_colorp, png_bytep> getEmbeddedPal() const {
+ return {nbColors, embeddedPal, transparencyPal};
+ }
+
+ uint32_t getWidth() const { return width; }
+
+ uint32_t getHeight() const { return height; }
+
+ uint16_t &pixel(uint32_t x, uint32_t y) { return pixels[y * width + x]; }
+
+ uint16_t const &pixel(uint32_t x, uint32_t y) const { return pixels[y * width + x]; }
+
+ bool hasNonGray() const { return !isGrayOnly; }
+
+ /**
+ * 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::filesystem::path const &filePath) : path(filePath), colors() {
+ 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));
+ }
+
+ options.verbosePrint("Opened input file\n");
+
+ 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());
+ }
+
+ options.verbosePrint("PNG header signature is OK\n");
+
+ 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);
+
+ // TODO: use an allocator that doesn't zero on init to save potentially a lot of perf
+ // https://stackoverflow.com/questions/21028299/is-this-behavior-of-vectorresizesize-type-n-under-c11-and-boost-container/21028912#21028912
+ pixels.resize(static_cast<size_t>(width) * static_cast<size_t>(height));
+
+ auto colorTypeName = [this]() {
+ switch (colorType) {
+ case PNG_COLOR_TYPE_GRAY:
+ return "grayscale";
+ case PNG_COLOR_TYPE_GRAY_ALPHA:
+ return "grayscale + alpha";
+ case PNG_COLOR_TYPE_PALETTE:
+ return "palette";
+ case PNG_COLOR_TYPE_RGB:
+ return "RGB";
+ case PNG_COLOR_TYPE_RGB_ALPHA:
+ return "RGB + alpha";
+ default:
+ fatal("Unknown color type %d", colorType);
+ }
+ };
+ auto interlaceTypeName = [&interlaceType]() {
+ switch (interlaceType) {
+ case PNG_INTERLACE_NONE:
+ return "not interlaced";
+ case PNG_INTERLACE_ADAM7:
+ return "interlaced (Adam7)";
+ default:
+ fatal("Unknown interlace type %d", interlaceType);
+ }
+ };
+ options.verbosePrint("Input image: %" PRIu32 "x%" PRIu32 " pixels, %dbpp %s, %s\n", height,
+ width, bitDepth, colorTypeName(), interlaceTypeName());
+
+ if (png_get_PLTE(png, info, &embeddedPal, &nbColors) != 0) {
+ int nbTransparentEntries;
+ if (png_get_tRNS(png, info, &transparencyPal, &nbTransparentEntries, nullptr)) {
+ assert(nbTransparentEntries == nbColors);
+ }
+
+ options.verbosePrint("Embedded palette has %d colors: [", nbColors);
+ for (int i = 0; i < nbColors; ++i) {
+ auto const &color = embeddedPal[i];
+ options.verbosePrint("#%02x%02x%02x%s", color.red, color.green, color.blue,
+ i != nbColors - 1 ? ", " : "]\n");
+ }
+ } else {
+ options.verbosePrint("No embedded palette\n");
+ }
+
+ // 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 we read a tRNS chunk, convert it to alpha
+ if (png_get_valid(png, info, PNG_INFO_tRNS))
+ png_set_tRNS_to_alpha(png);
+ // Otherwise, if we lack an alpha channel, default to full opacity
+ else if (!(colorType & PNG_COLOR_MASK_ALPHA))
+ 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);
+
+ // Set interlace handling (MUST be done before `png_read_update_info`)
+ int nbPasses = png_set_interlace_handling(png);
+
+ // 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
+
+ std::vector<png_byte> row(png_get_rowbytes(png, info));
+
+ 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]);
+
+ colors.registerColor(rgba);
+ pixel(x, y) = rgba.cgbColor();
+ isGrayOnly &= rgba.isGray();
+ }
+ }
+ } else {
+ // For interlace to work properly, we must read the image `nbPasses` times
+ for (int pass = 0; pass < nbPasses; ++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]);
+
+ colors.registerColor(rgba);
+ pixel(x, y) = rgba.cgbColor();
+ isGrayOnly &= rgba.isGray();
+ ptr += 4;
+ }
+ }
+ }
+ }
+
+ png_read_end(png,
+ nullptr); // We don't care about chunks after the image data (comments, etc.)
+ }
+
+ ~Png() { png_destroy_read_struct(&png, &info, nullptr); }
+
+ class TilesVisitor {
+ Png const &_png;
+ bool const _columnMajor;
+ uint32_t const _width, _height;
+ uint32_t const _limit = _columnMajor ? _height : _width;
+
+ public:
+ TilesVisitor(Png const &png, bool columnMajor, uint32_t width, uint32_t height)
+ : _png(png), _columnMajor(columnMajor), _width(width), _height(height) {}
+
+ class Tile {
+ Png const &_png;
+ uint32_t const _x, _y;
+
+ public:
+ Tile(Png const &png, uint32_t x, uint32_t y) : _png(png), _x(x), _y(y) {}
+
+ uint16_t pixel(uint32_t xOfs, uint32_t yOfs) const {
+ return _png.pixel(_x + xOfs, _y + yOfs);
+ }
+ };
+
+ private:
+ struct iterator {
+ TilesVisitor const &parent;
+ uint32_t const limit;
+ uint32_t x, y;
+
+ public:
+ std::pair<uint32_t, uint32_t> coords() const { return {x, y}; }
+ Tile operator*() const { return {parent._png, x, y}; }
+
+ iterator &operator++() {
+ auto [major, minor] = parent._columnMajor ? std::tie(y, x) : std::tie(x, y);
+ major += 8;
+ if (major == limit) {
+ minor += 8;
+ major = 0;
+ }
+
+ return *this;
+ }
+
+ bool operator!=(iterator const &rhs) const {
+ return coords() != rhs.coords(); // Compare the returned coord pairs
+ }
+ };
+
+ public:
+ iterator begin() const { return {*this, _width, 0, 0}; }
+ iterator end() const {
+ iterator it{*this, _width, _width - 8, _height - 8}; // Last valid one
+ return ++it; // Now one-past-last
+ }
+ };
+public:
+ TilesVisitor visitAsTiles(bool columnMajor) const {
+ return {*this, columnMajor, width, height};
+ }
+};
+
+class RawTiles {
+public:
+ /**
+ * A tile which only contains indices into the image's global palette
+ */
+ class RawTile {
+ std::array<std::array<size_t, 8>, 8> _pixelIndices{};
+
+ public:
+ // Not super clean, but it's closer to matrix notation
+ size_t &operator()(size_t x, size_t y) { return _pixelIndices[y][x]; }
+ };
+
+private:
+ std::vector<RawTile> _tiles;
+
+public:
+ /**
+ * Creates a new raw tile, and returns a reference to it so it can be filled in
+ */
+ RawTile &newTile() {
+ _tiles.emplace_back();
+ return _tiles.back();
+ }
+};
+
+struct AttrmapEntry {
+ size_t protoPaletteID;
+ uint16_t tileID;
+ bool yFlip;
+ bool xFlip;
+};
+
+static void outputPalettes(std::vector<Palette> const &palettes) {
+ std::filebuf output;
+ output.open(options.palettes, std::ios_base::out | std::ios_base::binary);
+
+ for (Palette const &palette : palettes) {
+ for (uint16_t color : palette) {
+ output.sputc(color & 0xFF);
+ output.sputc(color >> 8);
+ }
+ }
+}
+
+namespace unoptimized {
+
+// TODO: this is very redundant with `TileData`; try merging both?
+static void outputTileData(Png const &png, DefaultInitVec<AttrmapEntry> const &attrmap,
+ std::vector<Palette> const &palettes,
+ DefaultInitVec<size_t> const &mappings) {
+ std::filebuf output;
+ output.open(options.tilemap, std::ios_base::out | std::ios_base::binary);
+
+ auto iter = attrmap.begin();
+ for (auto tile : png.visitAsTiles(options.columnMajor)) {
+ Palette const &palette = palettes[mappings[iter->protoPaletteID]];
+ for (uint32_t y = 0; y < 8; ++y) {
+ uint16_t row = 0;
+ for (uint32_t x = 0; x < 8; ++x) {
+ row <<= 1;
+ uint8_t index = palette.indexOf(tile.pixel(x, y));
+ if (index & 1) {
+ row |= 0x001;
+ }
+ if (index & 2) {
+ row |= 0x100;
+ }
+ }
+ output.sputc(row & 0xFF);
+ output.sputc(row >> 8);
+ }
+ ++iter;
+ }
+ assert(iter == attrmap.end());
+}
+
+static void outputMaps(Png const &png, DefaultInitVec<AttrmapEntry> const &attrmap,
+ DefaultInitVec<size_t> const &mappings) {
+ std::optional<std::filebuf> tilemapOutput, attrmapOutput;
+ if (!options.tilemap.empty()) {
+ tilemapOutput.emplace();
+ tilemapOutput->open(options.tilemap, std::ios_base::out | std::ios_base::binary);
+ }
+ if (!options.attrmap.empty()) {
+ attrmapOutput.emplace();
+ attrmapOutput->open(options.attrmap, std::ios_base::out | std::ios_base::binary);
+ }
+
+ uint8_t tileID = 0;
+ uint8_t bank = 0;
+ auto iter = attrmap.begin();
+ for ([[maybe_unused]] auto tile : png.visitAsTiles(options.columnMajor)) {
+ if (tileID == options.maxNbTiles[bank]) {
+ assert(bank == 0);
+ bank = 1;
+ tileID = 0;
+ }
+
+ if (tilemapOutput.has_value()) {
+ tilemapOutput->sputc(tileID + options.baseTileIDs[bank]);
+ }
+ if (attrmapOutput.has_value()) {
+ uint8_t palID = mappings[iter->protoPaletteID] & 7;
+ attrmapOutput->sputc(palID | bank << 3); // The other flags are all 0
+ ++iter;
+ }
+ ++tileID;
+ }
+}
+
+} // namespace unoptimized
+
+static uint8_t flip(uint8_t byte) {
+ // To flip all the bits, we'll flip both nibbles, then each nibble half, etc.
+ byte = (byte & 0x0F) << 4 | (byte & 0xF0) >> 4;
+ byte = (byte & 0x33) << 2 | (byte & 0xCC) >> 2;
+ byte = (byte & 0x55) << 1 | (byte & 0xAA) >> 1;
+ return byte;
+}
+
+class TileData {
+ // TODO: might want to switch to `std::byte` instead?
+ std::array<uint8_t, 16> _data;
+ // The hash is a bit lax: it's the XOR of all lines, and every other nibble is identical
+ // if horizontal mirroring is in effect. It should still be a reasonable tie-breaker in
+ // non-pathological cases.
+ uint16_t _hash;
+public:
+ mutable size_t tileID;
+
+ TileData(Png::TilesVisitor::Tile const &tile, Palette const &palette) : _hash(0) {
+ for (uint32_t y = 0; y < 8; ++y) {
+ uint16_t bitplanes = 0;
+ for (uint32_t x = 0; x < 8; ++x) {
+ bitplanes <<= 1;
+ uint8_t index = palette.indexOf(tile.pixel(x, y));
+ if (index & 1) {
+ bitplanes |= 1;
+ }
+ if (index & 2) {
+ bitplanes |= 0x100;
+ }
+ }
+ _data[y * 2] = bitplanes & 0xFF;
+ _data[y * 2 + 1] = bitplanes >> 8;
+
+ // Update the hash
+ _hash ^= bitplanes;
+ if (options.allowMirroring) {
+ // Count the line itself as mirrorred; vertical mirroring is
+ // already taken care of because the symmetric line will be XOR'd
+ // the same way. (...which is a problem, but probably benign.)
+ _hash ^= flip(bitplanes >> 8) << 8 | flip(bitplanes & 0xFF);
+ }
+ }
+ }
+
+ auto const &data() const { return _data; }
+ uint16_t hash() const { return _hash; }
+
+ enum MatchType {
+ NOPE,
+ EXACT,
+ HFLIP,
+ VFLIP,
+ VHFLIP,
+ };
+ MatchType tryMatching(TileData const &other) const {
+ if (std::equal(_data.begin(), _data.end(), other._data.begin()))
+ return MatchType::EXACT;
+
+ if (options.allowMirroring) {
+ // TODO
+ }
+
+ return MatchType::NOPE;
+ }
+ friend bool operator==(TileData const &lhs, TileData const &rhs) {
+ return lhs.tryMatching(rhs) != MatchType::NOPE;
+ }
+};
+
+template<>
+struct std::hash<TileData> {
+ std::size_t operator()(TileData const &tile) const { return tile.hash(); }
+};
+
+namespace optimized {
+
+struct UniqueTiles {
+ std::unordered_set<TileData> tileset;
+ std::vector<TileData const *> tiles;
+
+ UniqueTiles() = default;
+ // Copies are likely to break pointers, so we really don't want those.
+ // Copy elision should be relied on to be more sure that refs won't be invalidated, too!
+ UniqueTiles(UniqueTiles const &) = delete;
+ UniqueTiles(UniqueTiles &&) = default;
+
+ /**
+ * Adds a tile to the collection, and returns its ID
+ */
+ std::tuple<uint16_t, TileData::MatchType> addTile(Png::TilesVisitor::Tile const &tile,
+ Palette const &palette) {
+ TileData newTile(tile, palette);
+ auto [tileData, inserted] = tileset.insert(newTile);
+
+ TileData::MatchType matchType = TileData::EXACT;
+ if (inserted) {
+ // Give the new tile the next available unique ID
+ tileData->tileID = tiles.size();
+ // Pointers are never invalidated!
+ tiles.emplace_back(&*tileData);
+ } else {
+ matchType = tileData->tryMatching(newTile);
+ }
+ return {tileData->tileID, matchType};
+ }
+
+ auto size() const { return tiles.size(); }
+
+ auto begin() const { return tiles.begin(); }
+ auto end() const { return tiles.end(); }
+};
+
+static UniqueTiles dedupTiles(Png const &png, DefaultInitVec<AttrmapEntry> &attrmap,
+ std::vector<Palette> const &palettes,
+ DefaultInitVec<size_t> const &mappings) {
+ // Iterate throughout the image, generating tile data as we go
+ // (We don't need the full tile data to be able to dedup tiles, but we don't lose anything
+ // by caching the full tile data anyway, so we might as well.)
+ UniqueTiles tiles;
+
+ auto iter = attrmap.begin();
+ for (auto tile : png.visitAsTiles(options.columnMajor)) {
+ auto [tileID, matchType] = tiles.addTile(tile, palettes[mappings[iter->protoPaletteID]]);
+
+ iter->xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP;
+ iter->yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP;
+ assert(tileID < 1 << 10);
+ iter->tileID = tileID;
+
+ ++iter;
+ }
+ assert(iter == attrmap.end());
+
+ return tiles; // Copy elision should prevent the contained `unordered_set` from being
+ // re-constructed
+}
+
+static void outputTileData(UniqueTiles const &tiles) {
+ std::filebuf output;
+ output.open(options.output, std::ios_base::out | std::ios_base::binary);
+
+ uint16_t tileID = 0;
+ for (TileData const *tile : tiles) {
+ assert(tile->tileID == tileID);
+ ++tileID;
+ output.sputn(reinterpret_cast<char const *>(tile->data().data()), tile->data().size());
+ }
+}
+
+static void outputTilemap(DefaultInitVec<AttrmapEntry> const &attrmap) {
+ std::filebuf output;
+ output.open(options.tilemap, std::ios_base::out | std::ios_base::binary);
+
+ assert(options.baseTileIDs[0] == 0 && options.baseTileIDs[1] == 0); // TODO: deal with offset
+ for (AttrmapEntry const &entry : attrmap) {
+ output.sputc(entry.tileID & 0xFF);
+ }
+}
+
+static void outputAttrmap(DefaultInitVec<AttrmapEntry> const &attrmap,
+ DefaultInitVec<size_t> const &mappings) {
+ std::filebuf output;
+ output.open(options.attrmap, std::ios_base::out | std::ios_base::binary);
+
+ assert(options.baseTileIDs[0] == 0 && options.baseTileIDs[1] == 0); // TODO: deal with offset
+ for (AttrmapEntry const &entry : attrmap) {
+ uint8_t attr = entry.xFlip << 5 | entry.yFlip << 6;
+ attr |= (entry.tileID >= options.maxNbTiles[0]) << 3;
+ attr |= mappings[entry.protoPaletteID] & 7;
+ output.sputc(attr);
+ }
+}
+
+} // namespace optimized
+
+void process() {
+ options.verbosePrint("Using libpng %s\n", png_get_libpng_ver(nullptr));
+
+ options.verbosePrint("Reading tiles...\n");
+ Png png(options.input);
+ ImagePalette const &colors = png.getColors();
+
+ // Now, we have all the image's colors in `colors`
+ // The next step is to order the palette
+
+ if (options.beVerbose) {
+ options.verbosePrint("Image colors: [ ");
+ size_t i = 0;
+ for (auto const &slot : colors) {
+ if (!slot.has_value()) {
+ continue;
+ }
+ options.verbosePrint("#%02x%02x%02x%02x%s", slot->red, slot->green, slot->blue,
+ slot->alpha, i != colors.size() - 1 ? ", " : "");
+ ++i;
+ }
+ options.verbosePrint("]\n");
+ }
+
+ // Now, iterate through the tiles, generating proto-palettes as we go
+ // We do this unconditionally because this performs the image validation (which we want to
+ // perform even if no output is requested), and because it's necessary to generate any
+ // output (with the exception of an un-duplicated tilemap, but that's an acceptable loss.)
+ std::vector<ProtoPalette> protoPalettes;
+ DefaultInitVec<AttrmapEntry> attrmap{};
+
+ for (auto tile : png.visitAsTiles(options.columnMajor)) {
+ ProtoPalette tileColors;
+ AttrmapEntry &attrs = attrmap.emplace_back();
+
+ for (uint32_t y = 0; y < 8; ++y) {
+ for (uint32_t x = 0; x < 8; ++x) {
+ tileColors.add(tile.pixel(x, y));
+ }
+ }
+
+ // Insert the palette, making sure to avoid overlaps
+ // TODO: if inserting (0, 1), (0, 2), and then (0, 1, 2), we might have a problem!
+ for (size_t i = 0; i < protoPalettes.size(); ++i) {
+ switch (tileColors.compare(protoPalettes[i])) {
+ case ProtoPalette::WE_BIGGER:
+ protoPalettes[i] = tileColors; // Override them
+ [[fallthrough]];
+ case ProtoPalette::THEY_BIGGER:
+ // Do nothing, they already contain us
+ attrs.protoPaletteID = i;
+ goto contained;
+ case ProtoPalette::NEITHER:
+ break; // Keep going
+ }
+ }
+ attrs.protoPaletteID = protoPalettes.size();
+ protoPalettes.push_back(tileColors);
+contained:;
+ }
+
+ options.verbosePrint("Image contains %zu proto-palette%s\n", protoPalettes.size(),
+ protoPalettes.size() != 1 ? "s" : "");
+
+ // Sort the proto-palettes by size, which improves the packing algorithm's efficiency
+ // TODO: try keeping the palettes stored while inserting them instead, might perform better
+ std::sort(
+ protoPalettes.begin(), protoPalettes.end(),
+ [](ProtoPalette const &lhs, ProtoPalette const &rhs) { return lhs.size() < rhs.size(); });
+
+ // Run a "pagination" problem solver
+ // TODO: allow picking one of several solvers?
+ auto [mappings, nbPalettes] = packing::overloadAndRemove(protoPalettes);
+ assert(mappings.size() == protoPalettes.size());
+ if (options.beVerbose) {
+ options.verbosePrint("Proto-palette mappings: (%zu palettes)\n", nbPalettes);
+ for (size_t i = 0; i < mappings.size(); ++i) {
+ options.verbosePrint("%zu -> %zu\n", i, mappings[i]);
+ }
+ }
+
+ // TODO: optionally, "decant" the result
+
+ // Generate the actual palettes from the mappings
+ std::vector<Palette> palettes(nbPalettes);
+ for (size_t protoPalID = 0; protoPalID < mappings.size(); ++protoPalID) {
+ auto &pal = palettes[mappings[protoPalID]];
+ for (uint16_t color : protoPalettes[protoPalID]) {
+ pal.addColor(color);
+ }
+ }
+
+ // "Sort" colors in the generated palettes, see the man page for the flowchart
+ auto [palSize, palRGB, palAlpha] = png.getEmbeddedPal();
+ if (palRGB) {
+ sorting::indexed(palettes, palSize, palRGB, palAlpha);
+ } else if (png.hasNonGray()) {
+ sorting::rgb(palettes);
+ } else {
+ sorting::grayscale(palettes);
+ }
+
+ if (options.beVerbose) {
+ for (auto &&palette : palettes) {
+ options.verbosePrint("{ ");
+ for (uint16_t colorIndex : palette) {
+ options.verbosePrint("%04" PRIx16 ", ", colorIndex);
+ }
+ options.verbosePrint("}\n");
+ }
+ }
+
+ if (!options.palettes.empty()) {
+ outputPalettes(palettes);
+ }
+
+ // If deduplication is not happening, we just need to output the tile data and/or maps as-is
+ if (!options.allowDedup) {
+ uint32_t const nbTilesH = png.getHeight() / 8, nbTilesW = png.getWidth() / 8;
+
+ // Check the tile count
+ if (nbTilesW * nbTilesH > options.maxNbTiles[0] + options.maxNbTiles[1]) {
+ fatal("Image contains %" PRIu32 " tiles, exceeding the limit of %" PRIu16 " + %" PRIu16,
+ nbTilesW * nbTilesH, options.maxNbTiles[0], options.maxNbTiles[1]);
+ }
+
+ if (!options.output.empty()) {
+ options.verbosePrint("Generating unoptimized tile data...\n");
+
+ unoptimized::outputTileData(png, attrmap, palettes, mappings);
+ }
+
+ if (!options.tilemap.empty() || !options.attrmap.empty()) {
+ options.verbosePrint("Generating unoptimized tilemap and/or attrmap...\n");
+
+ unoptimized::outputMaps(png, attrmap, mappings);
+ }
+ } else {
+ // All of these require the deduplication process to be performed to be output
+ options.verbosePrint("Deduplicating tiles...\n");
+ optimized::UniqueTiles tiles = optimized::dedupTiles(png, attrmap, palettes, mappings);
+
+ if (tiles.size() > options.maxNbTiles[0] + options.maxNbTiles[1]) {
+ fatal("Image contains %zu tiles, exceeding the limit of %" PRIu16 " + %" PRIu16,
+ tiles.size(), options.maxNbTiles[0], options.maxNbTiles[1]);
+ }
+
+ if (!options.output.empty()) {
+ options.verbosePrint("Generating optimized tile data...\n");
+
+ optimized::outputTileData(tiles);
+ }
+
+ if (!options.tilemap.empty()) {
+ options.verbosePrint("Generating optimized tilemap...\n");
+
+ optimized::outputTilemap(attrmap);
+ }
+
+ if (!options.attrmap.empty()) {
+ options.verbosePrint("Generating optimized attrmap...\n");
+
+ optimized::outputAttrmap(attrmap, mappings);
+ }
+ }
+}
--- a/src/gfx/gb.c
+++ /dev/null
@@ -1,385 +1,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);
-}
--- a/src/gfx/main.c
+++ /dev/null
@@ -1,358 +1,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;
-}
--- /dev/null
+++ b/src/gfx/main.cpp
@@ -1,0 +1,321 @@
+/*
+ * 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 <charconv>
+#include <filesystem>
+#include <inttypes.h>
+#include <limits>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "extern/getopt.h"
+#include "version.h"
+
+#include "gfx/convert.hpp"
+
+Options options;
+static uintmax_t nbErrors;
+
+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++;
+
+ fprintf(stderr, "Conversion aborted after %ju error%s\n", nbErrors, nbErrors == 1 ? "" : "s");
+ exit(1);
+}
+
+void Options::verbosePrint(char const *fmt, ...) const {
+ if (beVerbose) {
+ va_list ap;
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ }
+}
+
+// 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);
+}
+
+void fputsv(std::string_view const &view, FILE *f) {
+ if (view.data()) {
+ fwrite(view.data(), sizeof(char), view.size(), f);
+ }
+}
+
+int main(int argc, char *argv[]) {
+ int opt;
+ bool autoAttrmap = false, autoTilemap = false, autoPalettes = false;
+
+ auto parseDecimalArg = [&opt](auto &out) {
+ char const *end = &musl_optarg[strlen(musl_optarg)];
+ // `options.bitDepth` is not modified if the parse fails entirely
+ auto result = std::from_chars(musl_optarg, end, out, 10);
+
+ if (result.ec == std::errc::result_out_of_range) {
+ error("Argument to option '%c' (\"%s\") is out of range", opt, musl_optarg);
+ return false;
+ } else if (result.ptr != end) {
+ error("Invalid argument to option '%c' (\"%s\")", opt, musl_optarg);
+ return false;
+ }
+ return true;
+ };
+
+ while ((opt = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1) {
+ switch (opt) {
+ case 'A':
+ autoAttrmap = true;
+ break;
+ case 'a':
+ autoAttrmap = false;
+ options.attrmap = musl_optarg;
+ break;
+ case 'C':
+ options.useColorCurve = true;
+ break;
+ case 'd':
+ if (parseDecimalArg(options.bitDepth) && options.bitDepth != 1
+ && options.bitDepth != 2) {
+ error("Bit depth must be 1 or 2, not %" PRIu8 "");
+ options.bitDepth = 2;
+ }
+ break;
+ case 'f':
+ options.fixInput = true;
+ break;
+ case 'h':
+ warning("`-h` is deprecated, use `-???` instead");
+ [[fallthrough]];
+ case '?': // TODO
+ options.columnMajor = true;
+ break;
+ case 'm':
+ options.allowMirroring = true;
+ [[fallthrough]]; // Imply `-u`
+ case 'u':
+ options.allowDedup = true;
+ break;
+ case 'o':
+ options.output = musl_optarg;
+ break;
+ case 'P':
+ autoPalettes = true;
+ break;
+ case 'p':
+ autoPalettes = false;
+ options.palettes = musl_optarg;
+ 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':
+ options.beVerbose = true;
+ break;
+ case 'x':
+ parseDecimalArg(options.trim);
+ break;
+ case 'D':
+ case 'F':
+ warning("Ignoring option '%c'", musl_optopt);
+ break;
+ default:
+ print_usage();
+ exit(1);
+ }
+ }
+
+ if (options.nbColorsPerPal == 0) {
+ options.nbColorsPerPal = 1 << 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);
+ }
+
+ if (musl_optind == argc) {
+ fputs("FATAL: No input image specified\n", stderr);
+ print_usage();
+ exit(1);
+ } else if (argc - musl_optind != 1) {
+ fprintf(stderr, "FATAL: %d input images were specified instead of 1\n", argc - musl_optind);
+ print_usage();
+ exit(1);
+ }
+
+ options.input = argv[argc - 1];
+
+ auto autoOutPath = [](bool autoOptEnabled, std::filesystem::path &path, char const *extension) {
+ if (autoOptEnabled) {
+ path = options.input;
+ path.replace_extension(extension);
+ }
+ };
+ autoOutPath(autoAttrmap, options.attrmap, ".attrmap");
+ autoOutPath(autoTilemap, options.tilemap, ".tilemap");
+ autoOutPath(autoPalettes, options.palettes, ".pal");
+
+ if (options.beVerbose) {
+ fprintf(stderr, "rgbgfx %s\n", get_package_version_string());
+ fputs("Options:\n", stderr);
+ if (options.fixInput)
+ fputs("\tConvert input to indexed\n", stderr);
+ if (options.columnMajor)
+ fputs("\tOutput {tile,attr}map 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);
+ fprintf(stderr, "\tTrim the last %" PRIu64 " tiles\n", options.trim);
+ fprintf(stderr, "\tBase tile IDs: [%" PRIu8 ", %" PRIu8 "]\n", options.baseTileIDs[0],
+ options.baseTileIDs[1]);
+ fprintf(stderr, "\tMax number of tiles: [%" PRIu16 ", %" PRIu16 "]\n",
+ options.maxNbTiles[0], options.maxNbTiles[1]);
+ auto printPath = [](char const *name, std::filesystem::path 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);
+ }
+
+ process();
+
+ 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;
+ }
+ }
+}
+
+uint8_t Palette::indexOf(uint16_t color) const {
+ return std::distance(colors.begin(), std::find(colors.begin(), colors.end(), color));
+}
+
+auto Palette::begin() -> decltype(colors)::iterator {
+ return colors.begin();
+}
+auto Palette::end() -> decltype(colors)::iterator {
+ return std::find(colors.begin(), colors.end(), UINT16_MAX);
+}
+
+auto Palette::begin() const -> decltype(colors)::const_iterator {
+ return colors.begin();
+}
+auto Palette::end() const -> decltype(colors)::const_iterator {
+ return std::find(colors.begin(), colors.end(), UINT16_MAX);
+}
--- a/src/gfx/makepng.c
+++ /dev/null
@@ -1,806 +1,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);
-}
--- /dev/null
+++ b/src/gfx/pal_packing.cpp
@@ -1,0 +1,359 @@
+/*
+ * This file is part of RGBDS.
+ *
+ * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include "gfx/pal_packing.hpp"
+
+#include <assert.h>
+#include <bitset>
+#include <inttypes.h>
+#include <numeric>
+#include <optional>
+#include <queue>
+#include <tuple>
+#include <type_traits>
+#include <unordered_set>
+#include <vector>
+
+#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 palIndex;
+ /**
+ * 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) : palIndex(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(decltype(_protoPals) 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::input_iterator_tag;
+
+ private:
+ Constness<decltype(_assigned)> *_array = nullptr;
+ Inner _iter{};
+
+ Iter(decltype(_array) array, decltype(_iter) &&iter) : _array(array), _iter(iter) {
+ skipEmpty();
+ }
+ void skipEmpty() {
+ while (_iter != _array->end() && !(*_iter).has_value()) {
+ ++_iter;
+ }
+ }
+
+ 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()}; }
+ 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()}; }
+ 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>
+ auto 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)...);
+ }
+ return freeSlot;
+ }
+ void remove(iterator const &iter) {
+ (*iter._iter).reset(); // This time, we want to access the `optional` itself
+ }
+ void clear() { _assigned.clear(); }
+
+ /**
+ * Computes the "relative size" of a proto-palette on this palette
+ */
+ double relSizeOf(ProtoPalette const &protoPal) const {
+ return std::transform_reduce(
+ protoPal.begin(), protoPal.end(), .0, std::plus<>(), [this](uint16_t color) {
+ // 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.
+ return 1.
+ / (1
+ + std::count_if(
+ begin(), end(), [this, &color](ProtoPalAttrs const &attrs) {
+ ProtoPalette const &pal = _protoPals[attrs.palIndex];
+ return std::find(pal.begin(), pal.end(), color) != pal.end();
+ }));
+ });
+ }
+private:
+ 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();
+ for (ProtoPalAttrs const &attrs : *this) {
+ for (uint16_t color : _protoPals[attrs.palIndex]) {
+ colors.insert(color);
+ }
+ }
+ 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();
+ for (uint16_t color : protoPal) {
+ colors.insert(color);
+ }
+ return colors.size() <= 4;
+ }
+};
+
+std::tuple<DefaultInitVec<size_t>, size_t>
+ overloadAndRemove(std::vector<ProtoPalette> const &protoPalettes) {
+ options.verbosePrint("Paginating palettes using \"overload-and-remove\" strategy...\n");
+
+ struct Iota {
+ using value_type = size_t;
+ using difference_type = size_t;
+ using pointer = value_type const *;
+ using reference = value_type const &;
+ using iterator_category = std::input_iterator_tag;
+
+ // Use aggregate init etc.
+ value_type i;
+
+ bool operator!=(Iota const &other) { return i != other.i; }
+ reference operator*() const { return i; }
+ pointer operator->() const { return &i; }
+ Iota operator++() {
+ ++i;
+ return *this;
+ }
+ Iota operator++(int) {
+ Iota copy = *this;
+ ++i;
+ return copy;
+ }
+ };
+
+ // Begin with all proto-palettes queued up for insertion
+ std::queue queue(std::deque<ProtoPalAttrs>(Iota{0}, Iota{protoPalettes.size()}));
+ // Begin with no pages
+ std::vector<AssignedProtos> assignments{};
+
+ for (; !queue.empty(); queue.pop()) {
+ ProtoPalAttrs const &attrs = queue.front(); // Valid until the `queue.pop()`
+
+ ProtoPalette const &protoPal = protoPalettes[attrs.palIndex];
+ 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("%zu: Rel size: %f (size = %zu)\n", i,
+ 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() > 4) {
+ options.verbosePrint("Palette %zu is overloaded! (%zu > 4)\n", bestPalIndex,
+ bestPal.volume());
+
+ // 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.palIndex])
+ < efficiency(protoPalettes[rhs.palIndex]);
+ });
+
+ // 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->palIndex])
+ - efficiency(protoPalettes[minEfficiencyIter->palIndex])
+ < .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() > 4) {
+ 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.palIndex];
+ 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("Adding new palette for overflow\n");
+ assignments.emplace_back(protoPalettes, std::move(attrs));
+ } else {
+ options.verbosePrint("Assigning overflow to palette %zu\n", iter - assignments.begin());
+ iter->assign(std::move(attrs));
+ }
+ queue.pop();
+ }
+ // Deal with any empty palettes left over from the "un-overloading" step
+ // TODO (can there be any?)
+
+ if (options.beVerbose) {
+ for (auto &&assignment : assignments) {
+ options.verbosePrint("{ ");
+ for (auto &&attrs : assignment) {
+ for (auto &&colorIndex : protoPalettes[attrs.palIndex]) {
+ options.verbosePrint("%04" PRIx16 ", ", colorIndex);
+ }
+ }
+ options.verbosePrint("} (%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.palIndex] = i;
+ }
+ }
+ return {mappings, assignments.size()};
+}
+
+} // namespace packing
--- /dev/null
+++ b/src/gfx/pal_sorting.cpp
@@ -1,0 +1,64 @@
+
+#include "gfx/pal_sorting.hpp"
+
+#include <algorithm>
+#include <png.h>
+#include <vector>
+
+#include "helpers.h"
+
+#include "gfx/main.hpp"
+
+namespace sorting {
+
+void indexed(std::vector<Palette> &palettes, int palSize, png_color const *palRGB,
+ png_byte *palAlpha) {
+ options.verbosePrint("Sorting palettes using embedded palette...\n");
+
+ 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) {
+ if (palAlpha && palAlpha[i])
+ continue;
+ auto const &c = palRGB[i];
+ uint16_t cgbColor = c.red >> 3 | (c.green >> 3) << 5 | (c.blue >> 3) << 10;
+ // Return whether lhs < rhs
+ if (cgbColor == rhs) {
+ return false;
+ }
+ if (cgbColor == lhs) {
+ return true;
+ }
+ }
+ unreachable_(); // This should not be possible
+ });
+ }
+}
+
+void grayscale(std::vector<Palette> &palettes) {
+ options.verbosePrint("Sorting grayscale-only palettes...\n");
+
+ for (Palette &pal : palettes) {
+ (void)pal; // TODO
+ }
+}
+
+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("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
--- /dev/null
+++ b/src/gfx/proto_palette.cpp
@@ -1,0 +1,79 @@
+/*
+ * 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 <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; // EOF
+ }
+ // 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 {
+ 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 {
+ ++theirs;
+ weBigger = false;
+ }
+ }
+ weBigger &= ours == _colorIndices.end();
+ theyBigger &= theirs == other._colorIndices.end();
+
+ return theyBigger ? THEY_BIGGER : (weBigger ? WE_BIGGER : NEITHER);
+}
+
+ProtoPalette &ProtoPalette::operator=(ProtoPalette const &other) {
+ _colorIndices = other._colorIndices;
+ return *this;
+}
+
+size_t ProtoPalette::size() const {
+ return std::distance(begin(), end());
+}
+
+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);
+}