AuburnSounds / gamut

Image encoding and decoding library for D. Detailed layout control. Experimental codec QOIX.
Boost Software License 1.0
41 stars 2 forks source link

Difference of RGBA8 to SDL’s RGBA8888 #55

Closed 0xEAB closed 1 year ago

0xEAB commented 1 year ago

SDL2 and Gamut don’t seem to agree on their interpretation of RGBA8 (or “RGBA8888” in SDL). When you consider one to be 0xRR_GG_BB_AA the other appears like 0xAA_BB_GG_RR.

Reproducer

alias Pixel = uint;

Pixel[] loadSDL2(const(char)* path) {
    import bindbc.sdl;

    loadSDL();
    loadSDLImage();

    SDL_PixelFormat* pixelFormat = SDL_AllocFormat(SDL_PIXELFORMAT_RGBA8888);
    scope (exit)
        SDL_FreeFormat(pixelFormat);

    SDL_Surface* surface = IMG_Load(path);
    scope (exit)
        SDL_FreeSurface(surface);

    SDL_Surface* surfaceConverted = SDL_ConvertSurface(surface, pixelFormat, 0);
    scope (exit)
        SDL_FreeSurface(surfaceConverted);

    immutable size = surfaceConverted.w * surfaceConverted.h;
    return (cast(Pixel*) surfaceConverted.pixels)[0 .. size].dup;

}

Pixel[] loadGamut(string path) {
    import gamut;

    enum flags = LAYOUT_GAPLESS | LAYOUT_VERT_STRAIGHT | LOAD_RGB | LOAD_ALPHA | LOAD_8BIT;

    Image img;
    img.loadFromFile(path, flags);
    assert(img.isValid);

    return (cast(Pixel[])(cast(void[]) img.allPixelsAtOnce)).dup;
}

void main() {
    import std.stdio;

    Pixel[] sdl2 = loadSDL2("gamut_test.png");
    Pixel[] gamut = loadGamut("gamut_test.png");

    foreach (px; sdl2)
        writef!"%08X "(px);
    writeln();
    foreach (px; gamut)
        writef!"%08X "(px);
    writeln();
}
000000FF 000000FF FFFFFFFF FFFFFFFF FF009980 FF009980 77DD44B3 77DD44B3 3344AAFF 3344AAFF 99887766 99887766
FF000000 FF000000 FFFFFFFF FFFFFFFF 809900FF 809900FF B344DD77 B344DD77 FFAA4433 FFAA4433 66778899 66778899

Test image

Resolution: 12×1 Format: PNG-8

gamut_test.png Link

Findings

What Gamut calls PixelType.rgba8 is SDL_PIXELFORMAT_ABGR8888 in SDL2.

-    SDL_PixelFormat* pixelFormat = SDL_AllocFormat(SDL_PIXELFORMAT_RGBA8888);
+    SDL_PixelFormat* pixelFormat = SDL_AllocFormat(SDL_PIXELFORMAT_ABGR8888);
FF000000 FF000000 FFFFFFFF FFFFFFFF 809900FF 809900FF B344DD77 B344DD77 FFAA4433 FFAA4433 66778899 66778899
FF000000 FF000000 FFFFFFFF FFFFFFFF 809900FF 809900FF B344DD77 B344DD77 FFAA4433 FFAA4433 66778899 66778899
p0nce commented 1 year ago

Well; in that case it seems to me SDL is wrong.

which means it also invert order for 16-bit and 32-bit float components?

image

What I intended for gamut is that it would be "R then G then B then A" in memory order, anything else will inevitably create confusion in my opinion. Compare length of discussion in Skia vs SDL.

I'm surprised by the extent to which libraries do support ABGR layout though, but that is a separate question. Not sure why it would be more efficient, in what situations. Windows GDI takes BGR order, and .bmp, but that's not a very important use case nowadays?

0xEAB commented 1 year ago

Thank you for the detailed response. I feel flattered by all the effort put into answering this. I don’t think this gives it its due, but I’m not sure what more I could add of value than “thanks a lot!”

p0nce commented 1 year ago

Thanks :) A separate issue would be: should Gamut support bgra8 / argb8 / abgr8 ? What is your use case?

0xEAB commented 1 year ago

TL;DR to load data for 2D software rendering.

--

While I currently use SDL2 to bring my images to screen, I’d like to eventually replace it with a more modest solution. So I’d prefer not to depend on it for image decoding as well. That brought me to Gamut.

For my use case it’s enough to understand which image formats are expected resp provided by the libraries I use. Now that I know that I can expect Gamut to produce pixels as [R, G, B, A], I have essentially all information that I needed.

My code loads the image using Gamut, copies the data to GC memory for further use and disposes of the Gamut object. I don’t rely on Gamut for any sort of image manipulation. (I might add screenshot functionality that uses Gamut to save the image in the future.)

I currently don’t see much value in Gamut supporting other 8-bit channel R/G/B/A pixel formats. It’s quite easy to convert them in user code if necessary. And while I can speak for my own code only, I think it’s easier when you only have to deal with one pixel format (or a few) throughout the codebase.

The use-case for the ABGR/XBGR formats that I could see is writing colors as integer literals in little-endian code, e.g. 0xFF__CC_33__44 or 0x__CC_33_44. What that’s relevant in real-world code is a different question however…

p0nce commented 1 year ago

Interverting bytes should be almost free vs memory access. Yes indeed less pixel formats simplify things here.

0xEAB commented 1 year ago

Also I’ve started to experiment with this change:

- alias Pixel = uint;
+ alias Pixel = ubyte[4];

And I think it makes my code more comprehensible (while it seems to have no effect on codegen).

…until I accidentally sped up my code with this change:

- //alias Pixel = uint;
- //const(ubyte[]) pxSrc = (cast(const(ubyte)*)&src[idx])[0 .. 4];
- //immutable alphaSrc = pxSrc[3] | (pxSrc[3] << 8);

- immutable alphaSrc = src[idx][3] | (src[idx][3] << 8);
+ const(Pixel) pxSrc = src[idx];
+ immutable alphaSrc = pxSrc[3] | (pxSrc[3] << 8);
0xEAB commented 1 year ago

I’d think, we can close this, right?