From a100a434e41eea45202b062fb2514f7dac7c3940 Mon Sep 17 00:00:00 2001 From: Samuel Gomes <47574584+a740g@users.noreply.github.com> Date: Sat, 17 Sep 2022 08:53:32 +0530 Subject: [PATCH 1/3] Fix #175 --- Makefile | 1 + internal/c/libqb.cpp | 7 +- internal/c/libqb/include/image.h | 58 ++++ internal/c/parts/video/image/build.mk | 14 + internal/c/parts/video/image/image.cpp | 408 +++++++++++++++++++++++++ internal/c/parts/video/image/src.c | 333 -------------------- 6 files changed, 482 insertions(+), 339 deletions(-) create mode 100644 internal/c/libqb/include/image.h create mode 100644 internal/c/parts/video/image/build.mk create mode 100644 internal/c/parts/video/image/image.cpp delete mode 100644 internal/c/parts/video/image/src.c diff --git a/Makefile b/Makefile index 2ea264355..07dd61d65 100644 --- a/Makefile +++ b/Makefile @@ -186,6 +186,7 @@ include $(PATH_INTERNAL_C)/parts/audio/build.mk include $(PATH_INTERNAL_C)/parts/core/build.mk include $(PATH_INTERNAL_C)/parts/input/game_controller/build.mk include $(PATH_INTERNAL_C)/parts/video/font/ttf/build.mk +include $(PATH_INTERNAL_C)/parts/video/image/build.mk .PHONY: all clean diff --git a/internal/c/libqb.cpp b/internal/c/libqb.cpp index 761d05f73..6fc2a1273 100644 --- a/internal/c/libqb.cpp +++ b/internal/c/libqb.cpp @@ -24,6 +24,7 @@ #include "mutex.h" #include "audio.h" +#include "image.h" int32 disableEvents = 0; @@ -24403,12 +24404,6 @@ int32 func__newimage(int32 x, int32 y, int32 bpp, int32 passed) { return -i; } -#ifdef QB64_BACKSLASH_FILESYSTEM -# include "parts\\video\\image\\src.c" -#else -# include "parts/video/image/src.c" -#endif - int32 func__copyimage(int32 i, int32 mode, int32 passed) { static int32 i2, bytes; static img_struct *s, *d; diff --git a/internal/c/libqb/include/image.h b/internal/c/libqb/include/image.h new file mode 100644 index 000000000..f0cb5a208 --- /dev/null +++ b/internal/c/libqb/include/image.h @@ -0,0 +1,58 @@ +//---------------------------------------------------------------------------------------------------- +// ___ ___ __ _ _ ___ ___ ___ _ _ _ +// / _ \| _ ) / /| | || _ \ __| |_ _|_ __ __ _ __ _ ___ | | (_) |__ _ _ __ _ _ _ _ _ +// | (_) | _ \/ _ \_ _| _/ _| | || ' \/ _` / _` / -_) | |__| | '_ \ '_/ _` | '_| || | +// \__\_\___/\___/ |_||_| |___| |___|_|_|_\__,_\__, \___| |____|_|_.__/_| \__,_|_| \_, | +// |___/ |__/ +// +// QB64-PE Image Library +// Powered by stb_image (https://github.com/nothings/stb) & dr_pcx (https://github.com/mackron/dr_pcx) +// +// Copyright (c) 2022 Samuel Gomes +// https://github.com/a740g +// +//----------------------------------------------------------------------------------------------------- + +#pragma once + +//----------------------------------------------------------------------------------------------------- +// HEADER FILES +//----------------------------------------------------------------------------------------------------- +#include +#include +//----------------------------------------------------------------------------------------------------- + +//----------------------------------------------------------------------------------------------------- +// MACROS +//----------------------------------------------------------------------------------------------------- +#if defined(IMAGE_DEBUG) && IMAGE_DEBUG > 0 +# ifdef _MSC_VER +# define IMAGE_DEBUG_PRINT(_fmt_, ...) fprintf(stderr, "DEBUG: %s:%d:%s(): " _fmt_ "\n", __FILE__, __LINE__, __func__, __VA_ARGS__) +# else +# define IMAGE_DEBUG_PRINT(_fmt_, _args_...) fprintf(stderr, "DEBUG: %s:%d:%s(): " _fmt_ "\n", __FILE__, __LINE__, __func__, ##_args_) +# endif +# define IMAGE_DEBUG_CHECK(_exp_) \ + if (!(_exp_)) \ + IMAGE_DEBUG_PRINT("Condition (%s) failed", #_exp_) +#else +# ifdef _MSC_VER +# define IMAGE_DEBUG_PRINT(_fmt_, ...) // Don't do anything in release builds +# else +# define IMAGE_DEBUG_PRINT(_fmt_, _args_...) // Don't do anything in release builds +# endif +# define IMAGE_DEBUG_CHECK(_exp_) // Don't do anything in release builds +#endif +//----------------------------------------------------------------------------------------------------- + +//----------------------------------------------------------------------------------------------------- +// FORWARD DECLARATIONS +//----------------------------------------------------------------------------------------------------- +struct qbs; +//----------------------------------------------------------------------------------------------------- + +//----------------------------------------------------------------------------------------------------- +// FUNCTIONS +//----------------------------------------------------------------------------------------------------- +int32_t func__loadimage(qbs *f, int32_t bpp, int32_t passed); +//----------------------------------------------------------------------------------------------------- +//----------------------------------------------------------------------------------------------------- diff --git a/internal/c/parts/video/image/build.mk b/internal/c/parts/video/image/build.mk new file mode 100644 index 000000000..5634a7ac8 --- /dev/null +++ b/internal/c/parts/video/image/build.mk @@ -0,0 +1,14 @@ + +IMAGE_SRCS := \ + image.cpp + +IMAGE_OBJS := $(patsubst %.cpp,$(PATH_INTERNAL_C)/parts/video/image/%.o,$(IMAGE_SRCS)) + +$(PATH_INTERNAL_C)/parts/video/image/%.o: $(PATH_INTERNAL_C)/parts/video/image/%.cpp + $(CXX) $(CXXFLAGS) -Wall -DDEPENDENCY_CONSOLE_ONLY $< -c -o $@ + +ifdef DEP_IMAGE_CODEC + EXE_LIBS += $(IMAGE_OBJS) +endif + +CLEAN_LIST += $(IMAGE_OBJS) diff --git a/internal/c/parts/video/image/image.cpp b/internal/c/parts/video/image/image.cpp new file mode 100644 index 000000000..fa0643b6f --- /dev/null +++ b/internal/c/parts/video/image/image.cpp @@ -0,0 +1,408 @@ +//---------------------------------------------------------------------------------------------------- +// ___ ___ __ _ _ ___ ___ ___ _ _ _ +// / _ \| _ ) / /| | || _ \ __| |_ _|_ __ __ _ __ _ ___ | | (_) |__ _ _ __ _ _ _ _ _ +// | (_) | _ \/ _ \_ _| _/ _| | || ' \/ _` / _` / -_) | |__| | '_ \ '_/ _` | '_| || | +// \__\_\___/\___/ |_||_| |___| |___|_|_|_\__,_\__, \___| |____|_|_.__/_| \__,_|_| \_, | +// |___/ |__/ +// +// QB64-PE Image Library +// Powered by stb_image (https://github.com/nothings/stb) & dr_pcx (https://github.com/mackron/dr_pcx) +// +// Copyright (c) 2022 Samuel Gomes +// https://github.com/a740g +// +//----------------------------------------------------------------------------------------------------- + +//----------------------------------------------------------------------------------------------------- +// HEADER FILES +//----------------------------------------------------------------------------------------------------- +// Set this to 1 if we want to print debug messages to stderr +#define IMAGE_DEBUG 0 +#include "image.h" +#define DR_PCX_IMPLEMENTATION +#include "dr_pcx.h" +#define STB_IMAGE_IMPLEMENTATION +#include "stb_image.h" +// The below include is a bad idea because of reasons mentioned in https://github.com/QB64-Phoenix-Edition/QB64pe/issues/172 +// However, we need a bunch of things like the 'qbs' and 'image' structs and some more +// We'll likely keep the 'include' this way because I do not want to duplicate stuff and cause issues +// Matt is already doing work to separate and modularize libqb +// So, this will be replaced with relevant stuff once that work is done +#include "../../libqb.h" +//----------------------------------------------------------------------------------------------------- + +//----------------------------------------------------------------------------------------------------- +// CONSTANTS +//----------------------------------------------------------------------------------------------------- +// This is returned to the caller if something goes wrong while loading the image +#define INVALID_IMAGE_HANDLE -1 +//----------------------------------------------------------------------------------------------------- + +//----------------------------------------------------------------------------------------------------- +// MACROS +//----------------------------------------------------------------------------------------------------- +// The byte ordering here are straight from libqb.cpp. So, if libqb.cpp is wrong, then we are wrong! ;) +#define IMAGE_GET_BGRA_RED(c) (uint32_t(c) >> 16 & 0xFF) +#define IMAGE_GET_BGRA_GREEN(c) (uint32_t(c) >> 8 & 0xFF) +#define IMAGE_GET_BGRA_BLUE(c) (uint32_t(c) & 0xFF) +#define IMAGE_GET_BGRA_ALPHA(c) (uint32_t(c) >> 24) +#define IMAGE_MAKE_BGRA(r, g, b, a) (uint32_t((uint8_t(b) | (uint16_t(uint8_t(g)) << 8)) | (uint32_t(uint8_t(r)) << 16) | (uint32_t(uint8_t(a)) << 24))) +// Calculates the RGB distance in the RGB color cube +#define IMAGE_CALCULATE_RGB_DISTANCE(r1, g1, b1, r2, g2, b2) \ + sqrt(((float(r2) - float(r1)) * (float(r2) - float(r1))) + ((float(g2) - float(g1)) * (float(g2) - float(g1))) + \ + ((float(b2) - float(b1)) * (float(b2) - float(b1)))) + +#ifdef QB64_WINDOWS +# define ZERO_VARIABLE(_v_) ZeroMemory(&(_v_), sizeof(_v_)) +#else +# define ZERO_VARIABLE(_v_) memset(&(_v_), 0, sizeof(_v_)) +#endif +//----------------------------------------------------------------------------------------------------- + +//----------------------------------------------------------------------------------------------------- +// FORWARD DECLARATIONS +//----------------------------------------------------------------------------------------------------- +// These should be replaced with appropriate header files when Matt finishes cleaning up libqb +qbs *qbs_new_txt_len(const char *, int32); // Not declared in libqb.h +void sub__freeimage(int32, int32); // Not declared in libqb.h + +extern img_struct *img; // Required by func__loadimage +extern img_struct *write_page; // Required by func__loadimage +extern uint32 palette_256[]; // Required by func__loadimage +//----------------------------------------------------------------------------------------------------- + +//----------------------------------------------------------------------------------------------------- +// FUNCTIONS +//----------------------------------------------------------------------------------------------------- +/// +/// Decodes an image file using the dr_pcx & stb_image libraries. +/// +/// A valid filename +/// Out: width in pixels. This cannot be NULL +/// Out: height in pixels. This cannot be NULL +/// A pointer to the raw pixel data in RGBA format or NULL on failure +static uint8_t *image_decode(const char *fileName, int *xOut, int *yOut) { + auto compOut = 0; + + IMAGE_DEBUG_PRINT("Image dimensions (passed) = (%i, %i)", *xOut, *yOut); + + // Attempt to load file as a PCX first using dr_pcx + auto pixels = drpcx_load_file(fileName, DRPCX_FALSE, xOut, yOut, &compOut, 4); + IMAGE_DEBUG_PRINT("Image dimensions (dr_pcx) = (%i, %i)", *xOut, *yOut); + if (!pixels) { + // If dr_pcx failed to load, then use stb_image + pixels = stbi_load(fileName, xOut, yOut, &compOut, 4); + IMAGE_DEBUG_PRINT("Image dimensions (stb_image) = (%i, %i)", *xOut, *yOut); + if (!pixels) + return nullptr; // Return NULL if all attempts failed + } + + IMAGE_DEBUG_CHECK(compOut > 2); // Returned component should always be 3 or more + + return pixels; +} + +/// +/// Clamps a color channel to the range 0 - 255. +/// +/// The color component +/// The clamped value +static inline uint8_t image_clamp_component(int32_t n) { + n &= -(n >= 0); + return n | ((255 - n) >> 31); +} + +/// +/// This takes in a 32bpp (BGRA) image raw data and spits out an 8bpp raw image along with it's 256 color (BGRA) palette. +/// +/// The source raw image data. This must be in BGRA format and not NULL +/// The widht of the image in pixels +/// The height of the image in pixels +/// A 256 color palette if the operation was successful. This cannot be NULL +/// A pointer to a 8bpp raw image or NULL if operation failed +static uint8_t *image_convert_8bpp(uint8_t *src, int w, int h, uint32_t *paletteOut) { + static struct { + uint32_t r, g, b; + uint32_t count; + } cubes[256]; + + // https://en.wikipedia.org/wiki/Ordered_dithering + static uint8_t bayerMatrix[16] = {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5}; + + IMAGE_DEBUG_PRINT("Converting 32bpp image (%i, %i) to 8bpp", w, h); + + // Allocate memory for new image (8-bit indexed) + auto pixels = (uint8_t *)malloc(w * h); + if (!pixels) { + return nullptr; + } + + ZERO_VARIABLE(cubes); + + // Quantization phase + auto dst = pixels; + for (auto y = 0; y < h; y++) { + for (auto x = 0; x < w; x++) { + int32_t t = bayerMatrix[((y & 3) << 2) + (x & 3)]; + int32_t b = image_clamp_component((*src++) + (t << 1)); + int32_t g = image_clamp_component((*src++) + (t << 1)); + int32_t r = image_clamp_component((*src++) + (t << 1)); + ++src; // Ignore alpha + + // Quantize + uint8_t k = ((r >> 5) << 5) + ((g >> 5) << 2) + (b >> 6); + (*dst++) = k; + + // Prepare RGB cubes for CLUT + cubes[k].r += r; + cubes[k].g += g; + cubes[k].b += b; + cubes[k].count++; + } + } + + // Generate a uniform CLUT based on the quantized colors + for (auto i = 0; i < 256; i++) { + if (cubes[i].count) { + paletteOut[i] = IMAGE_MAKE_BGRA(cubes[i].r / cubes[i].count, cubes[i].g / cubes[i].count, cubes[i].b / cubes[i].count, 0xFF); + } else { + paletteOut[i] = IMAGE_MAKE_BGRA(0, 0, 0, 0xFF); + } + } + + return pixels; +} + +/// +/// This takes in a 32bpp (BGRA) image raw data and spits out an 8bpp raw image along with it's 256 color (BGRA) palette. +/// If the number of unique colors in the 32bpp image > 256, then the functions returns a NULL. +/// Unlike image_convert_8bpp(), no 'real' conversion takes place. +/// +/// The source raw image data. This must be in BGRA format and not NULL +/// The widht of the image in pixels +/// The height of the image in pixels +/// A 256 color palette if the operation was successful. This cannot be NULL +/// A pointer to a 8bpp raw image or NULL if operation failed +static uint8_t *image_make_8bpp(uint8_t *src, int w, int h, uint32_t *paletteOut) { + IMAGE_DEBUG_PRINT("Extracting 8bpp image (%i, %i) from 32bpp", w, h); + + // Allocate memory for new image (8-bit indexed) + auto pixels = (uint8_t *)malloc(w * h); + if (!pixels) { + return nullptr; + } + + int i, j; + auto uniqueColors = 0; // As long as this is <= 256 we will keep going until we are done + auto src32bpp = (uint32_t *)src; + for (i = 0; i < w * h; i++) { + auto srcColor = src32bpp[i]; + + // Check if the src color exists in our palette + for (j = 0; j < uniqueColors; j++) { + if (srcColor == paletteOut[j]) + break; // If we have a match then exit the loop + } + + if (j >= uniqueColors) { + // If we reached here, then the color is not in our table + ++uniqueColors; + if (uniqueColors > 256) { + IMAGE_DEBUG_PRINT("Image has more than 256 unique colors (%i)", uniqueColors); + free(pixels); + return nullptr; // Exit with failure if we have > 256 colors + } + + paletteOut[uniqueColors - 1] = srcColor; // Store the color as unique + pixels[i] = uniqueColors - 1; + } else { + // If we reached here, then the color is in our table + pixels[i] = j; + } + } + + return pixels; +} + +/// +/// This modifies an *8bpp* image 'src' to use 'dst_pal' instead of 'src_pal' +/// +/// A pointer to the 8bpp image pixel data. This modifies data 'src' points to and cannot be NULL +/// The width of the image in pixels +/// The height of the image in pixels +/// The image's original palette. This cannot be NULL +/// The destination palette. This cannot be NULL +static void image_remap_palette(uint8_t *src, int w, int h, uint32_t *src_pal, uint32_t *dst_pal) { + static uint32_t palMap[256]; + + IMAGE_DEBUG_PRINT("Remapping 8bpp image (%i, %i) palette", w, h); + + ZERO_VARIABLE(palMap); + + // Match the palette + for (auto x = 0; x < 256; x++) { + auto oldDist = IMAGE_CALCULATE_RGB_DISTANCE(0, 0, 0, 255, 255, 255); // The farthest we can go in the color cube + for (auto y = 0; y < 256; y++) { + auto newDist = IMAGE_CALCULATE_RGB_DISTANCE(IMAGE_GET_BGRA_RED(src_pal[x]), IMAGE_GET_BGRA_GREEN(src_pal[x]), IMAGE_GET_BGRA_BLUE(src_pal[x]), + IMAGE_GET_BGRA_RED(dst_pal[y]), IMAGE_GET_BGRA_GREEN(dst_pal[y]), IMAGE_GET_BGRA_BLUE(dst_pal[y])); + + if (oldDist > newDist) { + oldDist = newDist; + palMap[x] = y; + } + } + } + + // Update the bitmap to use the matched palette + for (auto c = 0; c < (w * h); c++) { + src[c] = palMap[src[c]]; + } +} + +/// +/// This function loads an image into memory and returns valid LONG image handle values that are less than -1. +/// +/// The filename of the image +/// Mode: 32=32bpp, 33=hardware acclerated 32bpp, 256=8bpp or 257=8bpp without palette remap +/// How many parameters were passed? +/// Valid LONG image handle values that are less than -1 or -1 on failure +int32_t func__loadimage(qbs *fileName, int32_t bpp, int32_t passed) { + // QB string that we'll need null terminate the filename + static qbs *fileNameZ = nullptr; + + if (new_error) + return 0; + + if (!fileNameZ) + fileNameZ = qbs_new(0, 0); + + auto isHardware = false; + auto dontRemapPalette = false; + + // Handle special cases + if (bpp == 33) { + bpp = 32; + isHardware = true; + IMAGE_DEBUG_PRINT("Hardware image requested"); + } else if (bpp == 257) { + bpp = 256; + dontRemapPalette = true; + IMAGE_DEBUG_PRINT("No palette remap requested"); + } + + // Validate bpp + if (passed) { + if ((bpp != 32) && (bpp != 256)) { + error(5); + return 0; + } + } else { + if (write_page->text) { + error(5); + return 0; + } + bpp = -1; + IMAGE_DEBUG_PRINT("BPP was not spcified. Defaulting to 32bpp"); + } + + qbs_set(fileNameZ, qbs_add(fileName, qbs_new_txt_len("\0", 1))); // s1 = filename + CHR$(0) + if (fileNameZ->len == 1) + return INVALID_IMAGE_HANDLE; // Return invalid handle if null length string + + int x, y; + // Try to load the image + auto pixels = image_decode((const char *)fileNameZ->chr, &x, &y); + if (!pixels) + return INVALID_IMAGE_HANDLE; // Return invalid handle if loading the image failed + + IMAGE_DEBUG_PRINT("'%s' successfully loaded", fileNameZ->chr); + + // Convert RGBA to BGRA + auto cp = pixels; + for (auto y2 = 0; y2 < y; y2++) { + for (auto x2 = 0; x2 < x; x2++) { + auto r = cp[0]; + auto b = cp[2]; + cp[0] = b; + cp[2] = r; + cp += 4; + } + } + + int32_t i; // Image handle to be returned + + // Convert image to 8bpp if requested by the user + if (bpp == 256) { + IMAGE_DEBUG_PRINT("Entering 8bpp path"); + + i = func__newimage(x, y, 256, 1); + if (i == -1) { + free(pixels); + return INVALID_IMAGE_HANDLE; + } + + auto palette = (uint32_t *)malloc(256 * sizeof(uint32_t)); // 3 bytes for bgr + 1 for alpha (basically a uint32_t) + if (!palette) { + free(pixels); + return INVALID_IMAGE_HANDLE; + } + + auto pixels256 = image_make_8bpp(pixels, x, y, palette); // Try to simply 'extract' the 8bpp image first + if (!pixels256) { + pixels256 = image_convert_8bpp(pixels, x, y, palette); // If that fails, then 'convert' it to 8bpp + if (!pixels256) { + free(palette); + free(pixels); + return INVALID_IMAGE_HANDLE; + } + } + + if (dontRemapPalette) { + // Copy the 8bpp pixel data and then free it + memcpy(img[-i].offset, pixels256, x * y); + free(pixels256); + + // Copy the palette and then free it + memcpy(img[-i].pal, palette, 256 * sizeof(uint32_t)); + free(palette); + } else { + // Remap the image indexes to QB64 default palette and then free our palette + image_remap_palette(pixels256, x, y, palette, palette_256); + free(palette); + + // Copy the 8bpp pixel data and then free it + memcpy(img[-i].offset, pixels256, x * y); + free(pixels256); + + // Copy the default QB64 palette + memcpy(img[-i].pal, palette_256, 256 * sizeof(uint32_t)); + } + } else { + IMAGE_DEBUG_PRINT("Entering 32bpp path"); + + i = func__newimage(x, y, 32, 1); + if (i == -1) { + free(pixels); + return INVALID_IMAGE_HANDLE; + } + memcpy(img[-i].offset, pixels, x * y * sizeof(uint32_t)); + } + + // Free pixel memory. We can do this because both dr_pcx and stb_image uses free() + free(pixels); + + // This only executes if bpp is 32 + if (isHardware) { + IMAGE_DEBUG_PRINT("Making hardware image"); + + auto iHardware = func__copyimage(i, 33, 1); + sub__freeimage(i, 1); + i = iHardware; + } + + IMAGE_DEBUG_PRINT("Returning handle value = %i", i); + + return i; +} +//----------------------------------------------------------------------------------------------------- +//----------------------------------------------------------------------------------------------------- diff --git a/internal/c/parts/video/image/src.c b/internal/c/parts/video/image/src.c deleted file mode 100644 index 38e0597a6..000000000 --- a/internal/c/parts/video/image/src.c +++ /dev/null @@ -1,333 +0,0 @@ -//---------------------------------------------------------------------------------------------------- -// ___ ___ __ _ _ ___ ___ ___ _ _ _ -// / _ \| _ ) / /| | || _ \ __| |_ _|_ __ __ _ __ _ ___ | | (_) |__ _ _ __ _ _ _ _ _ -// | (_) | _ \/ _ \_ _| _/ _| | || ' \/ _` / _` / -_) | |__| | '_ \ '_/ _` | '_| || | -// \__\_\___/\___/ |_||_| |___| |___|_|_|_\__,_\__, \___| |____|_|_.__/_| \__,_|_| \_, | -// |___/ |__/ -// -// QB64-PE Image Library -// Powered by stb_image (https://github.com/nothings/stb) & dr_pcx (https://github.com/mackron/dr_pcx) -// -// Copyright (c) 2022 Samuel Gomes -// https://github.com/a740g -// -//----------------------------------------------------------------------------------------------------- - -#ifndef DEPENDENCY_IMAGE_CODEC -// Stub(s): -int32_t func__loadimage(qbs *f, int32_t bpp, int32_t passed); - -#else -# define DR_PCX_IMPLEMENTATION -# include "dr_pcx.h" -# define STB_IMAGE_IMPLEMENTATION -# include "stb_image.h" - -// The byte ordering here are straight from libqb.cpp. So, if libqb.cpp is wrong, then we are wrong! ;) -# define IMAGE_GET_BGRA_RED(c) (uint32_t(c) >> 16 & 0xFF) -# define IMAGE_GET_BGRA_GREEN(c) (uint32_t(c) >> 8 & 0xFF) -# define IMAGE_GET_BGRA_BLUE(c) (uint32_t(c) & 0xFF) -# define IMAGE_GET_BGRA_ALPHA(c) (uint32_t(c) >> 24) -# define IMAGE_MAKE_BGRA(r, g, b, a) (uint32_t((uint8_t(b) | (uint16_t(uint8_t(g)) << 8)) | (uint32_t(uint8_t(r)) << 16) | (uint32_t(uint8_t(a)) << 24))) -// Calculates the RGB distance in the RGB color cube -# define IMAGE_CALCULATE_RGB_DISTANCE(r1, g1, b1, r2, g2, b2) \ - sqrt(((float(r2) - float(r1)) * (float(r2) - float(r1))) + ((float(g2) - float(g1)) * (float(g2) - float(g1))) + \ - ((float(b2) - float(b1)) * (float(b2) - float(b1)))) - -/// -/// Decodes a PCX image using the dr_pcx library. -/// -/// A pointer to the file in memory -/// The length of the file -/// Out: bit 1=Success, bit 2=32bit. This cannot be NULL[BGRA] -/// Out: width in pixels. This cannot be NULL -/// Out: height in pixels. This cannot be NULL -/// A pointer to the raw pixel data in RGBA format or NULL on failure -static uint8_t *image_decode_drpcx(uint8_t *content, int32_t bytes, int32_t *result, int32_t *x, int32_t *y) { - auto h = 0, w = 0, comp = 0; - *result = 0; - - auto out = drpcx_load_memory(content, bytes, DRPCX_FALSE, &w, &h, &comp, 4); - if (!out) - return nullptr; - - *result = 1 + 2; - *x = w; - *y = h; - return out; -} - -/// -/// Decodes an image using the stb_image library. -/// -/// A pointer to the file in memory -/// The length of the file -/// Out: bit 1=Success, bit 2=32bit[BGRA]. This cannot be NULL -/// Out: width in pixels. This cannot be NULL -/// Out: height in pixels. This cannot be NULL -/// A pointer to the raw pixel data in RGBA format or NULL on failure -static uint8_t *image_decode_stbi(uint8_t *content, int32_t bytes, int32_t *result, int32_t *x, int32_t *y) { - auto h = 0, w = 0, comp = 0; - *result = 0; - - auto out = stbi_load_from_memory(content, bytes, &w, &h, &comp, 4); - if (!out) - return nullptr; - - *result = 1 + 2; - *x = w; - *y = h; - return out; -} - -/// -/// Clamps a color channel to the range 0 - 255. -/// -/// The color component -/// The clamped value -static inline uint8_t image_clamp_component(int32_t n) { - n &= -(n >= 0); - return n | ((255 - n) >> 31); -} - -/// -/// This takes in a 32bpp (BGRA) image raw data and spits out an 8bpp raw image along with it's 256 color (BGRA) palette. -/// -/// The source raw image data. This must be in BGRA format and not NULL -/// The widht of the image in pixels -/// The height of the image in pixels -/// A 256 color palette if the operation was successful. This cannot be NULL -/// A pointer to a 8bpp raw image or NULL if operation failed -static uint8_t *image_convert_8bpp(uint8_t *src, int32_t w, int32_t h, uint32_t *paletteOut) { - static struct { - uint32_t r, g, b; - uint32_t count; - } cubes[256]; - - // https://en.wikipedia.org/wiki/Ordered_dithering - static uint8_t bayerMatrix[16] = {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5}; - - // Allocate memory for new image (8-bit indexed) - auto pixels = (uint8_t *)malloc(w * h); - if (!pixels) { - return nullptr; - } - - memset(cubes, NULL, sizeof(cubes)); - - // Quantization phase - auto dst = pixels; - for (auto y = 0; y < h; y++) { - for (auto x = 0; x < w; x++) { - int32_t t = bayerMatrix[((y & 3) << 2) + (x & 3)]; - int32_t b = image_clamp_component((*src++) + (t << 1)); - int32_t g = image_clamp_component((*src++) + (t << 1)); - int32_t r = image_clamp_component((*src++) + (t << 1)); - ++src; // Ignore alpha - - // Quantize - uint8_t k = ((r >> 5) << 5) + ((g >> 5) << 2) + (b >> 6); - (*dst++) = k; - - // Prepare RGB cubes for CLUT - cubes[k].r += r; - cubes[k].g += g; - cubes[k].b += b; - cubes[k].count++; - } - } - - // Generate a uniform CLUT based on the quantized colors - for (auto i = 0; i < 256; i++) { - if (cubes[i].count) { - paletteOut[i] = IMAGE_MAKE_BGRA(cubes[i].r / cubes[i].count, cubes[i].g / cubes[i].count, cubes[i].b / cubes[i].count, 0xFF); - } else { - paletteOut[i] = IMAGE_MAKE_BGRA(0, 0, 0, 0xFF); - } - } - - return pixels; -} - -/// -/// This modifies an *8bpp* image 'src' to use 'dst_pal' instead of 'src_pal' -/// -/// A pointer to the 8bpp image pixel data. This modifies data 'src' points to and cannot be NULL -/// The width of the image in pixels -/// The height of the image in pixels -/// The image's original palette. This cannot be NULL -/// The destination palette. This cannot be NULL -static void image_remap_palette(uint8_t *src, int32_t w, int32_t h, uint32_t *src_pal, uint32_t *dst_pal) { - static uint32_t palMap[256]; - - memset(palMap, NULL, sizeof(palMap)); - - // Match the palette - for (auto x = 0; x < 256; x++) { - auto oldDist = IMAGE_CALCULATE_RGB_DISTANCE(0, 0, 0, 255, 255, 255); // The farthest we can go in the color cube - for (auto y = 0; y < 256; y++) { - auto newDist = IMAGE_CALCULATE_RGB_DISTANCE(IMAGE_GET_BGRA_RED(src_pal[x]), IMAGE_GET_BGRA_GREEN(src_pal[x]), IMAGE_GET_BGRA_BLUE(src_pal[x]), - IMAGE_GET_BGRA_RED(dst_pal[y]), IMAGE_GET_BGRA_GREEN(dst_pal[y]), IMAGE_GET_BGRA_BLUE(dst_pal[y])); - - if (oldDist > newDist) { - oldDist = newDist; - palMap[x] = y; - } - } - } - - // Update the bitmap to use the matched palette - for (auto c = 0; c < (w * h); c++) { - src[c] = palMap[src[c]]; - } -} - -/// -/// This function loads an image into memory and returns valid LONG image handle values that are less than -1. -/// -/// The filename of the image -/// Mode: 32=32bpp, 33=hardware acclerated 32bpp, 256=8bpp or 257=8bpp without palette remap -/// How many parameters were passed? -/// Valid LONG image handle values that are less than -1 or -1 on failure -int32_t func__loadimage(qbs *f, int32_t bpp, int32_t passed) { - if (new_error) - return 0; - - auto isHardware = false; - auto dontRemapPalette = false; - - // Handle special cases - if (bpp == 33) { - bpp = 32; - isHardware = true; - } else if (bpp == 257) { - bpp = 256; - dontRemapPalette = true; - } - - // Validate bpp - if (passed) { - if ((bpp != 32) && (bpp != 256)) { - error(5); - return 0; - } - } else { - if (write_page->text) { - error(5); - return 0; - } - bpp = -1; - } - if (!f->len) - return -1; // return invalid handle if null length string - - // Load the file - auto fh = gfs_open(f, 1, 0, 0); - if (fh < 0) - return -1; - auto lof = gfs_lof(fh); - auto content = (uint8 *)malloc(lof); - if (!content) { - gfs_close(fh); - return -1; - } - auto result = gfs_read(fh, -1, content, lof); - gfs_close(fh); - if (result < 0) { - free(content); - return -1; - } - - int32_t x, y; - // Try to load the image using dr_pcx - auto pixels = image_decode_drpcx(content, lof, &result, &x, &y); - // If that failed try loading via stb_image - if (!(result & 1)) { - pixels = image_decode_stbi(content, lof, &result, &x, &y); - } - - // Free the memory holding the file - free(content); - - // Return failure if nothing was able to load the image - if (!(result & 1)) - return -1; - - // Convert RGBA to BGRA - auto cp = pixels; - for (auto y2 = 0; y2 < y; y2++) { - for (auto x2 = 0; x2 < x; x2++) { - auto r = cp[0]; - auto b = cp[2]; - cp[0] = b; - cp[2] = r; - cp += 4; - } - } - - int32_t i; // Image handle to be returned - - // Convert image to 8bpp if requested by the user - if (bpp == 256) { - i = func__newimage(x, y, 256, 1); - if (i == -1) { - free(pixels); - return -1; - } - - auto palette = (uint32_t *)malloc(256 * sizeof(uint32_t)); // 3 bytes for bgr + 1 for alpha (basically a uint32_t) - if (!palette) { - free(pixels); - return -1; - } - - auto pixels256 = image_convert_8bpp(pixels, x, y, palette); - if (!pixels256) { - free(palette); - free(pixels); - return -1; - } - - if (dontRemapPalette) { - // Copy the 8bpp pixel data and then free it - memcpy(img[-i].offset, pixels256, x * y); - free(pixels256); - - // Copy the palette and then free it - memcpy(img[-i].pal, palette, 256 * sizeof(uint32_t)); - free(palette); - } else { - // Remap the image indexes to QB64 default palette and then free our palette - image_remap_palette(pixels256, x, y, palette, palette_256); - free(palette); - - // Copy the 8bpp pixel data and then free it - memcpy(img[-i].offset, pixels256, x * y); - free(pixels256); - - // Copy the default QB64 palette - memcpy(img[-i].pal, palette_256, 256 * sizeof(uint32_t)); - } - } else { - i = func__newimage(x, y, 32, 1); - if (i == -1) { - free(pixels); - return -1; - } - memcpy(img[-i].offset, pixels, x * y * sizeof(uint32_t)); - } - - // Free pixel memory. We can do this because both dr_pcx and stb_image uses free() - free(pixels); - - // This only executes if bpp is 32 - if (isHardware) { - auto iHardware = func__copyimage(i, 33, 1); - sub__freeimage(i, 1); - i = iHardware; - } - - return i; -} - -#endif From 9681aa1d30736ca465d6320d4256d9a7ea294636 Mon Sep 17 00:00:00 2001 From: Samuel Gomes <47574584+a740g@users.noreply.github.com> Date: Sun, 18 Sep 2022 00:03:08 +0530 Subject: [PATCH 2/3] Update code to use C++ unordered_map --- internal/c/parts/video/image/image.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/c/parts/video/image/image.cpp b/internal/c/parts/video/image/image.cpp index fa0643b6f..1ee97affd 100644 --- a/internal/c/parts/video/image/image.cpp +++ b/internal/c/parts/video/image/image.cpp @@ -19,6 +19,7 @@ // Set this to 1 if we want to print debug messages to stderr #define IMAGE_DEBUG 0 #include "image.h" +#include #define DR_PCX_IMPLEMENTATION #include "dr_pcx.h" #define STB_IMAGE_IMPLEMENTATION @@ -186,25 +187,21 @@ static uint8_t *image_convert_8bpp(uint8_t *src, int w, int h, uint32_t *palette static uint8_t *image_make_8bpp(uint8_t *src, int w, int h, uint32_t *paletteOut) { IMAGE_DEBUG_PRINT("Extracting 8bpp image (%i, %i) from 32bpp", w, h); + unordered_map colorMap; + // Allocate memory for new image (8-bit indexed) auto pixels = (uint8_t *)malloc(w * h); if (!pixels) { return nullptr; } - int i, j; auto uniqueColors = 0; // As long as this is <= 256 we will keep going until we are done auto src32bpp = (uint32_t *)src; - for (i = 0; i < w * h; i++) { + for (auto i = 0; i < w * h; i++) { auto srcColor = src32bpp[i]; // Check if the src color exists in our palette - for (j = 0; j < uniqueColors; j++) { - if (srcColor == paletteOut[j]) - break; // If we have a match then exit the loop - } - - if (j >= uniqueColors) { + if (colorMap.find(srcColor) == colorMap.end()) { // If we reached here, then the color is not in our table ++uniqueColors; if (uniqueColors > 256) { @@ -214,10 +211,11 @@ static uint8_t *image_make_8bpp(uint8_t *src, int w, int h, uint32_t *paletteOut } paletteOut[uniqueColors - 1] = srcColor; // Store the color as unique + colorMap[srcColor] = uniqueColors - 1; // Add this color to the map pixels[i] = uniqueColors - 1; } else { // If we reached here, then the color is in our table - pixels[i] = j; + pixels[i] = colorMap[srcColor]; // Simply fetch the index from the map } } From c0e3f3608c508078158cd6347d09c1eec5db2f13 Mon Sep 17 00:00:00 2001 From: Samuel Gomes <47574584+a740g@users.noreply.github.com> Date: Sun, 18 Sep 2022 23:46:54 +0530 Subject: [PATCH 3/3] Add O2 to build.mk --- internal/c/parts/video/image/build.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/c/parts/video/image/build.mk b/internal/c/parts/video/image/build.mk index 5634a7ac8..e75a2740f 100644 --- a/internal/c/parts/video/image/build.mk +++ b/internal/c/parts/video/image/build.mk @@ -5,7 +5,7 @@ IMAGE_SRCS := \ IMAGE_OBJS := $(patsubst %.cpp,$(PATH_INTERNAL_C)/parts/video/image/%.o,$(IMAGE_SRCS)) $(PATH_INTERNAL_C)/parts/video/image/%.o: $(PATH_INTERNAL_C)/parts/video/image/%.cpp - $(CXX) $(CXXFLAGS) -Wall -DDEPENDENCY_CONSOLE_ONLY $< -c -o $@ + $(CXX) -O2 $(CXXFLAGS) -DDEPENDENCY_CONSOLE_ONLY -Wall $< -c -o $@ ifdef DEP_IMAGE_CODEC EXE_LIBS += $(IMAGE_OBJS)