summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 00:16:09 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 00:16:09 +0100
commit4f384f94504a2027febc8b5cc3e2b3e3034c0b23 (patch)
tree68491e9e1accbfc822e46187312d1d57a7fb66ed
parentSkybox image: upload via procedural texture, not CreateTextureFromBits (diff)
downloadhl2h-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 '')
-rw-r--r--CONTEXT.md15
-rw-r--r--src/world.cpp235
2 files changed, 204 insertions, 46 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index 669c1fb..5bd0436 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -363,11 +363,16 @@ vocabulary / decisions.
363- Replaces the map skybox with `<config dir>/skybox.png` (same directory as 363- Replaces the map skybox with `<config dir>/skybox.png` (same directory as
364 the INI). Not `sv_skyname`: that did not take effect at runtime in this 364 the INI). Not `sv_skyname`: that did not take effect at runtime in this
365 build, so the skybox card now works on materials instead. 365 build, so the skybox card now works on materials instead.
366- PNG is decoded with libpng16's simplified API (`dlopen`ed, no link 366- The PNG is decoded by our own reader (`zlib`'s `uncompress` + PNG filters;
367 dependency). This build's `IMaterialSystem` interface ends at 367 8-bit non-interlaced RGB/RGBA/gray/palette). Do **not** use the system
368 `GetDisplayDeviceName` (the slots after it belong to `CMaterialSystem` 368 libpng: `client.so`, `engine.so` and `vguimatsurface.so` each export their
369 itself), so there is **no `CreateTextureFromBits`**. The pixels are uploaded 369 own `png_create_read_struct`, so libpng16's internal PLT calls bind to the
370 through `CreateProceduralTexture` (vtable 81) + an `ITextureRegenerator` 370 game's older libpng with a different `png_struct` layout and crash in
371 `png_safe_warning` (error_ptr at +0xb0 reads NULL).
372- This build's `IMaterialSystem` interface ends at `GetDisplayDeviceName`
373 (the slots after it belong to `CMaterialSystem` itself), so there is **no
374 `CreateTextureFromBits`**. The pixels are uploaded through
375 `CreateProceduralTexture` (vtable 81) + an `ITextureRegenerator`
371 (`SetTextureRegenerator` 12, `Download` 13), which fills mip 0 through 376 (`SetTextureRegenerator` 12, `Download` 13), which fills mip 0 through
372 `IVTFTexture::ImageData(frame,face,mip)` (39). `IVTFTexture`'s virtual 377 `IVTFTexture::ImageData(frame,face,mip)` (39). `IVTFTexture`'s virtual
373 destructor occupies two slots, so Width=15, Height=16, RowSizeInBytes=19. 378 destructor occupies two slots, so Width=15, Height=16, RowSizeInBytes=19.
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
349namespace { 349namespace {
350 350
351struct 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]; 355using UncompressFn = int (*)(unsigned char *, unsigned long *,
356}; 356 const unsigned char *, unsigned long);
357 357
358bool load_png_rgba(const char *path, std::vector<unsigned char> &pixels, 358UncompressFn 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
373uint32_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
378int 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
388bool 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