From fcffed3382e843f95db73538f219cf3024ca3f23 Mon Sep 17 00:00:00 2001 From: Fabio Rocha Date: Wed, 19 Aug 2026 12:06:03 +0100 Subject: [PATCH 1/4] Add deterministic-seeding tests for the engine RNG The engine exposes seedRandom(), but there is no way for an embedder to make a run reproducible: initWillStuff() reseeds the RNG from the wall clock (randomlySeedRandom() -> gettimeofday), and all four of its callers run doSimInit() before returning. The map scans in doSimInit() draw from the RNG and write the results straight into map tiles, e.g. zone.cpp:503 map[xx][yy] = HOUSE + BLBNCNBIT + getRandom(2) + value * 3; so a caller-supplied seed is discarded before it can affect anything observable. Loading the same city with the same seed yields a different world on every run, and generateSomeCity(int seed) is broken the same way: its own seedRandom(seed) is clobbered by initWillStuff() on the next line. These tests are added before the fix, as the reproduction. Three of the four fail at this commit; the following commits make them pass. Coverage: - generateMap(seed) is reproducible. This is the control: it seeds the same RNG through the same seedRandom() but never routes through initWillStuff(). It passes both before and after the fix, which is what rules out "the PRNG is just nondeterministic under wasm" and isolates the bug to the reseeding. - seedRandom() + loadCity() is reproducible across processes. This is the property that actually matters and the one the bug breaks. It has to run in child processes: init() itself seeds from the clock, and the first load in a process is the only one starting from a virgin world. - seedRandom() + loadCity() is reproducible in-process, with and without ticking. Everything runs on a single Micropolis instance per process, deliberately. Constructing a second instance in one process trips an unrelated uninitialized-`callback` bug, so reusing one instance keeps these tests measuring seeding and nothing else. The two in-process tests skip their first iteration, because loadCity() does not fully reset world state: a load into a virgin post-init() world differs from a load over a previously loaded one even with identical seeding. That is a separate bug, noted in a comment and left out of scope here; the cross-process test is what pins the seeding contract. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/lib/seedDeterminism.test.ts | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 apps/micropolis/src/lib/seedDeterminism.test.ts diff --git a/apps/micropolis/src/lib/seedDeterminism.test.ts b/apps/micropolis/src/lib/seedDeterminism.test.ts new file mode 100644 index 0000000..e016a0b --- /dev/null +++ b/apps/micropolis/src/lib/seedDeterminism.test.ts @@ -0,0 +1,154 @@ +/** + * Deterministic-seeding tests. + * + * The engine exposes seedRandom(), but initWillStuff() reseeds the RNG from the + * wall clock (randomlySeedRandom() -> gettimeofday) and every caller of + * initWillStuff() runs doSimInit() before returning to the embedder. The map + * scans in doSimInit() draw from the RNG and write the results straight into map + * tiles (e.g. zone.cpp: `map[x][y] = ... + getRandom(2)`), so a caller-supplied + * seed is discarded before it can affect anything observable. + * + * Covers: + * - generateMap(seed) is reproducible (control: the PRNG itself is fine) + * - seedRandom(n) + loadCity() is reproducible + * - seedRandom(n) + loadCity() + ticks is reproducible + * + * Everything runs on a single Micropolis instance, on purpose. Constructing a + * second instance in one process is its own (unrelated) bug, so reusing one + * instance keeps these tests measuring seeding and nothing else. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { createHash } from 'node:crypto'; +import { execFileSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { loadMicropolisMainModule } from './wasm/node'; +import { callbackMethodNames } from './wasm/callbacks'; +import type { MainModule, JSCallback, Micropolis } from '../types/micropolisengine.d.js'; + +const SEED = 42; +const CITY = '/cities/kobe.cty'; + +const testDir = path.dirname(fileURLToPath(import.meta.url)); +const projectRoot = path.resolve(testDir, '../..'); +const nodeLoaderUrl = pathToFileURL(path.join(testDir, 'wasm/node.ts')).href; +const callbacksUrl = pathToFileURL(path.join(testDir, 'wasm/callbacks.ts')).href; + +/** One in-game turn is one cityTime increment = 16 ticks (simulate.cpp phaseCycle). */ +const TICKS_PER_TURN = 16; + +describe('deterministic seeding', () => { + let engine: MainModule; + let micropolis: Micropolis; + let jsCallbackWrapper: JSCallback; + + beforeAll(async () => { + engine = await loadMicropolisMainModule(); + micropolis = new engine.Micropolis(); + jsCallbackWrapper = new engine.JSCallback( + Object.fromEntries(callbackMethodNames.map((name) => [name, () => {}])) + ); + micropolis.setCallback(jsCallbackWrapper, {}); + micropolis.init(); + }); + + afterAll(() => { + try { micropolis.delete(); } catch { /* ignore */ } + try { jsCallbackWrapper.delete(); } catch { /* ignore */ } + }); + + /** + * Digest of the entire tile map — the sim state that actually matters, and + * where the RNG output physically lands. + */ + function mapDigest(): string { + const buffer = Buffer.allocUnsafe(engine.WORLD_W * engine.WORLD_H * 2); + let offset = 0; + for (let x = 0; x < engine.WORLD_W; x++) { + for (let y = 0; y < engine.WORLD_H; y++) { + buffer.writeUInt16LE(micropolis.getTile(x, y) & 0xffff, offset); + offset += 2; + } + } + return createHash('sha256').update(buffer).digest('hex'); + } + + /** Collect a digest per repetition of `run`. */ + function digestsOf(runs: number, run: () => void): string[] { + const digests: string[] = []; + for (let i = 0; i < runs; i++) { + run(); + digests.push(mapDigest()); + } + return digests; + } + + it('seedRandom() + loadCity() is reproducible across processes', () => { + // The property that actually matters, and the one the bug broke: a fresh + // process doing init() -> seedRandom(n) -> loadCity() must land on the same + // world every time. This has to run in child processes because the first + // load in a process is the only one that starts from a virgin world, and + // because init() itself seeds from the clock. + const script = ` + import { createHash } from 'node:crypto'; + import { loadMicropolisMainModule } from ${JSON.stringify(nodeLoaderUrl)}; + import { callbackMethodNames } from ${JSON.stringify(callbacksUrl)}; + const engine = await loadMicropolisMainModule(); + const m = new engine.Micropolis(); + m.setCallback(new engine.JSCallback(Object.fromEntries(callbackMethodNames.map((n) => [n, () => {}]))), {}); + m.init(); + m.seedRandom(${SEED}); + m.loadCity(${JSON.stringify(CITY)}); + const buf = Buffer.allocUnsafe(engine.WORLD_W * engine.WORLD_H * 2); + let o = 0; + for (let x = 0; x < engine.WORLD_W; x++) + for (let y = 0; y < engine.WORLD_H; y++) { buf.writeUInt16LE(m.getTile(x, y) & 0xffff, o); o += 2; } + process.stdout.write(createHash('sha256').update(buf).digest('hex')); + `; + + const digests = Array.from({ length: 3 }, () => + execFileSync(process.execPath, ['--input-type=module', '-e', script], { + cwd: projectRoot, + encoding: 'utf8' + }).trim() + ); + + expect(digests[0]).toMatch(/^[0-9a-f]{64}$/); + expect(new Set(digests).size).toBe(1); + }); + + it('generateMap(seed) produces an identical map every time', () => { + // Control. generateMap() calls seedRandom(seed) and never routes through + // initWillStuff(), so this isolates the PRNG from the reseeding bug: if this + // ever fails, the problem is the generator, not the seed handling. + const digests = digestsOf(3, () => micropolis.generateMap(SEED)); + + expect(new Set(digests).size).toBe(1); + }); + + it('seedRandom() + loadCity() produces an identical map every time', () => { + // Note the first load is skipped: loadCity() does not fully reset world + // state, so a load into a virgin post-init() world differs from a load over + // a previously loaded one, regardless of seeding. That is a separate issue. + // What matters here is that repeating the same seeded load is reproducible. + const [, ...digests] = digestsOf(4, () => { + micropolis.seedRandom(SEED); + micropolis.loadCity(CITY); + }); + + expect(new Set(digests).size).toBe(1); + }); + + it('seedRandom() + loadCity() + ticks produces an identical map every time', () => { + const [, ...digests] = digestsOf(4, () => { + micropolis.seedRandom(SEED); + micropolis.loadCity(CITY); + for (let tick = 0; tick < 3 * TICKS_PER_TURN; tick++) { + micropolis.simTick(); + } + }); + + expect(new Set(digests).size).toBe(1); + }); +}); From a7508ba017300e3cdb45b3d4dfee84ddc3a1de5b Mon Sep 17 00:00:00 2001 From: Fabio Rocha Date: Tue, 18 Aug 2026 12:51:57 +0100 Subject: [PATCH 2/4] Fix engine build under current Emscripten; regenerate WASM artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuilding the C++ engine with a current Emscripten (6.0.7) produced a module that aborted immediately on load in Node: Aborted(`Module.wasmBinary` was supplied but `wasmBinary` not included in INCOMING_MODULE_JS_API) Emscripten only honours Module.* properties named in INCOMING_MODULE_JS_API and hard-aborts on any it does not recognize. The default list omits both `wasmBinary` and `getPreloadedPackage`, which apps/micropolis/src/lib/wasm/ node.ts passes in order to load the engine from a Buffer instead of over HTTP. Spelling out the list (upstream default plus those two) fixes it. We do not know which Emscripten version produced the previously committed artifacts — the version string is not retained in the generated output, the wasm producers section is stripped, and the codegen fingerprints do not match any release we tried. Rather than keep guessing, this pins the build to what the toolchain does today and fixes the incompatibility, so `make install` works for anyone with a current emsdk. The regenerated micropolisengine.{js,wasm} are committed together with the makefile change, since the two only make sense as a pair. The .d.ts and .data outputs came out byte-identical and are unchanged. Verified no behavior change: the app test suite gives exactly the same results before and after (same 3 known simCrash failures, same 4 pre-existing monorepo.integration failures that need a prior `pnpm build`). Co-Authored-By: Claude Opus 5 (1M context) --- apps/micropolis/src/lib/micropolisengine.js | 802 ++++++++++-------- apps/micropolis/src/lib/micropolisengine.wasm | Bin 436059 -> 439159 bytes packages/micropolis-engine/makefile | 10 + 3 files changed, 471 insertions(+), 341 deletions(-) diff --git a/apps/micropolis/src/lib/micropolisengine.js b/apps/micropolis/src/lib/micropolisengine.js index e6aa63c..c2cc08c 100644 --- a/apps/micropolis/src/lib/micropolisengine.js +++ b/apps/micropolis/src/lib/micropolisengine.js @@ -5,8 +5,7 @@ // When targeting node and ES6 we use `await import ..` in the generated code // so the outer function needs to be marked as async. async function Module(moduleArg = {}) { - var moduleRtn; - + var Module = moduleArg; // include: shell.js // include: minimum_runtime_check.js (function() { @@ -25,9 +24,14 @@ async function Module(moduleArg = {}) { // Note: We use a typeof check here instead of optional chaining using // globalThis because older browsers might not have globalThis defined. - var currentNodeVersion = typeof process !== 'undefined' && process.versions?.node ? humanReadableVersionToPacked(process.versions.node) : TARGET_NOT_SUPPORTED; - if (currentNodeVersion < 180300) { - throw new Error(`This emscripten-generated code requires node v${ packedVersionToHumanReadable(180300) } (detected v${packedVersionToHumanReadable(currentNodeVersion)})`); + + // We skip the node version checking when running on Bun/Deno since the node + // version they report doesn't seem to be useful. + if (typeof process !== 'undefined' && !process.versions?.bun && typeof Deno == "undefined") { + var currentNodeVersion = process.versions?.node ? humanReadableVersionToPacked(process.versions.node) : TARGET_NOT_SUPPORTED; + if (currentNodeVersion < 180300) { + throw new Error(`This emscripten-generated code requires node v${ packedVersionToHumanReadable(180300) } (detected v${packedVersionToHumanReadable(currentNodeVersion)})`); + } } var userAgent = typeof navigator !== 'undefined' && navigator.userAgent; @@ -65,7 +69,6 @@ async function Module(moduleArg = {}) { // after the generated code, you will need to define var Module = {}; // before the code. Then that object will be used in the code, and you // can continue to use Module afterwards as well. -var Module = moduleArg; // Determine the runtime environment we are in. You can customize this by // setting the ENVIRONMENT setting at compile time (see settings.js). @@ -89,7 +92,7 @@ if (ENVIRONMENT_IS_NODE) { // --pre-jses are emitted after the Module integration code, so that they can // refer to Module (if they choose; they can also define Module) -// include: /var/folders/78/byld8t797qb4qt1fj9r1xz_w0000gn/T/tmpkkkgbbly.js +// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpix8say0m.js if (!Module['expectedDataFileDownloads']) Module['expectedDataFileDownloads'] = 0; Module['expectedDataFileDownloads']++; @@ -179,11 +182,6 @@ if (ENVIRONMENT_IS_NODE) { } Module['FS_createPath']("/", "cities", true, true); - for (var file of metadata['files']) { - var name = file['filename'] - Module['addRunDependency'](`fp ${name}`); - } - async function processPackageData(arrayBuffer) { assert(arrayBuffer, 'Loading data file failed.'); assert(arrayBuffer.constructor.name === ArrayBuffer.name, 'bad input to processPackageData ' + arrayBuffer.constructor.name); @@ -195,7 +193,6 @@ Module['FS_createPath']("/", "cities", true, true); var data = byteArray.subarray(file['start'], file['end']); // canOwn this data in the filesystem, it is a slice into the heap that will never change Module['FS_createDataFile'](name, null, data, true, true, true); - Module['removeRunDependency'](`fp ${name}`); } Module['removeRunDependency']('datafile_build/micropolisengine.data'); } @@ -207,10 +204,11 @@ Module['FS_createPath']("/", "cities", true, true); if (!fetched) { fetched = await fetchPromise; } - processPackageData(fetched); + await processPackageData(fetched); } - if (Module['calledRun']) { + // Detect whether the module JS file has already been loaded. + if (Module['FS_createPath']) { runWithFS(Module); } else { if (!Module['preRun']) Module['preRun'] = []; @@ -222,24 +220,24 @@ Module['FS_createPath']("/", "cities", true, true); })(); -// end include: /var/folders/78/byld8t797qb4qt1fj9r1xz_w0000gn/T/tmpkkkgbbly.js -// include: /var/folders/78/byld8t797qb4qt1fj9r1xz_w0000gn/T/tmp9cazitgs.js +// end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpix8say0m.js +// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpe45adt7b.js // All the pre-js content up to here must remain later on, we need to run // it. if ((typeof ENVIRONMENT_IS_WASM_WORKER != 'undefined' && ENVIRONMENT_IS_WASM_WORKER) || (typeof ENVIRONMENT_IS_PTHREAD != 'undefined' && ENVIRONMENT_IS_PTHREAD) || (typeof ENVIRONMENT_IS_AUDIO_WORKLET != 'undefined' && ENVIRONMENT_IS_AUDIO_WORKLET)) Module['preRun'] = []; var necessaryPreJSTasks = Module['preRun'].slice(); - // end include: /var/folders/78/byld8t797qb4qt1fj9r1xz_w0000gn/T/tmp9cazitgs.js -// include: /var/folders/78/byld8t797qb4qt1fj9r1xz_w0000gn/T/tmpbrr8dyda.js + // end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpe45adt7b.js +// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpzebvsldf.js if (!Module['preRun']) throw 'Module.preRun should exist because file support used it; did a pre-js delete it?'; necessaryPreJSTasks.forEach((task) => { if (Module['preRun'].indexOf(task) < 0) throw 'All preRun tasks that exist before user pre-js code should remain after; did you replace Module or modify Module.preRun?'; }); - // end include: /var/folders/78/byld8t797qb4qt1fj9r1xz_w0000gn/T/tmpbrr8dyda.js + // end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpzebvsldf.js -var arguments_ = []; +var programArgs = []; var thisProgram = './this.program'; var quit_ = (status, toThrow) => { throw toThrow; @@ -292,7 +290,7 @@ readAsync = async (filename, binary = true) => { thisProgram = process.argv[1].replace(/\\/g, '/'); } - arguments_ = process.argv.slice(2); + programArgs = process.argv.slice(2); quit_ = (status, toThrow) => { process.exitCode = status; @@ -360,7 +358,7 @@ var NODEFS = 'NODEFS is no longer included by default; build with -lnodefs.js'; // perform assertions in shell.js after we set up out() and err(), as otherwise // if an assertion fails it cannot print the message -assert(!ENVIRONMENT_IS_SHELL, 'shell environment detected but not enabled at build time. Add `shell` to `-sENVIRONMENT` to enable.'); +assert(!ENVIRONMENT_IS_SHELL, 'shell environment detected but not enabled at build time (add `shell` to `-sENVIRONMENT` to enable)'); // end include: shell.js @@ -417,44 +415,6 @@ function assert(condition, text) { var isFileURI = (filename) => filename.startsWith('file://'); // include: runtime_common.js -// include: runtime_stack_check.js -// Initializes the stack cookie. Called at the startup of main and at the startup of each thread in pthreads mode. -function writeStackCookie() { - var max = _emscripten_stack_get_end(); - assert((max & 3) == 0); - // If the stack ends at address zero we write our cookies 4 bytes into the - // stack. This prevents interference with SAFE_HEAP and ASAN which also - // monitor writes to address zero. - if (max == 0) { - max += 4; - } - // The stack grow downwards towards _emscripten_stack_get_end. - // We write cookies to the final two words in the stack and detect if they are - // ever overwritten. - HEAPU32[((max)>>2)] = 0x02135467; - HEAPU32[(((max)+(4))>>2)] = 0x89BACDFE; - // Also test the global address 0 for integrity. - HEAPU32[((0)>>2)] = 1668509029; -} - -function checkStackCookie() { - if (ABORT) return; - var max = _emscripten_stack_get_end(); - // See writeStackCookie(). - if (max == 0) { - max += 4; - } - var cookie1 = HEAPU32[((max)>>2)]; - var cookie2 = HEAPU32[(((max)+(4))>>2)]; - if (cookie1 != 0x02135467 || cookie2 != 0x89BACDFE) { - abort(`Stack overflow! Stack cookie has been overwritten at ${ptrToString(max)}, expected hex dwords 0x89BACDFE and 0x2135467, but received ${ptrToString(cookie2)} ${ptrToString(cookie1)}`); - } - // Also test the global address 0 for integrity. - if (HEAPU32[((0)>>2)] != 0x63736d65 /* 'emsc' */) { - abort('Runtime error: The application has corrupted its heap memory area (address zero)!'); - } -} -// end include: runtime_stack_check.js // include: runtime_exceptions.js // Base Emscripten EH error class class EmscriptenEH {} @@ -482,15 +442,31 @@ function dbg(...args) { })(); function consumedModuleProp(prop) { - if (!Object.getOwnPropertyDescriptor(Module, prop)) { - Object.defineProperty(Module, prop, { - configurable: true, - set() { - abort(`Attempt to set \`Module.${prop}\` after it has already been processed. This can happen, for example, when code is injected via '--post-js' rather than '--pre-js'`); - + var value = Module[prop]; + var msg = `Attempt to modify \`Module.${prop}\` after it has already been processed. This can happen, for example, when code is injected via '--post-js' rather than '--pre-js'`; + if (Array.isArray(value)) { + value = new Proxy(value, { + set(target, key, val) { + abort(msg); + return false; + }, + defineProperty(target, key, descriptor) { + abort(msg); + return false; + }, + deleteProperty(target, key) { + abort(msg); + return false; } }); } + Object.defineProperty(Module, prop, { + configurable: true, + get() { return value; }, + set() { + abort(msg); + } + }); } function makeInvalidEarlyAccess(name) { @@ -541,16 +517,68 @@ function unexportedRuntimeSymbol(sym) { } // end include: runtime_debug.js -var readyPromiseResolve, readyPromiseReject; +// include: runtime_stack_check.js +const stackCookie1 = 0x02135467; +const stackCookie2 = 0x89BACDFE; +// Initializes the stack cookie. Called at the startup of main and at the startup of each thread in pthreads mode. +function writeStackCookie() { + var max = _emscripten_stack_get_end(); + assert((max & 3) == 0); + // If the stack ends at address zero we write our cookies 4 bytes into the + // stack. This prevents interference with SAFE_HEAP and ASAN which also + // monitor writes to address zero. + if (max == 0) { + max += 4; + } + // The stack grow downwards towards _emscripten_stack_get_end. + // We write cookies to the final two words in the stack and detect if they are + // ever overwritten. + HEAPU32[((max)>>2)] = stackCookie1; + HEAPU32[(((max)+(4))>>2)] = stackCookie2; + // Also test the global address 0 for integrity. + HEAPU32[((0)>>2)] = 1668509029; +} + +function u32ToHexString(num) { + return '0x' + (num >>> 0).toString(16).padStart(8, '0'); +} + +function checkStackCookie() { + if (ABORT) return; + var max = _emscripten_stack_get_end(); + // See writeStackCookie(). + if (max == 0) { + max += 4; + } + var val1 = HEAPU32[((max)>>2)]; + var val2 = HEAPU32[(((max)+(4))>>2)]; + if (val1 != stackCookie1 || val2 != stackCookie2) { + abort(`Stack overflow! Stack cookie has been overwritten at ${ptrToString(max)}, expected hex dwords ${u32ToHexString(stackCookie2)} and ${u32ToHexString(stackCookie1)}, but received ${u32ToHexString(val2)} ${u32ToHexString(val1)}`); + } + // Also test the global address 0 for integrity. + if (HEAPU32[((0)>>2)] != 0x63736d65 /* 'emsc' */) { + abort('Runtime error: The application has corrupted its heap memory area (address zero)!'); + } +} +// end include: runtime_stack_check.js // Memory management var runtimeInitialized = false; +// When ALLOW_MEMORY_GROWTH is enabled, the conversion from Wasm +// memory to ArrayBuffer requires some additional logic. +function getMemoryBuffer() { + return wasmMemory.buffer; +} + function updateMemoryViews() { - var b = wasmMemory.buffer; + // If we already have a heap that is resizeable/growable buffer we don't + // need to do anything in updateMemoryViews. + if (HEAP8?.buffer?.resizable) return; + var b = getMemoryBuffer(); HEAP8 = new Int8Array(b); HEAP16 = new Int16Array(b); HEAPU8 = new Uint8Array(b); @@ -570,11 +598,10 @@ assert(globalThis.Int32Array && globalThis.Float64Array && Int32Array.prototype. 'JS engine does not provide full typed array support'); function preRun() { - if (Module['preRun']) { - if (typeof Module['preRun'] == 'function') Module['preRun'] = [Module['preRun']]; - while (Module['preRun'].length) { - addOnPreRun(Module['preRun'].shift()); - } + var preRun = Module['preRun']; + if (preRun) { + if (typeof preRun == 'function') preRun = [preRun]; + onPreRuns.push(...preRun); } consumedModuleProp('preRun'); // Begin ATPRERUNS hooks @@ -598,17 +625,17 @@ TTY.init(); // Begin ATPOSTCTORS hooks FS.ignorePermissions = false; // End ATPOSTCTORS hooks + + checkStackCookie(); } function postRun() { checkStackCookie(); - // PThreads reuse the runtime from the main thread. - if (Module['postRun']) { - if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']]; - while (Module['postRun'].length) { - addOnPostRun(Module['postRun'].shift()); - } + var postRun = Module['postRun']; + if (postRun) { + if (typeof postRun == 'function') postRun = [postRun]; + onPostRuns.push(...postRun); } consumedModuleProp('postRun'); @@ -646,21 +673,19 @@ function abort(what) { /** @suppress {checkTypes} */ var e = new WebAssembly.RuntimeError(what); - readyPromiseReject?.(e); // Throw the error whether or not MODULARIZE is set because abort is used // in code paths apart from instantiation where an exception is expected // to be thrown when abort is called. throw e; } -function createExportWrapper(name, nargs) { +function createExportWrapper(name, func, nargs) { + assert(func); return (...args) => { assert(runtimeInitialized, `native function \`${name}\` called before runtime initialization`); - var f = wasmExports[name]; - assert(f, `exported native function \`${name}\` not found`); // Only assert for too many arguments. Too few can be valid since the missing arguments will be zero filled. assert(args.length <= nargs, `native function \`${name}\` called with ${args.length} args but expects ${nargs}`); - return f(...args); + return func(...args); }; } @@ -761,8 +786,7 @@ async function createWasm() { // Load the wasm module and create an instance of using native support in the JS engine. // handle a generated wasm instance, receiving its exports and // performing other necessary setup - /** @param {WebAssembly.Module=} module*/ - function receiveInstance(instance, module) { + function receiveInstance(instance) { wasmExports = instance.exports; assignWasmExports(wasmExports); @@ -795,15 +819,14 @@ async function createWasm() { // performing. // Also pthreads and wasm workers initialize the wasm instance through this // path. - if (Module['instantiateWasm']) { - return new Promise((resolve, reject) => { + var instantiateWasm = Module['instantiateWasm']; + if (instantiateWasm) { + return new Promise((resolve) => { try { - Module['instantiateWasm'](info, (inst, mod) => { - resolve(receiveInstance(inst, mod)); - }); + instantiateWasm(info, (inst) => resolve(receiveInstance(inst))); } catch(e) { err(`Module.instantiateWasm callback failed with error: ${e}`); - reject(e); + throw e; } }); } @@ -827,36 +850,15 @@ async function createWasm() { } } - /** @type {!Int16Array} */ - var HEAP16; - /** @type {!Int32Array} */ var HEAP32; - /** not-@type {!BigInt64Array} */ - var HEAP64; - /** @type {!Int8Array} */ var HEAP8; - /** @type {!Float32Array} */ - var HEAPF32; - - /** @type {!Float64Array} */ - var HEAPF64; - - /** @type {!Uint16Array} */ - var HEAPU16; - /** @type {!Uint32Array} */ var HEAPU32; - /** not-@type {!BigUint64Array} */ - var HEAPU64; - - /** @type {!Uint8Array} */ - var HEAPU8; - var callRuntimeCallbacks = (callbacks) => { while (callbacks.length > 0) { // Pass the module as the first argument. @@ -870,26 +872,6 @@ async function createWasm() { var addOnPreRun = (cb) => onPreRuns.push(cb); - - /** - * @param {number} ptr - * @param {string} type - */ - function getValue(ptr, type = 'i8') { - if (type.endsWith('*')) type = '*'; - switch (type) { - case 'i1': return HEAP8[ptr]; - case 'i8': return HEAP8[ptr]; - case 'i16': return HEAP16[((ptr)>>1)]; - case 'i32': return HEAP32[((ptr)>>2)]; - case 'i64': return HEAP64[((ptr)>>3)]; - case 'float': return HEAPF32[((ptr)>>2)]; - case 'double': return HEAPF64[((ptr)>>3)]; - case '*': return HEAPU32[((ptr)>>2)]; - default: abort(`invalid type for getValue: ${type}`); - } - } - var noExitRuntime = true; function ptrToString(ptr) { @@ -899,27 +881,6 @@ async function createWasm() { return '0x' + ptr.toString(16).padStart(8, '0'); } - - /** - * @param {number} ptr - * @param {number} value - * @param {string} type - */ - function setValue(ptr, value, type = 'i8') { - if (type.endsWith('*')) type = '*'; - switch (type) { - case 'i1': HEAP8[ptr] = value; break; - case 'i8': HEAP8[ptr] = value; break; - case 'i16': HEAP16[((ptr)>>1)] = value; break; - case 'i32': HEAP32[((ptr)>>2)] = value; break; - case 'i64': HEAP64[((ptr)>>3)] = BigInt(value); break; - case 'float': HEAPF32[((ptr)>>2)] = value; break; - case 'double': HEAPF64[((ptr)>>3)] = value; break; - case '*': HEAPU32[((ptr)>>2)] = value; break; - default: abort(`invalid type for setValue: ${type}`); - } - } - var stackRestore = (val) => __emscripten_stack_restore(val); var stackSave = () => _emscripten_stack_get_current(); @@ -937,6 +898,14 @@ async function createWasm() { var UTF8Decoder = globalThis.TextDecoder && new TextDecoder(); + + /** + * heapOrArray is either a regular array, or a JavaScript typed array view. + * @param {number} idx + * @param {number=} maxBytesToRead + * @param {boolean=} ignoreNul + * @return {number} + */ var findStringEnd = (heapOrArray, idx, maxBytesToRead, ignoreNul) => { var maxIdx = idx + maxBytesToRead; if (ignoreNul) return maxIdx; @@ -995,6 +964,9 @@ async function createWasm() { return str; }; + /** @type {!Uint8Array} */ + var HEAPU8; + /** * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the * emscripten HEAP, returns a copy of that string as a Javascript String object. @@ -1015,6 +987,7 @@ async function createWasm() { var ___assert_fail = (condition, filename, line, func) => abort(`Assertion failed: ${UTF8ToString(condition)}, at: ` + [filename ? UTF8ToString(filename) : 'unknown filename', line, func ? UTF8ToString(func) : 'unknown function']); + class ExceptionInfo { // excPtr - Thrown object pointer to wrap. Metadata pointer is calculated from it. constructor(excPtr) { @@ -1073,12 +1046,16 @@ async function createWasm() { } var uncaughtExceptionCount = 0; + + var __Unwind_RaiseException = (ex) => { + assert(false, 'Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.'); + }; var ___cxa_throw = (ptr, type, destructor) => { var info = new ExceptionInfo(ptr); // Initialize ExceptionInfo content after it was allocated in __cxa_allocate_exception. info.init(type, destructor); uncaughtExceptionCount++; - assert(false, 'Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.'); + __Unwind_RaiseException(ptr); }; var syscallGetVarargI = () => { @@ -1156,7 +1133,7 @@ var initRandomFill = () => { // This block is not needed on v19+ since crypto.getRandomValues is builtin if (ENVIRONMENT_IS_NODE) { var nodeCrypto = require('node:crypto'); - return (view) => nodeCrypto.randomFillSync(view); + return (view) => (nodeCrypto.randomFillSync(view), 0); } return (view) => (crypto.getRandomValues(view), 0); @@ -1396,7 +1373,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { } catch (e) { throw new FS.ErrnoError(29); } - if (result === undefined && bytesRead === 0) { + if (result === undefined && !bytesRead) { throw new FS.ErrnoError(6); } if (result === null || result === undefined) break; @@ -1487,6 +1464,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { var mmapAlloc = (size) => { abort('internal error: mmapAlloc called but `emscripten_builtin_memalign` native symbol not exported'); }; + var MEMFS = { ops_table:null, mount(mount) { @@ -1630,7 +1608,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { return attr; }, setattr(node, attr) { - for (const key of ["mode", "atime", "mtime", "ctime"]) { + for (const key of ['mode', 'atime', 'mtime', 'ctime']) { if (attr[key] != null) { node[key] = attr[key]; } @@ -1716,10 +1694,10 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { node.mtime = node.ctime = Date.now(); if (canOwn) { - assert(position === 0, 'canOwn must imply no weird position inside the file'); + assert(!position, 'canOwn must imply no weird position inside the file'); node.contents = buffer.subarray(offset, offset + length); node.usedBytes = length; - } else if (node.usedBytes === 0 && position === 0) { // If this is a simple first write to an empty file, do a fast set since we don't need to care about old data. + } else if (!node.usedBytes && !position) { // If this is a simple first write to an empty file, do a fast set since we don't need to care about old data. node.contents = buffer.slice(offset, offset + length); node.usedBytes = length; } else { @@ -1965,10 +1943,12 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { } }; + var dependenciesPromise = null; + var resolveRunDependencies = async () => dependenciesPromise; var runDependencies = 0; - var dependenciesFulfilled = null; + var dependenciesPromiseResolve = null; var runDependencyTracking = { }; @@ -1982,21 +1962,22 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { assert(id, 'removeRunDependency requires an ID'); assert(runDependencyTracking[id]); delete runDependencyTracking[id]; - if (runDependencies == 0) { + if (!runDependencies) { if (runDependencyWatcher !== null) { clearInterval(runDependencyWatcher); runDependencyWatcher = null; } - if (dependenciesFulfilled) { - var callback = dependenciesFulfilled; - dependenciesFulfilled = null; - callback(); // can add another dependenciesFulfilled - } + dependenciesPromiseResolve(); } }; + + var addRunDependency = (id) => { + if (!runDependencies) { + dependenciesPromise = new Promise((resolve) => dependenciesPromiseResolve = resolve); + } runDependencies++; Module['monitorRunDependencies']?.(runDependencies); @@ -2004,7 +1985,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { assert(id, 'addRunDependency requires an ID') assert(!runDependencyTracking[id]); runDependencyTracking[id] = 1; - if (runDependencyWatcher === null && globalThis.setInterval) { + if (!runDependencyWatcher && globalThis.setInterval) { // Check for missing dependencies every few seconds runDependencyWatcher = setInterval(() => { if (ABORT) { @@ -2071,6 +2052,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { var FS_createPreloadedFile = (parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn, preFinish) => { FS_preloadFile(parent, name, url, canRead, canWrite, dontCreateFile, canOwn, preFinish).then(onload).catch(onerror); }; + var FS = { root:null, mounts:[], @@ -2169,6 +2151,48 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { get isDevice() { return FS.isChrdev(this.mode); } + // The per-inode readiness wait-queue. The node carries a Set of listener + // entries {cb}; producers (SOCKFS, PIPEFS) call notifyListeners on a + // readiness transition, and poll()/epoll consume it. It lives on the node + // (not the fd) so dup'd fds share one queue. Only nodes that derive real + // readiness (sockets, pipes, and an epoll's own node) ever use this - + // always-ready types (regular files, ttys) never register or notify. + addListener(cb, exclusive = false) { + var entry = {cb, exclusive}; + var listeners = (this.listeners ??= new Set()); + listeners.add(entry); + return {listeners, entry}; + } + notifyListeners(flags) { + // Iterates the set without copying, which is safe ONLY under a + // load-bearing contract that every internal listener must honour: + // 1. A listener must not run user code synchronously (a poll waiter only + // resolves a Promise; an epoll registration only re-lists + + // re-notifies; the epoll callback only schedules a tick). User code + // runs on a later tick, never inside this loop. + // 2. A listener may delete entries only from ITS OWN waiter, never from + // a sibling node's set that may be mid-iteration. (Deleting an entry + // of the set being iterated here is fine - a Set tolerates removal of + // a not-yet-visited entry mid-iteration; mutating a *different* node's + // set is fine because that set is not being iterated.) + // Violating either gives silently skipped wakeups that are near-impossible + // to reproduce. Any new producer/listener must preserve it. + if (!this.listeners) return; + // Fire every non-exclusive listener. Among EPOLLEXCLUSIVE registrations + // (one fd watched by several epolls) wake only one, rotating round-robin + // per node, to avoid a thundering herd. (Only epoll registrations are ever + // exclusive; poll waiters and a node's own consumers are not.) + var excl; + for (var entry of this.listeners) { + if (entry.exclusive) (excl ||= []).push(entry); + else entry.cb(flags); + } + if (excl) { + var i = (this.exclTurn || 0) % excl.length; + this.exclTurn = i + 1; + excl[i].cb(flags); + } + } }, lookupPath(path, opts = {}) { if (!path) { @@ -2180,7 +2204,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { path = FS.cwd() + '/' + path; } - // limit max consecutive symlinks to 40 (SYMLOOP_MAX). + // limit max consecutive symlinks to SYMLOOP_MAX. linkloop: for (var nlinks = 0; nlinks < 40; nlinks++) { // split the absolute path var parts = path.split('/').filter((p) => !!p); @@ -2472,7 +2496,14 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { var arg = setattr ? stream : node; setattr ??= node.node_ops.setattr; FS.checkOpExists(setattr, 63) - setattr(arg, attr); + try { + setattr(arg, attr); + } catch (e) { + if (e instanceof RangeError) { + throw new FS.ErrnoError(22); + } + throw e; + } }, chrdev_stream_ops:{ open(stream) { @@ -2739,6 +2770,25 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { } return parent.node_ops.symlink(parent, newname, oldpath); }, + link(oldpath, newpath, flags) { + var lookup = FS.lookupPath(newpath, { parent: true }); + var parent = lookup.node; + if (!parent) { + throw new FS.ErrnoError(44); + } + var newname = PATH.basename(newpath); + var errCode = FS.mayCreate(parent, newname); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + // Hardlinks are only supported by filesystem backends that provide a + // `link` node op (e.g. NODERAWFS backed by the host). NODEFS omits it: + // a host hardlink cannot be confined to the mount root. + if (!parent.node_ops.link) { + throw new FS.ErrnoError(34); + } + return parent.node_ops.link(parent, newname, oldpath, flags); + }, rename(old_path, new_path) { var old_dirname = PATH.dirname(old_path); var new_dirname = PATH.dirname(new_path); @@ -2985,17 +3035,16 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { } FS.doTruncate(stream, stream.node, len); }, - utime(path, atime, mtime) { - var lookup = FS.lookupPath(path, { follow: true }); - var node = lookup.node; - var setattr = FS.checkOpExists(node.node_ops.setattr, 63); - setattr(node, { + utime(path, atime, mtime, dontFollow) { + var lookup = FS.lookupPath(path, { follow: !dontFollow }); + FS.doSetAttr(null, lookup.node, { atime: atime, - mtime: mtime + mtime: mtime, + dontFollow }); }, open(path, flags, mode = 0o666) { - if (path === "") { + if (path === '') { throw new FS.ErrnoError(44); } flags = FS_modeStringToFlags(flags); @@ -3009,7 +3058,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { if (typeof path == 'object') { node = path; } else { - isDirPath = path.endsWith("/"); + isDirPath = path.endsWith('/'); // noent_okay makes it so that if the final component of the path // doesn't exist, lookupPath returns `node: undefined`. `path` will be // updated to point to the target of all symlinks. @@ -3092,6 +3141,11 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { throw new FS.ErrnoError(8); } if (stream.getdents) stream.getdents = null; // free readdir state + // The fd is going away: wake anything waiting on it (poll/epoll) with + // POLLNVAL so a blocking wait unblocks and an epoll registration is evicted + // on its next derive. Only sockets/pipes/epoll ever carry a wait-queue, so + // for every other stream (incl. nodeless noderawfs stdio) this is a no-op. + stream.node?.notifyListeners(32); try { if (stream.stream_ops.close) { stream.stream_ops.close(stream); @@ -3186,8 +3240,8 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { // to write to file opened in read-only mode with MAP_PRIVATE flag, // as all modifications will be visible only in the memory of // the current process. - if ((prot & 2) !== 0 - && (flags & 2) === 0 + if ((prot & 2) + && !(flags & 2) && (stream.flags & 2097155) !== 2) { throw new FS.ErrnoError(2); } @@ -3216,8 +3270,8 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { return stream.stream_ops.ioctl(stream, cmd, arg); }, readFile(path, opts = {}) { - opts.flags = opts.flags || 0; - opts.encoding = opts.encoding || 'binary'; + opts.flags = opts.flags ?? 0; + opts.encoding = opts.encoding ?? 'binary'; if (opts.encoding !== 'utf8' && opts.encoding !== 'binary') { abort(`Invalid encoding type "${opts.encoding}"`); } @@ -3233,7 +3287,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { return buf; }, writeFile(path, data, opts = {}) { - opts.flags = opts.flags || 577; + opts.flags = opts.flags ?? 577; var stream = FS.open(path, opts.flags, opts.mode); data = FS_fileDataToTypedArray(data); FS.write(stream, data, 0, data.byteLength, undefined, opts.canOwn); @@ -3280,7 +3334,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { // use a buffer to avoid overhead of individual crypto calls per byte var randomBuffer = new Uint8Array(1024), randomLeft = 0; var randomByte = () => { - if (randomLeft === 0) { + if (!randomLeft) { randomFill(randomBuffer); randomLeft = randomBuffer.byteLength; } @@ -3496,7 +3550,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { } catch (e) { throw new FS.ErrnoError(29); } - if (result === undefined && bytesRead === 0) { + if (result === undefined && !bytesRead) { throw new FS.ErrnoError(6); } if (result === null || result === undefined) break; @@ -3527,7 +3581,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { forceLoadFile(obj) { if (obj.isDevice || obj.isFolder || obj.link || obj.contents) return true; if (globalThis.XMLHttpRequest) { - abort("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread."); + abort('Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread.'); } else { // Command-line. try { obj.contents = readBinary(obj.url); @@ -3558,11 +3612,11 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { var xhr = new XMLHttpRequest(); xhr.open('HEAD', url, false); xhr.send(null); - if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort("Couldn't load " + url + ". Status: " + xhr.status); - var datalength = Number(xhr.getResponseHeader("Content-length")); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort(`Couldn't load ${url}. Status: ${xhr.status}`); + var datalength = Number(xhr.getResponseHeader('Content-length')); var header; - var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; - var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip"; + var hasByteServing = (header = xhr.getResponseHeader('Accept-Ranges')) && header === 'bytes'; + var usesGzip = (header = xhr.getResponseHeader('Content-Encoding')) && header === 'gzip'; var chunkSize = 1024*1024; // Chunk size in bytes @@ -3570,13 +3624,13 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { // Function to get a range from the remote URL. var doXHR = (from, to) => { - if (from > to) abort("invalid range (" + from + ", " + to + ") or no bytes requested!"); - if (to > datalength-1) abort("only " + datalength + " bytes available! programmer error!"); + if (from > to) abort(`invalid range (${from}, ${to}) or no bytes requested!`); + if (to > datalength-1) abort(`only ${datalength} bytes available! programmer error!`); // TODO: Use mozResponseArrayBuffer, responseStream, etc. if available. var xhr = new XMLHttpRequest(); xhr.open('GET', url, false); - if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); + if (datalength !== chunkSize) xhr.setRequestHeader('Range', `bytes=${from}-${to}`); // Some hints to the browser that we want binary data. xhr.responseType = 'arraybuffer'; @@ -3585,11 +3639,11 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { } xhr.send(null); - if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort("Couldn't load " + url + ". Status: " + xhr.status); + if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) abort(`Couldn't load ${url}. Status: ${xhr.status}`); if (xhr.response !== undefined) { return new Uint8Array(/** @type{Array} */(xhr.response || [])); } - return intArrayFromString(xhr.responseText || '', true); + return intArrayFromString(xhr.responseText ?? '', true); }; var lazyArray = this; lazyArray.setDataGetter((chunkNum) => { @@ -3608,7 +3662,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { chunkSize = datalength = 1; // this will force getter(0)/doXHR do download the whole file datalength = this.getter(0).length; chunkSize = datalength; - out("LazyFiles on gzip forces download of the whole file when length is accessed"); + out('LazyFiles on gzip forces download of the whole file when length is accessed'); } this._length = datalength; @@ -3698,7 +3752,14 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { }, }; + + + + + /** not-@type {!BigInt64Array} */ + var HEAP64; var SYSCALLS = { + currentUmask:18, calculateAt(dirfd, path, allowEmpty) { if (PATH.isAbs(path)) { return path; @@ -3761,7 +3822,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { // MAP_PRIVATE calls need not to be synced back to underlying fs return 0; } - var buffer = HEAPU8.slice(addr, addr + len); + var buffer = HEAPU8.subarray(addr, addr + len); FS.msync(stream, buffer, offset, len, flags); }, getStreamFromFD(fd) { @@ -3774,6 +3835,9 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { return ret; }, }; + + /** @type {!Int16Array} */ + var HEAP16; function ___syscall_fcntl64(fd, cmd, varargs) { SYSCALLS.varargs = varargs; try { @@ -3799,7 +3863,8 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { return stream.flags; case 4: { var arg = syscallGetVarargI(); - stream.flags |= arg; + var mask = 289792; + stream.flags = (stream.flags & ~mask) | (arg & mask); return 0; } case 12: { @@ -3826,6 +3891,9 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { + + + function ___syscall_ioctl(fd, op, varargs) { SYSCALLS.varargs = varargs; try { @@ -3931,6 +3999,9 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { path = SYSCALLS.getStr(path); path = SYSCALLS.calculateAt(dirfd, path); var mode = varargs ? syscallGetVarargI() : 0; + if (flags & 64) { + mode &= ~SYSCALLS.currentUmask; + } return FS.open(path, flags, mode).fd; } catch (e) { if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; @@ -3960,7 +4031,12 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { var typeDependencies = { }; - var BindingError = class BindingError extends Error { constructor(message) { super(message); this.name = 'BindingError'; }}; + class BindingError extends Error { + constructor(message) { + super(message); + this.name = 'BindingError'; + } + } var throwBindingError = (message) => { throw new BindingError(message); }; /** @param {Object=} options */ function sharedRegisterType(rawType, registeredInstance, options = {}) { @@ -3990,6 +4066,17 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { return sharedRegisterType(rawType, registeredInstance, options); } + + + + /** @type {!Uint16Array} */ + var HEAPU16; + + + + + /** not-@type {!BigUint64Array} */ + var HEAPU64; var integerReadValueFromPointer = (name, width, signed) => { // integers are quite common, so generate very specialized functions switch (width) { @@ -4047,10 +4134,10 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { name, fromWireType: fromWireType, toWireType: (destructors, value) => { - if (typeof value == "number") { + if (typeof value == 'number') { value = BigInt(value); } - else if (typeof value != "bigint") { + else if (typeof value != 'bigint') { throw new TypeError(`Cannot convert "${embindRepr(value)}" to ${name}`); } assertIntegerRange(name, value, minRange, maxRange); @@ -4062,6 +4149,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { }; + /** @suppress {globalThis} */ var __embind_register_bool = (rawType, name, trueValue, falseValue) => { name = AsciiToString(name); @@ -4158,7 +4246,12 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { return registeredInstances[ptr]; }; - var InternalError = class InternalError extends Error { constructor(message) { super(message); this.name = 'InternalError'; }}; + class InternalError extends Error { + constructor(message) { + super(message); + this.name = 'InternalError'; + } + } var throwInternalError = (message) => { throw new InternalError(message); }; var makeClassHandle = (prototype, record) => { @@ -4278,10 +4371,10 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { // This is more useful than the empty stacktrace of `FinalizationRegistry` // callback. var cls = $$.ptrType.registeredClass; - var err = new Error(`Embind found a leaked C++ instance ${cls.name} <${ptrToString($$.ptr)}>.\n` + - "We'll free it automatically in this case, but this functionality is not reliable across various environments.\n" + - "Make sure to invoke .delete() manually once you're done with the instance instead.\n" + - "Originally allocated"); // `.stack` will add "at ..." after this sentence + var err = new Error(`Embind found a leaked C++ instance ${cls.name} <${ptrToString($$.ptr)}>. +We'll free it automatically in this case, but this functionality is not reliable across various environments. +Make sure to invoke .delete() manually once you're done with the instance instead. +Originally allocated`); // `.stack` will add "at ..." after this sentence if ('captureStackTrace' in Error) { Error.captureStackTrace(err, RegisteredPointer_fromWireType); } @@ -4311,7 +4404,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { let proto = ClassHandle.prototype; Object.assign(proto, { - "isAliasOf"(other) { + 'isAliasOf'(other) { if (!(this instanceof ClassHandle)) { return false; } @@ -4338,7 +4431,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { return leftClass === rightClass && left === right; }, - "clone"() { + 'clone'() { if (!this.$$.ptr) { throwInstanceAlreadyDeleted(this); } @@ -4359,7 +4452,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { } }, - "delete"() { + 'delete'() { if (!this.$$.ptr) { throwInstanceAlreadyDeleted(this); } @@ -4377,11 +4470,11 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { } }, - "isDeleted"() { + 'isDeleted'() { return !this.$$.ptr; }, - "deleteLater"() { + 'deleteLater'() { if (!this.$$.ptr) { throwInstanceAlreadyDeleted(this); } @@ -4716,11 +4809,11 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { wasmTableMirror[funcPtr] = func = wasmTable.get(funcPtr); } /** @suppress {checkTypes} */ - assert(wasmTable.get(funcPtr) == func, 'JavaScript-side Wasm function table mirror is out of date!'); + assert(wasmTable.get(funcPtr) == func, 'table mirror is out of date'); return func; }; var embind__requireFunction = (signature, rawFunction, isAsync = false) => { - assert(!isAsync, 'Async bindings are only supported with JSPI.'); + assert(!isAsync, 'async bindings are only supported with JSPI'); signature = AsciiToString(signature); @@ -4968,19 +5061,19 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { argsList.push(`arg${i}`) argsListWired.push(`arg${i}Wired`) } - argsList = argsList.join(',') - argsListWired = argsListWired.join(',') + argsList = argsList.join() + argsListWired = argsListWired.join() var invokerFnBody = `return function (${argsList}) {\n`; - invokerFnBody += "checkArgCount(arguments.length, minArgs, maxArgs, humanName, throwBindingError);\n"; + invokerFnBody += 'checkArgCount(arguments.length, minArgs, maxArgs, humanName, throwBindingError);\n'; if (needsDestructorStack) { - invokerFnBody += "var destructors = [];\n"; + invokerFnBody += 'var destructors = [];\n'; } - var dtorStack = needsDestructorStack ? "destructors" : "null"; - var args1 = ["humanName", "throwBindingError", "invoker", "fn", "runDestructors", "fromRetWire", "toClassParamWire"]; + var dtorStack = needsDestructorStack ? 'destructors' : 'null'; + var args1 = ['humanName', 'throwBindingError', 'invoker', 'fn', 'runDestructors', 'fromRetWire', 'toClassParamWire']; if (isClassMethodFunc) { invokerFnBody += `var thisWired = toClassParamWire(${dtorStack}, this);\n`; @@ -4992,15 +5085,15 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { args1.push(argName); } - invokerFnBody += (returns || isAsync ? "var rv = ":"") + `invoker(${argsListWired});\n`; + invokerFnBody += (returns || isAsync ? 'var rv = ' : '') + `invoker(${argsListWired});\n`; - var returnVal = returns ? "rv" : ""; + var returnVal = returns ? 'rv' : ''; if (needsDestructorStack) { - invokerFnBody += "runDestructors(destructors);\n"; + invokerFnBody += 'runDestructors(destructors);\n'; } else { for (var i = isClassMethodFunc?1:2; i < argTypes.length; ++i) { // Skip return value at index 0 - it's not deleted here. Also skip class type if not a method. - var paramName = (i === 1 ? "thisWired" : ("arg"+(i - 2)+"Wired")); + var paramName = (i === 1 ? 'thisWired' : `arg${i - 2}Wired`); if (argTypes[i].destructorFunction !== null) { invokerFnBody += `${paramName}_dtor(${paramName});\n`; args1.push(`${paramName}_dtor`); @@ -5009,12 +5102,12 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { } if (returns) { - invokerFnBody += "var ret = fromRetWire(rv);\n" + - "return ret;\n"; + invokerFnBody += 'var ret = fromRetWire(rv);\n' + + 'return ret;\n'; } else { } - invokerFnBody += "}\n"; + invokerFnBody += '}\n'; args1.push('checkArgCount', 'minArgs', 'maxArgs'); invokerFnBody = `if (arguments.length !== ${args1.length}){ throw new Error(humanName + "Expected ${args1.length} closure arguments " + arguments.length + " given."); }\n${invokerFnBody}`; @@ -5045,15 +5138,15 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { var argCount = argTypes.length; if (argCount < 2) { - throwBindingError("argTypes array size mismatch! Must at least get return value and 'this' types!"); + throwBindingError('argTypes array size mismatch! Must at least get return value and receiver (this) types!'); } - assert(!isAsync, 'Async bindings are only supported with JSPI.'); + assert(!isAsync, 'async bindings are only supported with JSPI'); var isClassMethodFunc = (argTypes[1] !== null && classType !== null); // Free functions with signature "void function()" do not need an invoker that marshalls between wire types. // TODO: This omits argument count check - enable only at -O3 or similar. - // if (ENABLE_UNSAFE_OPTS && argCount == 2 && argTypes[0].name == "void" && !isClassMethodFunc) { + // if (ENABLE_UNSAFE_OPTS && argCount == 2 && argTypes[0].name == 'void' && !isClassMethodFunc) { // return FUNCTION_TABLE[fn]; // } @@ -5134,9 +5227,9 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { var getFunctionName = (signature) => { signature = signature.trim(); - const argsIndex = signature.indexOf("("); + const argsIndex = signature.indexOf('('); if (argsIndex === -1) return signature; - assert(signature.endsWith(")"), "Parentheses for argument names should match."); + assert(signature.endsWith(')'), 'Parentheses for argument names should match.'); return signature.slice(0, argsIndex); }; var __embind_register_class_function = (rawClassType, @@ -5158,7 +5251,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { classType = classType[0]; var humanName = `${classType.name}.${methodName}`; - if (methodName.startsWith("@@")) { + if (methodName.startsWith('@@')) { methodName = Symbol[methodName.substring(2)]; } @@ -5308,7 +5401,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { var emval_handles = [0,1,,1,null,1,true,1,false,1]; var __emval_decref = (handle) => { if (handle > 9 && 0 === --emval_handles[handle + 1]) { - assert(emval_handles[handle] !== undefined, `Decref for unallocated handle.`); + assert(emval_handles[handle] !== undefined, `decref for unallocated handle`); var value = emval_handles[handle]; emval_handles[handle] = undefined; emval_freelist.push(handle); @@ -5359,6 +5452,11 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { var __embind_register_emval = (rawType) => registerType(rawType, EmValType); + + + + + var enumReadValueFromPointer = (name, width, signed) => { switch (width) { case 1: return signed ? @@ -5379,7 +5477,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { function getEnumValueType(rawValueType) { // This must match the values of enum_value_type in wire.h - return rawValueType === 0 ? 'object' : (rawValueType === 1 ? 'number' : 'string'); + return !rawValueType ? 'object' : (rawValueType === 1 ? 'number' : 'string'); } /** @suppress {globalThis} */ var __embind_register_enum = (rawType, name, size, isSigned, rawValueType) => { @@ -5492,6 +5590,11 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { } }; + /** @type {!Float32Array} */ + var HEAPF32; + + /** @type {!Float64Array} */ + var HEAPF64; var floatReadValueFromPointer = (name, width) => { switch (width) { case 4: return function(pointer) { @@ -5513,7 +5616,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { name, fromWireType: (value) => value, toWireType: (destructors, value) => { - if (typeof value != "number" && typeof value != "boolean") { + if (typeof value != 'number' && typeof value != 'boolean') { throw new TypeError(`Cannot convert ${embindRepr(value)} to ${name}`); } // The VM will perform JS to Wasm value conversion, according to the spec: @@ -5572,7 +5675,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { name, fromWireType: fromWireType, toWireType: (destructors, value) => { - if (typeof value != "number" && typeof value != "boolean") { + if (typeof value != 'number' && typeof value != 'boolean') { throw new TypeError(`Cannot convert "${embindRepr(value)}" to ${name}`); } assertIntegerRange(name, value, minRange, maxRange); @@ -5586,6 +5689,8 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { }; + + var __embind_register_memory_view = (rawType, dataTypeIndex, name) => { var typeMapping = [ Int8Array, @@ -5622,14 +5727,17 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { + var stringToUTF8 = (str, outPtr, maxBytesToWrite) => { - assert(typeof maxBytesToWrite == 'number', 'stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!'); + assert(typeof maxBytesToWrite == 'number', 'stringToUTF8 requires a third parameter that specifies the length of the output buffer'); return stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); }; + + var __embind_register_std_string = (rawType, name) => { name = AsciiToString(name); var stdStringIsUTF8 = true; @@ -5712,8 +5820,9 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { var UTF16Decoder = globalThis.TextDecoder ? new TextDecoder('utf-16le') : undefined;; + var UTF16ToString = (ptr, maxBytesToRead, ignoreNul) => { - assert(ptr % 2 == 0, 'Pointer passed to UTF16ToString must be aligned to two bytes!'); + assert(ptr % 2 == 0, 'pointer passed to UTF16ToString must be 2-byte aligned'); var idx = ((ptr)>>1); var endIdx = findStringEnd(HEAPU16, idx, maxBytesToRead / 2, ignoreNul); @@ -5737,11 +5846,9 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { return str; }; - var stringToUTF16 = (str, outPtr, maxBytesToWrite) => { - assert(outPtr % 2 == 0, 'Pointer passed to stringToUTF16 must be aligned to two bytes!'); - assert(typeof maxBytesToWrite == 'number', 'stringToUTF16(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!'); - // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. - maxBytesToWrite ??= 0x7FFFFFFF; + var stringToUTF16 = (str, outPtr, maxBytesToWrite = 0x7FFFFFFF) => { + assert(outPtr % 2 == 0, 'pointer passed to stringToUTF16 must be 2-byte aligned'); + assert(typeof maxBytesToWrite == 'number', 'stringToUTF16 requires a third parameter that specifies the length of the output buffer'); if (maxBytesToWrite < 2) return 0; maxBytesToWrite -= 2; // Null terminator. var startPtr = outPtr; @@ -5760,7 +5867,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { var lengthBytesUTF16 = (str) => str.length*2; var UTF32ToString = (ptr, maxBytesToRead, ignoreNul) => { - assert(ptr % 4 == 0, 'Pointer passed to UTF32ToString must be aligned to four bytes!'); + assert(ptr % 4 == 0, 'pointer passed to UTF32ToString must be 2-byte aligned'); var str = ''; var startIdx = ((ptr)>>2); // If maxBytesToRead is not passed explicitly, it will be undefined, and this @@ -5773,11 +5880,9 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { return str; }; - var stringToUTF32 = (str, outPtr, maxBytesToWrite) => { - assert(outPtr % 4 == 0, 'Pointer passed to stringToUTF32 must be aligned to four bytes!'); - assert(typeof maxBytesToWrite == 'number', 'stringToUTF32(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!'); - // Backwards compatibility: if max bytes is not specified, assume unsafe unbounded write is allowed. - maxBytesToWrite ??= 0x7FFFFFFF; + var stringToUTF32 = (str, outPtr, maxBytesToWrite = 0x7FFFFFFF) => { + assert(outPtr % 4 == 0, 'pointer passed to stringToUTF32 must be 4-byte aligned'); + assert(typeof maxBytesToWrite == 'number', 'stringToUTF32 requires a third parameter that specifies the length of the output buffer'); if (maxBytesToWrite < 4) return 0; var startPtr = outPtr; var endPtr = startPtr + maxBytesToWrite - 4; @@ -5811,6 +5916,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { return len; }; + var __embind_register_std_wstring = (rawType, charSize, name) => { name = AsciiToString(name); var decodeString, encodeString, lengthBytesUTF; @@ -5878,6 +5984,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { return id; }; + var emval_lookupTypes = (argCount, argTypes) => { var a = new Array(argCount); for (var i = 0; i < argCount; ++i) { @@ -5888,6 +5995,7 @@ var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { }; + var emval_returnValue = (toReturnWire, destructorsRef, handle) => { var destructors = []; var result = toReturnWire(destructors, handle); @@ -5999,7 +6107,7 @@ ${functionBody} 2147483648; var alignMemory = (size, alignment) => { - assert(alignment, "alignment argument is required"); + assert(alignment, 'alignment argument is required'); return Math.ceil(size / alignment) * alignment; }; @@ -6014,9 +6122,10 @@ ${functionBody} } catch(e) { err(`growMemory: Attempted to grow heap from ${oldHeapSize} bytes to ${size} bytes, but got error: ${e}`); } - // implicit 0 return to save code size (caller will cast "undefined" into 0 + // implicit 0 return to save code size (caller will cast 'undefined' into 0 // anyhow) }; + var _emscripten_resize_heap = (requestedSize) => { var oldSize = HEAPU8.length; // With CAN_ADDRESS_2GB or MEMORY64, pointers are already unsigned. @@ -6092,7 +6201,6 @@ ${functionBody} // if exit() was called explicitly, warn the user if the runtime isn't actually being shut down if (keepRuntimeAlive() && !implicit) { var msg = `program exited (with status: ${status}), but keepRuntimeAlive() is set (counter=${runtimeKeepaliveCounter}) due to an async operation, so halting execution but not exiting the runtime or preventing further async execution (you can use emscripten_force_exit, if you want to force a true shutdown)`; - readyPromiseReject?.(msg); err(msg); } @@ -6113,6 +6221,7 @@ ${functionBody} } + /** @param {number=} offset */ var doReadv = (stream, iov, iovcnt, offset) => { var ret = 0; @@ -6120,7 +6229,18 @@ ${functionBody} var ptr = HEAPU32[((iov)>>2)]; var len = HEAPU32[(((iov)+(4))>>2)]; iov += 8; - var curr = FS.read(stream, HEAP8, ptr, len, offset); + try { + var curr = FS.read(stream, HEAP8, ptr, len, offset); + } catch (e) { + // On a non-blocking stream a subsequent read may would-block after we + // already gathered data. POSIX readv is a single gather-read: return + // what we have rather than failing the whole call. + if (ret > 0 && e instanceof FS.ErrnoError && + (e.errno == 6 || e.errno == 6)) { + break; + } + throw e; + } if (curr < 0) return -1; ret += curr; if (curr < len) break; // nothing more to read @@ -6131,6 +6251,7 @@ ${functionBody} return ret; }; + function _fd_read(fd, iov, iovcnt, pnum) { try { @@ -6150,17 +6271,18 @@ ${functionBody} var INT53_MIN = -9007199254740992; var bigintToI53Checked = (num) => (num < INT53_MIN || num > INT53_MAX) ? NaN : Number(num); + function _fd_seek(fd, offset, whence, newOffset) { offset = bigintToI53Checked(offset); try { - if (isNaN(offset)) return 61; + if (isNaN(offset)) return 22; var stream = SYSCALLS.getStreamFromFD(fd); FS.llseek(stream, offset, whence); HEAP64[((newOffset)>>3)] = BigInt(stream.position); - if (stream.getdents && offset === 0 && whence === 0) stream.getdents = null; // reset readdir state + if (stream.getdents && !offset && whence === 0) stream.getdents = null; // reset readdir state return 0; } catch (e) { if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e; @@ -6169,27 +6291,34 @@ ${functionBody} ; } + + /** @param {number=} offset */ var doWritev = (stream, iov, iovcnt, offset) => { - var ret = 0; - for (var i = 0; i < iovcnt; i++) { + // Gather all iovecs into one contiguous buffer and issue a single + // FS.write, matching POSIX writev's single gather-write semantics (as + // __syscall_sendmsg already does). Per-iovec writes fragment a stream + // socket send into multiple segments, breaking stream byte semantics. + if (iovcnt == 1) { + // Single iovec: write directly from HEAP8, no gather buffer needed. + return FS.write(stream, HEAP8, HEAPU32[((iov)>>2)], HEAPU32[(((iov)+(4))>>2)], offset); + } + var total = 0; + for (var i = 0, p = iov; i < iovcnt; i++, p += 8) { + total += HEAPU32[(((p)+(4))>>2)]; + } + var view = new Uint8Array(total); + var voff = 0; + for (var i = 0; i < iovcnt; i++, iov += 8) { var ptr = HEAPU32[((iov)>>2)]; var len = HEAPU32[(((iov)+(4))>>2)]; - iov += 8; - var curr = FS.write(stream, HEAP8, ptr, len, offset); - if (curr < 0) return -1; - ret += curr; - if (curr < len) { - // No more space to write. - break; - } - if (typeof offset != 'undefined') { - offset += curr; - } + view.set(HEAPU8.subarray(ptr, ptr + len), voff); + voff += len; } - return ret; + return FS.write(stream, view, 0, total, offset); }; + function _fd_write(fd, iov, iovcnt, pnum) { try { @@ -6235,7 +6364,7 @@ assert(emval_handles.length === 5 * 2); // Begin ATMODULES hooks if (Module['noExitRuntime']) noExitRuntime = Module['noExitRuntime']; -if (Module['preloadPlugins']) preloadPlugins = Module['preloadPlugins']; + if (Module['print']) out = Module['print']; if (Module['printErr']) err = Module['printErr']; if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; @@ -6243,7 +6372,7 @@ if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; checkIncomingModuleAPI(); - if (Module['arguments']) arguments_ = Module['arguments']; + if (Module['arguments']) programArgs = Module['arguments']; if (Module['thisProgram']) thisProgram = Module['thisProgram']; // Assertions on removed incoming Module JS APIs. @@ -6262,10 +6391,13 @@ if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; assert(typeof Module['wasmMemory'] == 'undefined', 'Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally'); assert(typeof Module['INITIAL_MEMORY'] == 'undefined', 'Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically'); - if (Module['preInit']) { - if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; - while (Module['preInit'].length > 0) { - Module['preInit'].shift()(); + var preInit = Module['preInit']; + if (preInit) { + if (typeof preInit == 'function') Module['preInit'] = preInit = [preInit]; + // Written as a loop so that preInit functions that themselves add more + // preInit functions. Is this actually needed? + while (preInit.length > 0) { + preInit.shift()(); } } consumedModuleProp('preInit'); @@ -6331,6 +6463,8 @@ if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; 'getFunctionAddress', 'addFunction', 'removeFunction', + 'setValue', + 'getValue', 'intArrayToString', 'stringToAscii', 'stringToNewUTF8', @@ -6354,12 +6488,14 @@ if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; 'registerOrientationChangeEventCallback', 'fillFullscreenChangeEventData', 'registerFullscreenChangeEventCallback', + 'callCanvasResizedCallback', 'JSEvents_requestFullscreen', 'JSEvents_resizeCanvasForFullscreen', 'registerRestoreOldStyle', 'hideEverythingExceptGivenElement', 'restoreHiddenElements', 'setLetterbox', + 'currentFullscreenStrategy', 'softFullscreenResizeWebGLRenderTarget', 'doRequestFullscreen', 'fillPointerlockChangeEventData', @@ -6391,6 +6527,7 @@ if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; 'registerPreMainLoop', 'getPromise', 'makePromise', + 'addPromise', 'idsToPromises', 'makePromiseCallback', 'findMatchingCatch', @@ -6419,6 +6556,7 @@ if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; 'colorChannelsInGlTextureFormat', 'emscriptenWebGLGetTexPixelData', 'emscriptenWebGLGetUniform', + 'webglGetProgramUniformLocation', 'webglGetUniformLocation', 'webglPrepareUniformLocationsBeforeFirstUse', 'webglGetLeftBracePos', @@ -6427,9 +6565,6 @@ if (Module['wasmBinary']) wasmBinary = Module['wasmBinary']; 'writeGLArray', 'registerWebGlEventCallback', 'runAndAbortIfError', - 'ALLOC_NORMAL', - 'ALLOC_STACK', - 'allocate', 'writeStringToMemory', 'writeAsciiToMemory', 'allocateUTF8', @@ -6498,8 +6633,6 @@ missingLibrarySymbols.forEach(missingLibrarySymbol) 'addOnPostRun', 'freeTableIndexes', 'functionsInTableMap', - 'setValue', - 'getValue', 'PATH', 'PATH_FS', 'UTF8Decoder', @@ -6520,7 +6653,6 @@ missingLibrarySymbols.forEach(missingLibrarySymbol) 'JSEvents', 'specialHTMLTargets', 'findCanvasEventTarget', - 'currentFullscreenStrategy', 'restoreOldWindowedStyle', 'UNWIND_CACHE', 'ExitStatus', @@ -6537,7 +6669,6 @@ missingLibrarySymbols.forEach(missingLibrarySymbol) 'ExceptionInfo', 'Browser', 'requestFullscreen', - 'requestFullScreen', 'setCanvasSize', 'getUserMedia', 'createContext', @@ -6618,6 +6749,7 @@ missingLibrarySymbols.forEach(missingLibrarySymbol) 'FS_mkdir', 'FS_mkdev', 'FS_symlink', + 'FS_link', 'FS_rename', 'FS_rmdir', 'FS_readdir', @@ -6772,6 +6904,25 @@ function checkIncomingModuleAPI() { ignoredModuleProp('onRealloc'); ignoredModuleProp('onFree'); ignoredModuleProp('onSbrkGrow'); + ignoredModuleProp('onCOSCacheHit'); + ignoredModuleProp('onCOSCacheMiss'); + ignoredModuleProp('onCOSStore'); + ignoredModuleProp('GL_MAX_TEXTURE_IMAGE_UNITS'); + ignoredModuleProp('SDL_canPlayWithWebAudio'); + ignoredModuleProp('SDL_numSimultaneouslyQueuedBuffers'); + ignoredModuleProp('freePreloadedMediaOnUse'); + ignoredModuleProp('preinitializedWebGLContext'); + ignoredModuleProp('keyboardListeningElement'); + ignoredModuleProp('doNotCaptureKeyboard'); + ignoredModuleProp('extraStackTrace'); + ignoredModuleProp('preloadPlugins'); + ignoredModuleProp('preMainLoop'); + ignoredModuleProp('postMainLoop'); + ignoredModuleProp('forcedAspectRatio'); + ignoredModuleProp('mainScriptUrlOrBlob'); + ignoredModuleProp('onFullScreen'); + ignoredModuleProp('INITIAL_MEMORY'); + ignoredModuleProp('wasmMemory'); } // Imports from the Wasm binary. @@ -6807,13 +6958,13 @@ function assignWasmExports(wasmExports) { assert(typeof wasmExports['emscripten_stack_get_current'] != 'undefined', 'missing Wasm export: emscripten_stack_get_current'); assert(typeof wasmExports['memory'] != 'undefined', 'missing Wasm export: memory'); assert(typeof wasmExports['__indirect_function_table'] != 'undefined', 'missing Wasm export: __indirect_function_table'); - ___getTypeName = createExportWrapper('__getTypeName', 1); - _malloc = createExportWrapper('malloc', 1); - _free = createExportWrapper('free', 1); - _fflush = createExportWrapper('fflush', 1); + ___getTypeName = createExportWrapper('__getTypeName', wasmExports['__getTypeName'], 1); + _malloc = createExportWrapper('malloc', wasmExports['malloc'], 1); + _free = createExportWrapper('free', wasmExports['free'], 1); + _fflush = createExportWrapper('fflush', wasmExports['fflush'], 1); _emscripten_stack_get_end = wasmExports['emscripten_stack_get_end']; _emscripten_stack_get_base = wasmExports['emscripten_stack_get_base']; - _strerror = createExportWrapper('strerror', 1); + _strerror = createExportWrapper('strerror', wasmExports['strerror'], 1); _emscripten_stack_init = wasmExports['emscripten_stack_init']; _emscripten_stack_get_free = wasmExports['emscripten_stack_get_free']; __emscripten_stack_restore = wasmExports['_emscripten_stack_restore']; @@ -6919,54 +7070,37 @@ function stackCheckInit() { writeStackCookie(); } -function run() { - - if (runDependencies > 0) { - dependenciesFulfilled = run; - return; - } +async function run() { + assert(!calledRun); + calledRun = true; stackCheckInit(); preRun(); - // a preRun added a dependency, run will be called later - if (runDependencies > 0) { - dependenciesFulfilled = run; - return; + if (runDependencies) { + await resolveRunDependencies(); } - function doRun() { - // run may have just been called through dependencies being fulfilled just in this very frame, - // or while the async setStatus time below was happening - assert(!calledRun); - calledRun = true; - Module['calledRun'] = true; - - if (ABORT) return; + var setStatus = Module['setStatus']; + if (setStatus) { + setStatus('Running...'); + // Yield to the event loop to allow the browser to paint "Running..." + await new Promise((resolve) => setTimeout(resolve, 1)); + // Then we want to clear the status text, but only after the rest of this function runs. + setTimeout(setStatus, 1, ''); + } - initRuntime(); + if (ABORT) return; - readyPromiseResolve?.(Module); - Module['onRuntimeInitialized']?.(); - consumedModuleProp('onRuntimeInitialized'); + initRuntime(); - assert(!Module['_main'], 'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]'); + Module['onRuntimeInitialized']?.(); + consumedModuleProp('onRuntimeInitialized'); - postRun(); - } + assert(!Module['_main'], 'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]'); - if (Module['setStatus']) { - Module['setStatus']('Running...'); - setTimeout(() => { - setTimeout(() => Module['setStatus'](''), 1); - doRun(); - }, 1); - } else - { - doRun(); - } - checkStackCookie(); + postRun(); } function checkUnflushedContent() { @@ -7012,28 +7146,14 @@ var wasmExports; // In modularize mode the generated code is within a factory function so we // can use await here (since it's not top-level-await). -wasmExports = await (createWasm()); - -run(); +wasmExports = await createWasm(); +await run(); // end include: postamble.js // include: postamble_modularize.js // In MODULARIZE mode we wrap the generated code in a factory function // and return either the Module itself, or a promise of the module. -// -// We assign to the `moduleRtn` global here and configure closure to see -// this as an extern so it won't get minified. - -if (runtimeInitialized) { - moduleRtn = Module; -} else { - // Set up the promise that indicates the Module is initialized - moduleRtn = new Promise((resolve, reject) => { - readyPromiseResolve = resolve; - readyPromiseReject = reject; - }); -} // Assertion for attempting to access module properties on the incoming // moduleArg. In the past we used this object as the prototype of the module @@ -7054,7 +7174,7 @@ for (const prop of Object.keys(Module)) { - return moduleRtn; + return Module; } // Export using a UMD style export, or ES6 exports if selected diff --git a/apps/micropolis/src/lib/micropolisengine.wasm b/apps/micropolis/src/lib/micropolisengine.wasm index f991f6c39944b2352d7d811d9fbbf92c18c36396..1082bf892d73f0ba2c5a8466344b73845415ef99 100755 GIT binary patch delta 35347 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 delta 32527 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 Date: Wed, 19 Aug 2026 12:07:27 +0100 Subject: [PATCH 3/4] Don't reseed the RNG in initWillStuff(); let seedRandom() stick initWillStuff() opened with randomlySeedRandom(), which seeds from gettimeofday(). Every one of its four callers runs doSimInit() before returning to the caller: micropolis.cpp simInit() (reached from init()) fileio.cpp loadFile() (reached from loadCity()) fileio.cpp loadScenario() generate.cpp generateSomeCity() and doSimInit()'s map scans draw from the RNG and write the results straight into map tiles, e.g. `map[xx][yy] = HOUSE + BLBNCNBIT + getRandom(2) + ...` in zone.cpp. So the clock-derived seed was always baked into the world before the caller regained control, and there was no ordering of the public API that produced a reproducible run. Calling seedRandom() afterwards could not help: the divergence had already happened. Reseeding was also not this function's job. Its own doc comment reads "Reset many game state variables", and randomizing the RNG is not resetting state. Each caller already knows whether it wants a fresh random world, so the call moves out to the one that does. generateSomeCity(int seed) was broken by this in a more pointed way: it calls generateMap(seed), which correctly does seedRandom(seed), and then initWillStuff() on the very next line threw that seed away before doSimInit() ran. The terrain honoured the seed; everything derived from it did not. It is fixed as a side effect of this change, with no edit to generate.cpp. Behaviour is otherwise unchanged. randomlySeedRandom() moves to simInit(), so a freshly initialized simulation still gets a random world by default -- verified: four unseeded init()+loadCity() runs still produce four different worlds. Embedders opt into determinism with init() -> seedRandom(n) -> loadCity(), and anyone wanting the old behaviour can call the already-exposed randomlySeedRandom() themselves. The tests added two commits ago now pass 4/4; three of them failed before this change. Verified in both directions by reintroducing the reseed and confirming they go red again. Also confirmed the generateMap() control digest is byte-identical before and after (ddf9ecd4952ca093), i.e. map generation itself is untouched. Rest of the app suite is unaffected: 80 passing, with only the 4 pre-existing monorepo.integration failures that need a prior `pnpm build`. The regenerated micropolisengine.{js,wasm} are committed alongside. The .js diff is only Emscripten temp-file paths in comments; the .data and .d.ts outputs came out byte-identical and are unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- apps/micropolis/src/lib/micropolisengine.js | 12 ++++++------ apps/micropolis/src/lib/micropolisengine.wasm | Bin 439159 -> 439159 bytes packages/micropolis-engine/src/initialize.cpp | 12 ++++++++++-- packages/micropolis-engine/src/micropolis.cpp | 6 ++++++ 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/apps/micropolis/src/lib/micropolisengine.js b/apps/micropolis/src/lib/micropolisengine.js index c2cc08c..8554856 100644 --- a/apps/micropolis/src/lib/micropolisengine.js +++ b/apps/micropolis/src/lib/micropolisengine.js @@ -92,7 +92,7 @@ if (ENVIRONMENT_IS_NODE) { // --pre-jses are emitted after the Module integration code, so that they can // refer to Module (if they choose; they can also define Module) -// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpix8say0m.js +// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmp_xnx_5sa.js if (!Module['expectedDataFileDownloads']) Module['expectedDataFileDownloads'] = 0; Module['expectedDataFileDownloads']++; @@ -220,21 +220,21 @@ Module['FS_createPath']("/", "cities", true, true); })(); -// end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpix8say0m.js -// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpe45adt7b.js +// end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmp_xnx_5sa.js +// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmp47a08t7w.js // All the pre-js content up to here must remain later on, we need to run // it. if ((typeof ENVIRONMENT_IS_WASM_WORKER != 'undefined' && ENVIRONMENT_IS_WASM_WORKER) || (typeof ENVIRONMENT_IS_PTHREAD != 'undefined' && ENVIRONMENT_IS_PTHREAD) || (typeof ENVIRONMENT_IS_AUDIO_WORKLET != 'undefined' && ENVIRONMENT_IS_AUDIO_WORKLET)) Module['preRun'] = []; var necessaryPreJSTasks = Module['preRun'].slice(); - // end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpe45adt7b.js -// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpzebvsldf.js + // end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmp47a08t7w.js +// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpfdagb_it.js if (!Module['preRun']) throw 'Module.preRun should exist because file support used it; did a pre-js delete it?'; necessaryPreJSTasks.forEach((task) => { if (Module['preRun'].indexOf(task) < 0) throw 'All preRun tasks that exist before user pre-js code should remain after; did you replace Module or modify Module.preRun?'; }); - // end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpzebvsldf.js + // end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpfdagb_it.js var programArgs = []; diff --git a/apps/micropolis/src/lib/micropolisengine.wasm b/apps/micropolis/src/lib/micropolisengine.wasm index 1082bf892d73f0ba2c5a8466344b73845415ef99..5f021c997be6543f2de0f95dad9bc4c606a56047 100755 GIT binary patch delta 114 zcmezVOzQhHsfHHD7N!>FEi9X48T+PRT)?Wf{h=(&DLJNnOw$dSS=6Rqwqd!#v~AM% zg|;j@tRlbIq#O@+wlpv(H#RghFk~t6IsV%I(22#5SL7Nynv|2{wRYzK79eKb?i|2o Gk_-Uzq$%hC delta 118 zcmezVOzQhHsfHHD7N!>FEi9X48K+IZxPVoSok?KZq=p8D?KfmuPRlWFoBq*|MSc2d z8 Date: Wed, 19 Aug 2026 12:23:37 +0100 Subject: [PATCH 4/4] Don't run doSimInit() twice when loading a city loadCity() called doSimInit() on a city that loadFile() had already initialized. loadFile() ends with: initSimLoad = 1; doInitialEval = false; doSimInit(); invalidateMaps(); and doSimInit() dispatches on initSimLoad -- simLoadInit() for a just-loaded city (initSimLoad == 1), initSimMemory() for a new one (== 2) -- then clears initSimLoad to 0 on the way out. So by the time loadCity() called doSimInit() again, that dispatch was dead: neither branch ran, and what executed was the unconditional tail of the function, including mapScan(0, WORLD_W); which is a full zone-simulation pass. Zones grew. Loading kobe left 50 of 12000 tiles different from the same city loaded a second time -- e.g. tile 612 (INDBASE, an empty industrial zone) had already developed into 657 by the time loadCity() returned. The city an embedder gets was one uninitialized simulation pass beyond the city on disk. Fixed by dropping the redundant call. loadFile() already leaves the simulation initialized, and it is the only path into loadCity() that needs it. Verified: repeat seeded loads of kobe now produce a byte-identical map (0 of 12000 tiles differ, previously 50), and a first load is identical to a second. The cross-process seeded-load-and-tick digest is stable 4/4. Two things this does not fix, both pre-existing and both left alone deliberately: - simLoadInit() runs only on the first load in a process, so it draws a different number of values from the RNG than the reload path does. The RNG stream position entering a tick loop therefore differs between a first load and a reload. World state is identical; only the stream offset is not. The repeat-load ticking test reseeds immediately before ticking to isolate that, and says so. - Relatedly, crimeAverage and landValueAverage come from the file's miscHist via simLoadInit() on a first load, but are recomputed from the scanned world on reloads, so they can differ by a point or two. Cosmetic, and fixing it means deciding which value is authoritative -- out of scope here. The cross-process test grows a tick loop as part of this change. It is the only test that exercises a first load, so it is where a first-load simulation difference has to be caught; without ticking it was only checking the loaded map, which is the weaker claim. Co-Authored-By: Claude Opus 5 (1M context) --- apps/micropolis/src/lib/micropolisengine.js | 12 +++--- apps/micropolis/src/lib/micropolisengine.wasm | Bin 439159 -> 439151 bytes .../src/lib/seedDeterminism.test.ts | 37 ++++++++++++------ packages/micropolis-engine/src/fileio.cpp | 2 - 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/apps/micropolis/src/lib/micropolisengine.js b/apps/micropolis/src/lib/micropolisengine.js index 8554856..83d3f39 100644 --- a/apps/micropolis/src/lib/micropolisengine.js +++ b/apps/micropolis/src/lib/micropolisengine.js @@ -92,7 +92,7 @@ if (ENVIRONMENT_IS_NODE) { // --pre-jses are emitted after the Module integration code, so that they can // refer to Module (if they choose; they can also define Module) -// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmp_xnx_5sa.js +// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpm6t8wvg0.js if (!Module['expectedDataFileDownloads']) Module['expectedDataFileDownloads'] = 0; Module['expectedDataFileDownloads']++; @@ -220,21 +220,21 @@ Module['FS_createPath']("/", "cities", true, true); })(); -// end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmp_xnx_5sa.js -// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmp47a08t7w.js +// end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpm6t8wvg0.js +// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmptjaue3o2.js // All the pre-js content up to here must remain later on, we need to run // it. if ((typeof ENVIRONMENT_IS_WASM_WORKER != 'undefined' && ENVIRONMENT_IS_WASM_WORKER) || (typeof ENVIRONMENT_IS_PTHREAD != 'undefined' && ENVIRONMENT_IS_PTHREAD) || (typeof ENVIRONMENT_IS_AUDIO_WORKLET != 'undefined' && ENVIRONMENT_IS_AUDIO_WORKLET)) Module['preRun'] = []; var necessaryPreJSTasks = Module['preRun'].slice(); - // end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmp47a08t7w.js -// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpfdagb_it.js + // end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmptjaue3o2.js +// include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmp7sdfo8w2.js if (!Module['preRun']) throw 'Module.preRun should exist because file support used it; did a pre-js delete it?'; necessaryPreJSTasks.forEach((task) => { if (Module['preRun'].indexOf(task) < 0) throw 'All preRun tasks that exist before user pre-js code should remain after; did you replace Module or modify Module.preRun?'; }); - // end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmpfdagb_it.js + // end include: /var/folders/mk/wy170mjx2p5dkf12nlrqcsmc0000gn/T/tmp7sdfo8w2.js var programArgs = []; diff --git a/apps/micropolis/src/lib/micropolisengine.wasm b/apps/micropolis/src/lib/micropolisengine.wasm index 5f021c997be6543f2de0f95dad9bc4c606a56047..7a63c0a6364be54f51d1ebfa2ec14a86cfef4b21 100755 GIT binary patch delta 58 zcmezVOzQnJsSSS0j8B^VmD~N58G)E-yT3B?nrkfm%#8fg1OKr|ZI}GTEXUYx|C0IZf4iU0rr delta 61 zcmaF=OzQhHsSSS0jBlF#mD~N58G)E-yT3B?nrkf6nHl*f2WUxcm;A*n$H**jZ&JI_ PZ)PB7*>3ck)piE};Z+wN diff --git a/apps/micropolis/src/lib/seedDeterminism.test.ts b/apps/micropolis/src/lib/seedDeterminism.test.ts index e016a0b..8b6e234 100644 --- a/apps/micropolis/src/lib/seedDeterminism.test.ts +++ b/apps/micropolis/src/lib/seedDeterminism.test.ts @@ -10,8 +10,9 @@ * * Covers: * - generateMap(seed) is reproducible (control: the PRNG itself is fine) - * - seedRandom(n) + loadCity() is reproducible - * - seedRandom(n) + loadCity() + ticks is reproducible + * - seedRandom(n) + loadCity() + ticks is reproducible across processes + * - seedRandom(n) + loadCity() is reproducible on repeat loads + * - seedRandom(n) + loadCity() + ticks is reproducible on repeat loads * * Everything runs on a single Micropolis instance, on purpose. Constructing a * second instance in one process is its own (unrelated) bug, so reusing one @@ -84,12 +85,14 @@ describe('deterministic seeding', () => { return digests; } - it('seedRandom() + loadCity() is reproducible across processes', () => { + it('seedRandom() + loadCity() + ticks is reproducible across processes', () => { // The property that actually matters, and the one the bug broke: a fresh - // process doing init() -> seedRandom(n) -> loadCity() must land on the same - // world every time. This has to run in child processes because the first - // load in a process is the only one that starts from a virgin world, and - // because init() itself seeds from the clock. + // process doing init() -> seedRandom(n) -> loadCity() -> tick must land on + // the same world every time. This has to run in child processes because the + // first load in a process is the only one that starts from a virgin world, + // and because init() itself seeds from the clock. Ticking is included here + // precisely because this is the only test that covers a first load, so it is + // where a first-load simulation difference would have to show up. const script = ` import { createHash } from 'node:crypto'; import { loadMicropolisMainModule } from ${JSON.stringify(nodeLoaderUrl)}; @@ -100,6 +103,7 @@ describe('deterministic seeding', () => { m.init(); m.seedRandom(${SEED}); m.loadCity(${JSON.stringify(CITY)}); + for (let tick = 0; tick < ${3 * TICKS_PER_TURN}; tick++) m.simTick(); const buf = Buffer.allocUnsafe(engine.WORLD_W * engine.WORLD_H * 2); let o = 0; for (let x = 0; x < engine.WORLD_W; x++) @@ -128,11 +132,9 @@ describe('deterministic seeding', () => { }); it('seedRandom() + loadCity() produces an identical map every time', () => { - // Note the first load is skipped: loadCity() does not fully reset world - // state, so a load into a virgin post-init() world differs from a load over - // a previously loaded one, regardless of seeding. That is a separate issue. - // What matters here is that repeating the same seeded load is reproducible. - const [, ...digests] = digestsOf(4, () => { + // Reloading the same city with the same seed must land on the same map, every + // time, including the very first load into a virgin post-init() world. + const digests = digestsOf(4, () => { micropolis.seedRandom(SEED); micropolis.loadCity(CITY); }); @@ -140,10 +142,19 @@ describe('deterministic seeding', () => { expect(new Set(digests).size).toBe(1); }); - it('seedRandom() + loadCity() + ticks produces an identical map every time', () => { + it('seedRandom() + loadCity() + ticks is reproducible on repeat loads', () => { + // Reseeding happens after the load, not before it, and the first iteration is + // excluded -- both for the same reason. simLoadInit() runs only on the first + // load of a process, and it draws a different number of values from the RNG + // than the reload path does, so the RNG stream position entering the tick + // loop is not the same on load 1 as on loads 2+. Pinning the seed + // immediately before ticking removes that offset and isolates what this test + // is actually about: that ticking from an identical map yields an identical + // result. Cross-process first-load-with-ticks coverage is the test above. const [, ...digests] = digestsOf(4, () => { micropolis.seedRandom(SEED); micropolis.loadCity(CITY); + micropolis.seedRandom(SEED); for (let tick = 0; tick < 3 * TICKS_PER_TURN; tick++) { micropolis.simTick(); } diff --git a/packages/micropolis-engine/src/fileio.cpp b/packages/micropolis-engine/src/fileio.cpp index 30e91a0..94cea58 100644 --- a/packages/micropolis-engine/src/fileio.cpp +++ b/packages/micropolis-engine/src/fileio.cpp @@ -592,8 +592,6 @@ bool Micropolis::loadCity(const std::string &filename) std::string newCityName = cityFileName.substr(pos, last - pos); setCityName(newCityName); - doSimInit(); - didLoadCity(filename); return true;