diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-04 00:16:09 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-04 00:16:09 +0100 |
| commit | 4f384f94504a2027febc8b5cc3e2b3e3034c0b23 (patch) | |
| tree | 68491e9e1accbfc822e46187312d1d57a7fb66ed /src | |
| parent | Skybox image: upload via procedural texture, not CreateTextureFromBits (diff) | |
| download | hl2h-4f384f94504a2027febc8b5cc3e2b3e3034c0b23.tar.gz hl2h-4f384f94504a2027febc8b5cc3e2b3e3034c0b23.zip | |
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.
Diffstat (limited to 'src')
| -rw-r--r-- | src/world.cpp | 235 |
1 files changed, 194 insertions, 41 deletions
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() { | |||
| 348 | 348 | ||
| 349 | namespace { | 349 | namespace { |
| 350 | 350 | ||
| 351 | struct PngImage { | 351 | // The game modules export their own (older) libpng symbols, so loading the |
| 352 | void *opaque; | 352 | // system libpng16 makes its internal calls bind to the game's libpng with a |
| 353 | uint32_t version, width, height, format, flags, colormap_entries, | 353 | // different png_struct layout (crash in png_safe_warning). Decode the PNG |
| 354 | warning_or_error; | 354 | // ourselves with zlib instead: no symbol interposition, no extra dependency. |
| 355 | char message[64]; | 355 | using UncompressFn = int (*)(unsigned char *, unsigned long *, |
| 356 | }; | 356 | const unsigned char *, unsigned long); |
| 357 | 357 | ||
| 358 | bool load_png_rgba(const char *path, std::vector<unsigned char> &pixels, | 358 | UncompressFn zlib_uncompress() { |
| 359 | int &w, int &h) { | ||
| 360 | static void *lib = nullptr; | 359 | static void *lib = nullptr; |
| 361 | static bool tried = false; | 360 | static bool tried = false; |
| 361 | static UncompressFn fn = nullptr; | ||
| 362 | if (!tried) { | 362 | if (!tried) { |
| 363 | tried = true; | 363 | tried = true; |
| 364 | lib = dlopen("libpng16.so.16", RTLD_LAZY | RTLD_GLOBAL); | 364 | lib = dlopen("libz.so.1", RTLD_LAZY | RTLD_GLOBAL); |
| 365 | if (!lib) | 365 | if (!lib) |
| 366 | lib = dlopen("libpng16.so", RTLD_LAZY | RTLD_GLOBAL); | 366 | lib = dlopen("libz.so", RTLD_LAZY | RTLD_GLOBAL); |
| 367 | if (lib) | ||
| 368 | fn = (UncompressFn)dlsym(lib, "uncompress"); | ||
| 367 | } | 369 | } |
| 368 | if (!lib) { | 370 | return fn; |
| 369 | logf("skybox: libpng16 not available"); | 371 | } |
| 372 | |||
| 373 | uint32_t be32(const unsigned char *p) { | ||
| 374 | return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | | ||
| 375 | ((uint32_t)p[2] << 8) | p[3]; | ||
| 376 | } | ||
| 377 | |||
| 378 | int paeth(int a, int b, int c) { | ||
| 379 | int p = a + b - c; | ||
| 380 | int pa = p > a ? p - a : a - p; | ||
| 381 | int pb = p > b ? p - b : b - p; | ||
| 382 | int pc = p > c ? p - c : c - p; | ||
| 383 | if (pa <= pb && pa <= pc) | ||
| 384 | return a; | ||
| 385 | return pb <= pc ? b : c; | ||
| 386 | } | ||
| 387 | |||
| 388 | bool load_png_rgba(const char *path, std::vector<unsigned char> &pixels, | ||
| 389 | int &out_w, int &out_h) { | ||
| 390 | FILE *f = fopen(path, "rb"); | ||
| 391 | if (!f) { | ||
| 392 | logf("skybox: cannot open %s", path); | ||
| 393 | return false; | ||
| 394 | } | ||
| 395 | fseek(f, 0, SEEK_END); | ||
| 396 | long fsz = ftell(f); | ||
| 397 | fseek(f, 0, SEEK_SET); | ||
| 398 | if (fsz <= 0 || fsz > 32 * 1024 * 1024) { | ||
| 399 | fclose(f); | ||
| 400 | logf("skybox: %s has an unsupported file size", path); | ||
| 401 | return false; | ||
| 402 | } | ||
| 403 | std::vector<unsigned char> file((size_t)fsz); | ||
| 404 | size_t got = fread(file.data(), 1, file.size(), f); | ||
| 405 | fclose(f); | ||
| 406 | if (got != file.size()) { | ||
| 407 | logf("skybox: short read on %s", path); | ||
| 370 | return false; | 408 | return false; |
| 371 | } | 409 | } |
| 372 | using BeginFn = int (*)(PngImage *, const char *); | 410 | |
| 373 | using FinishFn = int (*)(PngImage *, void *, void *, int32_t, void *); | 411 | static const unsigned char sig[8] = {0x89, 'P', 'N', 'G', '\r', |
| 374 | using FreeFn = void (*)(PngImage *); | 412 | '\n', 0x1a, '\n'}; |
| 375 | static BeginFn begin = | 413 | if (file.size() < 8 || memcmp(file.data(), sig, 8) != 0) { |
| 376 | (BeginFn)dlsym(lib, "png_image_begin_read_from_file"); | 414 | logf("skybox: %s is not a PNG", path); |
| 377 | static FinishFn finish = (FinishFn)dlsym(lib, "png_image_finish_read"); | ||
| 378 | static FreeFn free_img = (FreeFn)dlsym(lib, "png_image_free"); | ||
| 379 | if (!begin || !finish || !free_img) { | ||
| 380 | logf("skybox: libpng16 simplified API missing"); | ||
| 381 | return false; | 415 | return false; |
| 382 | } | 416 | } |
| 383 | 417 | ||
| 384 | PngImage img{}; | 418 | uint32_t w = 0, h = 0; |
| 385 | img.version = 1; // PNG_IMAGE_VERSION | 419 | int depth = 0, ctype = -1, comp = 0, filt = 0, inter = 0; |
| 386 | img.format = 0x03; // PNG_FORMAT_RGBA | 420 | bool have_ihdr = false; |
| 387 | if (!begin(&img, path)) { | 421 | std::vector<unsigned char> idat, plte, trns; |
| 388 | logf("skybox: cannot open %s (%s)", path, | 422 | size_t pos = 8; |
| 389 | img.message[0] ? img.message : "read error"); | 423 | while (pos + 12 <= file.size()) { |
| 424 | uint32_t len = be32(file.data() + pos); | ||
| 425 | pos += 4; | ||
| 426 | const unsigned char *type = file.data() + pos; | ||
| 427 | pos += 4; | ||
| 428 | if ((size_t)len > file.size() - pos - 4) | ||
| 429 | break; | ||
| 430 | const unsigned char *data = file.data() + pos; | ||
| 431 | if (memcmp(type, "IHDR", 4) == 0 && len >= 13) { | ||
| 432 | w = be32(data); | ||
| 433 | h = be32(data + 4); | ||
| 434 | depth = data[8]; | ||
| 435 | ctype = data[9]; | ||
| 436 | comp = data[10]; | ||
| 437 | filt = data[11]; | ||
| 438 | inter = data[12]; | ||
| 439 | have_ihdr = true; | ||
| 440 | } else if (memcmp(type, "PLTE", 4) == 0) { | ||
| 441 | plte.assign(data, data + len); | ||
| 442 | } else if (memcmp(type, "tRNS", 4) == 0) { | ||
| 443 | trns.assign(data, data + len); | ||
| 444 | } else if (memcmp(type, "IDAT", 4) == 0) { | ||
| 445 | idat.insert(idat.end(), data, data + len); | ||
| 446 | } else if (memcmp(type, "IEND", 4) == 0) { | ||
| 447 | break; | ||
| 448 | } | ||
| 449 | pos += (size_t)len + 4; // data + CRC | ||
| 450 | } | ||
| 451 | |||
| 452 | if (!have_ihdr || w == 0 || h == 0 || w > 8192 || h > 8192) { | ||
| 453 | logf("skybox: %s has an invalid PNG header", path); | ||
| 454 | return false; | ||
| 455 | } | ||
| 456 | if (depth != 8 || comp != 0 || filt != 0 || inter != 0) { | ||
| 457 | logf("skybox: %s: only 8-bit non-interlaced PNGs are supported", path); | ||
| 390 | return false; | 458 | return false; |
| 391 | } | 459 | } |
| 392 | if (img.width == 0 || img.height == 0 || img.width > 8192 || | 460 | int channels = ctype == 0 ? 1 |
| 393 | img.height > 8192) { | 461 | : ctype == 2 ? 3 |
| 394 | logf("skybox: %s has unsupported size %ux%u", path, img.width, img.height); | 462 | : ctype == 3 ? 1 |
| 395 | free_img(&img); | 463 | : ctype == 4 ? 2 |
| 464 | : ctype == 6 ? 4 | ||
| 465 | : 0; | ||
| 466 | if (channels == 0 || idat.empty()) { | ||
| 467 | logf("skybox: %s: unsupported PNG colour type %d", path, ctype); | ||
| 396 | return false; | 468 | return false; |
| 397 | } | 469 | } |
| 398 | pixels.assign((size_t)img.width * img.height * 4, 255); | 470 | if (ctype == 3 && plte.size() < 3) { |
| 399 | if (!finish(&img, nullptr, pixels.data(), 0, nullptr)) { | 471 | logf("skybox: %s: paletted PNG without a palette", path); |
| 400 | logf("skybox: cannot decode %s (%s)", path, | ||
| 401 | img.message[0] ? img.message : "decode error"); | ||
| 402 | pixels.clear(); | ||
| 403 | free_img(&img); | ||
| 404 | return false; | 472 | return false; |
| 405 | } | 473 | } |
| 406 | w = (int)img.width; | 474 | |
| 407 | h = (int)img.height; | 475 | size_t stride = (size_t)w * (size_t)channels; |
| 408 | free_img(&img); | 476 | size_t raw_size = (stride + 1) * (size_t)h; |
| 477 | std::vector<unsigned char> raw(raw_size); | ||
| 478 | unsigned long dest_len = (unsigned long)raw_size; | ||
| 479 | UncompressFn uncompress_fn = zlib_uncompress(); | ||
| 480 | if (!uncompress_fn) { | ||
| 481 | logf("skybox: zlib unavailable"); | ||
| 482 | return false; | ||
| 483 | } | ||
| 484 | if (uncompress_fn(raw.data(), &dest_len, idat.data(), | ||
| 485 | (unsigned long)idat.size()) != 0 || | ||
| 486 | dest_len < raw_size) { | ||
| 487 | logf("skybox: %s: zlib could not inflate the image data", path); | ||
| 488 | return false; | ||
| 489 | } | ||
| 490 | |||
| 491 | std::vector<unsigned char> scan(stride * (size_t)h); | ||
| 492 | int bpp = channels; | ||
| 493 | for (uint32_t y = 0; y < h; y++) { | ||
| 494 | const unsigned char *src = raw.data() + (size_t)y * (stride + 1); | ||
| 495 | unsigned char ft = src[0]; | ||
| 496 | const unsigned char *in = src + 1; | ||
| 497 | unsigned char *out = scan.data() + (size_t)y * stride; | ||
| 498 | const unsigned char *prev = | ||
| 499 | y ? scan.data() + (size_t)(y - 1) * stride : nullptr; | ||
| 500 | for (size_t x = 0; x < stride; x++) { | ||
| 501 | int a = x >= (size_t)bpp ? out[x - (size_t)bpp] : 0; | ||
| 502 | int b = prev ? prev[x] : 0; | ||
| 503 | int c = (prev && x >= (size_t)bpp) ? prev[x - (size_t)bpp] : 0; | ||
| 504 | int v = in[x]; | ||
| 505 | switch (ft) { | ||
| 506 | case 0: break; | ||
| 507 | case 1: v += a; break; | ||
| 508 | case 2: v += b; break; | ||
| 509 | case 3: v += (a + b) >> 1; break; | ||
| 510 | case 4: v += paeth(a, b, c); break; | ||
| 511 | default: | ||
| 512 | logf("skybox: %s: bad PNG row filter %d", path, (int)ft); | ||
| 513 | return false; | ||
| 514 | } | ||
| 515 | out[x] = (unsigned char)(v & 0xff); | ||
| 516 | } | ||
| 517 | } | ||
| 518 | |||
| 519 | pixels.resize((size_t)w * (size_t)h * 4); | ||
| 520 | for (uint32_t y = 0; y < h; y++) { | ||
| 521 | const unsigned char *in = scan.data() + (size_t)y * stride; | ||
| 522 | unsigned char *o = pixels.data() + (size_t)y * (size_t)w * 4; | ||
| 523 | for (uint32_t x = 0; x < w; x++) { | ||
| 524 | switch (ctype) { | ||
| 525 | case 6: | ||
| 526 | o[0] = in[0]; o[1] = in[1]; o[2] = in[2]; o[3] = in[3]; | ||
| 527 | in += 4; | ||
| 528 | break; | ||
| 529 | case 2: | ||
| 530 | o[0] = in[0]; o[1] = in[1]; o[2] = in[2]; o[3] = 255; | ||
| 531 | in += 3; | ||
| 532 | break; | ||
| 533 | case 0: | ||
| 534 | o[0] = o[1] = o[2] = in[0]; o[3] = 255; | ||
| 535 | in += 1; | ||
| 536 | break; | ||
| 537 | case 4: | ||
| 538 | o[0] = o[1] = o[2] = in[0]; o[3] = in[1]; | ||
| 539 | in += 2; | ||
| 540 | break; | ||
| 541 | case 3: { | ||
| 542 | unsigned idx = in[0]; | ||
| 543 | in += 1; | ||
| 544 | if ((size_t)idx * 3 + 2 < plte.size()) { | ||
| 545 | o[0] = plte[(size_t)idx * 3]; | ||
| 546 | o[1] = plte[(size_t)idx * 3 + 1]; | ||
| 547 | o[2] = plte[(size_t)idx * 3 + 2]; | ||
| 548 | o[3] = (size_t)idx < trns.size() ? trns[idx] : 255; | ||
| 549 | } else { | ||
| 550 | o[0] = o[1] = o[2] = 0; | ||
| 551 | o[3] = 255; | ||
| 552 | } | ||
| 553 | break; | ||
| 554 | } | ||
| 555 | } | ||
| 556 | o += 4; | ||
| 557 | } | ||
| 558 | } | ||
| 559 | |||
| 560 | out_w = (int)w; | ||
| 561 | out_h = (int)h; | ||
| 409 | return true; | 562 | return true; |
| 410 | } | 563 | } |
| 411 | 564 | ||
