Skip to content

Commit 5ab021a

Browse files
committed
Merge branch 'main' into simplify-thread-exit
2 parents 44d0df9 + 71d627a commit 5ab021a

15 files changed

Lines changed: 187 additions & 88 deletions

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,7 @@ jobs:
10951095
core0.test_async_ccall_promise_jspi*
10961096
core0.test_cubescript_jspi
10971097
core0.test_poll_blocking_asyncify_jspi
1098+
wasm64.test_pthread_join_and_asyncify
10981099
"
10991100
- upload-test-results
11001101
test-other:

ChangeLog.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ See docs/process.md for more on how version tagging works.
2020

2121
5.0.8 (in development)
2222
----------------------
23+
- When performing a streaming Fetch operation, the max chunk size of downloaded
24+
bytes that is handed over to the Wasm side from JS is now capped to maximum
25+
of 8 megabytes. This ensures that a streaming Fetch stays streaming, rather
26+
than transferring the whole (potentially large) file as one huge chunk, which
27+
might not fit in the WebAssembly memory. (#26898)
2328
- The minimum versions of browser engines supported by emscripten's generated
2429
code were bumped, allowing us to remove our internal support for transpilation
2530
via babel:
@@ -31,6 +36,11 @@ See docs/process.md for more on how version tagging works.
3136
emscripten. If you still need to support extremely old browsers, you can
3237
manually transpile the output of emscripten (e.g. using babel for JS and
3338
binaryen for wasm). (#26677)
39+
- The `-m64` compiler flag is now honored, and works as an alias for
40+
`-sMEMORY64` and/or `--target=wasm64`. (#26765)
41+
- The autopersistence feature in IDBFS mount now supports registering a global
42+
callback `IDBFS.onAutoPersistStateChanged = active => {}`, which will be
43+
notified of all IDBFS sync start and end events. (#26895)
3444

3545
5.0.7 - 04/30/26
3646
----------------
@@ -54,8 +64,6 @@ See docs/process.md for more on how version tagging works.
5464
a Wasm Worker. This mode increases the memory used by each Wasm Worker by
5565
~500 bytes (in the same way that declaring ~500 bytes of TLS data would).
5666
(#26757)
57-
- The `-m64` compiler flag is now honored, and works are an alias for
58-
`-sMEMORY64` and/or `--target=wasm64`. (#26765)
5967
- The filesystem opteration that create new files now honor the global umask,
6068
which defaults for 0o222 and can be updated by calling `umask()`. (#50739)
6169

src/Fetch.js

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -661,30 +661,47 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) {
661661
return;
662662
}
663663
var ptrLen = (fetchAttrLoadToMemory && fetchAttrStreamData) ? xhr.response?.byteLength ?? 0 : 0;
664-
var ptr = 0;
665-
if (ptrLen > 0 && fetchAttrLoadToMemory && fetchAttrStreamData) {
664+
665+
// Specifies the maximum chunk size that a streaming fetch will transfer from
666+
// JS over to WebAssembly side. Used to cap a streaming fetch to avoid
667+
// overallocating WebAssembly memory needlessly.
668+
var FETCH_STREAMING_MAX_CHUNK_SIZE = 8*1024*1024;
669+
670+
for (var bytePos = 0; bytePos < ptrLen || !ptrLen;) {
671+
var sz = Math.min(ptrLen - bytePos, FETCH_STREAMING_MAX_CHUNK_SIZE);
672+
673+
var ptr = 0;
674+
if (sz > 0 && fetchAttrLoadToMemory && fetchAttrStreamData) {
675+
// Even though we are doing a streaming fetch (i.e. in small chunks), Safari may call onprogress with a huge
676+
// chunk size. This will be a problem for Wasm applications that intend to use streaming fetch to process
677+
// an input file in small chunks (to avoid blowing up the WebAssembly heap size). Therefore apply a max
678+
// chunk size ceiling to the received chunks, and transfer the data over to WebAssembly using max sized chunks.
679+
666680
#if FETCH_DEBUG
667-
dbg(`fetch: allocating ${ptrLen} bytes in Emscripten heap for xhr data`);
681+
dbg(`fetch: allocating ${sz} bytes in Emscripten heap for xhr data`);
668682
#endif
669683
#if ASSERTIONS
670-
assert(onprogress, 'streaming fetch requires an onprogress handler');
684+
assert(onprogress, 'streaming fetch requires an onprogress handler');
671685
#endif
672-
// The data pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is
673-
// freed when emscripten_fetch_close() is called.
674-
ptr = _realloc({{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, '*') }}}, ptrLen);
675-
HEAPU8.set(new Uint8Array(/** @type{Array<number>} */(xhr.response)), ptr);
686+
// The data pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is
687+
// freed when emscripten_fetch_close() is called.
688+
ptr = _realloc({{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, '*') }}}, sz);
689+
HEAPU8.set(new Uint8Array(/** @type{Array<number>} */(xhr.response), bytePos, sz), ptr);
690+
}
691+
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}}
692+
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, sz);
693+
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, e.loaded - ptrLen + bytePos);
694+
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, e.total);
695+
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 'xhr.readyState', 'i16') }}}
696+
var status = xhr.status;
697+
// If loading files from a source that does not give HTTP status code, assume success if we get data bytes
698+
if (xhr.readyState >= 3 && xhr.status === 0 && e.loaded > 0) status = 200;
699+
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 'status', 'i16') }}}
700+
if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
701+
onprogress(fetch, e);
702+
bytePos += sz;
703+
if (!ptrLen) break;
676704
}
677-
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}}
678-
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, ptrLen);
679-
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, e.loaded - ptrLen);
680-
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, e.total);
681-
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 'xhr.readyState', 'i16') }}}
682-
var status = xhr.status;
683-
// If loading files from a source that does not give HTTP status code, assume success if we get data bytes
684-
if (xhr.readyState >= 3 && xhr.status === 0 && e.loaded > 0) status = 200;
685-
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 'status', 'i16') }}}
686-
if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
687-
onprogress(fetch, e);
688705
};
689706
xhr.onreadystatechange = (e) => {
690707
// check if xhr was aborted by user and don't try to call back

src/lib/libglemu.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ var LibraryGLEmulation = {
857857
#if !FULL_ES2
858858
$GLImmediate__postset: 'GLImmediate.setupFuncs(); Browser.moduleContextCreatedCallbacks.push(() => GLImmediate.init());',
859859
#endif
860-
$GLImmediate__deps: ['$Browser', '$GL', '$GLEmulation'],
860+
$GLImmediate__deps: ['$Browser', '$GL', '$GLEmulation', '$webglBufferSubData'],
861861
$GLImmediate: {
862862
MapTreeLib: null,
863863
spawnMapTreeLib: () => {
@@ -2552,7 +2552,7 @@ var LibraryGLEmulation = {
25522552
GLImmediate.lastArrayBuffer = arrayBuffer;
25532553
}
25542554

2555-
GLctx.bufferSubData(GLctx.ARRAY_BUFFER, start, GLImmediate.vertexData.subarray(start >> 2, end >> 2));
2555+
webglBufferSubData(GLctx.ARRAY_BUFFER, start, (end - start) >> 2, start >> 2, GLImmediate.vertexData);
25562556
}
25572557
#if GL_UNSAFE_OPTS
25582558
if (canSkip) return;
@@ -3046,12 +3046,13 @@ var LibraryGLEmulation = {
30463046
}
30473047
if (!GLctx.currentElementArrayBufferBinding) {
30483048
// If no element array buffer is bound, then indices is a literal pointer to clientside data
3049+
var byteSize = numProvidedIndexes << 1;
30493050
#if ASSERTIONS
3050-
assert(numProvidedIndexes << 1 <= GL.MAX_TEMP_BUFFER_SIZE, 'too many immediate mode indexes (a)');
3051+
assert(byteSize <= GL.MAX_TEMP_BUFFER_SIZE, 'too many immediate mode indexes (a)');
30513052
#endif
3052-
var indexBuffer = GL.getTempIndexBuffer(numProvidedIndexes << 1);
3053+
var indexBuffer = GL.getTempIndexBuffer(byteSize);
30533054
GLctx.bindBuffer(GLctx.ELEMENT_ARRAY_BUFFER, indexBuffer);
3054-
GLctx.bufferSubData(GLctx.ELEMENT_ARRAY_BUFFER, 0, {{{ makeHEAPView('U16', 'ptr', 'ptr + (numProvidedIndexes << 1)') }}});
3055+
webglBufferSubData(GLctx.ELEMENT_ARRAY_BUFFER, 0, byteSize, ptr);
30553056
ptr = 0;
30563057
emulatedElementArrayBuffer = true;
30573058
}

src/lib/libidbfs.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,31 @@ addToLibrary({
1818
DB_VERSION: 21,
1919
DB_STORE_NAME: 'FILE_DATA',
2020

21+
// When using the autopersistence mechanism, users can set
22+
// IDBFS.onAutoPersistStateChanged callback to receive notification events
23+
// for when persistence operations are in-flight. Use the following syntax:
24+
/*
25+
IDBFS.onAutoPersistStateChanged = autoPersistActive => {
26+
if (autoPersistActive) {
27+
console.log('IDBFS persistence operation has started.');
28+
} else {
29+
console.log('IDBFS persistence operation has finished.');
30+
}
31+
};
32+
*/
33+
2134
// Queues a new VFS -> IDBFS synchronization operation
2235
queuePersist: (mount) => {
2336
function onPersistComplete() {
2437
if (mount.idbPersistState === 'again') startPersist(); // If a new sync request has appeared in between, kick off a new sync
25-
else mount.idbPersistState = 0; // Otherwise reset sync state back to idle to wait for a new sync later
38+
else {
39+
mount.idbPersistState = 0; // Otherwise reset sync state back to idle to wait for a new sync later
40+
IDBFS.onAutoPersistStateChanged?.(false);
41+
}
2642
}
2743
function startPersist() {
2844
mount.idbPersistState = 'idb'; // Mark that we are currently running a sync operation
45+
IDBFS.onAutoPersistStateChanged?.(true);
2946
IDBFS.syncfs(mount, /*populate:*/false, onPersistComplete);
3047
}
3148

src/lib/libwebgl.js

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ for (/**@suppress{duplicate}*/var i = 0; i <= {{{ GL_POOL_TEMP_BUFFERS_SIZE }}};
249249
#endif // GL_SUPPORT_AUTOMATIC_ENABLE_EXTENSIONS
250250
#if FULL_ES2 || LEGACY_GL_EMULATION
251251
'$registerPreMainLoop',
252+
'$webglBufferSubData',
252253
#endif
253254
],
254255
#if FULL_ES2 || LEGACY_GL_EMULATION
@@ -584,9 +585,7 @@ for (/**@suppress{duplicate}*/var i = 0; i <= {{{ GL_POOL_TEMP_BUFFERS_SIZE }}};
584585
var size = GL.calcBufLength(cb.size, cb.type, cb.stride, count);
585586
var buf = GL.getTempVertexBuffer(size);
586587
GLctx.bindBuffer(0x8892 /*GL_ARRAY_BUFFER*/, buf);
587-
GLctx.bufferSubData(0x8892 /*GL_ARRAY_BUFFER*/,
588-
0,
589-
HEAPU8.subarray(cb.ptr, cb.ptr + size));
588+
webglBufferSubData(0x8892 /*GL_ARRAY_BUFFER*/, 0, size, cb.ptr);
590589
#if GL_ASSERTIONS
591590
GL.validateVertexAttribPointer(cb.size, cb.type, cb.stride, 0);
592591
#endif
@@ -1258,6 +1257,23 @@ for (/**@suppress{duplicate}*/var i = 0; i <= {{{ GL_POOL_TEMP_BUFFERS_SIZE }}};
12581257

12591258
},
12601259

1260+
// Wrapper around GLctx.bufferSubData that can hangle both WebGL1 (which
1261+
// requires new subarray on each call) and WebGL2 (which does not).
1262+
// Argument ordering is a little strange here, since we want a default
1263+
// for `src` is has to come last.
1264+
$webglBufferSubData__internal: true,
1265+
$webglBufferSubData: (target, offset, size, data, src = HEAPU8) => {
1266+
#if WEBGL_USE_GARBAGE_FREE_APIS
1267+
if ({{{ isCurrentContextWebGL2() }}}) {
1268+
size && GLctx.bufferSubData(target, offset, src, data, size);
1269+
return;
1270+
}
1271+
#endif
1272+
#if INCLUDE_WEBGL1_FALLBACK
1273+
GLctx.bufferSubData(target, offset, src.subarray(data, data + size));
1274+
#endif
1275+
},
1276+
12611277
$webglGetExtensions__internal: true,
12621278
$webglGetExtensions__deps: ['$getEmscriptenSupportedExtensions'],
12631279
$webglGetExtensions: () => {
@@ -1915,17 +1931,10 @@ for (/**@suppress{duplicate}*/var i = 0; i <= {{{ GL_POOL_TEMP_BUFFERS_SIZE }}};
19151931
#endif
19161932
},
19171933

1918-
glBufferSubData: (target, offset, size, data) => {
1919-
#if WEBGL_USE_GARBAGE_FREE_APIS
1920-
if ({{{ isCurrentContextWebGL2() }}}) {
1921-
size && GLctx.bufferSubData(target, offset, HEAPU8, data, size);
1922-
return;
1923-
}
1924-
#endif
1925-
#if INCLUDE_WEBGL1_FALLBACK
1926-
GLctx.bufferSubData(target, offset, HEAPU8.subarray(data, data+size));
1927-
#endif
1928-
},
1934+
// This cannot be simple alias because under wasm64 we need to be able modify
1935+
// the function at compile time to provide automatically marshal of the pointer arguments.
1936+
glBufferSubData__deps: ['$webglBufferSubData'],
1937+
glBufferSubData: (target, offset, size, data) => webglBufferSubData(target, offset, size, data),
19291938

19301939
// Queries EXT
19311940
glGenQueriesEXT__sig: 'vip',
@@ -3874,6 +3883,7 @@ for (/**@suppress{duplicate}*/var i = 0; i <= {{{ GL_POOL_TEMP_BUFFERS_SIZE }}};
38743883
#endif
38753884
},
38763885

3886+
glDrawElements__deps: ['$webglBufferSubData'],
38773887
glDrawElements: (mode, count, type, indices) => {
38783888
#if FULL_ES2
38793889
var buf;
@@ -3882,9 +3892,7 @@ for (/**@suppress{duplicate}*/var i = 0; i <= {{{ GL_POOL_TEMP_BUFFERS_SIZE }}};
38823892
var size = GL.calcBufLength(1, type, 0, count);
38833893
buf = GL.getTempIndexBuffer(size);
38843894
GLctx.bindBuffer(0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/, buf);
3885-
GLctx.bufferSubData(0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/,
3886-
0,
3887-
HEAPU8.subarray(indices, indices + size));
3895+
webglBufferSubData(0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/, 0, size, indices);
38883896

38893897
// Calculating vertex count if shader's attribute data is on client side
38903898
if (count > 0) {
@@ -4210,7 +4218,7 @@ for (/**@suppress{duplicate}*/var i = 0; i <= {{{ GL_POOL_TEMP_BUFFERS_SIZE }}};
42104218
}
42114219
},
42124220

4213-
glFlushMappedBufferRange__deps: ['$emscriptenWebGLGetBufferBinding', '$emscriptenWebGLValidateMapBufferTarget'],
4221+
glFlushMappedBufferRange__deps: ['$emscriptenWebGLGetBufferBinding', '$emscriptenWebGLValidateMapBufferTarget', '$webglBufferSubData'],
42144222
glFlushMappedBufferRange: (target, offset, length) => {
42154223
if (!emscriptenWebGLValidateMapBufferTarget(target)) {
42164224
GL.recordError(0x500/*GL_INVALID_ENUM*/);
@@ -4236,10 +4244,7 @@ for (/**@suppress{duplicate}*/var i = 0; i <= {{{ GL_POOL_TEMP_BUFFERS_SIZE }}};
42364244
return;
42374245
}
42384246

4239-
GLctx.bufferSubData(
4240-
target,
4241-
mapping.offset,
4242-
HEAPU8.subarray(mapping.mem + offset, mapping.mem + offset + length));
4247+
webglBufferSubData(target, mapping.offset, length, mapping.mem + offset);
42434248
},
42444249

42454250
glUnmapBuffer__deps: ['$emscriptenWebGLGetBufferBinding', '$emscriptenWebGLValidateMapBufferTarget', 'free'],
@@ -4259,18 +4264,9 @@ for (/**@suppress{duplicate}*/var i = 0; i <= {{{ GL_POOL_TEMP_BUFFERS_SIZE }}};
42594264
}
42604265

42614266
if (!(mapping.access & 0x10)) { /* GL_MAP_FLUSH_EXPLICIT_BIT */
4262-
#if WEBGL_USE_GARBAGE_FREE_APIS
4263-
if ({{{ isCurrentContextWebGL2() }}}) {
4264-
GLctx.bufferSubData(target, mapping.offset, HEAPU8, mapping.mem, mapping.length);
4265-
}
4266-
#if INCLUDE_WEBGL1_FALLBACK
4267-
else
4268-
#endif
4269-
#endif
4270-
#if INCLUDE_WEBGL1_FALLBACK
4271-
GLctx.bufferSubData(target, mapping.offset, HEAPU8.subarray(mapping.mem, mapping.mem+mapping.length));
4272-
#endif
4267+
webglBufferSubData(target, mapping.offset, mapping.length, mapping.mem);
42734268
}
4269+
42744270
_free(mapping.mem);
42754271
mapping.mem = 0;
42764272
return 1;

src/parseTools.mjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,7 @@ function makeDynCall(sig, funcPtr, promising = false) {
682682
}
683683
args = args.join(', ');
684684

685+
const needRtnConversion = MEMORY64 && sig[0] == 'p';
685686
const needArgConversion = MEMORY64 && sig.includes('p');
686687
let callArgs = args;
687688
if (needArgConversion) {
@@ -751,7 +752,15 @@ Please update to new syntax.`);
751752
}
752753

753754
if (needArgConversion) {
754-
return `((${args}) => ${getWasmTableEntry}.call(null, ${callArgs}))`;
755+
if (needRtnConversion) {
756+
if (promising) {
757+
return `((${args}) => ${getWasmTableEntry}.call(null, ${callArgs}).then(Number))`;
758+
} else {
759+
return `((${args}) => Number(${getWasmTableEntry}.call(null, ${callArgs})))`;
760+
}
761+
} else {
762+
return `((${args}) => ${getWasmTableEntry}.call(null, ${callArgs}))`;
763+
}
755764
}
756765
return getWasmTableEntry;
757766
}

test/browser_harness.html

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,32 +23,32 @@
2323
}
2424
</style>
2525
<script>
26-
var COMMAND_PREFIX = 'COMMAND:';
2726
var counter = 0;
28-
function check() {
29-
fetch('/check')
30-
.then((rsp) => rsp.text())
31-
.then((responseText) => {
32-
if (responseText.startsWith(COMMAND_PREFIX)) {
33-
var url = responseText.substr(COMMAND_PREFIX.length);
34-
iframe.src = url;
35-
document.getElementById('count').textContent = counter++;
36-
}
37-
// Polling for the next test to start: we just keep asking if we
38-
// should load a new page, since the iframe doesn't currently have
39-
// a way to tell us it completed (even if it did, we'd need to poll
40-
// the server to know when the next test page is ready to load).
41-
// (A timeout value of 100msecs has been seen to stall test
42-
// browser.test_audio_worklet_worker on Chrome)
43-
setTimeout(check, 200);
44-
})
45-
.catch(() => {
46-
document.body.innerHTML = 'Tests complete. View log in console.';
47-
// Attempt to close the main window. This works on chrome
48-
// but fails on firefox with: `Scripts may not close windows that
49-
// were not opened by script.`
50-
window.close();
51-
});
27+
async function check() {
28+
try {
29+
const rsp = await fetch('/check');
30+
const responseText = await rsp.text();
31+
32+
const COMMAND_PREFIX = 'COMMAND:';
33+
if (responseText.startsWith(COMMAND_PREFIX)) {
34+
var url = responseText.substr(COMMAND_PREFIX.length);
35+
iframe.src = url;
36+
document.getElementById('count').textContent = counter++;
37+
}
38+
// Polling for the next test to start: we just keep asking if we
39+
// should load a new page, since the iframe doesn't currently have
40+
// a way to tell us it completed (even if it did, we'd need to poll
41+
// the server to know when the next test page is ready to load).
42+
// (A timeout value of 100msecs has been seen to stall test
43+
// browser.test_audio_worklet_worker on Chrome)
44+
setTimeout(check, 200);
45+
} catch {
46+
document.body.innerHTML = 'Tests complete. View log in console.';
47+
// Attempt to close the main window. This works on chrome
48+
// but fails on firefox with: `Scripts may not close windows that
49+
// were not opened by script.`
50+
window.close();
51+
}
5252
}
5353
document.addEventListener('DOMContentLoaded', check);
5454
</script>

test/codesize/test_codesize_hello_dylink_all.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"a.out.js": 268033,
2+
"a.out.js": 267889,
33
"a.out.nodebug.wasm": 587151,
4-
"total": 855184,
4+
"total": 855040,
55
"sent": [
66
"IMG_Init",
77
"IMG_Load",

0 commit comments

Comments
 (0)