diff --git a/nx2navi.ini.example b/nx2navi.ini.example index c230ad56..c875a3f8 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -1101,6 +1101,12 @@ # "ref" is perf_lock_timing_here, which the link map names), the recorder's waits, and the texture lock's holds over # 1 ms. Off by default. # lock_time = 0 +# 1: with trace = perf, the "mem: sizes" line also counts the recording copies (the game's recorded command lists, +# kept until the game hands their control memory over again) that no submission used for 5 minutes and whose control +# memory the game has written over since, or no longer maps: copies the game will not use again. A thread of its own +# reads 16 bytes of game memory per copy every 5 seconds; nothing is freed. The count is a lower bound: memory written +# over with the same bytes is not seen. Read at start. Default: 0. +# copy_retire = 0 # Texture entries the bindless texture pool may create in one refresh (images and their uploads); the rest wait for # the next refresh and show their stand-in until then. Lower spreads streaming over more frames (smaller hitches, a # frame or two of pop-in); higher loads sooner. diff --git a/runtime/cfg.c b/runtime/cfg.c index f49f48b6..a1ea0309 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -84,6 +84,7 @@ static const char *const known[] = { "TEXUP_LOG_MS", "DYN_RES", "DYN_RES_TARGET", "DYN_RES_STEP", "TEXTURE_DETAIL", "VRAM_USE", // vram_use: the removed Video memory use row, read by nothing "RECORDER_CORE", "RECORDER_PRIO", "REPLAY_TIME", "LOCK_TIME", "POOL_CREATE_BUDGET", "POOL_CREATE_MS", "POOL_FINI_IDLE", "POOL_LOAD_BACKLOG", "POOL_LOAD_MS", "VMA_BLOCK_MB", "SYNC_WAKE_ALL", "LOG_ASYNC", "DEVICE_LOST_AT", "STAGING_FAIL_AT", "VRAM_BALLAST_MB", + "COPY_RETIRE", "MEM_BUDGET_TEST_VRAM_MB", "MEM_BUDGET_TEST_RAM_MB", "TEX_COMPACT", "TEX_COMPACT_MIN_MB", "TEX_COMPACT_FRAME_MB", // texture compaction (vk_texture.c); no menu row "STANDIN_FILTER", // the filter of the scene upscaler's stand-in (vk_tonemap.c); no menu row @@ -170,6 +171,7 @@ static const struct { const char *key, *section; } sections[] = { {"WAIT_WATCH_S", "diagnostics"}, {"HANDLE_CENSUS", "diagnostics"}, {"VMA_BLOCK_MB", "diagnostics"}, {"DEVICE_LOST_AT", "diagnostics"}, {"STAGING_FAIL_AT", "diagnostics"}, {"VRAM_BALLAST_MB", "diagnostics"}, + {"COPY_RETIRE", "diagnostics"}, {"MEM_BUDGET_TEST_VRAM_MB", "diagnostics"}, {"MEM_BUDGET_TEST_RAM_MB", "diagnostics"}, {"TEX_COMPACT", "performance"}, {"TEX_COMPACT_MIN_MB", "performance"}, {"TEX_COMPACT_FRAME_MB", "performance"}, }; @@ -441,6 +443,8 @@ static const CfgMeta metas[] = { {"MEM_BUDGET_TEST_VRAM_MB", "0", "0..1048576", RESTART}, {"MEM_BUDGET_TEST_RAM_MB", "0", "0..1048576", RESTART}, {"DEVICE_LOST_AT", "0", "0..1000000000", RESTART}, // vk_sync.c lost_at_due: a test of the lost device's end + // nvn_api_cmd.c retire_pass: counts the recording copies whose control memory the game reused; frees nothing + {"COPY_RETIRE", "0", "0|1", RESTART}, {"INPUT_RECORD", "", NULL, RESTART}, }; diff --git a/runtime/nvn_api_cmd.c b/runtime/nvn_api_cmd.c index 81d14938..5ff7ec8e 100644 --- a/runtime/nvn_api_cmd.c +++ b/runtime/nvn_api_cmd.c @@ -425,8 +425,10 @@ static uint64_t ncopies, copy_bytes, copies_made; // take at every EndRecording. A copy is retired only when the game adds its control memory again // (nvnCommandBufferAddControlMemory), and a session of 2026-11-13 grew from 9,138 live copies (0.7 MiB) to 223,753 // (58.3 MiB) in 7 minutes of play, about 38,000 more at each return to an area: these figures tell copies the game -// still submits from ones it left behind. Kept for COPY_AGE_MINS minutes of the run, a ring of counters by minute. -#define COPY_AGE_MINS 1024 +// still submits from ones it left behind. A ring of counters by minute, COPY_AGE_MINS long (45 days, so it does not +// wrap within a session: a copy last used that long before would share its slot with the minute now and count as +// recent); only the minutes that have passed touch its pages. +#define COPY_AGE_MINS 65536 #define COPY_IDLE_MINS 5 static volatile int64_t age_n[COPY_AGE_MINS], age_b[COPY_AGE_MINS]; @@ -548,6 +550,142 @@ static uint8_t *flatten_recording(const uint8_t *p, size_t n) { return out + sizeof(copy_prefix); } +// ---------- recording copies whose control memory the game reused (NX2NAVI_COPY_RETIRE=1: a count, nothing freed) +// The only event that retires a copy is AddControlMemory over its control memory again (release_copies), so a copy +// whose control memory the game has since used for other data stays for good; a session of 2026-11-13 held 223,646 +// (56.3 MiB) by frame 39600. This counts those copies before anything retires them. Our driver never writes control +// memory (cmd_t.ctl is accounting only), so the 16 bytes at a copy's control position are whatever the game left +// there, and a fingerprint of them that no longer matches says the game wrote there since. A reuse that writes the +// same bytes (zeros, often) is not seen: the counts are a lower bound. +// The "nvn copy sweep" thread does every read of guest memory, at its lowest priority: a recording thread taking the +// fingerprint at EndRecording could fault into the lazy heap (an untouched control block) and wait there. Every +// RETIRE_PERIOD_MS it fingerprints the copies made since its last pass (a queue copy_add fills, under retire_lock), +// then visits up to RETIRE_BATCH of the copies it knows, from a cursor, and counts those no submission used for +// COPY_IDLE_MINS whose bytes changed or whose memory is gone. It takes copy_lock shared only, RETIRE_CHUNK lookups at a +// time; a copy that is no longer live (retired, or its handle reused: made_at and bytes differ) leaves its records. +#define RETIRE_PERIOD_MS 5000 +#define RETIRE_BATCH 4096 +#define RETIRE_CHUNK 64 +#define FP_UNKNOWN 0ull // not read: a lazily armed range, which a read would fetch from the GPU +#define FP_GONE 2ull // not mapped; every fingerprint of bytes is odd +enum { RS_NONE, RS_CHANGED, RS_GONE }; // what a record counts in: nothing, changed, gone +typedef struct { uint64_t handle, lo, made_at, bytes; } retire_new; +typedef struct { uint64_t handle, lo, made_at, bytes, fp; int state; } retire_rec; +static PlatMutex retire_lock = PLAT_MUTEX_INIT; // the queue; never held with copy_lock +static retire_new *retire_q; +static uint32_t retire_qn, retire_qcap; +static retire_rec *retire_recs; // the sweep thread's own +static uint32_t retire_nrec, retire_cap, retire_cursor; +static volatile int64_t retire_changed_n, retire_changed_b, retire_gone_n, retire_gone_b; + +// 16 bytes of guest memory at lo as a fingerprint: odd, FP_GONE or FP_UNKNOWN. An untouched page of the lazy heap reads +// as zeros, so it is taken as zeros without the read that would commit it. +static uint64_t ctl_fingerprint(uint64_t lo) { + uint8_t b[16]; + if (mem_lazy_touches(lo, sizeof b)) return FP_UNKNOWN; + if (mem_untouched_run(lo, lo + sizeof b) >= lo + sizeof b) { + memset(b, 0, sizeof b); + } else { + if (!mem_read_hold(lo, sizeof b)) return FP_GONE; + memcpy(b, (const void *)(uintptr_t)lo, sizeof b); + mem_read_release(); + } + uint64_t h = 0xcbf29ce484222325ull; + for (size_t i = 0; i < sizeof b; i++) h = (h ^ b[i]) * 0x100000001b3ull; + return h | 1; +} + +static void retire_count(int state, uint64_t bytes, int64_t sign) { + if (state == RS_CHANGED) plat_add64(&retire_changed_n, sign), plat_add64(&retire_changed_b, sign * (int64_t)bytes); + if (state == RS_GONE) plat_add64(&retire_gone_n, sign), plat_add64(&retire_gone_b, sign * (int64_t)bytes); +} + +// One visit (sweep thread): live, used_min and bytes as copy_lock showed them; min the minute now. +static void retire_visit(retire_rec *r, int live, uint64_t used_min, uint64_t min) { + int state = RS_NONE; + if (live && r->fp != FP_UNKNOWN && r->fp != FP_GONE && min >= used_min + COPY_IDLE_MINS) { + uint64_t now = ctl_fingerprint(r->lo); + state = now == FP_UNKNOWN ? r->state : now == FP_GONE ? RS_GONE : now != r->fp ? RS_CHANGED : RS_NONE; + } + if (state != r->state) retire_count(r->state, r->bytes, -1), retire_count(state, r->bytes, 1), r->state = state; +} + +// NX2NAVI_COPY_RETIRE, read at the first copy. +static int retire_on(void) { + static volatile int32_t mode = -1; + int32_t m = __atomic_load_n(&mode, __ATOMIC_RELAXED); + if (m < 0) __atomic_store_n(&mode, m = cfg_int("NX2NAVI_COPY_RETIRE"), __ATOMIC_RELAXED); + return m > 0; +} + +// Called by copy_add once the copy is live, copy_lock no longer held. A failed allocation leaves the copy uncounted. +static void retire_queue(uint64_t handle, uint64_t lo, uint64_t made_at, uint64_t bytes) { + plat_mutex_lock(&retire_lock); + if (retire_qn == retire_qcap) { + uint32_t cap = retire_qcap ? retire_qcap * 2 : 1024; + retire_new *q = realloc(retire_q, cap * sizeof *q); + if (q) retire_q = q, retire_qcap = cap; + } + if (retire_qn < retire_qcap) retire_q[retire_qn++] = (retire_new){handle, lo, made_at, bytes}; + plat_mutex_unlock(&retire_lock); +} + +// One pass of the sweep thread: the new copies' fingerprints, then up to RETIRE_BATCH visits from the cursor. +static void retire_pass(void) { + plat_mutex_lock(&retire_lock); + retire_new *q = retire_q; + uint32_t qn = retire_qn; + retire_q = NULL, retire_qn = retire_qcap = 0; + plat_mutex_unlock(&retire_lock); + for (uint32_t i = 0; i < qn; i++) { + if (retire_nrec == retire_cap) { + uint32_t cap = retire_cap ? retire_cap * 2 : 1024; + retire_rec *v = realloc(retire_recs, cap * sizeof *v); + if (!v) break; // the rest go uncounted + retire_recs = v, retire_cap = cap; + } + retire_recs[retire_nrec++] = + (retire_rec){q[i].handle, q[i].lo, q[i].made_at, q[i].bytes, ctl_fingerprint(q[i].lo), RS_NONE}; + } + free(q); + uint64_t min = copy_minute(); + uint32_t todo = retire_nrec < RETIRE_BATCH ? retire_nrec : RETIRE_BATCH; + while (todo && retire_nrec) { + if (retire_cursor >= retire_nrec) retire_cursor = 0; + uint32_t c = retire_cursor, n = retire_nrec - c; + if (n > todo) n = todo; + if (n > RETIRE_CHUNK) n = RETIRE_CHUNK; + int live[RETIRE_CHUNK]; + uint64_t used[RETIRE_CHUNK]; + plat_mutex_lock_shared(©_lock); + for (uint32_t k = 0; k < n; k++) { + const retire_rec *r = &retire_recs[c + k]; + const copy_prefix *pf = copy_find(r->handle); + live[k] = pf && pf->made_at == r->made_at && pf->bytes == r->bytes; + used[k] = live[k] ? __atomic_load_n(&pf->used_min, __ATOMIC_RELAXED) : 0; + } + plat_mutex_unlock_shared(©_lock); + // From the chunk's last record down: a removal moves the last record into the freed place, which then holds a + // record this chunk has visited or one from past it (visited at the next pass). + for (uint32_t k = n; k-- > 0;) { + retire_rec *r = &retire_recs[c + k]; + retire_visit(r, live[k], used[k], min); + if (!live[k]) *r = retire_recs[--retire_nrec]; + } + retire_cursor = c + n; + todo -= n; + } +} + +static void retire_main(void *arg) { + (void)arg; + plat_thread_priority(PLAT_PRIO_LOWEST); + for (;;) { + plat_sleep_ms(RETIRE_PERIOD_MS); + retire_pass(); + } +} + static void copy_add(uintptr_t lo, uintptr_t hi, uint8_t *copy) { copy_prefix *pf = (copy_prefix *)copy - 1; uint64_t handle = (uintptr_t)copy; @@ -574,7 +712,16 @@ static void copy_add(uintptr_t lo, uintptr_t hi, uint8_t *copy) { if (++copies_made % 20000 == 0) NVN_LOG("recording copies: %llu made, %llu live, %.1f MiB, submit %llu", (unsigned long long)copies_made, (unsigned long long)ncopies, copy_bytes / 1048576.0, (unsigned long long)nsubmits); + uint64_t made_at = pf->made_at, bytes = pf->bytes; plat_mutex_unlock(©_lock); + if (hi && retire_on()) { + static volatile int32_t started, failed; + if (!plat_xchg32(&started, 1) && !plat_thread_spawn(retire_main, NULL, "nvn copy sweep", 0)) { + plat_xchg32(&failed, 1); + rt_log("nvn: the copy sweep thread did not start: NX2NAVI_COPY_RETIRE counts nothing"); + } + if (!__atomic_load_n(&failed, __ATOMIC_RELAXED)) retire_queue(handle, lo, made_at, bytes); + } } // Adding control memory again retires every recording copy made in it (the driver's handle lived there). @@ -1383,6 +1530,18 @@ void nvn_cmd_sizes(PerfSizes *s) { perf_size(s, "recording copies", nc, 0, cb); perf_size(s, "of them unused for 5 minutes", idle_n, 0, idle_b); perf_size(s, "recording copy zombies", nz, 0, zb); + if (retire_on()) { + // NX2NAVI_COPY_RETIRE: of the copies unused for 5 minutes, those whose control memory the game has written + // since and those whose control memory is no longer mapped (retire_pass). A lower bound: a rewrite with the + // same bytes is not seen. + int64_t v[4] = {__atomic_load_n(&retire_changed_n, __ATOMIC_RELAXED), + __atomic_load_n(&retire_changed_b, __ATOMIC_RELAXED), + __atomic_load_n(&retire_gone_n, __ATOMIC_RELAXED), + __atomic_load_n(&retire_gone_b, __ATOMIC_RELAXED)}; + for (int i = 0; i < 4; i++) v[i] = v[i] > 0 ? v[i] : 0; + perf_size(s, "of those idle, control memory changed", (uint64_t)v[0], 0, (uint64_t)v[1]); + perf_size(s, "control memory gone", (uint64_t)v[2], 0, (uint64_t)v[3]); + } perf_size(s, "texture pins", pins, 0, PERF_SIZE_NONE); perf_size(s, "pin sweep queue", pq, 0, PERF_SIZE_NONE); perf_size(s, "recording-buffer pool", np, POOL_SLOTS, pb); diff --git a/tests/copy_sweep_check.py b/tests/copy_sweep_check.py new file mode 100644 index 00000000..db234b13 --- /dev/null +++ b/tests/copy_sweep_check.py @@ -0,0 +1,163 @@ +"""The count of recording copies whose control memory the game reused (runtime/nvn_api_cmd.c, NX2NAVI_COPY_RETIRE): +builds ctl_fingerprint, retire_visit, retire_queue and retire_pass, cut from nvn_api_cmd.c by their text, with a stub +table of live copies and stub guest memory, and checks that + + - a copy's control bytes are fingerprinted at the pass after it was made, and nothing is counted while it is in use; + - once no submission used it for 5 minutes, changed bytes count it as changed and unmapped memory as gone, unchanged + bytes count nothing, and a copy used again within 5 minutes leaves the counts; + - a lazily armed range is never read (unknown, not counted), and an untouched page of the lazy heap is taken as zeros + without a read; + - a retired copy, or one whose handle a new copy took, leaves its records and the counts; + - the visits go on where the last pass stopped: two passes over 5000 copies look each up at least once and none + more than twice. + +No game files. Usage: python tests/copy_sweep_check.py [--cc ] +""" +import argparse +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MSYS_BIN = Path(r'C:\msys64\ucrt64\bin') + +HARNESS = r''' +#include +#include +#include +#include +#include +typedef int PlatMutex; +#define PLAT_MUTEX_INIT 0 +static void plat_mutex_lock(PlatMutex *m) { (void)m; } +static void plat_mutex_unlock(PlatMutex *m) { (void)m; } +static void plat_mutex_lock_shared(PlatMutex *m) { (void)m; } +static void plat_mutex_unlock_shared(PlatMutex *m) { (void)m; } +static int64_t plat_add64(volatile int64_t *p, int64_t v) { return __atomic_add_fetch(p, v, __ATOMIC_SEQ_CST); } +static int cfg_int(const char *name) { (void)name; return 1; } +typedef struct { uint64_t magic, bytes, made_at, used_at, deferred, pins, used_min; } copy_prefix; +static PlatMutex copy_lock = PLAT_MUTEX_INIT; +#define COPY_IDLE_MINS 5 +#define NCOPY 6000 +static copy_prefix copies[NCOPY + 1]; // handle i: copies[i]; magic 0 means not live +static int lookups[NCOPY + 1]; +static copy_prefix *copy_find(uint64_t handle) { + if (handle > NCOPY) return NULL; + lookups[handle]++; + return copies[handle].magic ? &copies[handle] : NULL; +} +static uint64_t minute; +static uint64_t copy_minute(void) { return minute; } +// Guest memory: 16 bytes of control memory per copy; per copy, whether it is mapped, lazily armed or an untouched +// lazy-heap page (which must not be read). +static uint8_t ctl[NCOPY + 1][16]; +static uint8_t unmapped[NCOPY + 1], armed[NCOPY + 1], untouched[NCOPY + 1]; +static int reads_untouched, reads_armed; +static long slot_of(uint64_t a) { + uint64_t lo = (uint64_t)(uintptr_t)ctl; + return a >= lo && a < lo + sizeof ctl ? (long)((a - lo) / 16) : -1; +} +static int mem_lazy_touches(uint64_t a, uint64_t n) { (void)n; long i = slot_of(a); return i >= 0 && armed[i]; } +static uint64_t mem_untouched_run(uint64_t a, uint64_t end) { long i = slot_of(a); return i >= 0 && untouched[i] ? end : 0; } +static int mem_read_hold(uint64_t a, uint64_t n) { + (void)n; + long i = slot_of(a); + if (i < 0 || unmapped[i]) return 0; + reads_untouched += untouched[i], reads_armed += armed[i]; + return 1; +} +static void mem_read_release(void) {} +%s +static int fails; +#define CHECK(cond, ...) do { if (!(cond)) { printf("FAIL: " __VA_ARGS__); putchar('\n'); fails++; } } while (0) +static void make(uint64_t h, uint64_t bytes) { + copies[h] = (copy_prefix){.magic = 1, .bytes = bytes, .made_at = h * 3, .used_min = minute}; + retire_queue(h, (uint64_t)(uintptr_t)ctl[h], copies[h].made_at, bytes); +} +static int64_t ld(volatile int64_t *p) { return __atomic_load_n(p, __ATOMIC_SEQ_CST); } +int main(void) { + minute = 100; + for (int i = 1; i <= 6; i++) memset(ctl[i], i, 16); + armed[4] = 1; // copy 4: a lazily armed range + untouched[5] = 1; // copy 5: an untouched lazy-heap page + memset(ctl[5], 0, 16); + for (uint64_t h = 1; h <= 6; h++) make(h, 1000 * h); + retire_pass(); + CHECK(retire_nrec == 6, "six copies known (%%u)", retire_nrec); + CHECK(retire_recs[3].fp == FP_UNKNOWN, "an armed range is not read"); + CHECK(reads_armed == 0 && reads_untouched == 0, "no read of an armed range or an untouched page"); + CHECK(ld(&retire_changed_n) == 0 && ld(&retire_gone_n) == 0, "nothing counted while in use"); + // Six minutes on: copy 2's bytes changed, copy 3's memory unmapped, copy 5 written with other bytes, copy 6 changed + // but used a minute ago, copy 1 as it was. + minute = 106; + copies[6].used_min = 105; + ctl[2][0] ^= 0xff; + unmapped[3] = 1; + untouched[5] = 0, ctl[5][7] = 9; + ctl[6][3] ^= 1; + retire_pass(); + CHECK(ld(&retire_changed_n) == 2 && ld(&retire_changed_b) == 2000 + 5000, + "changed: copies 2 and 5 (%%lld, %%lld bytes)", (long long)ld(&retire_changed_n), (long long)ld(&retire_changed_b)); + CHECK(ld(&retire_gone_n) == 1 && ld(&retire_gone_b) == 3000, "gone: copy 3 (%%lld)", (long long)ld(&retire_gone_n)); + CHECK(reads_armed == 0, "the armed range stays unread"); + // Copy 2 used again: it leaves the changed count. Copy 3 retired (AddControlMemory again): it leaves its records. + // Copy 5's handle taken by a new copy: the old record goes, the new one is fingerprinted. + copies[2].used_min = 106; + copies[3].magic = 0; + copies[5] = (copy_prefix){.magic = 1, .bytes = 777, .made_at = 999, .used_min = minute}; + retire_queue(5, (uint64_t)(uintptr_t)ctl[5], 999, 777); + retire_pass(); + CHECK(ld(&retire_changed_n) == 0 && ld(&retire_changed_b) == 0, "used or replaced copies leave the changed count (%%lld)", + (long long)ld(&retire_changed_n)); + CHECK(ld(&retire_gone_n) == 0 && ld(&retire_gone_b) == 0, "a retired copy leaves the gone count"); + CHECK(retire_nrec == 5, "the retired copy and the replaced record are dropped (%%u records)", retire_nrec); + // 5000 idle, unchanged copies: two passes look each up at least once, none more than twice. + for (uint64_t h = 1; h <= NCOPY; h++) copies[h].magic = 0; + retire_pass(); // drops every record + CHECK(retire_nrec == 0, "every record dropped once nothing is live (%%u)", retire_nrec); + memset(armed, 0, sizeof armed), memset(unmapped, 0, sizeof unmapped), memset(untouched, 0, sizeof untouched); + minute = 200; + for (uint64_t h = 1; h <= 5000; h++) make(h, 8); + minute = 210; + memset(lookups, 0, sizeof lookups); + retire_pass(); + retire_pass(); + int low = 0, high = 0; + for (uint64_t h = 1; h <= 5000; h++) low += lookups[h] < 1, high += lookups[h] > 2; + CHECK(!low && !high, "two passes look every copy up once or twice (%%d never, %%d more than twice)", low, high); + CHECK(ld(&retire_changed_n) == 0 && ld(&retire_gone_n) == 0, "unchanged idle copies count nothing"); + printf(fails ? "copy sweep: %%d failures\n" : "copy sweep: ok\n", fails); + return fails != 0; +} +''' + + +def cut(text, start, end_marker): + a = text.index(start) + b = text.index(end_marker, a) + return text[a:b] + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument('--cc', help='the C compiler (default: MSYS2 UCRT64 clang on Windows, clang elsewhere)') + args = ap.parse_args() + win = os.name == 'nt' + cc = args.cc or (str(MSYS_BIN / 'clang.exe') if win and MSYS_BIN.exists() else 'clang') + env = dict(os.environ) + if win and MSYS_BIN.exists(): + env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '') + src = (ROOT / 'runtime' / 'nvn_api_cmd.c').read_text(encoding='utf-8') + body = cut(src, '// ---------- recording copies whose control memory the game reused', '\nstatic void retire_main(') + with tempfile.TemporaryDirectory(prefix='nx2navi-copy-sweep-') as tmp: + c = Path(tmp) / 'check.c' + c.write_text(HARNESS % body, encoding='utf-8') + exe = Path(tmp) / ('check.exe' if win else 'check') + subprocess.check_call([cc, '-O2', '-Wall', '-Wno-unused-function', str(c), '-o', str(exe)], env=env) + return subprocess.call([str(exe)]) + + +if __name__ == '__main__': + sys.exit(main())