Fix RGBGFX to support -C/--color-curve in -r/--reverse mode (#2110)

Clarifies error message when RGBGFX reverse mode has `-c` mismatching `-p`.
This commit is contained in:
Rangi
2026-09-18 17:35:22 -04:00
committed by GitHub
parent 0286d4119e
commit 89403ca23b
9 changed files with 75 additions and 39 deletions
+9 -19
View File
@@ -20,23 +20,6 @@ struct Rgba {
explicit constexpr Rgba(uint32_t rgba = 0)
: red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {}
// CGB colors are RGB555, so we use bit 15 to signify that the color is transparent instead
// Since the rest of the bits don't matter then, we return 0x8000 (1 << 15) exactly.
static constexpr uint16_t transparent = 0b1'00000'00000'00000;
static constexpr Rgba fromCGBColor(uint16_t color) {
constexpr auto _5to8 = [](uint8_t channel) -> uint8_t {
channel &= 0b11111; // For caller's convenience
return channel << 3 | channel >> 2;
};
return {
_5to8(color),
_5to8(color >> 5),
_5to8(color >> 10),
static_cast<uint8_t>(color & transparent ? 0x00 : 0xFF),
};
}
// Returns this RGBA as a 32-bit number that can be printed in hex (`#%08x`)
// to yield its CSS representation (`#rrggbbaa`).
uint32_t toCSS() const {
@@ -45,15 +28,22 @@ struct Rgba {
};
return shl(red, 24) | shl(green, 16) | shl(blue, 8) | shl(alpha, 0);
}
bool operator==(Rgba const &rhs) const { return toCSS() == rhs.toCSS(); }
// We allow some leeway to consider colors as transparent or opaque,
// but intermediate alpha values are still ambiguous.
static constexpr uint8_t transparency_threshold = 0x10;
bool isTransparent() const { return alpha < transparency_threshold; }
static constexpr uint8_t opacity_threshold = 0xF0;
bool isOpaque() const { return alpha >= opacity_threshold; }
bool isAmbiguous() const { return isTransparent() == isOpaque(); }
// Computes the equivalent CGB color, respects the color curve depending on options
// CGB colors are RGB555, so we use bit 15 to signify that the color is transparent instead
// Since the rest of the bits don't matter then, we return 0x8000 (1 << 15) exactly.
static constexpr uint16_t transparent = 0b1'00000'00000'00000;
// Computes the equivalent RGB888 color; respects the color curve depending on argument
static Rgba fromCGBColor(uint16_t color, bool useColorCurve);
// Computes the equivalent RGB555 color; respects the color curve depending on options
uint16_t cgbColor() const;
bool isGray() const { return red == green && green == blue; }
+1 -1
View File
@@ -528,7 +528,7 @@ static void verboseOutputConfig() {
for (auto const &pal : options.palSpec) {
fputs("\t\t", stderr);
for (auto const &color : pal) {
if (color) {
if (color.has_value()) {
fprintf(stderr, "#%06x, ", color->toCSS() >> 8);
} else {
fputs("#none, ", stderr);
+4 -4
View File
@@ -542,10 +542,10 @@ static void parseGBCFile(char const *filename, std::filebuf &file) {
}
options.palSpec.push_back({
Rgba::fromCGBColor(toWord(buf[0], buf[1])),
Rgba::fromCGBColor(toWord(buf[2], buf[3])),
Rgba::fromCGBColor(toWord(buf[4], buf[5])),
Rgba::fromCGBColor(toWord(buf[6], buf[7])),
Rgba::fromCGBColor(toWord(buf[0], buf[1]), false),
Rgba::fromCGBColor(toWord(buf[2], buf[3]), false),
Rgba::fromCGBColor(toWord(buf[4], buf[5]), false),
Rgba::fromCGBColor(toWord(buf[6], buf[7]), false),
});
}
}
+16 -7
View File
@@ -119,10 +119,6 @@ void reverse() {
warnx("Tile deduplication is enabled, but no tilemap is provided");
}
if (options.useColorCurve) {
warnx("The color curve is not yet supported in reverse mode");
}
if (options.inputSlice.left != 0 || options.inputSlice.top != 0
|| options.inputSlice.height != 0) {
warnx("\"Sliced-off\" pixels are ignored in reverse mode");
@@ -230,14 +226,14 @@ void reverse() {
palSize
);
}
// Expand the colors
// Expand the little-endian RGB555 colors to RGB888
auto &palette = palettes.emplace_back();
std::generate(
palette.begin(),
palette.begin() + options.nbColorsPerPal,
[&buf, i = 0]() mutable {
i += 2;
return Rgba::fromCGBColor(buf[i - 2] | buf[i - 1] << 8); // little-endian
return Rgba::fromCGBColor(buf[i - 2] | buf[i - 1] << 8, options.useColorCurve);
}
);
}
@@ -251,7 +247,20 @@ void reverse() {
}
if (options.hasExplicitPalSpec() && palettes != options.palSpec) {
warnx("Colors in the palette file do not match those specified with '-c'");
// The explicit `-c` pal spec does not match the input `-p` palette file.
// Check whether their 8-to-5-bit-reduced GB colors nevertheless match.
std::vector<std::array<std::optional<Rgba>, 4>> palSpecQuantized(options.palSpec);
for (auto &pal : palSpecQuantized) {
for (auto &color : pal) {
if (color.has_value()) {
color = Rgba::fromCGBColor(color->cgbColor(), options.useColorCurve);
}
}
}
warnx(
"Colors %s the palette file do not match those specified with '-c'",
palettes == palSpecQuantized ? "reversed from" : "in"
);
// This spacing aligns "...versus with `-c`" above the column of `-c` palettes
fputs("Colors specified in the palette file: ...versus with '-c':\n", stderr);
for (size_t i = 0; i < palettes.size() || i < options.palSpec.size(); ++i) {
+38 -8
View File
@@ -24,16 +24,24 @@ std::string toCGB(uint16_t color) {
return buf;
}
// Based on inverting the "Modern - Accurate" formula used by SameBoy
// since commit b5a611c5db46d6a0649d04d24d8d6339200f9ca1 (Dec 2020),
// with gaps in the scale curve filled by polynomial interpolation.
// Copied from the "Modern - Accurate" (`GB_COLOR_CORRECTION_MODERN_ACCURATE`)
// formula used by SameBoy in its `scale_channel_with_curve` function since
// commit b5a611c5db46d6a0649d04d24d8d6339200f9ca1 (Dec 2020).
// clang-format off: vertically align columns of values
static std::array<uint8_t, 32> color_curve{
0, 6, 12, 20, 28, 36, 45, 56, 66, 76, 88, 100, 113, 125, 137, 149,
161, 172, 182, 192, 202, 210, 218, 225, 232, 238, 243, 247, 250, 252, 254, 255,
};
// clang-format on
// Inverted `color_curve`, with gaps filled by polynomial interpolation.
// clang-format off: vertically align columns of values
static std::array<uint8_t, 256> reverse_curve{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1,
1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3,
3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5,
5, 5, 5, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7,
7, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1,
1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3,
3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5,
5, 5, 5, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7,
7, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10,
10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13,
13, 13, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 16, 16, 16,
16, 16, 16, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 19, 19,
@@ -48,6 +56,28 @@ static std::array<uint8_t, 256> reverse_curve{
};
// clang-format on
Rgba Rgba::fromCGBColor(uint16_t color, bool useColorCurve) {
uint8_t r = color & 0b11111, g = (color >> 5) & 0b11111, b = (color >> 10) & 0b11111;
if (useColorCurve) {
r = color_curve[r];
g = color_curve[g];
b = color_curve[b];
if (g != b) {
g = round(pow((pow(g / 255.0, 2.2) * 3 + pow(b / 255.0, 2.2)) / 4, 1 / 2.2) * 255);
}
} else {
r = r << 3 | r >> 2;
g = g << 3 | g >> 2;
b = b << 3 | b >> 2;
}
return {
r,
g,
b,
static_cast<uint8_t>(color & transparent ? 0x00 : 0xFF),
};
}
uint16_t Rgba::cgbColor() const {
if (isTransparent()) {
return transparent;
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
warning: Colors reversed from the palette file do not match those specified with '-c'
Colors specified in the palette file: ...versus with '-c':
[#adceefff, <none> , <none> , <none> ] [#abcdefff, <none> , <none> , <none> ]
+3
View File
@@ -0,0 +1,3 @@
-c #abcdef
-s 1
-p reverse_color_mismatch.pal
+1
View File
@@ -0,0 +1 @@
5w