From 4f384f94504a2027febc8b5cc3e2b3e3034c0b23 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 00:16:09 +0100 Subject: Skybox image: decode PNG ourselves to avoid symbol interposition client.so, engine.so and vguimatsurface.so all export their own png_create_read_struct. Loading the system libpng16 made its internal calls bind to the game's older libpng, whose png_struct layout differs, so png_safe_warning read a NULL error_ptr and crashed. Replace libpng with a small PNG reader: zlib's uncompress (dlopen'ed) plus the five PNG row filters, supporting 8-bit non-interlaced RGB/RGBA/gray/palette images with tRNS alpha for palettes. Output verified byte-identical to libpng for the skybox.png in use. --- src/world.cpp | 235 ++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 194 insertions(+), 41 deletions(-) (limited to 'src') diff --git a/src/world.cpp b/src/world.cpp index 391ab1a..621e5f7 100644 --- a/src/world.cpp +++ b/src/world.cpp @@ -348,64 +348,217 @@ void fog_update() { namespace { -struct PngImage { - void *opaque; - uint32_t version, width, height, format, flags, colormap_entries, - warning_or_error; - char message[64]; -}; - -bool load_png_rgba(const char *path, std::vector &pixels, - int &w, int &h) { +// The game modules export their own (older) libpng symbols, so loading the +// system libpng16 makes its internal calls bind to the game's libpng with a +// different png_struct layout (crash in png_safe_warning). Decode the PNG +// ourselves with zlib instead: no symbol interposition, no extra dependency. +using UncompressFn = int (*)(unsigned char *, unsigned long *, + const unsigned char *, unsigned long); + +UncompressFn zlib_uncompress() { static void *lib = nullptr; static bool tried = false; + static UncompressFn fn = nullptr; if (!tried) { tried = true; - lib = dlopen("libpng16.so.16", RTLD_LAZY | RTLD_GLOBAL); + lib = dlopen("libz.so.1", RTLD_LAZY | RTLD_GLOBAL); if (!lib) - lib = dlopen("libpng16.so", RTLD_LAZY | RTLD_GLOBAL); + lib = dlopen("libz.so", RTLD_LAZY | RTLD_GLOBAL); + if (lib) + fn = (UncompressFn)dlsym(lib, "uncompress"); } - if (!lib) { - logf("skybox: libpng16 not available"); + return fn; +} + +uint32_t be32(const unsigned char *p) { + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | p[3]; +} + +int paeth(int a, int b, int c) { + int p = a + b - c; + int pa = p > a ? p - a : a - p; + int pb = p > b ? p - b : b - p; + int pc = p > c ? p - c : c - p; + if (pa <= pb && pa <= pc) + return a; + return pb <= pc ? b : c; +} + +bool load_png_rgba(const char *path, std::vector &pixels, + int &out_w, int &out_h) { + FILE *f = fopen(path, "rb"); + if (!f) { + logf("skybox: cannot open %s", path); + return false; + } + fseek(f, 0, SEEK_END); + long fsz = ftell(f); + fseek(f, 0, SEEK_SET); + if (fsz <= 0 || fsz > 32 * 1024 * 1024) { + fclose(f); + logf("skybox: %s has an unsupported file size", path); + return false; + } + std::vector file((size_t)fsz); + size_t got = fread(file.data(), 1, file.size(), f); + fclose(f); + if (got != file.size()) { + logf("skybox: short read on %s", path); return false; } - using BeginFn = int (*)(PngImage *, const char *); - using FinishFn = int (*)(PngImage *, void *, void *, int32_t, void *); - using FreeFn = void (*)(PngImage *); - static BeginFn begin = - (BeginFn)dlsym(lib, "png_image_begin_read_from_file"); - static FinishFn finish = (FinishFn)dlsym(lib, "png_image_finish_read"); - static FreeFn free_img = (FreeFn)dlsym(lib, "png_image_free"); - if (!begin || !finish || !free_img) { - logf("skybox: libpng16 simplified API missing"); + + static const unsigned char sig[8] = {0x89, 'P', 'N', 'G', '\r', + '\n', 0x1a, '\n'}; + if (file.size() < 8 || memcmp(file.data(), sig, 8) != 0) { + logf("skybox: %s is not a PNG", path); return false; } - PngImage img{}; - img.version = 1; // PNG_IMAGE_VERSION - img.format = 0x03; // PNG_FORMAT_RGBA - if (!begin(&img, path)) { - logf("skybox: cannot open %s (%s)", path, - img.message[0] ? img.message : "read error"); + uint32_t w = 0, h = 0; + int depth = 0, ctype = -1, comp = 0, filt = 0, inter = 0; + bool have_ihdr = false; + std::vector idat, plte, trns; + size_t pos = 8; + while (pos + 12 <= file.size()) { + uint32_t len = be32(file.data() + pos); + pos += 4; + const unsigned char *type = file.data() + pos; + pos += 4; + if ((size_t)len > file.size() - pos - 4) + break; + const unsigned char *data = file.data() + pos; + if (memcmp(type, "IHDR", 4) == 0 && len >= 13) { + w = be32(data); + h = be32(data + 4); + depth = data[8]; + ctype = data[9]; + comp = data[10]; + filt = data[11]; + inter = data[12]; + have_ihdr = true; + } else if (memcmp(type, "PLTE", 4) == 0) { + plte.assign(data, data + len); + } else if (memcmp(type, "tRNS", 4) == 0) { + trns.assign(data, data + len); + } else if (memcmp(type, "IDAT", 4) == 0) { + idat.insert(idat.end(), data, data + len); + } else if (memcmp(type, "IEND", 4) == 0) { + break; + } + pos += (size_t)len + 4; // data + CRC + } + + if (!have_ihdr || w == 0 || h == 0 || w > 8192 || h > 8192) { + logf("skybox: %s has an invalid PNG header", path); + return false; + } + if (depth != 8 || comp != 0 || filt != 0 || inter != 0) { + logf("skybox: %s: only 8-bit non-interlaced PNGs are supported", path); return false; } - if (img.width == 0 || img.height == 0 || img.width > 8192 || - img.height > 8192) { - logf("skybox: %s has unsupported size %ux%u", path, img.width, img.height); - free_img(&img); + int channels = ctype == 0 ? 1 + : ctype == 2 ? 3 + : ctype == 3 ? 1 + : ctype == 4 ? 2 + : ctype == 6 ? 4 + : 0; + if (channels == 0 || idat.empty()) { + logf("skybox: %s: unsupported PNG colour type %d", path, ctype); return false; } - pixels.assign((size_t)img.width * img.height * 4, 255); - if (!finish(&img, nullptr, pixels.data(), 0, nullptr)) { - logf("skybox: cannot decode %s (%s)", path, - img.message[0] ? img.message : "decode error"); - pixels.clear(); - free_img(&img); + if (ctype == 3 && plte.size() < 3) { + logf("skybox: %s: paletted PNG without a palette", path); return false; } - w = (int)img.width; - h = (int)img.height; - free_img(&img); + + size_t stride = (size_t)w * (size_t)channels; + size_t raw_size = (stride + 1) * (size_t)h; + std::vector raw(raw_size); + unsigned long dest_len = (unsigned long)raw_size; + UncompressFn uncompress_fn = zlib_uncompress(); + if (!uncompress_fn) { + logf("skybox: zlib unavailable"); + return false; + } + if (uncompress_fn(raw.data(), &dest_len, idat.data(), + (unsigned long)idat.size()) != 0 || + dest_len < raw_size) { + logf("skybox: %s: zlib could not inflate the image data", path); + return false; + } + + std::vector scan(stride * (size_t)h); + int bpp = channels; + for (uint32_t y = 0; y < h; y++) { + const unsigned char *src = raw.data() + (size_t)y * (stride + 1); + unsigned char ft = src[0]; + const unsigned char *in = src + 1; + unsigned char *out = scan.data() + (size_t)y * stride; + const unsigned char *prev = + y ? scan.data() + (size_t)(y - 1) * stride : nullptr; + for (size_t x = 0; x < stride; x++) { + int a = x >= (size_t)bpp ? out[x - (size_t)bpp] : 0; + int b = prev ? prev[x] : 0; + int c = (prev && x >= (size_t)bpp) ? prev[x - (size_t)bpp] : 0; + int v = in[x]; + switch (ft) { + case 0: break; + case 1: v += a; break; + case 2: v += b; break; + case 3: v += (a + b) >> 1; break; + case 4: v += paeth(a, b, c); break; + default: + logf("skybox: %s: bad PNG row filter %d", path, (int)ft); + return false; + } + out[x] = (unsigned char)(v & 0xff); + } + } + + pixels.resize((size_t)w * (size_t)h * 4); + for (uint32_t y = 0; y < h; y++) { + const unsigned char *in = scan.data() + (size_t)y * stride; + unsigned char *o = pixels.data() + (size_t)y * (size_t)w * 4; + for (uint32_t x = 0; x < w; x++) { + switch (ctype) { + case 6: + o[0] = in[0]; o[1] = in[1]; o[2] = in[2]; o[3] = in[3]; + in += 4; + break; + case 2: + o[0] = in[0]; o[1] = in[1]; o[2] = in[2]; o[3] = 255; + in += 3; + break; + case 0: + o[0] = o[1] = o[2] = in[0]; o[3] = 255; + in += 1; + break; + case 4: + o[0] = o[1] = o[2] = in[0]; o[3] = in[1]; + in += 2; + break; + case 3: { + unsigned idx = in[0]; + in += 1; + if ((size_t)idx * 3 + 2 < plte.size()) { + o[0] = plte[(size_t)idx * 3]; + o[1] = plte[(size_t)idx * 3 + 1]; + o[2] = plte[(size_t)idx * 3 + 2]; + o[3] = (size_t)idx < trns.size() ? trns[idx] : 255; + } else { + o[0] = o[1] = o[2] = 0; + o[3] = 255; + } + break; + } + } + o += 4; + } + } + + out_w = (int)w; + out_h = (int)h; return true; } -- cgit v1.2.3