gbdev / rgbds

Rednex Game Boy Development System - An assembly toolchain for the Nintendo Game Boy and Game Boy Color
https://rgbds.gbdev.io
MIT License
1.33k stars 175 forks source link

RGBGFX's `--color-curve` may be incorrect #1226

Closed ISSOtm closed 10 months ago

ISSOtm commented 10 months ago

Picking up after #1200: @coffeevalenbat shared captures (which should be reposted here) of an image being converted with and without the setting, and enabling it tended to de-saturate the image, when the opposite was expected.

Rangi42 commented 10 months ago

rgba.cpp credits its color curve to https://github.com/LIJI32/SameBoy/commit/65dd02cc52f531dbbd3a7e6014e99d5b24e71a4c. SameBoy's color correction is designed to make PC colors appear like GBC hardware ones, with less contrast and brightness. RGBGFX would want to do the reverse, and exaggerate the colors so playing on real hardware would look like the designer's PC mockups.

To do: fill in the gaps in this reversed mapping:

static std::array<uint8_t, 256> reverse_curve{
    0, __,  1, __,  2, __, __,  3, __, __, __, __,  4, __, __, __, // These
    __, __,  5, __, __, __, __, __, __,  6, __, __, __, __, __, __, // comments
    __, __,  7, __, __, __, __, __, __, __,  8, __, __, __, __, __, // prevent
    __, __, __, __,  9, __, __, __, __, __, __, __, __, __, 10, __, // clang-format
    __, __, __, __, __, __, __, __, __, 11, __, __, __, __, __, __, // from
    __, __, __, __, __, 12, __, __, __, __, __, __, __, __, __, __, // reflowing
    __, 13, __, __, __, __, __, __, __, __, __, __, __, 14, __, __, // these
    __, __, __, __, __, __, __, __, __, 15, __, __, __, __, __, __, // sixteen
    __, __, __, __, __, __, 16, __, __, __, __, __, __, __, __, __, // 16-item
    __, __, 17, __, __, __, __, __, __, __, __, __, __, __, 18, __, // lines,
    __, __, __, __, __, __, __, __, __, __, 19, __, __, __, __, __, // which,
    __, __, __, __, __, __, 20, __, __, __, __, __, __, __, __, __, // in
    __, 21, __, __, __, __, __, __, __, __, __, 22, __, __, __, __, // my
    __, __, __, __, __, 23, __, __, __, __, __, __, __, 24, __, __, // opinion,
    __, __, __, __, __, __, 25, __, __, __, __, __, __, 26, __, __, // help
    __, __, __, 27, __, __, __, __, 28, __, __, 29, __, 30, __, 31, // visualization!
};

image

ISSOtm commented 10 months ago

Is the curve there identical to that in SameBoy?

Rangi42 commented 10 months ago

Is the curve there identical to that in SameBoy?

Yes.

ISSOtm commented 10 months ago

SameBoy has a lot of colour correction settings, but I think we should stick to GB_COLOR_CORRECTION_CORRECT_CURVES (which, I believe, corresponds to what @LIJI32 calls "Harsh Reality"). Then comes the question of which curve we want to stick to; I'd favour the "default" one over SGB or AGB.

ISSOtm commented 10 months ago

I was slightly lazy and linearly interpolated between those points:

static std::array<uint8_t, 256> reverse_curve{
     0,  0,  0,  1,  1,  1,  1,  1,  1,  2,  2,  2,  2,  2,  2,  2, // These
     3,  3,  3,  3,  3,  3,  3,  3,  4,  4,  4,  4,  4,  4,  4,  4, // comments
     5,  5,  5,  5,  5,  5,  5,  5,  5,  6,  6,  6,  6,  6,  6,  6, // prevent
     6,  6,  6,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  8,  8,  8, // clang-format
     8,  8,  8,  8,  8,  8,  8,  9,  9,  9,  9,  9,  9,  9,  9,  9, // from
     9,  9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, // reflowing
    11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, // these
    12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, // sixteen
    13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, // 16-item
    15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, // lines,
    16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, // which
    17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, // in my
    19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, // opinion,
    21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, // improves
    23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, // the
    25, 26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 29, 29, 30, 30, 31, // readability!
};
Code used to generate this: ```rs const CURVE: [u8; 32] = [ 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, ]; fn main() { for i in 0..=255 { let rev = match CURVE.binary_search(&i) { Ok(idx) => idx, Err(insert_idx) => { let lower = CURVE[insert_idx - 1]; let higher = CURVE[insert_idx]; // Linearly interpolate between these two. if i - lower < higher - i { insert_idx - 1 } else { insert_idx } } }; print!("{rev:2}, "); if i % 16 == 15 { println!(); } } } ```
aaaaaa123456789 commented 10 months ago

Just by looking at the data, I can't recognize the function itself, but the second derivative is small enough that a linear interpolation shouldn't be too far off.

ISSOtm commented 10 months ago

I got a polynomial regression of -0.0134x³ + 0.5484x² + 4.2072x - 5.3585 to match with R² = 0.9998. I don't know if that helps.

Rangi42 commented 10 months ago

I was slightly lazy and linearly interpolated between those points:

That curve is still the reverse of what we need. It should be identical to the reverse_curve above, but with the __s filled in.

I generated one with NumPy; here's the code:

curve.py ```python #!/usr/bin/env python3 import numpy # Copied from https://github.com/LIJI32/SameBoy/commit/65dd02cc#diff-34ffdb0f13b7232c28e80c9e78e86d5ea95bc08ca0a45d8c3ab195f18e4e1921R230 xs = [0,2,4,7,12,18,25,34,42,52,62,73,85,97,109,121,134,146,158,170,182,193,203,213,221,230,237,243,248,251,253,255] ys = [(v << 3) | (v >> 2) for v in range(32)] degree = 9 # minimum need to not have errors greater than 1 fit = numpy.polynomial.polynomial.polyfit(xs, ys, deg=degree) def interpolate(x): return int(round(sum(fit[i]*x**i for i in range(degree + 1)))) print('x', 'y', 'interpolated', sep='\t') for x, y in zip(xs, ys): interpolated = interpolate(x) error = ['!'] if abs(y - interpolated) > 1 else [] print(*[x, y, interpolated, *error], sep='\t') print() data = [(ys[xs.index(x)] if x in xs else interpolate(x)) >> 3 for x in range(256)] comments = ['These', 'comments', 'prevent', 'clang-format', 'from', 'reflowing', 'these', 'sixteen', '16-item', 'lines,', 'which,', 'in', 'my', 'opinion,', 'help', 'visualization!',] print('static std::array reverse_curve{') for i in range(16): print(f" {', '.join(f'{v:2}' for v in data[i*16:i*16+16])}, // {comments[i]}") print('};') ```

This is the completely interpolated array:

static std::array<uint8_t, 256> reverse_curve{
    0,  0,  1,  1,  2,  2,  2,  3,  3,  3,  3,  4,  4,  4,  4,  4, // These
    4,  5,  5,  5,  5,  5,  5,  5,  6,  6,  6,  6,  6,  6,  6,  6, // comments
    7,  7,  7,  7,  7,  7,  7,  7,  7,  8,  8,  8,  8,  8,  8,  8, // prevent
    8,  8,  9,  9,  9,  9,  9,  9,  9,  9,  9, 10, 10, 10, 10, 10, // clang-format
    10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, // from
    12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, // reflowing
    13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, // these
    14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, // sixteen
    16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, // 16-item
    17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, // lines,
    18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, // which,
    20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, // in
    21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, // my
    23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, // opinion,
    25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 27, // help
    27, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 30, 30, 31, 31, // visualization!
};
Rangi42 commented 10 months ago

Hmm, that's based on an old curve; https://github.com/LIJI32/SameBoy/blob/master/Core/display.c#L257 is different.

When I use those values for xs in the above curve.py script, I get this reverse curve:

static std::array<uint8_t, 256> reverse_curve{
    0,  0,  0,  0,  0,  0,  1,  1,  1,  1,  1,  1,  2,  2,  2,  2, // These
    2,  2,  2,  2,  3,  3,  3,  3,  3,  3,  3,  3,  4,  4,  4,  4, // comments
    4,  4,  4,  4,  5,  5,  5,  5,  5,  5,  5,  5,  6,  6,  6,  6, // prevent
    6,  6,  6,  6,  6,  7,  7,  7,  7,  7,  7,  7,  7,  7,  7,  8, // clang-format
    8,  8,  8,  8,  8,  8,  8,  8,  8,  8,  9,  9,  9,  9,  9,  9, // from
    9,  9,  9,  9,  9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, // reflowing
    10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, // these
    12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, // sixteen
    13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, // 16-item
    15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, // lines,
    16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, // which,
    17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, // in
    19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, // my
    21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, // opinion,
    23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, // help
    26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 28, 29, 29, 30, 30, 31, // visualization!
};

I've updated #1241 to use this data.

Rangi42 commented 10 months ago

If that's still desaturated, then swapping xs and ys gives this:

static std::array<uint8_t, 256> reverse_curve{
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  1,  1,  1,  1, // These
    1,  1,  1,  1,  2,  2,  2,  2,  2,  2,  2,  2,  2,  3,  3,  3, // comments
    3,  3,  3,  3,  3,  4,  4,  4,  4,  4,  4,  4,  5,  5,  5,  5, // prevent
    5,  5,  5,  6,  6,  6,  6,  6,  6,  7,  7,  7,  7,  7,  7,  7, // clang-format
    7,  8,  8,  8,  8,  8,  8,  9,  9,  9,  9,  9, 10, 10, 10, 10, // from
    10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 13, 13, 13, // reflowing
    13, 13, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 16, 16, 16, // these
    16, 16, 16, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 19, 19, // sixteen
    19, 19, 19, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 22, // 16-item
    22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, // lines,
    24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, // which,
    26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, // in
    28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, // my
    29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, // opinion,
    31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, // help
    31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, // visualization!
};
Rangi42 commented 10 months ago

Testing all the possible colors:

Without a reverse curve: raw

With the first curve: rev1

With the second curve: rev2

Code:

colors.py ```python #!/usr/bin/env python3 import sys import png mode = sys.argv[1] if len(sys.argv) > 1 else 'raw' if mode not in {'raw', 'rev1', 'rev2'}: print(f'Usage: {sys.argv[0]} (raw | rev1 | rev2)', file=sys.stderr) exit(1) def up(c): if mode == 'rev1': # https://github.com/gbdev/rgbds/issues/1226#issuecomment-1809210877 c = [ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 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, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 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, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 28, 29, 29, 30, 30, 31, ][(c << 3) | (c >> 2)] elif mode == 'rev2': # https://github.com/gbdev/rgbds/issues/1226#issuecomment-1809231223 c = [ 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, 19, 19, 19, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, ][(c << 3) | (c >> 2)] return (c << 3) | (c >> 2) ch_bits = 5 ch_size = 2**ch_bits # 32 width_sq = 8 height_sq = 4 assert width_sq * height_sq == ch_size width_px = width_sq * ch_size # 256 height_px = height_sq * ch_size # 128 rows = [[(0, 0, 0)] * width_px for _ in range(height_px)] for r in range(32): for g in range(32): for b in range(32): x = (r % width_sq) * ch_size + b y = (r // width_sq) * ch_size + g rows[y][x] = (up(r), up(g), up(b)) rows = [sum(row, ()) for row in rows] writer = png.Writer(width_px, height_px, greyscale=False, bitdepth=8, compression=9) with open(f'colors_{mode}.png', 'wb') as file: writer.write(file, rows) ```

On that basis I've updated the PR to use rev2.

Rangi42 commented 10 months ago

Hmm, so SameBoy's "Harsh reality" color correction, which is presumably the closest to real GBC hardware, does more than just map through that curve array. Here's the reduced code for CGB with GB_COLOR_CORRECTION_LOW_CONTRAST:

static inline uint8_t scale_channel_with_curve(uint8_t x)
{
    return inline_const(uint8_t[], {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})[x];
}

// given r,g,b in 0-31 range, update their values to be harshly realistic
r = scale_channel_with_curve(r);
g = scale_channel_with_curve(g);
b = scale_channel_with_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);
}
uint8_t new_r = r * 15 / 16 + (    g + b) / 32;
uint8_t new_g = g * 15 / 16 + (r   +   b) / 32;
uint8_t new_b = b * 15 / 16 + (r + g    ) / 32;
r = new_r;
g = new_g;
b = new_b;
r = r * (162 - 45) / 255 + 45;
g = g * (167 - 41) / 255 + 41;
b = b * (157 - 38) / 255 + 38;
r >>= 3;
g >>= 3;
b >>= 3;

Maybe we should figure out a more complex reverse transformation than just a per-channel lookup table.

Rangi42 commented 10 months ago

This will print out all 32,768 raw->scaled colors. Ideally we'd want to reverse-map every color on the right to its original on the left.

#include <stdio.h>
#include <math.h>

int main() {
    static int scale_channel_with_curve[32] = {
        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
    };
    for (int raw_r = 0; raw_r < 32; raw_r++) {
        for (int raw_g = 0; raw_g < 32; raw_g++) {
            for (int raw_b = 0; raw_b < 32; raw_b++) {
                int scaled_r = scale_channel_with_curve[raw_r];
                int scaled_g = scale_channel_with_curve[raw_g];
                int scaled_b = scale_channel_with_curve[raw_b];
                if (scaled_g != scaled_b) {
                    scaled_g = round(pow((pow(scaled_g / 255.0, 2.2) * 3 +
                        pow(scaled_b / 255.0, 2.2)) / 4, 1 / 2.2) * 255);
                }
                int new_r = scaled_r * 15 / 16 + (scaled_g + scaled_b) / 32;
                int new_g = scaled_g * 15 / 16 + (scaled_r + scaled_b) / 32;
                int new_b = scaled_b * 15 / 16 + (scaled_r + scaled_g) / 32;
                new_r = new_r * (162 - 45) / 255 + 45;
                new_g = new_g * (167 - 41) / 255 + 41;
                new_b = new_b * (157 - 38) / 255 + 38;
                new_r >>= 3;
                new_g >>= 3;
                new_b >>= 3;
                printf("%02d,%02d,%02d -> %02d,%02d,%02d\n",
                    raw_r, raw_g, raw_b, new_r, new_g, new_b);
            }
        }
    }
    return 0;
}
Rangi42 commented 10 months ago

These are the ranges of the harsh-reality-scaled values:

min R: [05,05,06,06,07,07,08,08,09,09,10,10,11,12,12,13,14,14,15,15,16,16,17,17,18,18,18,18,19,19,19,19]
max R: [06,06,07,07,07,08,08,09,09,10,11,11,12,13,13,14,15,15,16,16,17,17,18,18,18,19,19,19,19,20,20,20]

min G: [05,05,05,06,06,06,07,07,08,08,09,10,10,11,12,12,13,13,14,14,15,15,16,16,16,17,17,17,17,17,18,18]
max G: [13,13,13,13,13,14,14,14,14,14,14,15,15,15,16,16,17,17,17,18,18,18,19,19,19,20,20,20,20,20,20,20]

min B: [04,05,05,05,06,06,07,07,08,08,09,10,10,11,12,12,13,14,14,15,15,16,16,17,17,17,18,18,18,18,18,18]
max B: [05,05,06,06,07,07,08,08,09,09,10,11,11,12,13,13,14,15,15,16,16,17,17,17,18,18,18,19,19,19,19,19]

The red and blue channels could use lookup tables and only be off by +/- 1, but green needs more complex logic.

LIJI32 commented 10 months ago

I honestly think it's better to use "Modern – Accurate" rather than Harsh Reality in this context. Reversing the "Harsh Reality" color transformation would cause way too much color clipping due to the harshly reduced contrast.

Rangi42 commented 10 months ago

Okay, the GB_COLOR_CORRECTION_MODERN_ACCURATE transformation looks easier to just invert, since it doesn't have the step where each channel gets lerped with 1/16th of the others. Although there's still the way that green depends on blue in the gamma-based formula. But if we ignore that, then it really is just the inverted scale_channel_with_curve table which I've already implemented.

uint8_t r = (color) & 0x1F;
uint8_t g = (color >> 5) & 0x1F;
uint8_t b = (color >> 10) & 0x1F;
r = scale_channel_with_curve(r);
g = scale_channel_with_curve(g);
b = scale_channel_with_curve(b);
uint8_t new_r = r;
uint8_t new_g = g != b ? round(pow((pow(g / 255.0, 2.2) * 3 + pow(b / 255.0, 2.2)) / 4, 1 / 2.2) * 255) : g;
uint8_t new_b = b;
r = new_r;
g = new_g;
b = new_b;

Edit: inverting the gamma step:

g = pow((pow(new_g / 31, 2.2) * 4 - pow(new_b / 31, 2.2)) / 3, 1 / 2.2) * 31;