From d91067adb15951899d1d3f2728bbfd866af85dd6 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Wed, 22 Apr 2026 10:14:47 +0300 Subject: [PATCH 01/21] ZipCompressTests: performance testing --- source/MRTest/MRZipCompressTests.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/MRTest/MRZipCompressTests.cpp b/source/MRTest/MRZipCompressTests.cpp index f8b27379d917..2ec47f238475 100644 --- a/source/MRTest/MRZipCompressTests.cpp +++ b/source/MRTest/MRZipCompressTests.cpp @@ -27,7 +27,7 @@ TEST( MRMesh, CompressSphereToZip ) UniqueTemporaryFolder srcFolder; ASSERT_TRUE( bool( srcFolder ) ); - constexpr int targetVerts = 1000; // increase it to make the file being compressed larger, 100'000 vertices -> 12M bytes + constexpr int targetVerts = 100000; // increase it to make the file being compressed larger, 100'000 vertices -> 12M bytes SphereParams params; params.radius = 1.0f; params.numMeshVertices = targetVerts; @@ -81,9 +81,9 @@ TEST( MRMesh, CompressManySmallFilesToZip ) ASSERT_TRUE( bool( srcFolder ) ); // increase both below numbers to make the files being compressed larger, 200 * 2 files * 60'000 bytes -> 24M bytes - constexpr int numBinaryFiles = 20; + constexpr int numBinaryFiles = 200; constexpr int numJsonFiles = numBinaryFiles; - constexpr size_t bytesPerFile = 6000; + constexpr size_t bytesPerFile = 60000; // Simple LCG used to produce deterministic pseudo-random bytes. // Keeps the test reproducible across runs and platforms while avoiding From af4499a8e22cc2856a7eaf109db4d80665970d91 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Wed, 22 Apr 2026 14:21:56 +0300 Subject: [PATCH 02/21] MRZlib: implement zlibCompressStream via zlib-ng (native mode) Scoped to the single public function the user requested: Expected zlibCompressStream( std::istream&, std::ostream&, const ZlibCompressParams& ) plus its thin int-level overload, which forwards to it. Decompression (zlibDecompressStream) and every other zlib consumer (libzip's own deflate path inside compressZip/decompressZip, etc.) stay on stock zlib -- this PR does NOT swap zlib for zlib-ng globally. zlib-ng is used in its native "-ng" mode: the library exposes zng_ prefixed symbols and the zlib-ng.h header, the SONAME is libz-ng (not libz), so it can coexist with stock zlib in the same process without any ABI overlap or symbol collisions. Per-platform source of the library: - Windows vcpkg: zlib-ng port (added to requirements/windows.txt) - Rocky Linux vcpkg: zlib-ng port (added to requirements/vcpkg-linux.txt) - macOS Homebrew: zlib-ng formula (added to requirements/macos.txt; not keg-only, native-mode upstream defaults) - Ubuntu apt: no standalone package, built from thirdparty/zlib-ng - Emscripten: no package, built from thirdparty/zlib-ng thirdparty/CMakeLists.txt gates add_subdirectory(./zlib-ng) on NOT WIN32 AND NOT APPLE AND NOT MESHLIB_USE_VCPKG, matching the split above. ZLIB_COMPAT stays OFF so the submodule build also produces native zng_ / libz-ng / zlib-ng.h, identical to what vcpkg and brew install. MRMesh/CMakeLists.txt uses find_library/find_path rather than find_package(zlib-ng CONFIG) for the same relocatability reason as libdeflate (upstream config can bake an absolute include path that breaks under the Ubuntu/Emscripten Docker COPY-shuffle). Source: split the compress body into a new MRZlibNg.cpp so that translation unit includes only and the untouched zlibDecompressStream in MRZlib.cpp keeps including only . Avoids any macro-redefinition ordering question between the two headers without forcing code to choose one. --- .gitmodules | 3 + requirements/macos.txt | 1 + requirements/vcpkg-linux.txt | 1 + requirements/windows.txt | 1 + source/MRMesh/CMakeLists.txt | 19 ++++++ source/MRMesh/MRZlib.cpp | 66 ++----------------- source/MRMesh/MRZlibNg.cpp | 120 +++++++++++++++++++++++++++++++++++ thirdparty/CMakeLists.txt | 13 ++++ thirdparty/zlib-ng | 1 + 9 files changed, 164 insertions(+), 61 deletions(-) create mode 100644 source/MRMesh/MRZlibNg.cpp create mode 160000 thirdparty/zlib-ng diff --git a/.gitmodules b/.gitmodules index ef9d7a0d5172..0dfdab471dec 100644 --- a/.gitmodules +++ b/.gitmodules @@ -85,3 +85,6 @@ [submodule "thirdparty/cpp-httplib"] path = thirdparty/cpp-httplib url = https://github.com/yhirose/cpp-httplib +[submodule "thirdparty/zlib-ng"] + path = thirdparty/zlib-ng + url = https://github.com/zlib-ng/zlib-ng.git diff --git a/requirements/macos.txt b/requirements/macos.txt index 9324a774689d..f6fefab9825a 100644 --- a/requirements/macos.txt +++ b/requirements/macos.txt @@ -24,3 +24,4 @@ tbb tinyxml2 tl-expected zlib +zlib-ng diff --git a/requirements/vcpkg-linux.txt b/requirements/vcpkg-linux.txt index d590e19fdff1..a770ca2f3987 100644 --- a/requirements/vcpkg-linux.txt +++ b/requirements/vcpkg-linux.txt @@ -37,3 +37,4 @@ tiff tinygltf tinyxml2 tl-expected +zlib-ng diff --git a/requirements/windows.txt b/requirements/windows.txt index 9bd83d7286fc..942effad832d 100644 --- a/requirements/windows.txt +++ b/requirements/windows.txt @@ -30,5 +30,6 @@ tiff tinygltf tinyxml2 tl-expected +zlib-ng glad glfw3 diff --git a/source/MRMesh/CMakeLists.txt b/source/MRMesh/CMakeLists.txt index 3621ac029714..aacc903447b3 100644 --- a/source/MRMesh/CMakeLists.txt +++ b/source/MRMesh/CMakeLists.txt @@ -65,6 +65,25 @@ ELSE() ENDIF() target_link_libraries(${PROJECT_NAME} PRIVATE libzip::zip) +# zlib-ng (native mode, zng_ prefix) powers MRZlib's zlibCompressStream; +# decompression and the compressZip/libzip pipeline stay on stock zlib. On +# vcpkg/brew it comes from the package; on Ubuntu apt and Emscripten the +# copy built from thirdparty/zlib-ng is installed alongside the rest of the +# thirdparty prefix. find_library + find_path is used instead of +# find_package(zlib-ng CONFIG) -- the latter has the same non-relocatable +# config issue we hit with libdeflate, so plain library/path lookup is +# more robust across the Docker COPY-shuffle. +find_library(LIBZLIBNG_LIBRARY + NAMES z-ng zlib-ng + REQUIRED +) +find_path(LIBZLIBNG_INCLUDE_DIR + NAMES zlib-ng.h + REQUIRED +) +target_link_libraries(${PROJECT_NAME} PRIVATE ${LIBZLIBNG_LIBRARY}) +target_include_directories(${PROJECT_NAME} PRIVATE ${LIBZLIBNG_INCLUDE_DIR}) + # TODO: CMake config target_include_directories(${PROJECT_NAME} PUBLIC diff --git a/source/MRMesh/MRZlib.cpp b/source/MRMesh/MRZlib.cpp index 259298dc21aa..08e804a903e6 100644 --- a/source/MRMesh/MRZlib.cpp +++ b/source/MRMesh/MRZlib.cpp @@ -59,67 +59,11 @@ int windowBitsFor( bool rawDeflate ) namespace MR { -Expected zlibCompressStream( std::istream& in, std::ostream& out, const ZlibCompressParams& params ) -{ - Buffer inChunk( cChunkSize ), outChunk( cChunkSize ); - z_stream stream { - .zalloc = Z_NULL, - .zfree = Z_NULL, - .opaque = Z_NULL, - }; - int ret; - if ( Z_OK != ( ret = deflateInit2( &stream, params.level, Z_DEFLATED, windowBitsFor( params.rawDeflate ), kDefaultMemLevel, Z_DEFAULT_STRATEGY ) ) ) - return unexpected( zlibToString( ret ) ); - - MR_FINALLY { - deflateEnd( &stream ); - }; - - if ( params.stats ) - *params.stats = {}; - - while ( !in.eof() ) - { - in.read( inChunk.data(), inChunk.size() ); - if ( in.bad() ) - return unexpected( "I/O error" ); - stream.next_in = reinterpret_cast( inChunk.data() ); - stream.avail_in = (unsigned)in.gcount(); - assert( stream.avail_in <= (unsigned)inChunk.size() ); - - if ( params.stats ) - { - params.stats->crc32 = (uint32_t)crc32( params.stats->crc32, stream.next_in, stream.avail_in ); - params.stats->uncompressedSize += stream.avail_in; - } - - const auto flush = in.eof() ? Z_FINISH : Z_NO_FLUSH; - do - { - stream.next_out = reinterpret_cast( outChunk.data() ); - stream.avail_out = (unsigned)outChunk.size(); - ret = deflate( &stream, flush ); - if ( Z_OK != ret && Z_STREAM_END != ret ) - return unexpected( zlibToString( ret ) ); - - assert( stream.avail_out <= (unsigned)outChunk.size() ); - const unsigned written = (unsigned)outChunk.size() - stream.avail_out; - out.write( outChunk.data(), written ); - if ( out.bad() ) - return unexpected( "I/O error" ); - if ( params.stats ) - params.stats->compressedSize += written; - } - while ( stream.avail_out == 0 ); - } - - return {}; -} - -Expected zlibCompressStream( std::istream& in, std::ostream& out, int level ) -{ - return zlibCompressStream( in, out, ZlibCompressParams{ .level = level } ); -} +// zlibCompressStream(...) and its int-level overload are implemented in +// MRZlibNg.cpp against zlib-ng's native API (zng_* prefix). Decompression +// stays on stock zlib -- zlibDecompressStream has to accept bytes written +// by any deflate producer, so compatibility matters more than speed, and +// libzip still uses stock zlib as well. Expected zlibDecompressStream( std::istream& in, std::ostream& out, const ZlibParams& params ) { diff --git a/source/MRMesh/MRZlibNg.cpp b/source/MRMesh/MRZlibNg.cpp new file mode 100644 index 000000000000..1ecd5bf2d4a4 --- /dev/null +++ b/source/MRMesh/MRZlibNg.cpp @@ -0,0 +1,120 @@ +#include "MRZlib.h" +#include "MRBuffer.h" +#include "MRFinally.h" + +// zlib-ng native mode: its zlib-ng.h lives alongside zlib.h on most +// platforms, uses the zng_ prefix on every symbol, and publishes its SONAME +// as libz-ng, so linking both into the same process is safe. The stock-zlib +// header is NOT included in this translation unit -- each of the +// Z_*/MAX_WBITS constants we need is also defined by under the +// same name, and keeping the two headers in separate TUs sidesteps any +// question of macro-redefinition ordering. +#include + +#include +#include + +namespace +{ + +constexpr size_t cChunkSize = 256 * 1024; // 256 KiB, same as the zlib path + +// windowBits is sign-encoded the same way zlib documents it: positive = +// zlib wrapper (RFC 1950), negative = raw deflate (RFC 1951, no wrapper). +// Magnitude is log2(window size); Z_MAX_WINDOWBITS = 15 gives a 32 KiB window. +constexpr int kZlibWrapperBits = Z_MAX_WINDOWBITS; +constexpr int kRawDeflateBits = -Z_MAX_WINDOWBITS; + +// memLevel controls the compressor's internal state size. 8 is zlib's and +// zlib-ng's shared default (matches the old MRZlib choice). +constexpr int kDefaultMemLevel = 8; + +std::string zngToString( int code ) +{ + switch ( code ) + { + case Z_OK: return "ok"; + case Z_STREAM_END: return "stream end"; + case Z_NEED_DICT: return "need dict"; + case Z_ERRNO: return "errno"; + case Z_STREAM_ERROR: return "stream error"; + case Z_DATA_ERROR: return "data error"; + case Z_MEM_ERROR: return "mem error"; + case Z_BUF_ERROR: return "buf error"; + case Z_VERSION_ERROR: return "version error"; + default: return "unknown code"; + } +} + +int windowBitsFor( bool rawDeflate ) +{ + return rawDeflate ? kRawDeflateBits : kZlibWrapperBits; +} + +} // namespace + +namespace MR +{ + +Expected zlibCompressStream( std::istream& in, std::ostream& out, const ZlibCompressParams& params ) +{ + Buffer inChunk( cChunkSize ), outChunk( cChunkSize ); + zng_stream stream{}; + int ret; + if ( Z_OK != ( ret = zng_deflateInit2( &stream, params.level, Z_DEFLATED, + windowBitsFor( params.rawDeflate ), + kDefaultMemLevel, Z_DEFAULT_STRATEGY ) ) ) + return unexpected( zngToString( ret ) ); + + MR_FINALLY { + zng_deflateEnd( &stream ); + }; + + if ( params.stats ) + *params.stats = {}; + + while ( !in.eof() ) + { + in.read( inChunk.data(), static_cast( inChunk.size() ) ); + if ( in.bad() ) + return unexpected( "I/O error" ); + stream.next_in = reinterpret_cast( inChunk.data() ); + stream.avail_in = static_cast( in.gcount() ); + assert( stream.avail_in <= static_cast( inChunk.size() ) ); + + if ( params.stats ) + { + params.stats->crc32 = static_cast( + zng_crc32( params.stats->crc32, stream.next_in, stream.avail_in ) ); + params.stats->uncompressedSize += stream.avail_in; + } + + const int flush = in.eof() ? Z_FINISH : Z_NO_FLUSH; + do + { + stream.next_out = reinterpret_cast( outChunk.data() ); + stream.avail_out = static_cast( outChunk.size() ); + ret = zng_deflate( &stream, flush ); + if ( Z_OK != ret && Z_STREAM_END != ret ) + return unexpected( zngToString( ret ) ); + + assert( stream.avail_out <= static_cast( outChunk.size() ) ); + const unsigned written = static_cast( outChunk.size() ) - stream.avail_out; + out.write( outChunk.data(), written ); + if ( out.bad() ) + return unexpected( "I/O error" ); + if ( params.stats ) + params.stats->compressedSize += written; + } + while ( stream.avail_out == 0 ); + } + + return {}; +} + +Expected zlibCompressStream( std::istream& in, std::ostream& out, int level ) +{ + return zlibCompressStream( in, out, ZlibCompressParams{ .level = level } ); +} + +} // namespace MR diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index e4734cf29af3..00cd1c47119c 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -85,6 +85,19 @@ ENDIF() add_subdirectory(./OpenCTM-git ./OpenCTM) +# Build zlib-ng from our submodule (native mode -- zng_ prefix, does not shadow +# stock zlib) on Ubuntu apt and Emscripten, which have no zlib-ng package. +# Windows + macOS + any MESHLIB_USE_VCPKG config pick up the package-manager +# copy instead (see requirements/{windows,macos,vcpkg-linux}.txt). +IF(NOT WIN32 AND NOT APPLE AND NOT MESHLIB_USE_VCPKG) + set(ZLIB_COMPAT OFF CACHE BOOL "") + set(ZLIB_ENABLE_TESTS OFF CACHE BOOL "") + set(ZLIBNG_ENABLE_TESTS OFF CACHE BOOL "") + set(WITH_GTEST OFF CACHE BOOL "") + set(WITH_GZFILEOP OFF CACHE BOOL "") + add_subdirectory(./zlib-ng) +ENDIF() + option(PHMAP_INSTALL "" ON) add_subdirectory(./parallel-hashmap) diff --git a/thirdparty/zlib-ng b/thirdparty/zlib-ng new file mode 160000 index 000000000000..860e4cff7917 --- /dev/null +++ b/thirdparty/zlib-ng @@ -0,0 +1 @@ +Subproject commit 860e4cff7917d93f54f5d7f0bc1d0e8b1a3cb988 From e66838a05b4f9ccd77c0a7ed1bb9ad0e2e34b914 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Wed, 22 Apr 2026 14:47:57 +0300 Subject: [PATCH 03/21] MRZlib: merge zlib-ng compress path back into MRZlib.cpp Drop the separate MRZlibNg.cpp; fold the zng_-prefixed zlibCompressStream overloads directly into MRZlib.cpp alongside zlibDecompressStream. Both and include cleanly in the same TU: the Z_* return-code / flush-mode / strategy constants are defined in both headers with identical values (C preprocessor allows a redefinition with the same replacement list, no warning), and the function names (deflate/inflate vs zng_deflate/zng_inflate) and struct types (z_stream vs zng_stream) are disjoint. The shared zlibToString and windowBitsFor helpers are reused across compress (zng_*) and decompress (stock zlib) since MAX_WBITS equals Z_MAX_WINDOWBITS = 15 and the Z_* code values match. No functional change relative to the previous commit on this branch; just collapses two TUs into one. --- source/MRMesh/MRZlib.cpp | 74 +++++++++++++++++++++-- source/MRMesh/MRZlibNg.cpp | 120 ------------------------------------- 2 files changed, 69 insertions(+), 125 deletions(-) delete mode 100644 source/MRMesh/MRZlibNg.cpp diff --git a/source/MRMesh/MRZlib.cpp b/source/MRMesh/MRZlib.cpp index 08e804a903e6..9dfefa596369 100644 --- a/source/MRMesh/MRZlib.cpp +++ b/source/MRMesh/MRZlib.cpp @@ -2,9 +2,18 @@ #include "MRBuffer.h" #include "MRFinally.h" +// zlibCompressStream uses zlib-ng in native mode (zng_ prefix, libz-ng). +// zlibDecompressStream stays on stock zlib. Both headers are included here +// because they share the Z_* return-code / flush-mode / strategy constants +// with identical values -- a redefinition with the same replacement list is +// allowed by the C preprocessor and does not produce a warning. The function +// names (deflate/inflate vs zng_deflate/zng_inflate) and struct types +// (z_stream vs zng_stream) are disjoint, so both libraries coexist cleanly. #include +#include #include +#include namespace { @@ -59,11 +68,66 @@ int windowBitsFor( bool rawDeflate ) namespace MR { -// zlibCompressStream(...) and its int-level overload are implemented in -// MRZlibNg.cpp against zlib-ng's native API (zng_* prefix). Decompression -// stays on stock zlib -- zlibDecompressStream has to accept bytes written -// by any deflate producer, so compatibility matters more than speed, and -// libzip still uses stock zlib as well. +Expected zlibCompressStream( std::istream& in, std::ostream& out, const ZlibCompressParams& params ) +{ + Buffer inChunk( cChunkSize ), outChunk( cChunkSize ); + zng_stream stream{}; + int ret; + if ( Z_OK != ( ret = zng_deflateInit2( &stream, params.level, Z_DEFLATED, + windowBitsFor( params.rawDeflate ), + kDefaultMemLevel, Z_DEFAULT_STRATEGY ) ) ) + return unexpected( zlibToString( ret ) ); + + MR_FINALLY { + zng_deflateEnd( &stream ); + }; + + if ( params.stats ) + *params.stats = {}; + + while ( !in.eof() ) + { + in.read( inChunk.data(), static_cast( inChunk.size() ) ); + if ( in.bad() ) + return unexpected( "I/O error" ); + stream.next_in = reinterpret_cast( inChunk.data() ); + stream.avail_in = static_cast( in.gcount() ); + assert( stream.avail_in <= static_cast( inChunk.size() ) ); + + if ( params.stats ) + { + params.stats->crc32 = static_cast( + zng_crc32( params.stats->crc32, stream.next_in, stream.avail_in ) ); + params.stats->uncompressedSize += stream.avail_in; + } + + const int flush = in.eof() ? Z_FINISH : Z_NO_FLUSH; + do + { + stream.next_out = reinterpret_cast( outChunk.data() ); + stream.avail_out = static_cast( outChunk.size() ); + ret = zng_deflate( &stream, flush ); + if ( Z_OK != ret && Z_STREAM_END != ret ) + return unexpected( zlibToString( ret ) ); + + assert( stream.avail_out <= static_cast( outChunk.size() ) ); + const unsigned written = static_cast( outChunk.size() ) - stream.avail_out; + out.write( outChunk.data(), written ); + if ( out.bad() ) + return unexpected( "I/O error" ); + if ( params.stats ) + params.stats->compressedSize += written; + } + while ( stream.avail_out == 0 ); + } + + return {}; +} + +Expected zlibCompressStream( std::istream& in, std::ostream& out, int level ) +{ + return zlibCompressStream( in, out, ZlibCompressParams{ .level = level } ); +} Expected zlibDecompressStream( std::istream& in, std::ostream& out, const ZlibParams& params ) { diff --git a/source/MRMesh/MRZlibNg.cpp b/source/MRMesh/MRZlibNg.cpp deleted file mode 100644 index 1ecd5bf2d4a4..000000000000 --- a/source/MRMesh/MRZlibNg.cpp +++ /dev/null @@ -1,120 +0,0 @@ -#include "MRZlib.h" -#include "MRBuffer.h" -#include "MRFinally.h" - -// zlib-ng native mode: its zlib-ng.h lives alongside zlib.h on most -// platforms, uses the zng_ prefix on every symbol, and publishes its SONAME -// as libz-ng, so linking both into the same process is safe. The stock-zlib -// header is NOT included in this translation unit -- each of the -// Z_*/MAX_WBITS constants we need is also defined by under the -// same name, and keeping the two headers in separate TUs sidesteps any -// question of macro-redefinition ordering. -#include - -#include -#include - -namespace -{ - -constexpr size_t cChunkSize = 256 * 1024; // 256 KiB, same as the zlib path - -// windowBits is sign-encoded the same way zlib documents it: positive = -// zlib wrapper (RFC 1950), negative = raw deflate (RFC 1951, no wrapper). -// Magnitude is log2(window size); Z_MAX_WINDOWBITS = 15 gives a 32 KiB window. -constexpr int kZlibWrapperBits = Z_MAX_WINDOWBITS; -constexpr int kRawDeflateBits = -Z_MAX_WINDOWBITS; - -// memLevel controls the compressor's internal state size. 8 is zlib's and -// zlib-ng's shared default (matches the old MRZlib choice). -constexpr int kDefaultMemLevel = 8; - -std::string zngToString( int code ) -{ - switch ( code ) - { - case Z_OK: return "ok"; - case Z_STREAM_END: return "stream end"; - case Z_NEED_DICT: return "need dict"; - case Z_ERRNO: return "errno"; - case Z_STREAM_ERROR: return "stream error"; - case Z_DATA_ERROR: return "data error"; - case Z_MEM_ERROR: return "mem error"; - case Z_BUF_ERROR: return "buf error"; - case Z_VERSION_ERROR: return "version error"; - default: return "unknown code"; - } -} - -int windowBitsFor( bool rawDeflate ) -{ - return rawDeflate ? kRawDeflateBits : kZlibWrapperBits; -} - -} // namespace - -namespace MR -{ - -Expected zlibCompressStream( std::istream& in, std::ostream& out, const ZlibCompressParams& params ) -{ - Buffer inChunk( cChunkSize ), outChunk( cChunkSize ); - zng_stream stream{}; - int ret; - if ( Z_OK != ( ret = zng_deflateInit2( &stream, params.level, Z_DEFLATED, - windowBitsFor( params.rawDeflate ), - kDefaultMemLevel, Z_DEFAULT_STRATEGY ) ) ) - return unexpected( zngToString( ret ) ); - - MR_FINALLY { - zng_deflateEnd( &stream ); - }; - - if ( params.stats ) - *params.stats = {}; - - while ( !in.eof() ) - { - in.read( inChunk.data(), static_cast( inChunk.size() ) ); - if ( in.bad() ) - return unexpected( "I/O error" ); - stream.next_in = reinterpret_cast( inChunk.data() ); - stream.avail_in = static_cast( in.gcount() ); - assert( stream.avail_in <= static_cast( inChunk.size() ) ); - - if ( params.stats ) - { - params.stats->crc32 = static_cast( - zng_crc32( params.stats->crc32, stream.next_in, stream.avail_in ) ); - params.stats->uncompressedSize += stream.avail_in; - } - - const int flush = in.eof() ? Z_FINISH : Z_NO_FLUSH; - do - { - stream.next_out = reinterpret_cast( outChunk.data() ); - stream.avail_out = static_cast( outChunk.size() ); - ret = zng_deflate( &stream, flush ); - if ( Z_OK != ret && Z_STREAM_END != ret ) - return unexpected( zngToString( ret ) ); - - assert( stream.avail_out <= static_cast( outChunk.size() ) ); - const unsigned written = static_cast( outChunk.size() ) - stream.avail_out; - out.write( outChunk.data(), written ); - if ( out.bad() ) - return unexpected( "I/O error" ); - if ( params.stats ) - params.stats->compressedSize += written; - } - while ( stream.avail_out == 0 ); - } - - return {}; -} - -Expected zlibCompressStream( std::istream& in, std::ostream& out, int level ) -{ - return zlibCompressStream( in, out, ZlibCompressParams{ .level = level } ); -} - -} // namespace MR From f6796b24ee5ec8b638b56e8f810f05b792c5ef89 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Wed, 22 Apr 2026 15:09:26 +0300 Subject: [PATCH 04/21] Revert "MRZlib: merge zlib-ng compress path back into MRZlib.cpp" This reverts commit e66838a0. The merge looked clean under static inspection (Z_* macros identical between zlib.h and zlib-ng.h, deflate/inflate vs zng_deflate/zng_inflate disjoint, z_stream vs zng_stream disjoint) but failed to compile on macOS-arm64 Debug with: /opt/homebrew/include/zlib-ng.h:1079:20: error: typedef redefinition with different types ('uint32_t (*)(void *, const uint8_t **)' vs 'unsigned int (*)(void *, unsigned char **)') Both headers declare callback typedefs in_func and out_func (used by inflateBack) at global scope with the same name and different signatures: zlib.h: typedef unsigned (*in_func)(void*, unsigned char**); zlib-ng.h: typedef uint32_t (*in_func)(void*, const uint8_t**); C++ refuses the redefinition. The typedefs are unconditional in both headers -- there's no feature macro to hide them -- even though MeshLib's code uses neither inflateBack nor the two typedefs. Splitting the compress body into its own TU (MRZlibNg.cpp) so each TU sees at most one of the two headers is the clean fix. Run showing the failure: 24776570063. --- source/MRMesh/MRZlib.cpp | 74 ++--------------------- source/MRMesh/MRZlibNg.cpp | 120 +++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 69 deletions(-) create mode 100644 source/MRMesh/MRZlibNg.cpp diff --git a/source/MRMesh/MRZlib.cpp b/source/MRMesh/MRZlib.cpp index 9dfefa596369..08e804a903e6 100644 --- a/source/MRMesh/MRZlib.cpp +++ b/source/MRMesh/MRZlib.cpp @@ -2,18 +2,9 @@ #include "MRBuffer.h" #include "MRFinally.h" -// zlibCompressStream uses zlib-ng in native mode (zng_ prefix, libz-ng). -// zlibDecompressStream stays on stock zlib. Both headers are included here -// because they share the Z_* return-code / flush-mode / strategy constants -// with identical values -- a redefinition with the same replacement list is -// allowed by the C preprocessor and does not produce a warning. The function -// names (deflate/inflate vs zng_deflate/zng_inflate) and struct types -// (z_stream vs zng_stream) are disjoint, so both libraries coexist cleanly. #include -#include #include -#include namespace { @@ -68,66 +59,11 @@ int windowBitsFor( bool rawDeflate ) namespace MR { -Expected zlibCompressStream( std::istream& in, std::ostream& out, const ZlibCompressParams& params ) -{ - Buffer inChunk( cChunkSize ), outChunk( cChunkSize ); - zng_stream stream{}; - int ret; - if ( Z_OK != ( ret = zng_deflateInit2( &stream, params.level, Z_DEFLATED, - windowBitsFor( params.rawDeflate ), - kDefaultMemLevel, Z_DEFAULT_STRATEGY ) ) ) - return unexpected( zlibToString( ret ) ); - - MR_FINALLY { - zng_deflateEnd( &stream ); - }; - - if ( params.stats ) - *params.stats = {}; - - while ( !in.eof() ) - { - in.read( inChunk.data(), static_cast( inChunk.size() ) ); - if ( in.bad() ) - return unexpected( "I/O error" ); - stream.next_in = reinterpret_cast( inChunk.data() ); - stream.avail_in = static_cast( in.gcount() ); - assert( stream.avail_in <= static_cast( inChunk.size() ) ); - - if ( params.stats ) - { - params.stats->crc32 = static_cast( - zng_crc32( params.stats->crc32, stream.next_in, stream.avail_in ) ); - params.stats->uncompressedSize += stream.avail_in; - } - - const int flush = in.eof() ? Z_FINISH : Z_NO_FLUSH; - do - { - stream.next_out = reinterpret_cast( outChunk.data() ); - stream.avail_out = static_cast( outChunk.size() ); - ret = zng_deflate( &stream, flush ); - if ( Z_OK != ret && Z_STREAM_END != ret ) - return unexpected( zlibToString( ret ) ); - - assert( stream.avail_out <= static_cast( outChunk.size() ) ); - const unsigned written = static_cast( outChunk.size() ) - stream.avail_out; - out.write( outChunk.data(), written ); - if ( out.bad() ) - return unexpected( "I/O error" ); - if ( params.stats ) - params.stats->compressedSize += written; - } - while ( stream.avail_out == 0 ); - } - - return {}; -} - -Expected zlibCompressStream( std::istream& in, std::ostream& out, int level ) -{ - return zlibCompressStream( in, out, ZlibCompressParams{ .level = level } ); -} +// zlibCompressStream(...) and its int-level overload are implemented in +// MRZlibNg.cpp against zlib-ng's native API (zng_* prefix). Decompression +// stays on stock zlib -- zlibDecompressStream has to accept bytes written +// by any deflate producer, so compatibility matters more than speed, and +// libzip still uses stock zlib as well. Expected zlibDecompressStream( std::istream& in, std::ostream& out, const ZlibParams& params ) { diff --git a/source/MRMesh/MRZlibNg.cpp b/source/MRMesh/MRZlibNg.cpp new file mode 100644 index 000000000000..1ecd5bf2d4a4 --- /dev/null +++ b/source/MRMesh/MRZlibNg.cpp @@ -0,0 +1,120 @@ +#include "MRZlib.h" +#include "MRBuffer.h" +#include "MRFinally.h" + +// zlib-ng native mode: its zlib-ng.h lives alongside zlib.h on most +// platforms, uses the zng_ prefix on every symbol, and publishes its SONAME +// as libz-ng, so linking both into the same process is safe. The stock-zlib +// header is NOT included in this translation unit -- each of the +// Z_*/MAX_WBITS constants we need is also defined by under the +// same name, and keeping the two headers in separate TUs sidesteps any +// question of macro-redefinition ordering. +#include + +#include +#include + +namespace +{ + +constexpr size_t cChunkSize = 256 * 1024; // 256 KiB, same as the zlib path + +// windowBits is sign-encoded the same way zlib documents it: positive = +// zlib wrapper (RFC 1950), negative = raw deflate (RFC 1951, no wrapper). +// Magnitude is log2(window size); Z_MAX_WINDOWBITS = 15 gives a 32 KiB window. +constexpr int kZlibWrapperBits = Z_MAX_WINDOWBITS; +constexpr int kRawDeflateBits = -Z_MAX_WINDOWBITS; + +// memLevel controls the compressor's internal state size. 8 is zlib's and +// zlib-ng's shared default (matches the old MRZlib choice). +constexpr int kDefaultMemLevel = 8; + +std::string zngToString( int code ) +{ + switch ( code ) + { + case Z_OK: return "ok"; + case Z_STREAM_END: return "stream end"; + case Z_NEED_DICT: return "need dict"; + case Z_ERRNO: return "errno"; + case Z_STREAM_ERROR: return "stream error"; + case Z_DATA_ERROR: return "data error"; + case Z_MEM_ERROR: return "mem error"; + case Z_BUF_ERROR: return "buf error"; + case Z_VERSION_ERROR: return "version error"; + default: return "unknown code"; + } +} + +int windowBitsFor( bool rawDeflate ) +{ + return rawDeflate ? kRawDeflateBits : kZlibWrapperBits; +} + +} // namespace + +namespace MR +{ + +Expected zlibCompressStream( std::istream& in, std::ostream& out, const ZlibCompressParams& params ) +{ + Buffer inChunk( cChunkSize ), outChunk( cChunkSize ); + zng_stream stream{}; + int ret; + if ( Z_OK != ( ret = zng_deflateInit2( &stream, params.level, Z_DEFLATED, + windowBitsFor( params.rawDeflate ), + kDefaultMemLevel, Z_DEFAULT_STRATEGY ) ) ) + return unexpected( zngToString( ret ) ); + + MR_FINALLY { + zng_deflateEnd( &stream ); + }; + + if ( params.stats ) + *params.stats = {}; + + while ( !in.eof() ) + { + in.read( inChunk.data(), static_cast( inChunk.size() ) ); + if ( in.bad() ) + return unexpected( "I/O error" ); + stream.next_in = reinterpret_cast( inChunk.data() ); + stream.avail_in = static_cast( in.gcount() ); + assert( stream.avail_in <= static_cast( inChunk.size() ) ); + + if ( params.stats ) + { + params.stats->crc32 = static_cast( + zng_crc32( params.stats->crc32, stream.next_in, stream.avail_in ) ); + params.stats->uncompressedSize += stream.avail_in; + } + + const int flush = in.eof() ? Z_FINISH : Z_NO_FLUSH; + do + { + stream.next_out = reinterpret_cast( outChunk.data() ); + stream.avail_out = static_cast( outChunk.size() ); + ret = zng_deflate( &stream, flush ); + if ( Z_OK != ret && Z_STREAM_END != ret ) + return unexpected( zngToString( ret ) ); + + assert( stream.avail_out <= static_cast( outChunk.size() ) ); + const unsigned written = static_cast( outChunk.size() ) - stream.avail_out; + out.write( outChunk.data(), written ); + if ( out.bad() ) + return unexpected( "I/O error" ); + if ( params.stats ) + params.stats->compressedSize += written; + } + while ( stream.avail_out == 0 ); + } + + return {}; +} + +Expected zlibCompressStream( std::istream& in, std::ostream& out, int level ) +{ + return zlibCompressStream( in, out, ZlibCompressParams{ .level = level } ); +} + +} // namespace MR From d1b60f308957fd4aa71e5d64c77d4cc6b47ce710 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Wed, 22 Apr 2026 15:14:19 +0300 Subject: [PATCH 05/21] thirdparty(zlib-ng): force static-only on Emscripten zlib-ng's CMakeLists treats an undefined BUILD_SHARED_LIBS as "build both shared and static targets". Emscripten then demotes the SHARED one to STATIC, after which both rules emit the same libz-ng.a output and ninja fails at configure time: CMake Warning (dev) at zlib-ng/CMakeLists.txt:1165 (add_library): ADD_LIBRARY called with SHARED option but the target platform does not support dynamic linking. Building a STATIC library instead. ... ninja: error: build.ninja:1263: multiple rules generate libz-ng.a [-w dupbuild=err] Force BUILD_SHARED_LIBS=OFF only for the zlib-ng add_subdirectory on Emscripten. Ubuntu apt keeps the default (shared) so MRMesh.so still picks up libz-ng.so. Local set() -- no CACHE -- keeps the override scoped to this add_subdirectory and doesn't leak to the later Emscripten-specific thirdparty libs below. Seen on run 24776570063, job 72496287138. --- thirdparty/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index 00cd1c47119c..e22cf4f93cf9 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -95,6 +95,14 @@ IF(NOT WIN32 AND NOT APPLE AND NOT MESHLIB_USE_VCPKG) set(ZLIBNG_ENABLE_TESTS OFF CACHE BOOL "") set(WITH_GTEST OFF CACHE BOOL "") set(WITH_GZFILEOP OFF CACHE BOOL "") + IF(EMSCRIPTEN) + # zlib-ng's CMakeLists treats an undefined BUILD_SHARED_LIBS as "build both + # shared and static targets". Emscripten then demotes the SHARED one to + # STATIC, after which both rules emit the same libz-ng.a and ninja errors + # with "multiple rules generate libz-ng.a [-w dupbuild=err]". Force static + # only here -- Emscripten produces static output either way. + set(BUILD_SHARED_LIBS OFF) + ENDIF() add_subdirectory(./zlib-ng) ENDIF() From 0addcf50cab5f790892ba6e227c74e709faaa9fd Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Wed, 22 Apr 2026 15:16:51 +0300 Subject: [PATCH 06/21] MRMesh.vcxproj: register MRZlibNg.cpp for the MSBuild Windows build The CMake build globs source files automatically, but the MSBuild project is hand-maintained -- the new TU that carries zlibCompressStream against zlib-ng needs to be listed explicitly in both MRMesh.vcxproj (ClCompile) and MRMesh.vcxproj.filters (Source Files\IO, next to MRZlib.cpp). --- source/MRMesh/MRMesh.vcxproj | 1 + source/MRMesh/MRMesh.vcxproj.filters | 3 +++ 2 files changed, 4 insertions(+) diff --git a/source/MRMesh/MRMesh.vcxproj b/source/MRMesh/MRMesh.vcxproj index d90063127af2..63c353109a23 100644 --- a/source/MRMesh/MRMesh.vcxproj +++ b/source/MRMesh/MRMesh.vcxproj @@ -734,6 +734,7 @@ + diff --git a/source/MRMesh/MRMesh.vcxproj.filters b/source/MRMesh/MRMesh.vcxproj.filters index 1a90220ae273..63fb2e56a4e4 100644 --- a/source/MRMesh/MRMesh.vcxproj.filters +++ b/source/MRMesh/MRMesh.vcxproj.filters @@ -1991,6 +1991,9 @@ Source Files\IO + + Source Files\IO + Source Files\Polyline From 0e63f611c0fee0bb4a018f2a769735433ab71661 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Wed, 22 Apr 2026 15:40:06 +0300 Subject: [PATCH 07/21] MRZlibNg: use MAX_WBITS not non-existent Z_MAX_WINDOWBITS zlib-ng does not declare Z_MAX_WINDOWBITS -- I hallucinated that name when writing MRZlibNg.cpp. zlib-ng's zconf-ng.h re-exports MAX_WBITS (15) under exactly the same spelling stock zlib uses, so the native- mode header already gives us the constant we need. Fix the two constexpr initialisers that were using the wrong name, and tighten the surrounding comment to reflect what the header actually provides. Seen on macOS arm64 Debug (run 24777754710, job 72500510814): /source/MRMesh/MRZlibNg.cpp:25:34: error: use of undeclared identifier 'Z_MAX_WINDOWBITS' /source/MRMesh/MRZlibNg.cpp:26:35: error: use of undeclared identifier 'Z_MAX_WINDOWBITS' --- source/MRMesh/MRZlibNg.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/source/MRMesh/MRZlibNg.cpp b/source/MRMesh/MRZlibNg.cpp index 1ecd5bf2d4a4..2751703023f0 100644 --- a/source/MRMesh/MRZlibNg.cpp +++ b/source/MRMesh/MRZlibNg.cpp @@ -21,9 +21,11 @@ constexpr size_t cChunkSize = 256 * 1024; // 256 KiB, same as the zlib path // windowBits is sign-encoded the same way zlib documents it: positive = // zlib wrapper (RFC 1950), negative = raw deflate (RFC 1951, no wrapper). -// Magnitude is log2(window size); Z_MAX_WINDOWBITS = 15 gives a 32 KiB window. -constexpr int kZlibWrapperBits = Z_MAX_WINDOWBITS; -constexpr int kRawDeflateBits = -Z_MAX_WINDOWBITS; +// Magnitude is log2(window size); MAX_WBITS = 15 gives a 32 KiB window. +// zlib-ng re-exports the same MAX_WBITS macro through zconf-ng.h, so we +// can use it here without pulling in stock zlib's headers. +constexpr int kZlibWrapperBits = MAX_WBITS; +constexpr int kRawDeflateBits = -MAX_WBITS; // memLevel controls the compressor's internal state size. 8 is zlib's and // zlib-ng's shared default (matches the old MRZlib choice). From b8a22c8c0eb4d5e434ad69b34f6525f29eb6ed77 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Wed, 22 Apr 2026 16:26:54 +0300 Subject: [PATCH 08/21] test: make MRMesh.ZlibCompressStats engine-agnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test pinned stats.compressedSize against sizeof(cRawLevel{1,9}) and sizeof(cWrappedLevel{1,9}), i.e. reference blobs captured from stock zlib. Each alternative deflate implementation we've tried produces a valid, lossless output of a slightly different size: stock zlib: reference (70 / 76 bytes on this corpus) zlib-ng compat: stock - 1 byte zlib-ng native: stock + 7 bytes (seen here) libdeflate: stock ± small amount All of these are correct. Chasing tolerance windows (+/-4 was enough for zlib-ng-compat, +/-7 now for native, libdeflate may need more) is fragile. Replace the exact-size EXPECT_EQ with four engine- agnostic invariants that still catch any real regression in the stats API: - stats.crc32 matches an independent CRC-32 reference (was already) - stats.uncompressedSize == sizeof( input ) (was already) - stats.compressedSize == out.str().size() (API internal consistency) - 0 < stats.compressedSize < sizeof( input ) (non-empty, compressed) The round-trip check in ZlibCompressTestFixture + ZlibDecompressTest Fixture still guarantees byte-exact recovery, so any encoder bug that produces something parseable-but-wrong would still fail there. Seen on run 24778762984 (feat/zlib-compress-stream-zlib-ng): macOS arm64 Release, Debug source/MRTest/MRZlibTests.cpp:160-161: stats.compressedSize: 77 c.expectedCompSize: 70 stats.compressedSize: 83 c.expectedCompSize: 76 --- source/MRTest/MRZlibTests.cpp | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/source/MRTest/MRZlibTests.cpp b/source/MRTest/MRZlibTests.cpp index c4b6bf1724b9..d72e75f24528 100644 --- a/source/MRTest/MRZlibTests.cpp +++ b/source/MRTest/MRZlibTests.cpp @@ -138,12 +138,12 @@ TEST( MRMesh, ZlibCompressStats ) const std::string inputStr( reinterpret_cast( cInput ), sizeof( cInput ) ); const uint32_t expectedCrc = crc32Ref( cInput, sizeof( cInput ) ); - struct Case { bool rawDeflate; int level; size_t expectedCompSize; }; + struct Case { bool rawDeflate; int level; }; const Case cases[] = { - { true, 1, sizeof( cRawLevel1 ) }, - { true, 9, sizeof( cRawLevel9 ) }, - { false, 1, sizeof( cWrappedLevel1 ) }, - { false, 9, sizeof( cWrappedLevel9 ) }, + { true, 1 }, + { true, 9 }, + { false, 1 }, + { false, 9 }, }; for ( const auto& c : cases ) @@ -155,10 +155,19 @@ TEST( MRMesh, ZlibCompressStats ) MR::ZlibCompressParams{ { .rawDeflate = c.rawDeflate }, c.level, &stats } ); EXPECT_TRUE( res.has_value() ); + // Engine-agnostic assertions: CRC and uncompressed size are defined by + // the input; stats.compressedSize must equal out.str().size() (API + // consistency); compressed output must be non-empty and smaller than + // the input. We deliberately do NOT pin stats.compressedSize to the + // size of cRawLevel*/cWrappedLevel* -- those reference blobs were + // captured from stock zlib, and the exact compressed byte count drifts + // from one deflate implementation to another (stock zlib vs zlib-ng + // compat vs zlib-ng native vs libdeflate, all lossless). EXPECT_EQ( stats.crc32, expectedCrc ); EXPECT_EQ( stats.uncompressedSize, sizeof( cInput ) ); - EXPECT_EQ( stats.compressedSize, c.expectedCompSize ); - EXPECT_EQ( out.str().size(), c.expectedCompSize ); + EXPECT_EQ( stats.compressedSize, out.str().size() ); + EXPECT_GT( stats.compressedSize, 0u ); + EXPECT_LT( stats.compressedSize, sizeof( cInput ) ); } } From 2a4a5ac6c09f8e9a70027a9ffc4e1c65539c4267 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Wed, 22 Apr 2026 23:19:59 +0300 Subject: [PATCH 09/21] thirdparty(zlib-ng, emscripten): restore BUILD_SHARED_LIBS after add_subdirectory The static-only setting I added in d1b60f30 to work around zlib-ng's multiple-rules-generate-libz-ng.a collision on Emscripten leaked to every subsequent add_subdirectory in the same scope. Specifically jsoncpp further down in thirdparty/CMakeLists.txt (line 133) is configured as: set(JSONCPP_WITH_TESTS OFF) set(JSONCPP_WITH_POST_BUILD_UNITTEST OFF) set(BUILD_STATIC_LIBS OFF) add_subdirectory(./jsoncpp) That relies on BUILD_SHARED_LIBS being in its previous (unset or ON) state to produce the shared jsoncpp library. After my leak flipped BUILD_SHARED_LIBS to OFF, jsoncpp had both BUILD_SHARED_LIBS=OFF and BUILD_STATIC_LIBS=OFF -- neither flavour was built. Headers were still installed but the library file was not, so the main Emscripten build failed at configure time with: CMake Error (FindPackageHandleStandardArgs.cmake): Could NOT find JsonCpp (missing: JsonCpp_LIBRARY) (found version "1.9.5") Wrap the set(BUILD_SHARED_LIBS OFF) in save/restore and scope the add_subdirectory(./zlib-ng) call inside that window so the override is visible only to zlib-ng. Same pattern already used for googletest at lines 46-50. Seen on Multithreaded-64Bit, Multithreaded, and Singlethreaded Emscripten legs (run 24781052774). --- thirdparty/CMakeLists.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index e22cf4f93cf9..89e30bea7fe8 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -100,10 +100,20 @@ IF(NOT WIN32 AND NOT APPLE AND NOT MESHLIB_USE_VCPKG) # shared and static targets". Emscripten then demotes the SHARED one to # STATIC, after which both rules emit the same libz-ng.a and ninja errors # with "multiple rules generate libz-ng.a [-w dupbuild=err]". Force static - # only here -- Emscripten produces static output either way. + # only for this add_subdirectory -- Emscripten produces static output + # either way. Scope via save/restore: jsoncpp, fmt, spdlog, onetbb etc. + # added further down rely on BUILD_SHARED_LIBS being in its previous + # (unset/ON) state, and a naked set() here would otherwise silently flip + # every later add_subdirectory to static, breaking their find_package + # lookups (seen as "Could NOT find JsonCpp (missing: JsonCpp_LIBRARY)"). + set(ZLIB_NG_BUILD_SHARED_LIBS_BACKUP "${BUILD_SHARED_LIBS}") set(BUILD_SHARED_LIBS OFF) + add_subdirectory(./zlib-ng) + set(BUILD_SHARED_LIBS "${ZLIB_NG_BUILD_SHARED_LIBS_BACKUP}") + unset(ZLIB_NG_BUILD_SHARED_LIBS_BACKUP) + ELSE() + add_subdirectory(./zlib-ng) ENDIF() - add_subdirectory(./zlib-ng) ENDIF() option(PHMAP_INSTALL "" ON) From 049bd26fc908666822b9c8c02d4ca8c195862ed0 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Wed, 22 Apr 2026 23:24:30 +0300 Subject: [PATCH 10/21] ci: retrigger with skip-image-rebuild label removed (image needs rebuild) From b557a4be9c88ca6a0272a081f78b66ef467101d9 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Thu, 23 Apr 2026 00:06:50 +0300 Subject: [PATCH 11/21] thirdparty(zlib-ng, emscripten): unset BUILD_SHARED_LIBS after, not restore-from-backup The save/restore pattern I added in d68fd56b captured "" from an undefined BUILD_SHARED_LIBS and then "restored" BUILD_SHARED_LIBS to "" (defined-but-empty). That's a different state than the original (undefined), and jsoncpp's option(BUILD_SHARED_LIBS ... ON) fallback does NOT re-initialise an already-defined variable. So the empty value persisted into jsoncpp's lib_json CMakeLists line 179: set_target_properties(${OBJECT_LIB} PROPERTIES OUTPUT_NAME jsoncpp VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_SOVERSION} POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS} <- expands to "" ) The empty expansion broke the key/value pairing and produced "set_target_properties called with incorrect number of arguments", aborting thirdparty CMake configure on the Emscripten image build. On every Emscripten MeshLib build before this PR, BUILD_SHARED_LIBS was undefined when thirdparty/CMakeLists.txt reached the zlib-ng block (confirmed: zlib-ng is added before libzip/jsoncpp, and nothing upstream sets the variable). So unset() after the add_subdirectory is both correct and simpler than the if(DEFINED)/backup/restore dance, and it matches the state jsoncpp's option() wants to see. Seen on run 24800811051, job 72582653120. --- thirdparty/CMakeLists.txt | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index 89e30bea7fe8..f2afb7fa7419 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -101,16 +101,19 @@ IF(NOT WIN32 AND NOT APPLE AND NOT MESHLIB_USE_VCPKG) # STATIC, after which both rules emit the same libz-ng.a and ninja errors # with "multiple rules generate libz-ng.a [-w dupbuild=err]". Force static # only for this add_subdirectory -- Emscripten produces static output - # either way. Scope via save/restore: jsoncpp, fmt, spdlog, onetbb etc. - # added further down rely on BUILD_SHARED_LIBS being in its previous - # (unset/ON) state, and a naked set() here would otherwise silently flip - # every later add_subdirectory to static, breaking their find_package - # lookups (seen as "Could NOT find JsonCpp (missing: JsonCpp_LIBRARY)"). - set(ZLIB_NG_BUILD_SHARED_LIBS_BACKUP "${BUILD_SHARED_LIBS}") + # either way. + # + # After add_subdirectory(./zlib-ng) we MUST restore BUILD_SHARED_LIBS to + # its prior *undefined* state, not "defined but empty". jsoncpp further + # down expands ${BUILD_SHARED_LIBS} into set_target_properties() pairs and + # silently breaks with "set_target_properties called with incorrect number + # of arguments" when the expansion yields empty. jsoncpp's own option() + # fallback only fires for undefined, not empty. unset() is therefore the + # right tool here (BUILD_SHARED_LIBS was undefined before this block on + # every Emscripten MeshLib build). set(BUILD_SHARED_LIBS OFF) add_subdirectory(./zlib-ng) - set(BUILD_SHARED_LIBS "${ZLIB_NG_BUILD_SHARED_LIBS_BACKUP}") - unset(ZLIB_NG_BUILD_SHARED_LIBS_BACKUP) + unset(BUILD_SHARED_LIBS) ELSE() add_subdirectory(./zlib-ng) ENDIF() From cb9dfb01389e77426392c405d29fb1496f548c60 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Thu, 23 Apr 2026 14:59:59 +0300 Subject: [PATCH 12/21] ci(windows): add DLL-not-found diagnostic steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit STATUS_DLL_NOT_FOUND (exit code -1073741515 / 0xC0000135) at MeshViewer.exe launch kills the Run Start-and-Exit Tests step with zero useful output — the Windows loader aborts before main() and CI sees only a bare ##[error]Process completed with exit code ... Observed on the zlib-ng branch run 24802707812, job 72596857457 (msvc-2019 Debug CMake with the x64-windows-meshlib-iterator-debug triplet): vcpkg installed zlib-ng successfully but libz-ng.dll didn't end up next to MeshViewer.exe. Three other Windows legs in the same run with the default triplet succeeded, so the gap is specifically in the iterator-debug triplet's DLL-copy chain. Add two unconditional diagnostic steps so the next run of any Windows job self-documents the state the loader will see: 1. After vcpkg-integrate-install, "Diagnostic — vcpkg installed tree": lists the contents of C:\vcpkg\installed\\{bin,debug\bin,lib,debug\lib} so we see which DLLs vcpkg actually installed and under what names (e.g. libz-ng.dll vs libz-ngd.dll). Confirms or rules out "vcpkg didn't install the package" as the cause. 2. Right before "Run Start-and-Exit Tests", "Diagnostic — output bin DLL inventory + MRMesh imports": - Lists all .dll/.exe under source\x64\\ so we see which DLLs were copied next to MeshViewer.exe - Runs dumpbin /dependents on MeshViewer.exe, MRMesh.dll, MRTest.exe so we see which DLL names the loader is actually looking for at process start - Dumps the PATH so we can tell whether the vcpkg install dir would be a fallback lookup location Together, these three data points are enough to turn any DLL-not-found failure into a one-line diagnosis. Both steps have `if: always()` and `continue-on-error: true`, so they run even when the main Build step failed (often when you need the diagnostic most) and never mask a real failure themselves. Net cost: ~30 lines of pwsh output per Windows job when everything works; ~100 lines when a DLL is missing. Trivial relative to the multi-gigabyte Windows CI logs already emitted. --- .github/workflows/build-test-windows.yml | 70 ++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/.github/workflows/build-test-windows.yml b/.github/workflows/build-test-windows.yml index 43cfe15ddc03..4b875605f16c 100644 --- a/.github/workflows/build-test-windows.yml +++ b/.github/workflows/build-test-windows.yml @@ -111,6 +111,30 @@ jobs: working-directory: C:\vcpkg run: C:\vcpkg\vcpkg.exe integrate install + # Diagnostic step: inventory the vcpkg installed tree so DLL-not-found + # failures later in the build can be traced back to whether the DLL was + # (a) never installed, (b) installed under an unexpected name, or + # (c) installed fine but not copied to the build output. + - name: Diagnostic — vcpkg installed tree + if: ${{ always() }} + shell: pwsh + continue-on-error: true + run: | + $triplet = "${{ matrix.vcpkg_triplet || 'x64-windows-meshlib' }}" + Write-Host "=== Triplet: $triplet ===" + foreach ($sub in 'bin','debug\bin','lib','debug\lib') { + $dir = "C:\vcpkg\installed\$triplet\$sub" + Write-Host "" + Write-Host "--- $dir ---" + if (Test-Path $dir) { + Get-ChildItem $dir -File -ErrorAction SilentlyContinue | + Select-Object Name, @{N='SizeKB';E={[int]($_.Length/1KB)}} | + Sort-Object Name | Format-Table -AutoSize + } else { + Write-Host "(directory not present)" + } + } + - name: Restore CUDA Cache uses: actions/cache@v5 id: cuda-cache @@ -213,6 +237,52 @@ jobs: call "${{matrix.vc-path}}\Common7\Tools\VsDevCmd.bat" -arch=amd64 ${{ fromJSON('["", "-vcvars_ver=14.2"]')[matrix.cxx_compiler == 'msvc-2019'] }} call ./scripts/mrbind/generate_win.bat -B --trace MODE=none VS_MODE=${{matrix.config}} + # Diagnostic step: before launching MeshViewer.exe, dump the state the + # Windows loader will actually see. Prints the DLLs present next to the + # .exe, the import tables of the key binaries (so we know which DLL + # names the loader wants), and the PATH at launch. Lets DLL-not-found + # failures self-diagnose from the CI log. + - name: Diagnostic — output bin DLL inventory + MRMesh imports + if: ${{ always() }} + shell: pwsh + continue-on-error: true + working-directory: source\x64\${{ matrix.config }} + run: | + Write-Host "=== DLLs / EXEs in $(Get-Location) ===" + Get-ChildItem . -File -Include *.dll,*.exe -Recurse -ErrorAction SilentlyContinue | + Select-Object Name, @{N='SizeKB';E={[int]($_.Length/1KB)}} | + Sort-Object Name | Format-Table -AutoSize + + # Locate dumpbin.exe via the VC path hint passed in via matrix, then + # fall back to whatever is on PATH (Visual Studio Integration step + # earlier populates MSBuild PATH, not VC bin). + $vcpath = '${{ matrix.vc-path }}' + $dumpbin = (Get-ChildItem "$vcpath\VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe" -ErrorAction SilentlyContinue | + Select-Object -First 1).FullName + if (-not $dumpbin) { + $dumpbin = (Get-Command dumpbin.exe -ErrorAction SilentlyContinue).Source + } + + if ($dumpbin) { + Write-Host "" + Write-Host "Using dumpbin at: $dumpbin" + foreach ($target in 'MeshViewer.exe','MRMesh.dll','MRTest.exe') { + if (Test-Path $target) { + Write-Host "" + Write-Host "=== Import table of $target (dumpbin /dependents) ===" + & $dumpbin /dependents $target | + Where-Object { $_ -match '\.dll$' } | + ForEach-Object { " " + $_.Trim() } + } + } + } else { + Write-Warning "dumpbin.exe not found; skipping import-table dump" + } + + Write-Host "" + Write-Host "=== PATH at diagnostic step ===" + $env:PATH -split ';' | ForEach-Object { " $_" } + - name: Run Start-and-Exit Tests timeout-minutes: 3 working-directory: source\x64\${{ matrix.config }} From 729e7afd3252630300b7b659f54ff297378b0d9f Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Thu, 23 Apr 2026 15:07:41 +0300 Subject: [PATCH 13/21] ci: retrigger with Windows-only labels (skip images + disable non-Windows) From 83e7169b9d0d6891ca4527617b031112f65ee243 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Thu, 23 Apr 2026 18:26:31 +0300 Subject: [PATCH 14/21] MRMesh: on Windows find zlib-ng via find_package(CONFIG), not find_library The diagnostic output from run 24834309936 shows two linked problems on the msvc-2019 Debug CMake x64-windows-meshlib-iterator-debug leg: 1. Debug MRMesh.dll imports from zlib-ng2.dll (the RELEASE variant) instead of zlib-ngd2.dll (the matching Debug variant) that vcpkg also installed. Every other MRMesh.dll dependency resolved to its d-suffixed debug DLL (zlibd1.dll, spdlogd.dll, tbb12_debug.dll, tiffd.dll, fmtd.dll) -- those use find_package(... CONFIG) which exports IMPORTED_CONFIGURATIONS=DEBUG;RELEASE with distinct per- config IMPORTED_LOCATION properties. 2. Neither zlib-ng2.dll nor zlib-ngd2.dll was copied next to MeshViewer.exe by vcpkg's applocal-copy step. Applocal keys on imported-target metadata; a raw find_library() result doesn't carry the Debug/Release-aware config map that applocal expects, so the copy falls through. Together: Debug MeshViewer.exe launches, loader walks MRMesh.dll's import table, looks for zlib-ng2.dll in the output dir and on PATH, finds neither (vcpkg's install bin isn't on PATH), aborts with STATUS_DLL_NOT_FOUND (0xC0000135 / exit -1073741515) before main(). Fix both by using find_package(zlib-ng CONFIG REQUIRED) on Windows and linking the imported target zlib-ng::zlib (vcpkg's zlib-ng port ships the proper config package). Keep the existing find_library / find_path path for everything else -- Ubuntu apt and Emscripten build zlib-ng from thirdparty/zlib-ng and its generated config has the same non- relocatable-include-path bug we hit with libdeflate under the Docker multi-stage COPY-shuffle, and those platforms are single-config so the Debug/Release picking issue doesn't apply. Only the Windows find is gated; macOS and Linux keep their current working behaviour. --- source/MRMesh/CMakeLists.txt | 49 +++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/source/MRMesh/CMakeLists.txt b/source/MRMesh/CMakeLists.txt index aacc903447b3..647ac13f7dfb 100644 --- a/source/MRMesh/CMakeLists.txt +++ b/source/MRMesh/CMakeLists.txt @@ -66,23 +66,38 @@ ENDIF() target_link_libraries(${PROJECT_NAME} PRIVATE libzip::zip) # zlib-ng (native mode, zng_ prefix) powers MRZlib's zlibCompressStream; -# decompression and the compressZip/libzip pipeline stay on stock zlib. On -# vcpkg/brew it comes from the package; on Ubuntu apt and Emscripten the -# copy built from thirdparty/zlib-ng is installed alongside the rest of the -# thirdparty prefix. find_library + find_path is used instead of -# find_package(zlib-ng CONFIG) -- the latter has the same non-relocatable -# config issue we hit with libdeflate, so plain library/path lookup is -# more robust across the Docker COPY-shuffle. -find_library(LIBZLIBNG_LIBRARY - NAMES z-ng zlib-ng - REQUIRED -) -find_path(LIBZLIBNG_INCLUDE_DIR - NAMES zlib-ng.h - REQUIRED -) -target_link_libraries(${PROJECT_NAME} PRIVATE ${LIBZLIBNG_LIBRARY}) -target_include_directories(${PROJECT_NAME} PRIVATE ${LIBZLIBNG_INCLUDE_DIR}) +# decompression and the compressZip/libzip pipeline stay on stock zlib. +# +# Windows: use the proper CMake config package that vcpkg's zlib-ng port +# ships. Multi-config Debug/Release matters here: find_library() picks one +# .lib and uses it for every configuration, so a Debug build links against +# the release zlib-ng2.dll and the applocal-copy step doesn't copy it next +# to the .exe (STATUS_DLL_NOT_FOUND at launch, seen on msvc-2019 Debug +# CMake with the x64-windows-meshlib-iterator-debug triplet). The +# find_package(CONFIG) target has IMPORTED_CONFIGURATIONS=DEBUG;RELEASE +# and drives applocal-copy correctly. +# +# Other platforms: find_library + find_path. On Ubuntu apt and Emscripten +# we build zlib-ng from thirdparty/zlib-ng and its generated config has +# the same non-relocatable-include-path bug we hit with libdeflate (breaks +# under the Docker multi-stage COPY-shuffle). Plain library/path lookup +# sidesteps it, and those platforms are single-config so the multi-config +# concern above doesn't apply. +IF(WIN32) + find_package(zlib-ng CONFIG REQUIRED) + target_link_libraries(${PROJECT_NAME} PRIVATE zlib-ng::zlib) +ELSE() + find_library(LIBZLIBNG_LIBRARY + NAMES z-ng zlib-ng + REQUIRED + ) + find_path(LIBZLIBNG_INCLUDE_DIR + NAMES zlib-ng.h + REQUIRED + ) + target_link_libraries(${PROJECT_NAME} PRIVATE ${LIBZLIBNG_LIBRARY}) + target_include_directories(${PROJECT_NAME} PRIVATE ${LIBZLIBNG_INCLUDE_DIR}) +ENDIF() # TODO: CMake config target_include_directories(${PROJECT_NAME} From 46340ab7fa89cfba40cff92289e619df8d4e15fa Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Thu, 23 Apr 2026 19:06:06 +0300 Subject: [PATCH 15/21] MRMesh: manually construct zlib-ng imported target on Windows The find_package(zlib-ng CONFIG) approach from 83e7169b works on vcpkg 2026.03.18 (msvc-2022 legs) but fails on vcpkg 2024.10.21 pinned for the msvc-2019 legs with Could not find a package configuration file provided by "zlib-ng" with any of the following names: zlib-ngConfig.cmake zlib-ng-config.cmake The older vcpkg port ships zlib-ng 2.1.5 which vcpkg installed correctly (diagnostic output confirmed zlib-ng2.dll / zlib-ngd2.dll / zlib-ng.lib / zlib-ngd.lib all present in the install tree) but without a CMake config file. Visible from the port's install announcement: libzip advertises "provides CMake targets:" while zlib-ng only announces "provides pkg-config modules:". Neither find_library (loses Debug/Release selectivity) nor find_package(CONFIG) (unavailable on the older port) works. Manually declare the imported target with per-configuration IMPORTED_IMPLIB and IMPORTED_LOCATION properties, pointing directly at the triplet install tree via the vcpkg-toolchain-provided VCPKG_INSTALLED_DIR / VCPKG_TARGET_TRIPLET variables. That's what find_package(CONFIG) would emit if the port supplied one; doing it by hand makes the find resilient to the port's config availability. Keep the else-branch (Linux / macOS / Emscripten / Rocky Linux vcpkg) on the existing find_library path -- those platforms are single-config, don't have the Debug/Release selection issue, and have no applocal-copy dependency on imported-target metadata. Seen on run 24843744883, jobs 72724909304 (msvc-2019 Release) and 72724909339 (msvc-2019 Debug iterator-debug). --- source/MRMesh/CMakeLists.txt | 42 +++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/source/MRMesh/CMakeLists.txt b/source/MRMesh/CMakeLists.txt index 647ac13f7dfb..ee809142e17f 100644 --- a/source/MRMesh/CMakeLists.txt +++ b/source/MRMesh/CMakeLists.txt @@ -84,7 +84,47 @@ target_link_libraries(${PROJECT_NAME} PRIVATE libzip::zip) # sidesteps it, and those platforms are single-config so the multi-config # concern above doesn't apply. IF(WIN32) - find_package(zlib-ng CONFIG REQUIRED) + # Can't use find_package(zlib-ng CONFIG): the vcpkg zlib-ng 2.1.5 port + # that vcpkg 2024.10.21 (pinned for msvc-2019 legs) ships doesn't install + # a CMake config file. Can't use a plain find_library() either: it loses + # Debug/Release selectivity, linking Debug MRMesh.dll against release + # zlib-ng2.dll and breaking applocal-copy (see the diagnostic output on + # the iterator-debug triplet, STATUS_DLL_NOT_FOUND at MeshViewer.exe). + # + # Manually construct an imported target with per-configuration IMPLIB + + # LOCATION paths, locating the files directly under vcpkg's triplet + # install tree. Gives applocal-copy the metadata it needs to copy the + # correct DLL next to the .exe per configuration. + find_path(LIBZLIBNG_INCLUDE_DIR NAMES zlib-ng.h REQUIRED) + find_library(LIBZLIBNG_LIBRARY_RELEASE + NAMES zlib-ng + PATHS "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib" + NO_DEFAULT_PATH REQUIRED + ) + find_library(LIBZLIBNG_LIBRARY_DEBUG + NAMES zlib-ngd zlib-ng + PATHS "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/debug/lib" + NO_DEFAULT_PATH REQUIRED + ) + find_program(LIBZLIBNG_DLL_RELEASE + NAMES zlib-ng2.dll zlib-ng.dll + PATHS "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin" + NO_DEFAULT_PATH REQUIRED + ) + find_program(LIBZLIBNG_DLL_DEBUG + NAMES zlib-ngd2.dll zlib-ng2.dll zlib-ng.dll + PATHS "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/debug/bin" + NO_DEFAULT_PATH REQUIRED + ) + add_library(zlib-ng::zlib SHARED IMPORTED) + set_target_properties(zlib-ng::zlib PROPERTIES + IMPORTED_CONFIGURATIONS "DEBUG;RELEASE" + IMPORTED_IMPLIB_RELEASE "${LIBZLIBNG_LIBRARY_RELEASE}" + IMPORTED_LOCATION_RELEASE "${LIBZLIBNG_DLL_RELEASE}" + IMPORTED_IMPLIB_DEBUG "${LIBZLIBNG_LIBRARY_DEBUG}" + IMPORTED_LOCATION_DEBUG "${LIBZLIBNG_DLL_DEBUG}" + INTERFACE_INCLUDE_DIRECTORIES "${LIBZLIBNG_INCLUDE_DIR}" + ) target_link_libraries(${PROJECT_NAME} PRIVATE zlib-ng::zlib) ELSE() find_library(LIBZLIBNG_LIBRARY From 86ead0749f5f1cbef9a4c3bf9fb3e6c559241cc2 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Thu, 23 Apr 2026 20:04:43 +0300 Subject: [PATCH 16/21] MRZlib: route zlibDecompressStream through zlib-ng (zng_inflate*) Complements the earlier zlibCompressStream-on-zlib-ng swap on this PR: now both directions of MRMesh's public RFC 1950 / 1951 streaming API run through zlib-ng, not just deflate. Motivation is per upstream zlib-ng benchmarks and the AWS Open Source forks comparison: decompression in zlib-ng is ~2-3x faster than stock zlib at the inflate step (wider margin than the deflate speedup), driven by SIMD-vectorised inflate_chunkcopy (SSE2/SSSE3/AVX2 on x86, NEON on ARM). References in the upstream benchmark discussion github.com/zlib-ng/zlib-ng/discussions/871 and the 2025 re-run at github.com/zlib-ng/zlib-ng/issues/1486. Concrete MeshLib consumers that inherit the speedup: decompressZip, scene loaders that read .mrmesh blobs, and the round-trip verify step in CompressManySmallFilesToZip (~15 MB decompressed per test iteration, half its wallclock was inflate). Implementation notes: - Port the existing Buffer chunk-loop inflate code from MRZlib.cpp to MRZlibNg.cpp, replacing z_stream/inflate*/inflate with zng_stream/zng_inflate*/zng_inflate. Behaviour is identical: same 256 KiB chunks, same Z_NO_FLUSH/Z_STREAM_END handshake, same error codes (Z_* constants are shared between the two headers and have identical numeric values). - MRZlib.cpp had only the two zlibDecompressStream overloads left after the earlier compress move; with decompress now in MRZlibNg.cpp too, the old file has no live content. Delete it (and its ClCompile entries in MRMesh.vcxproj + MRMesh.vcxproj.filters). MRZlib.h stays -- it's the public API. - Round-trip remains compatible across forks. zlib-ng native's inflate reads any valid RFC 1950/1951 stream produced by stock zlib, libdeflate, zlib-ng, or any other DEFLATE encoder, so the parametrised ZlibDecompressTestFixture's pre-captured stock-zlib reference blobs continue to decompress correctly. - Stock zlib header is no longer included by any MRMesh TU. MRMesh/CMakeLists.txt still does find_package(ZLIB) + link ZLIB::ZLIB because libzip transitively depends on it and the existing link line doesn't hurt; a cleanup PR can drop that link later if desired. --- source/MRMesh/MRMesh.vcxproj | 1 - source/MRMesh/MRMesh.vcxproj.filters | 3 - source/MRMesh/MRZlib.cpp | 120 --------------------------- source/MRMesh/MRZlibNg.cpp | 56 ++++++++++++- 4 files changed, 53 insertions(+), 127 deletions(-) delete mode 100644 source/MRMesh/MRZlib.cpp diff --git a/source/MRMesh/MRMesh.vcxproj b/source/MRMesh/MRMesh.vcxproj index 63c353109a23..8f443807a687 100644 --- a/source/MRMesh/MRMesh.vcxproj +++ b/source/MRMesh/MRMesh.vcxproj @@ -733,7 +733,6 @@ - diff --git a/source/MRMesh/MRMesh.vcxproj.filters b/source/MRMesh/MRMesh.vcxproj.filters index 63fb2e56a4e4..f4d9b74815a7 100644 --- a/source/MRMesh/MRMesh.vcxproj.filters +++ b/source/MRMesh/MRMesh.vcxproj.filters @@ -1988,9 +1988,6 @@ Source Files\IO - - Source Files\IO - Source Files\IO diff --git a/source/MRMesh/MRZlib.cpp b/source/MRMesh/MRZlib.cpp deleted file mode 100644 index 08e804a903e6..000000000000 --- a/source/MRMesh/MRZlib.cpp +++ /dev/null @@ -1,120 +0,0 @@ -#include "MRZlib.h" -#include "MRBuffer.h" -#include "MRFinally.h" - -#include - -#include - -namespace -{ - -constexpr size_t cChunkSize = 256 * 1024; // 256 KiB - -// zlib's `windowBits` argument is sign-encoded: positive = zlib wrapper (RFC 1950); -// negative = raw deflate (RFC 1951, no wrapper). Magnitude is log2(window size); -// MAX_WBITS = 15 gives a 32 KiB window. -constexpr int kZlibWrapperBits = MAX_WBITS; -constexpr int kRawDeflateBits = -MAX_WBITS; - -// memLevel controls zlib's internal state size. 8 is zlib's default (its internal -// DEF_MEM_LEVEL in deflate.c is not exported; redeclared here). -constexpr int kDefaultMemLevel = 8; -static_assert( kDefaultMemLevel <= MAX_MEM_LEVEL ); - -std::string zlibToString( int code ) -{ - switch ( code ) - { - case Z_OK: - return "ok"; - case Z_STREAM_END: - return "stream end"; - case Z_NEED_DICT: - return "need dict"; - case Z_ERRNO: - return "errno"; - case Z_STREAM_ERROR: - return "stream error"; - case Z_DATA_ERROR: - return "data error"; - case Z_MEM_ERROR: - return "mem error"; - case Z_BUF_ERROR: - return "buf error"; - case Z_VERSION_ERROR: - return "version error"; - default: - return "unknown code"; - } -} - -int windowBitsFor( bool rawDeflate ) -{ - return rawDeflate ? kRawDeflateBits : kZlibWrapperBits; -} - -} // namespace - -namespace MR -{ - -// zlibCompressStream(...) and its int-level overload are implemented in -// MRZlibNg.cpp against zlib-ng's native API (zng_* prefix). Decompression -// stays on stock zlib -- zlibDecompressStream has to accept bytes written -// by any deflate producer, so compatibility matters more than speed, and -// libzip still uses stock zlib as well. - -Expected zlibDecompressStream( std::istream& in, std::ostream& out, const ZlibParams& params ) -{ - Buffer inChunk( cChunkSize ), outChunk( cChunkSize ); - z_stream stream { - .zalloc = Z_NULL, - .zfree = Z_NULL, - .opaque = Z_NULL, - }; - int ret; - if ( Z_OK != ( ret = inflateInit2( &stream, windowBitsFor( params.rawDeflate ) ) ) ) - return unexpected( zlibToString( ret ) ); - - MR_FINALLY { - inflateEnd( &stream ); - }; - - while ( !in.eof() ) - { - in.read( inChunk.data(), inChunk.size() ); - if ( in.bad() ) - return unexpected( "I/O error" ); - stream.next_in = reinterpret_cast( inChunk.data() ); - stream.avail_in = (unsigned)in.gcount(); - assert( stream.avail_in <= (unsigned)inChunk.size() ); - - do - { - stream.next_out = reinterpret_cast( outChunk.data() ); - stream.avail_out = (unsigned)outChunk.size(); - ret = inflate( &stream, Z_NO_FLUSH ); - if ( Z_OK != ret && Z_STREAM_END != ret ) - return unexpected( zlibToString( ret ) ); - - assert( stream.avail_out <= (unsigned)outChunk.size() ); - out.write( outChunk.data(), (unsigned)outChunk.size() - stream.avail_out ); - if ( out.bad() ) - return unexpected( "I/O error" ); - - if ( Z_STREAM_END == ret ) - return {}; - } - while ( stream.avail_out == 0 ); - } - - return {}; -} - -Expected zlibDecompressStream( std::istream& in, std::ostream& out ) -{ - return zlibDecompressStream( in, out, ZlibParams{} ); -} - -} // namespace MR diff --git a/source/MRMesh/MRZlibNg.cpp b/source/MRMesh/MRZlibNg.cpp index 2751703023f0..e437ec022cb6 100644 --- a/source/MRMesh/MRZlibNg.cpp +++ b/source/MRMesh/MRZlibNg.cpp @@ -7,8 +7,10 @@ // as libz-ng, so linking both into the same process is safe. The stock-zlib // header is NOT included in this translation unit -- each of the // Z_*/MAX_WBITS constants we need is also defined by under the -// same name, and keeping the two headers in separate TUs sidesteps any -// question of macro-redefinition ordering. +// same name. MRZlib.cpp (the previous stock-zlib inflate path) has been +// removed now that both compress and decompress run through zlib-ng here; +// MeshLib's direct consumers of RFC 1950/1951 streams go through the +// faster deflate AND the faster inflate uniformly. #include #include @@ -17,7 +19,7 @@ namespace { -constexpr size_t cChunkSize = 256 * 1024; // 256 KiB, same as the zlib path +constexpr size_t cChunkSize = 256 * 1024; // 256 KiB // windowBits is sign-encoded the same way zlib documents it: positive = // zlib wrapper (RFC 1950), negative = raw deflate (RFC 1951, no wrapper). @@ -119,4 +121,52 @@ Expected zlibCompressStream( std::istream& in, std::ostream& out, int leve return zlibCompressStream( in, out, ZlibCompressParams{ .level = level } ); } +Expected zlibDecompressStream( std::istream& in, std::ostream& out, const ZlibParams& params ) +{ + Buffer inChunk( cChunkSize ), outChunk( cChunkSize ); + zng_stream stream{}; + int ret; + if ( Z_OK != ( ret = zng_inflateInit2( &stream, windowBitsFor( params.rawDeflate ) ) ) ) + return unexpected( zngToString( ret ) ); + + MR_FINALLY { + zng_inflateEnd( &stream ); + }; + + while ( !in.eof() ) + { + in.read( inChunk.data(), static_cast( inChunk.size() ) ); + if ( in.bad() ) + return unexpected( "I/O error" ); + stream.next_in = reinterpret_cast( inChunk.data() ); + stream.avail_in = static_cast( in.gcount() ); + assert( stream.avail_in <= static_cast( inChunk.size() ) ); + + do + { + stream.next_out = reinterpret_cast( outChunk.data() ); + stream.avail_out = static_cast( outChunk.size() ); + ret = zng_inflate( &stream, Z_NO_FLUSH ); + if ( Z_OK != ret && Z_STREAM_END != ret ) + return unexpected( zngToString( ret ) ); + + assert( stream.avail_out <= static_cast( outChunk.size() ) ); + out.write( outChunk.data(), static_cast( outChunk.size() ) - stream.avail_out ); + if ( out.bad() ) + return unexpected( "I/O error" ); + + if ( Z_STREAM_END == ret ) + return {}; + } + while ( stream.avail_out == 0 ); + } + + return {}; +} + +Expected zlibDecompressStream( std::istream& in, std::ostream& out ) +{ + return zlibDecompressStream( in, out, ZlibParams{} ); +} + } // namespace MR From 30582484f4390d18044abfbc8324d8f5eece578e Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Thu, 23 Apr 2026 21:31:31 +0300 Subject: [PATCH 17/21] Revert "ci(windows): add DLL-not-found diagnostic steps" This reverts commit cb9dfb01389e77426392c405d29fb1496f548c60. --- .github/workflows/build-test-windows.yml | 70 ------------------------ 1 file changed, 70 deletions(-) diff --git a/.github/workflows/build-test-windows.yml b/.github/workflows/build-test-windows.yml index 4b875605f16c..43cfe15ddc03 100644 --- a/.github/workflows/build-test-windows.yml +++ b/.github/workflows/build-test-windows.yml @@ -111,30 +111,6 @@ jobs: working-directory: C:\vcpkg run: C:\vcpkg\vcpkg.exe integrate install - # Diagnostic step: inventory the vcpkg installed tree so DLL-not-found - # failures later in the build can be traced back to whether the DLL was - # (a) never installed, (b) installed under an unexpected name, or - # (c) installed fine but not copied to the build output. - - name: Diagnostic — vcpkg installed tree - if: ${{ always() }} - shell: pwsh - continue-on-error: true - run: | - $triplet = "${{ matrix.vcpkg_triplet || 'x64-windows-meshlib' }}" - Write-Host "=== Triplet: $triplet ===" - foreach ($sub in 'bin','debug\bin','lib','debug\lib') { - $dir = "C:\vcpkg\installed\$triplet\$sub" - Write-Host "" - Write-Host "--- $dir ---" - if (Test-Path $dir) { - Get-ChildItem $dir -File -ErrorAction SilentlyContinue | - Select-Object Name, @{N='SizeKB';E={[int]($_.Length/1KB)}} | - Sort-Object Name | Format-Table -AutoSize - } else { - Write-Host "(directory not present)" - } - } - - name: Restore CUDA Cache uses: actions/cache@v5 id: cuda-cache @@ -237,52 +213,6 @@ jobs: call "${{matrix.vc-path}}\Common7\Tools\VsDevCmd.bat" -arch=amd64 ${{ fromJSON('["", "-vcvars_ver=14.2"]')[matrix.cxx_compiler == 'msvc-2019'] }} call ./scripts/mrbind/generate_win.bat -B --trace MODE=none VS_MODE=${{matrix.config}} - # Diagnostic step: before launching MeshViewer.exe, dump the state the - # Windows loader will actually see. Prints the DLLs present next to the - # .exe, the import tables of the key binaries (so we know which DLL - # names the loader wants), and the PATH at launch. Lets DLL-not-found - # failures self-diagnose from the CI log. - - name: Diagnostic — output bin DLL inventory + MRMesh imports - if: ${{ always() }} - shell: pwsh - continue-on-error: true - working-directory: source\x64\${{ matrix.config }} - run: | - Write-Host "=== DLLs / EXEs in $(Get-Location) ===" - Get-ChildItem . -File -Include *.dll,*.exe -Recurse -ErrorAction SilentlyContinue | - Select-Object Name, @{N='SizeKB';E={[int]($_.Length/1KB)}} | - Sort-Object Name | Format-Table -AutoSize - - # Locate dumpbin.exe via the VC path hint passed in via matrix, then - # fall back to whatever is on PATH (Visual Studio Integration step - # earlier populates MSBuild PATH, not VC bin). - $vcpath = '${{ matrix.vc-path }}' - $dumpbin = (Get-ChildItem "$vcpath\VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe" -ErrorAction SilentlyContinue | - Select-Object -First 1).FullName - if (-not $dumpbin) { - $dumpbin = (Get-Command dumpbin.exe -ErrorAction SilentlyContinue).Source - } - - if ($dumpbin) { - Write-Host "" - Write-Host "Using dumpbin at: $dumpbin" - foreach ($target in 'MeshViewer.exe','MRMesh.dll','MRTest.exe') { - if (Test-Path $target) { - Write-Host "" - Write-Host "=== Import table of $target (dumpbin /dependents) ===" - & $dumpbin /dependents $target | - Where-Object { $_ -match '\.dll$' } | - ForEach-Object { " " + $_.Trim() } - } - } - } else { - Write-Warning "dumpbin.exe not found; skipping import-table dump" - } - - Write-Host "" - Write-Host "=== PATH at diagnostic step ===" - $env:PATH -split ';' | ForEach-Object { " $_" } - - name: Run Start-and-Exit Tests timeout-minutes: 3 working-directory: source\x64\${{ matrix.config }} From e5187e2cebdb16fdc260634b0d876e3715f0e4ba Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Thu, 23 Apr 2026 21:34:08 +0300 Subject: [PATCH 18/21] MRZlib: rename MRZlibNg.cpp -> MRZlib.cpp; restore test scale Now that both zlibCompressStream and zlibDecompressStream live in the same TU through zlib-ng (and there's no longer a second zlib-only TU to disambiguate from), rename MRZlibNg.cpp back to MRZlib.cpp -- the file is the stock place readers expect to find MRZlib's implementation. MRMesh.vcxproj and MRMesh.vcxproj.filters updated to match. Also restore source/MRTest/MRZipCompressTests.cpp to master's test- scale constants (1000 sphere vertices, 20+20 files x 6000 bytes). The larger constants were temporarily bumped during the benchmark comparisons on this branch; they're not appropriate for the default MRTest run. --- source/MRMesh/MRMesh.vcxproj | 2 +- source/MRMesh/MRMesh.vcxproj.filters | 2 +- source/MRMesh/{MRZlibNg.cpp => MRZlib.cpp} | 14 +++++--------- source/MRTest/MRZipCompressTests.cpp | 6 +++--- 4 files changed, 10 insertions(+), 14 deletions(-) rename source/MRMesh/{MRZlibNg.cpp => MRZlib.cpp} (89%) diff --git a/source/MRMesh/MRMesh.vcxproj b/source/MRMesh/MRMesh.vcxproj index 8f443807a687..d90063127af2 100644 --- a/source/MRMesh/MRMesh.vcxproj +++ b/source/MRMesh/MRMesh.vcxproj @@ -733,7 +733,7 @@ - + diff --git a/source/MRMesh/MRMesh.vcxproj.filters b/source/MRMesh/MRMesh.vcxproj.filters index f4d9b74815a7..1a90220ae273 100644 --- a/source/MRMesh/MRMesh.vcxproj.filters +++ b/source/MRMesh/MRMesh.vcxproj.filters @@ -1988,7 +1988,7 @@ Source Files\IO - + Source Files\IO diff --git a/source/MRMesh/MRZlibNg.cpp b/source/MRMesh/MRZlib.cpp similarity index 89% rename from source/MRMesh/MRZlibNg.cpp rename to source/MRMesh/MRZlib.cpp index e437ec022cb6..e83cf589ff4a 100644 --- a/source/MRMesh/MRZlibNg.cpp +++ b/source/MRMesh/MRZlib.cpp @@ -2,15 +2,11 @@ #include "MRBuffer.h" #include "MRFinally.h" -// zlib-ng native mode: its zlib-ng.h lives alongside zlib.h on most -// platforms, uses the zng_ prefix on every symbol, and publishes its SONAME -// as libz-ng, so linking both into the same process is safe. The stock-zlib -// header is NOT included in this translation unit -- each of the -// Z_*/MAX_WBITS constants we need is also defined by under the -// same name. MRZlib.cpp (the previous stock-zlib inflate path) has been -// removed now that both compress and decompress run through zlib-ng here; -// MeshLib's direct consumers of RFC 1950/1951 streams go through the -// faster deflate AND the faster inflate uniformly. +// zlib-ng in native mode: the zng_ prefix on every symbol keeps it ABI- +// distinct from stock zlib (which libzip still links as before), and +// re-exports the MAX_WBITS / Z_* constants we need under the +// same spelling as , so this TU doesn't include stock zlib's +// header at all. #include #include diff --git a/source/MRTest/MRZipCompressTests.cpp b/source/MRTest/MRZipCompressTests.cpp index 2e8b3eeacf7a..9cf9ff58b068 100644 --- a/source/MRTest/MRZipCompressTests.cpp +++ b/source/MRTest/MRZipCompressTests.cpp @@ -28,7 +28,7 @@ TEST( MRMesh, CompressSphereToZip ) UniqueTemporaryFolder srcFolder; ASSERT_TRUE( bool( srcFolder ) ); - constexpr int targetVerts = 100000; // increase it to make the file being compressed larger, 100'000 vertices -> 12M bytes + constexpr int targetVerts = 1000; // increase it to make the file being compressed larger, 100'000 vertices -> 12M bytes SphereParams params; params.radius = 1.0f; params.numMeshVertices = targetVerts; @@ -82,9 +82,9 @@ TEST( MRMesh, CompressManySmallFilesToZip ) ASSERT_TRUE( bool( srcFolder ) ); // increase both below numbers to make the files being compressed larger, 200 * 2 files * 60'000 bytes -> 24M bytes - constexpr int numBinaryFiles = 200; + constexpr int numBinaryFiles = 20; constexpr int numJsonFiles = numBinaryFiles; - constexpr size_t bytesPerFile = 60000; + constexpr size_t bytesPerFile = 6000; // Simple LCG used to produce deterministic pseudo-random bytes. // Keeps the test reproducible across runs and platforms while avoiding From fdb004a2e033ad19834c96113dd46c08aa3d6639 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Thu, 23 Apr 2026 22:21:06 +0300 Subject: [PATCH 19/21] ci: retrigger CI (previous run's vcpkg cache restore failed silently) From 29057ae91bbda07350bc7db6095d7335bb3cc791 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Fri, 24 Apr 2026 16:01:45 +0300 Subject: [PATCH 20/21] thirdparty(vcpkg): overlay zlib-ng port to strip its GNU symbol version script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream zlib-ng 2.3.3's CMakeLists.txt defines HAVE_SYMVER and passes -Wl,--version-script=zlib-ng.map on non-Apple, non-AIX Unix. Both together tag every exported symbol in libz-ng.so.2 with ZLIB_NG_2.0.0 / ZLIB_NG_2.1.0 version nodes, which land in DT_VERNEED of anything linking against it. auditwheel's manylinux policy database has no entry for the pair (libz-ng.so.2, ZLIB_NG_*), so the Linux-vcpkg NuGet wheel-repair step fails with "too-recent versioned symbols" even though no actual symbol is too recent — auditwheel's generic phrasing for any (lib, version- tag) pair it can't place in a known policy. This blocks #5959 on the Linux-vcpkg Clang 20 Release leg. Contrast with stock libz.so.1: auditwheel has that library explicitly on the manylinux allowlist with its ZLIB_1.2.0 tags pre-registered, so it passes trivially. zlib-ng is not on any allowlist. We don't exercise zlib-ng's ABI-versioning machinery — MeshLib's consumers rebuild against whatever libz-ng we ship — so neutralizing both knobs is safe. The overlay port replaces the guarding `if(NOT APPLE AND NOT CMAKE_SYSTEM_NAME STREQUAL AIX)` with `if(FALSE)`, skipping the HAVE_SYMVER define and the --version-script linker flag in one edit. Upstream's zlib-ng.map file is left on disk but never wired into the build. Co-Authored-By: Claude Opus 4.7 (1M context) --- thirdparty/vcpkg/ports/zlib-ng/portfile.cmake | 89 +++++++++++++++++++ thirdparty/vcpkg/ports/zlib-ng/vcpkg.json | 18 ++++ 2 files changed, 107 insertions(+) create mode 100644 thirdparty/vcpkg/ports/zlib-ng/portfile.cmake create mode 100644 thirdparty/vcpkg/ports/zlib-ng/vcpkg.json diff --git a/thirdparty/vcpkg/ports/zlib-ng/portfile.cmake b/thirdparty/vcpkg/ports/zlib-ng/portfile.cmake new file mode 100644 index 000000000000..a842eabbd402 --- /dev/null +++ b/thirdparty/vcpkg/ports/zlib-ng/portfile.cmake @@ -0,0 +1,89 @@ +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO zlib-ng/zlib-ng + REF "${VERSION}" + SHA512 e2057c764f1d5aaee738edee7e977182c5b097e3c95489dcd8de813f237d92a05daaa86d68d44b331d9fec5d1802586a8f6cfb658ba849874aaa14e72a8107f5 + HEAD_REF develop +) + +# MeshLib: strip zlib-ng's GNU symbol version script and the matching .symver +# pragmas in its C sources. +# +# Upstream's CMakeLists.txt defines -DHAVE_SYMVER (which turns on __asm__( +# ".symver foo, foo@@ZLIB_NG_2.0.0") pragmas in zbuild.h) and passes +# -Wl,--version-script=zlib-ng.map to the linker whenever the target is +# non-Apple, non-AIX UNIX. Both together tag every exported symbol in +# libz-ng.so with ZLIB_NG_2.0.0 / ZLIB_NG_2.1.0 version nodes, which end up +# in DT_VERNEED of anything linking against libz-ng. +# +# auditwheel's manylinux policy database has no entry for (libz-ng.so.2, +# ZLIB_NG_*), so the MeshLib NuGet wheel-repair step fails with "too-recent +# versioned symbols" even though no actual symbol is too recent. We don't +# exercise zlib-ng's ABI-versioning machinery (our consumers rebuild against +# whatever libz-ng we ship), so we neutralize both knobs by flipping the +# guarding condition to FALSE. Upstream's zlib-ng.map file is left on disk +# but never wired into the build. +vcpkg_replace_string( + "${SOURCE_PATH}/CMakeLists.txt" + "if(NOT APPLE AND NOT CMAKE_SYSTEM_NAME STREQUAL AIX)" + "if(FALSE) # MeshLib: symbol versioning disabled, see thirdparty/vcpkg/ports/zlib-ng/portfile.cmake" +) + +# Set ZLIB_COMPAT in the triplet file to turn on +if(NOT DEFINED ZLIB_COMPAT) + set(ZLIB_COMPAT OFF) +endif() + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + OPTIONS + "-DZLIB_FULL_VERSION=${ZLIB_FULL_VERSION}" + -DZLIB_ENABLE_TESTS=OFF + -DWITH_NEW_STRATEGIES=ON + -DZLIB_COMPAT=${ZLIB_COMPAT} + OPTIONS_RELEASE + -DWITH_OPTIM=ON +) +vcpkg_cmake_install() +vcpkg_copy_pdbs() + +# Condition in `WIN32`, from https://github.com/zlib-ng/zlib-ng/blob/2.1.5/CMakeLists.txt#L1081-L1100 +# (dynamic) for `zlib` or (static `MSVC) for `zlibstatic` or default `z` +# i.e. (windows) and not (static mingw) https://learn.microsoft.com/en-us/vcpkg/maintainers/variables#vcpkg_target_is_system +if(VCPKG_TARGET_IS_WINDOWS AND (NOT (VCPKG_LIBRARY_LINKAGE STREQUAL static AND VCPKG_TARGET_IS_MINGW))) + set(_port_suffix) + if(ZLIB_COMPAT) + set(_port_suffix "") + else() + set(_port_suffix "-ng") + endif() + + set(_port_output_name) + if(VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic") + set(_port_output_name "zlib${_port_suffix}") + else() + set(_port_output_name "zlibstatic${_port_suffix}") + endif() + + # CMAKE_DEBUG_POSTFIX from https://github.com/zlib-ng/zlib-ng/blob/2.1.5/CMakeLists.txt#L494 + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/zlib${_port_suffix}.pc" " -lz${_port_suffix}" " -l${_port_output_name}") + endif() + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/zlib${_port_suffix}.pc" " -lz${_port_suffix}" " -l${_port_output_name}d") + endif() +endif() + +vcpkg_fixup_pkgconfig() + +if(ZLIB_COMPAT) + set(_cmake_dir "ZLIB") +else() + set(_cmake_dir "zlib-ng") +endif() +vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/${_cmake_dir}) + +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/share" + "${CURRENT_PACKAGES_DIR}/debug/include" +) +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE.md") diff --git a/thirdparty/vcpkg/ports/zlib-ng/vcpkg.json b/thirdparty/vcpkg/ports/zlib-ng/vcpkg.json new file mode 100644 index 000000000000..380f3c9ab75e --- /dev/null +++ b/thirdparty/vcpkg/ports/zlib-ng/vcpkg.json @@ -0,0 +1,18 @@ +{ + "name": "zlib-ng", + "version": "2.3.3", + "port-version": 1, + "description": "zlib replacement with optimizations for 'next generation' systems", + "homepage": "https://github.com/zlib-ng/zlib-ng", + "license": "Zlib", + "dependencies": [ + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ] +} From 6032709d308d098e087dd82bb1155fa1320e151d Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Fri, 24 Apr 2026 16:06:44 +0300 Subject: [PATCH 21/21] ci: retrigger with skip-image-rebuild label removed (image needs rebuild for overlay zlib-ng port)