Skip to content

VFS: add .sdzst support (zip with zstd / method-93 entries)#3055

Draft
tomjn wants to merge 3 commits into
beyond-all-reason:masterfrom
tomjn:feature/vfs-sdzst-zstd
Draft

VFS: add .sdzst support (zip with zstd / method-93 entries)#3055
tomjn wants to merge 3 commits into
beyond-all-reason:masterfrom
tomjn:feature/vfs-sdzst-zstd

Conversation

@tomjn

@tomjn tomjn commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new VFS archive format .sdzst - a zip container whose entries are Zstandard-compressed (PKWARE compression method 93) - alongside the existing .sd7 (7-Zip) and .sdz (zip/deflate) formats.

Approach

The vendored minizip is not modified. minizip can already enumerate method-93 entries (the compression method is only validated on its decode-open path, not on info reads), so:

  • CZipArchive::GetFileZstd reads a zstd entry by locating its raw frame directly from the zip central-directory / local-file headers (using the unz_file_pos minizip already gives us per entry) and decompresses it with libzstd, verifying the CRC manually. Zip64 local-header offsets are handled.
  • This deliberately avoids patching minizip, which matters because the engine links a prebuilt minizip from spring-static-libs (so source patches to rts/lib/minizip are not used - this is what an earlier patch-based attempt tripped over in CI).
  • A new CZipStdArchiveFactory("sdzst") registers the same CZipArchive; GetType() stays ARCHIVE_TYPE_SDZ, so .sdzst is treated as a compressed archive everywhere for free.
  • The zstd decoder is a decompress-only static lib (zstd_dec) built from the already-vendored Tracy zstd sources - no new external dependency.

Decoding is global: an existing .sdz containing method-93 entries will now also read. The zip writer is untouched (read-only feature).

Verification

  • The exact decode logic (GetFileZstd), built with gcc-16 against a pristine, unpatched minizip (matching the prebuilt-lib build), round-trips 3/3 entries of a real method-93 .sdzst authored by Python 3.14's ZIP_ZSTANDARD - a multi-KB text file, a tiny Lua file, and an empty-file edge case - read back byte-identical with CRC validation.
  • zstd_dec source set compiles under gcc-16 and clang.

Not yet verified locally

  • A full engine CMake configure + link (no build tree on the dev machine; engine headers need the project's own arch setup). The CMake wiring mirrors the existing minizip pattern - CI build is the remaining check.
  • Deflate .sdz / .sd7 are unaffected: the zstd branch is gated on compression_method == 93 and the existing deflate/store path is unchanged.

Notes

  • Authoring .sdzst files is left to external tooling (e.g. Python 3.14 zipfile.ZIP_ZSTANDARD); this PR adds read support only.
  • Scope is the engine VFS; pr-downloader's separate minizip copy is untouched.

tomjn added 2 commits June 22, 2026 17:19
Adds a new .sdzst archive format: a zip container whose entries use
Zstandard (PKWARE compression method 93).

The vendored minizip reader is left almost untouched - the only change
is whitelisting method 93 in its two method-validation guards so such an
entry can be opened in raw mode. All zstd handling lives in
CZipArchive::GetFileImpl, which reads the raw frame and decompresses it
with libzstd, verifying the CRC manually (raw reads bypass minizip's
CRC-on-close check).

zstd decoding is global: existing .sdz archives that happen to contain
method-93 entries now read too; .sdzst is a naming/signalling convention,
registered as the same CZipArchive behind a new "sdzst" factory.

The decoder reuses the already-vendored Tracy zstd sources, built as a
decompress-only static lib (zstd_dec), so no new external dependency is
introduced. Read-only; the zip writer is unchanged.
The engine links a prebuilt minizip (from spring-static-libs), so the
earlier source patch to rts/lib/minizip was not used in CI: ZipArchive
failed to build (Z_ZSTDED undeclared) and, even fixed, the prebuilt
minizip would still reject method 93 on its open path at runtime.

Revert the minizip changes and instead read zstd entries self-contained:
minizip still enumerates method-93 entries fine (the method is only
validated on the decode-open path), so CZipArchive::GetFileZstd locates
the raw frame via the zip central-directory / local-file headers (from
the unz_file_pos we already store per entry) and decompresses it with
libzstd, verifying the CRC manually. Zip64 local-header offsets handled.

Verified by round-tripping a real Python-authored method-93 .sdzst
(text, lua, empty-file) byte-identically against a pristine minizip.
@tomjn

tomjn commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Some notes:

  • ideally we would upgrade minizip and use the newer minizip-ng, giving us zst support
    • initial commit used the zstd lib from Tracy and modified minizip on 2 lines to get the ball rolling, but the final form of how libzstd or a minizip upgrade takes form is undecided ( using the cmake macro for now to find the library which works fine with homebrew and if the library is alread installed globally? find_package(Zstd REQUIRED) )
  • while zst archives in this format are technically zip files, I chose .sdzst to differentiate since an sdz using zst compression would be indistinguishable from existing sdz files, but mysteriously not appear on older engines in map/game lists, potentially causing issues with lobbies and autohosts if not confused players. .sdzst makes this clear and obvious ( needs specific engine version or newer )
    • noting that we do have separate archive formats e.g. .sdp that are different so it's not totally unprecedented
  • my initial expected use here is autohosts and infrastructure, the 7zip level compression with the much higher decompression performance makes things a little more efficient

The previous commit built a zstd_dec static lib out of Tracy's internal
vendored zstd sources, which couples the VFS build to Tracy's layout and
would break on a Tracy update.

Source zstd as a first-class dependency instead: add FindZstd.cmake
(modelled on FindMiniZip.cmake) and resolve it with find_package_static,
the same way zlib and minizip are found from the static-libs / mingwlibs
bundle. SPRING_ZSTD_INCLUDE_DIR / SPRING_ZSTD_LIBRARY now point at the
found package; the consuming targets are unchanged.

Note: this requires libzstd to be present in the static-libs / mingwlibs
bundles. Until that is added CI cannot link, which is expected.
@tomjn

tomjn commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

As for why we might want zstd:

Screenshot 2026-06-22 at 19 24 30

It gives comparable compression to sd7 with performance better than sdz

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant