From 71e52159f9b99ffeff719a210ad94eaa278206f7 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 07:47:56 -0500 Subject: [PATCH] Add spv_store.h, which keeps a shader's SPIR-V zstd-compressed while nobody reads it, and its check. Readers pin the store and get the plain words, decoded from the frame when the plain copy is gone; one packer makes the frame of a store unused for a while, checks it by decoding it, and frees the plain copy. The lock is held for a few loads and stores only, never across a decode, an encode or a free. tests/spv_store_check.py walks a store through its states and runs four reader threads against a packer for three seconds, comparing every word they read. Co-Authored-By: Claude Opus 5.5 --- runtime/spv_store.h | 197 +++++++++++++++++++++++++++++++++++++++ tests/spv_store_check.c | 194 ++++++++++++++++++++++++++++++++++++++ tests/spv_store_check.py | 49 ++++++++++ 3 files changed, 440 insertions(+) create mode 100644 runtime/spv_store.h create mode 100644 tests/spv_store_check.c create mode 100644 tests/spv_store_check.py diff --git a/runtime/spv_store.h b/runtime/spv_store.h new file mode 100644 index 00000000..408b0ace --- /dev/null +++ b/runtime/spv_store.h @@ -0,0 +1,197 @@ +// A translated shader's SPIR-V that is kept zstd-compressed while nobody reads it (gfx_shader.c "SPIR-V at rest"). +// +// A store holds the SPIR-V as a plain copy, as a zstd frame, or as both. Readers pin it (spv_pin), which hands out the +// plain copy and decodes the frame first when the plain copy is gone, and unpin it when done (spv_unpin). One thread, +// the packer, makes the frame of a store that has gone unused for a while and then frees the plain copy (spv_pack_*). +// +// Lock rules, which make a reader never see a buffer swapped or freed under it: +// - every store of one owner shares one lock (PlatMutex), held only for a few loads and stores: never across a +// decode, an encode or a free, so a reader on the drawing thread waits for no codec; +// - plain changes under the lock only: a reader sets it from NULL to a buffer it decoded, the packer sets it back to +// NULL, and only while pins is 0. A reader gets the pointer and counts its pin in the same locked section, so +// the copy it holds cannot be freed before its unpin; +// - z is set once, by the packer, before any plain copy is dropped, and stays until spv_store_free: a reader that +// found plain NULL under the lock decodes z outside it; +// - one packer per owner at a time (the owner's own lock around its passes): spv_pack_begin and spv_pack_end of one +// store pair up, and nothing else drops plain copies. +// Two readers that find the frame alone both decode it; the first one to come back installs its copy and the other +// frees its own. The frame is checked by decoding it before the plain copy may go (spv_pack_encode), so a store never +// holds a frame that does not give back its SPIR-V bit for bit. +// +// Header-only so that tests/spv_store_check.c runs it against the platform layer and libzstd alone. +#pragma once +#include +#include +#include +#include + +#include "platform/platform.h" + +typedef struct { + uint32_t *plain; // the SPIR-V readers get; NULL while the frame alone holds it (changes under the lock) + uint64_t words; // its length in 32-bit words; fixed once the store is filled + void *z; // its zstd frame, exactly zsize bytes; set once, freed by spv_store_free only + uint32_t zsize; + uint32_t pins; // readers between spv_pin and spv_unpin (under the lock) + uint64_t used_ms; // when the last reader let go, or when the store was filled (under the lock) + uint64_t fnv; // FNV-1a (64-bit) over the SPIR-V bytes once fnv_ok is 1 + volatile int32_t fnv_ok; +} SpvStore; + +// The packer's buffers, kept from one store to the next: the encoder's output before it is copied out at its size, +// and the decode that checks it. +typedef struct { + ZSTD_CCtx *cc; + void *out; + size_t out_cap; + uint32_t *back; + size_t back_cap; +} SpvPacker; + +static inline uint64_t spv_fnv_words(const uint32_t *w, uint64_t words) { + const uint8_t *b = (const uint8_t *)w; + uint64_t h = 0xcbf29ce484222325ull; + for (uint64_t i = 0; i < words * 4; i++) h = (h ^ b[i]) * 0x100000001b3ull; + return h; +} + +// The store takes plain (malloc'd, words long) as its plain copy, last used at now_ms. +static inline void spv_store_fill(SpvStore *s, uint32_t *plain, uint64_t words, uint64_t now_ms) { + memset(s, 0, sizeof *s); + s->plain = plain, s->words = words, s->used_ms = now_ms; +} + +static inline void spv_store_free(SpvStore *s) { + free(s->plain), free(s->z); + s->plain = NULL, s->z = NULL, s->zsize = 0; +} + +// The plain SPIR-V, valid until the matching spv_unpin. *decode_ns: the time spent decoding the frame here, 0 when +// the plain copy was there. NULL only when the decode could not allocate its buffer or the frame did not decode to +// the store's length (spv_pack_encode checked it, so neither should happen). +static inline const uint32_t *spv_pin(SpvStore *s, PlatMutex *lk, uint64_t *decode_ns) { + *decode_ns = 0; + plat_mutex_lock(lk); + uint32_t *p = s->plain; + if (p) s->pins++; + plat_mutex_unlock(lk); + if (p) return p; + uint64_t t0 = plat_now_ns(); + size_t bytes = (size_t)s->words * 4; + uint32_t *buf = malloc(bytes ? bytes : 4); + if (!buf) return NULL; + size_t got = ZSTD_decompress(buf, bytes, s->z, s->zsize); + if (ZSTD_isError(got) || got != bytes) { + free(buf); + return NULL; + } + plat_mutex_lock(lk); + if (s->plain) p = s->plain; // another reader decoded it meanwhile: its copy, and this one goes + else __atomic_store_n(&s->plain, buf, __ATOMIC_RELAXED), p = buf, buf = NULL; + s->pins++; + plat_mutex_unlock(lk); + free(buf); + *decode_ns = plat_now_ns() - t0; + return p; +} + +static inline void spv_unpin(SpvStore *s, PlatMutex *lk, uint64_t now_ms) { + plat_mutex_lock(lk); + s->pins--; + if (now_ms > s->used_ms) s->used_ms = now_ms; + plat_mutex_unlock(lk); +} + +// The FNV-1a of the SPIR-V: worked out at the first call (pinning the store, so decoding it if it is compressed) and +// kept. spv_pack_encode works it out before it compresses, so asking a compressed store decodes nothing. +static inline uint64_t spv_fnv(SpvStore *s, PlatMutex *lk, uint64_t now_ms, uint64_t *decode_ns) { + *decode_ns = 0; + if (__atomic_load_n(&s->fnv_ok, __ATOMIC_ACQUIRE)) return __atomic_load_n(&s->fnv, __ATOMIC_RELAXED); + const uint32_t *w = spv_pin(s, lk, decode_ns); + if (!w) return 0; + uint64_t h = spv_fnv_words(w, s->words); + spv_unpin(s, lk, now_ms); + __atomic_store_n(&s->fnv, h, __ATOMIC_RELAXED); // racing callers store the same value + __atomic_store_n(&s->fnv_ok, 1, __ATOMIC_RELEASE); + return h; +} + +// Whether the store has gone unused for idle_ms at now_ms: no pin, and its last use at least that long ago (a use +// stamped after the packer read the clock counts as recent). Caller holds the lock. +static inline int spv_idle_locked(const SpvStore *s, uint64_t now_ms, uint64_t idle_ms) { + return !s->pins && now_ms >= s->used_ms && now_ms - s->used_ms >= idle_ms; +} + +enum { SPV_PACK_NONE, SPV_PACK_ENCODE, SPV_PACK_DROP }; + +// The packer's first step on a store: SPV_PACK_NONE when there is nothing to do (no plain copy, in use or used within +// idle_ms); SPV_PACK_DROP when the frame exists and the plain copy may go (spv_pack_end); SPV_PACK_ENCODE when the +// frame is to be made first: the plain copy is pinned for the packer, which encodes it (spv_pack_encode) and then +// calls spv_pack_end in any case. +static inline int spv_pack_begin(SpvStore *s, PlatMutex *lk, uint64_t now_ms, uint64_t idle_ms) { + plat_mutex_lock(lk); + int r = SPV_PACK_NONE; + if (s->plain && spv_idle_locked(s, now_ms, idle_ms)) { + r = s->z ? SPV_PACK_DROP : SPV_PACK_ENCODE; + if (r == SPV_PACK_ENCODE) s->pins++; + } + plat_mutex_unlock(lk); + return r; +} + +// Encodes the plain copy the packer pinned (spv_pack_begin gave SPV_PACK_ENCODE) at zstd level `level` and checks the +// frame by decoding it. No lock is held: readers go on using the plain copy meanwhile. Returns the frame (malloc'd, +// *zsize bytes) or NULL when the encoder failed or the check found a difference; the FNV is worked out here too. +static inline void *spv_pack_encode(SpvStore *s, SpvPacker *pk, int level, uint32_t *zsize) { + const uint32_t *w = s->plain; // pinned by spv_pack_begin: neither swapped nor freed until spv_pack_end + size_t bytes = (size_t)s->words * 4; + if (!__atomic_load_n(&s->fnv_ok, __ATOMIC_ACQUIRE)) { + __atomic_store_n(&s->fnv, spv_fnv_words(w, s->words), __ATOMIC_RELAXED); + __atomic_store_n(&s->fnv_ok, 1, __ATOMIC_RELEASE); + } + if (!pk->cc && !(pk->cc = ZSTD_createCCtx())) return NULL; + size_t cap = ZSTD_compressBound(bytes); + if (pk->out_cap < cap) { + void *o = realloc(pk->out, cap); + if (!o) return NULL; + pk->out = o, pk->out_cap = cap; + } + if (pk->back_cap < bytes + 4) { + void *b = realloc(pk->back, bytes + 4); + if (!b) return NULL; + pk->back = b, pk->back_cap = bytes + 4; + } + size_t n = ZSTD_compressCCtx(pk->cc, pk->out, cap, w, bytes, level); + if (ZSTD_isError(n) || n > UINT32_MAX) return NULL; + size_t got = ZSTD_decompress(pk->back, bytes + 4, pk->out, n); + if (ZSTD_isError(got) || got != bytes || memcmp(pk->back, w, bytes)) return NULL; + void *z = malloc(n ? n : 1); + if (!z) return NULL; + memcpy(z, pk->out, n); + *zsize = (uint32_t)n; + return z; +} + +// The packer's last step on a store. After SPV_PACK_ENCODE, z is what spv_pack_encode gave (NULL: the store stays +// plain) and the packer's pin goes; then the plain copy is freed if the store still has the frame and is still idle +// (a reader may have come meanwhile). Returns the bytes freed. +static inline uint64_t spv_pack_end(SpvStore *s, PlatMutex *lk, int step, void *z, uint32_t zsize, uint64_t now_ms, + uint64_t idle_ms) { + uint32_t *drop = NULL; + plat_mutex_lock(lk); + if (step == SPV_PACK_ENCODE) { + s->pins--; // the packer's: no use stamp, it is not a reader + if (z && !s->z) __atomic_store_n(&s->zsize, zsize, __ATOMIC_RELAXED), __atomic_store_n(&s->z, z, __ATOMIC_RELAXED), z = NULL; + } + if (s->plain && s->z && spv_idle_locked(s, now_ms, idle_ms)) drop = s->plain, __atomic_store_n(&s->plain, NULL, __ATOMIC_RELAXED); + plat_mutex_unlock(lk); + free(z); // a frame another packer step installed first (not with one packer per owner) + free(drop); + return drop ? (uint64_t)s->words * 4 : 0; +} + +static inline void spv_packer_free(SpvPacker *pk) { + if (pk->cc) ZSTD_freeCCtx(pk->cc); + free(pk->out), free(pk->back); + memset(pk, 0, sizeof *pk); +} diff --git a/tests/spv_store_check.c b/tests/spv_store_check.c new file mode 100644 index 00000000..45149b7b --- /dev/null +++ b/tests/spv_store_check.c @@ -0,0 +1,194 @@ +// runtime/spv_store.h: a shader's SPIR-V kept zstd-compressed while unused (gfx_shader.c "SPIR-V at rest"). Built and +// run by tests/spv_store_check.py against the platform layer and libzstd. +// +// The single-thread checks walk one store through its states: plain, pinned, packed (the frame made, checked and the +// plain copy freed), decoded back by a pin bit for bit, idle again (only the plain copy goes, the frame stays), and +// its FNV-1a read from a compressed store without a decode. A reader that arrives while the packer encodes keeps the +// plain copy. The stress check runs reader threads that pin random stores and compare every word against the +// original, while a packer thread packs every store as soon as nobody holds it; no reader may ever see other words, +// and every pin must be given back. +#include +#include +#include +#include + +#include "spv_store.h" + +static int fails; +#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0) + +void rt_log(const char *fmt, ...) { // the platform layer's lines + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); +} + +static uint64_t rng = 0x9e3779b97f4a7c15ull; +static uint32_t rnd(void) { + rng ^= rng << 13, rng ^= rng >> 7, rng ^= rng << 17; + return (uint32_t)(rng >> 32); +} + +// SPIR-V-like words: the header, then instructions with small opcodes, word counts and ids. They are more random than +// the translator's modules (a frame keeps about 65% of them, against 26% of real SPIR-V), which only makes the +// encoder's work larger. +static uint32_t *make_module(uint64_t words) { + uint32_t *w = malloc(words * 4); + w[0] = 0x07230203, w[1] = 0x00010300, w[2] = 0, w[3] = 4096, w[4] = 0; + for (uint64_t i = 5; i < words;) { + uint32_t len = 2 + rnd() % 5; + if (i + len > words) len = (uint32_t)(words - i); + w[i] = len << 16 | (rnd() % 8 ? 61 + rnd() % 8 : rnd() % 400); + for (uint32_t k = 1; k < len; k++) w[i + k] = rnd() % 4 ? 1000 + rnd() % 3000 : rnd(); + i += len; + } + return w; +} + +static PlatMutex lk = PLAT_MUTEX_INIT; + +static void single_checks(void) { + uint64_t words = 30000, ns; + uint32_t *orig = make_module(words), *plain = malloc(words * 4); + memcpy(plain, orig, words * 4); + SpvStore s; + spv_store_fill(&s, plain, words, 100); + SpvPacker pk = {0}; + + const uint32_t *w = spv_pin(&s, &lk, &ns); + CHECK(w == plain && ns == 0, "a plain store hands out its copy without a decode"); + CHECK(spv_pack_begin(&s, &lk, 100000, 1000) == SPV_PACK_NONE, "a pinned store is not packed"); + spv_unpin(&s, &lk, 500); + CHECK(s.used_ms == 500 && s.pins == 0, "unpin stamps the use"); + CHECK(spv_pack_begin(&s, &lk, 1400, 1000) == SPV_PACK_NONE, "used 0.9 s ago with a 1 s wait: kept plain"); + CHECK(spv_pack_begin(&s, &lk, 400, 1000) == SPV_PACK_NONE, "a use stamped after the packer's clock counts as recent"); + + int step = spv_pack_begin(&s, &lk, 1500, 1000); + CHECK(step == SPV_PACK_ENCODE && s.pins == 1, "idle for 1 s: the packer pins it to encode"); + uint32_t zn = 0; + void *z = spv_pack_encode(&s, &pk, 1, &zn); + CHECK(z && zn > 0 && zn < words * 4, "the frame is made (%u bytes of %llu)", zn, (unsigned long long)words * 4); + CHECK(s.fnv_ok && s.fnv == spv_fnv_words(orig, words), "the FNV is worked out before the plain copy can go"); + uint64_t freed = spv_pack_end(&s, &lk, step, z, zn, 1500, 1000); + CHECK(freed == words * 4 && !s.plain && s.z == z && s.zsize == zn && s.pins == 0, "the plain copy is freed, the frame kept"); + printf("round trip: %llu bytes kept as %u (%.1f%%)\n", (unsigned long long)words * 4, zn, 100.0 * zn / (words * 4.0)); + + uint64_t h = spv_fnv(&s, &lk, 1600, &ns); + CHECK(h == spv_fnv_words(orig, words) && ns == 0 && !s.plain, "the FNV of a compressed store needs no decode"); + + w = spv_pin(&s, &lk, &ns); + CHECK(w && s.plain == w && s.pins == 1, "a pin of a compressed store decodes it and installs the copy"); + CHECK(w && !memcmp(w, orig, words * 4), "the decoded SPIR-V is the original bit for bit"); + const uint32_t *w2 = spv_pin(&s, &lk, &ns); + CHECK(w2 == w && ns == 0 && s.pins == 2, "a second reader shares the decoded copy"); + spv_unpin(&s, &lk, 2000), spv_unpin(&s, &lk, 2000); + + CHECK(spv_pack_begin(&s, &lk, 2500, 1000) == SPV_PACK_NONE, "decoded and used again: plain until idle again"); + step = spv_pack_begin(&s, &lk, 3000, 1000); + CHECK(step == SPV_PACK_DROP && s.pins == 0, "idle again with its frame: only the plain copy goes"); + freed = spv_pack_end(&s, &lk, step, NULL, 0, 3000, 1000); + CHECK(freed == words * 4 && !s.plain && s.z == z, "the frame made before stays"); + + // a reader that comes while the packer encodes keeps the plain copy + SpvStore t; + uint32_t *p2 = malloc(words * 4); + memcpy(p2, orig, words * 4); + spv_store_fill(&t, p2, words, 0); + step = spv_pack_begin(&t, &lk, 5000, 1000); + CHECK(step == SPV_PACK_ENCODE, "idle: encode"); + w = spv_pin(&t, &lk, &ns); + CHECK(w == p2, "a reader during the encode gets the plain copy"); + z = spv_pack_encode(&t, &pk, 1, &zn); + freed = spv_pack_end(&t, &lk, step, z, zn, 5000, 1000); + CHECK(freed == 0 && t.plain == p2 && t.z == z && t.pins == 1, "the frame is kept and the pinned plain copy stays"); + CHECK(!memcmp(w, orig, words * 4), "the reader's words are untouched"); + spv_unpin(&t, &lk, 5001); + CHECK(spv_pack_begin(&t, &lk, 5500, 1000) == SPV_PACK_NONE, "used at the encode: not dropped before its wait"); + + spv_store_free(&s), spv_store_free(&t), spv_packer_free(&pk); + free(orig); +} + +// ---- stress: readers and one packer on shared stores +#define STORES 64 +#define READERS 4 +static SpvStore st[STORES]; +static uint32_t *orig_of[STORES]; +static volatile int32_t stop; +static volatile int64_t reads, decodes, mismatches; +static int64_t packed, dropped; + +static void reader(void *arg) { + uint64_t r = (uintptr_t)arg * 0x2545f4914f6cdd1dull + 1; + while (!__atomic_load_n(&stop, __ATOMIC_ACQUIRE)) { + r ^= r << 13, r ^= r >> 7, r ^= r << 17; + int i = (int)(r % STORES); + uint64_t ns, words = st[i].words; + const uint32_t *w = spv_pin(&st[i], &lk, &ns); + if (!w || memcmp(w, orig_of[i], words * 4)) plat_inc64(&mismatches); + if (ns) plat_inc64(&decodes); + if (r & 1) plat_yield(); // hold some pins across a time slice + if (!w || w[0] != 0x07230203 || w[words - 1] != orig_of[i][words - 1]) plat_inc64(&mismatches); + spv_unpin(&st[i], &lk, plat_now_ms()); + plat_inc64(&reads); + } +} + +static void packer(void *arg) { + (void)arg; + SpvPacker pk = {0}; + while (!__atomic_load_n(&stop, __ATOMIC_ACQUIRE)) { + for (int i = 0; i < STORES; i++) { + uint64_t now = plat_now_ms(); + int step = spv_pack_begin(&st[i], &lk, now, 0); + if (step == SPV_PACK_NONE) continue; + uint32_t zn = 0; + void *z = step == SPV_PACK_ENCODE ? spv_pack_encode(&st[i], &pk, 1, &zn) : NULL; + packed += z != NULL; + dropped += spv_pack_end(&st[i], &lk, step, z, zn, now, 0) != 0; + } + } + spv_packer_free(&pk); +} + +static void stress(void) { + for (int i = 0; i < STORES; i++) { + uint64_t words = 64 + rnd() % 40000; + orig_of[i] = make_module(words); + uint32_t *p = malloc(words * 4); + memcpy(p, orig_of[i], words * 4); + spv_store_fill(&st[i], p, words, 0); + } + PlatThread *th[READERS + 1]; + for (int i = 0; i < READERS; i++) th[i] = plat_thread_new(reader, (void *)(uintptr_t)(i + 1), "spv reader"); + th[READERS] = plat_thread_new(packer, NULL, "spv packer"); + plat_sleep_ms(3000); + __atomic_store_n(&stop, 1, __ATOMIC_RELEASE); + for (int i = 0; i <= READERS; i++) plat_thread_join(th[i]); + int pins = 0, frames = 0; + for (int i = 0; i < STORES; i++) pins += st[i].pins != 0, frames += st[i].z != NULL; + printf("stress: %lld reads, %lld decodes, %lld frames made, %lld plain copies dropped, %d of %d stores with a frame\n", + (long long)reads, (long long)decodes, (long long)packed, (long long)dropped, frames, STORES); + CHECK(!mismatches, "%lld reads saw other words than the original", (long long)mismatches); + CHECK(!pins, "%d stores still pinned after every reader ended", pins); + CHECK(packed == STORES && frames == STORES, "every store got exactly one frame (%lld made)", (long long)packed); + CHECK(decodes > 100 && dropped > 100, "the readers and the packer met (%lld decodes, %lld drops)", (long long)decodes, + (long long)dropped); + for (int i = 0; i < STORES; i++) { // the final state still gives back the original + uint64_t ns; + const uint32_t *w = spv_pin(&st[i], &lk, &ns); + CHECK(w && !memcmp(w, orig_of[i], st[i].words * 4), "store %d after the stress", i); + spv_unpin(&st[i], &lk, 0); + spv_store_free(&st[i]), free(orig_of[i]); + } +} + +int main(void) { + single_checks(); + stress(); + if (fails) printf("spv_store_check: %d checks failed\n", fails); + else printf("spv_store_check: all checks passed\n"); + return fails != 0; +} diff --git a/tests/spv_store_check.py b/tests/spv_store_check.py new file mode 100644 index 00000000..bb7e06b1 --- /dev/null +++ b/tests/spv_store_check.py @@ -0,0 +1,49 @@ +"""Build and run tests/spv_store_check.c: runtime/spv_store.h, which keeps a shader's SPIR-V zstd-compressed while +nobody reads it (runtime/gfx_shader.c "SPIR-V at rest"). The check walks one store through pin, pack, decode and idle +again, and runs reader threads against a packer that compresses every store as soon as it is free: no reader may see +other words than the original, and every pin must be given back. Linked with the platform layer and libzstd. + +No GPU, no game files, any Python. + +Usage: spv_store_check.py +""" +import os +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MSYS_BIN = Path(r'C:\msys64\ucrt64\bin') + + +def compiler(): + """$CC, else MSYS2 UCRT64's clang, else clang on PATH (as tests/diff/common.py picks it, without its Unicorn).""" + if os.environ.get('CC'): + return os.environ['CC'] + if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists(): + return str(MSYS_BIN / 'clang.exe') + return shutil.which('clang') or 'clang' + + +def main(): + work = ROOT / 'build' / 'test' / 'spv_store' + work.mkdir(parents=True, exist_ok=True) + win = os.name == 'nt' + exe = work / ('spv_store_check.exe' if win else 'spv_store_check') + env = dict(os.environ) + if win and MSYS_BIN.exists(): + env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '') + rt = ROOT / 'runtime' + plat = [rt / 'platform' / ('win32.c' if win else 'linux.c'), rt / 'platform' / 'common.c'] + libs = ['-lbcrypt', '-lws2_32', '-lpsapi', '-lwinmm', '-ldbghelp', '-lshell32', '-lole32', '-luser32', '-ladvapi32'] if win \ + else ['-lpthread'] + subprocess.check_call([compiler(), '-O1', '-g', '-Wall', '-Wno-unused-function', '-fno-strict-aliasing', + '-march=x86-64-v3', '-I', str(rt), + str(ROOT / 'tests' / 'spv_store_check.c'), *map(str, plat), '-o', str(exe), '-lzstd', '-lm', + *libs], env=env) + return subprocess.run([str(exe)], env=env, timeout=300).returncode + + +if __name__ == '__main__': + sys.exit(main())