Update live example

This commit is contained in:
Insality
2025-04-26 12:07:28 +03:00
parent 9fe8763c7f
commit 6d7556b5a0
13 changed files with 278 additions and 10177 deletions

View File

@@ -86,7 +86,7 @@ var CUSTOM_PARAMETERS = {
game_canvas.width = Math.floor(width * dpi);
game_canvas.height = Math.floor(height * dpi);
}
}
};
// file downloader
// wraps XMLHttpRequest and adds retry support and progress updates when the
@@ -184,16 +184,20 @@ var FileLoader = {
};
request.onretry = function(xhr, event, loadedSize, currentAttempt) {
onretry(loadedSize, currentAttempt);
}
};
request.send();
}
};
var EngineLoader = {
wasm_size: 2374239,
wasmjs_size: 340441,
asmjs_size: 4000000,
wasm_size: 2435513,
wasmjs_size: 270371,
asmjs_size: 5093239,
wasm_instantiate_progress: 0,
stream_wasm: "false" === "true",
@@ -209,9 +213,16 @@ var EngineLoader = {
ProgressUpdater.updateCurrent(delta);
},
function(error) { throw error; },
function(wasm) {
async function(wasm) {
if (wasm.byteLength != EngineLoader.wasm_size) {
throw "Invalid wasm size. Expected: " + EngineLoader.wasm_size + ", actual: " + wasm.byteLength;
console.warn("Unexpected wasm size: " + wasm.byteLength + ", expected: " + EngineLoader.wasm_size);
}
if (EngineLoader.wasm_sha1) {
const digest = await window.crypto.subtle.digest("SHA-1", wasm);
const sha1 = Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join('');
if (sha1 != EngineLoader.wasm_sha1) {
console.warn("Unexpected wasm sha1: " + sha1 + ", expected: " + EngineLoader.wasm_sha1);
}
}
var wasmInstantiate = WebAssembly.instantiate(new Uint8Array(wasm), imports).then(function(output) {
successCallback(output.instance);
@@ -270,21 +281,31 @@ var EngineLoader = {
}
return {}; // Compiling asynchronously, no exports.
};
EngineLoader.loadAndRunScriptAsync(exeName + '_wasm.js');
EngineLoader.loadAndRunScriptAsync(exeName + '_wasm.js', EngineLoader.wasmjs_size, EngineLoader.wasmjs_sha1);
},
loadAsmJsAsync: function(exeName) {
EngineLoader.loadAndRunScriptAsync(exeName + '_asmjs.js');
EngineLoader.loadAndRunScriptAsync(exeName + '_asmjs.js', EngineLoader.asmjs_size, EngineLoader.asmjs_sha1);
},
// load and start engine script (asm.js or wasm.js)
loadAndRunScriptAsync: function(src) {
loadAndRunScriptAsync: function(src, expectedLength, expectedSHA1) {
FileLoader.load(src, "text",
function(delta) {
ProgressUpdater.updateCurrent(delta);
},
function(error) { throw error; },
function(response) {
async function(response) {
if (response.length != expectedLength) {
console.warn("Unexpected JS size: " + response.length + ", expected: " + expectedLength);
}
if (expectedSHA1) {
const digest = await window.crypto.subtle.digest("SHA-1", new TextEncoder().encode(response));
const sha1 = Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join('');
if (sha1 != expectedSHA1) {
throw new Error("Unexpected sha1: " + sha1 + ", expected: " + expectedSHA1);
}
}
var tag = document.createElement("script");
tag.text = response;
document.body.appendChild(tag);
@@ -315,14 +336,14 @@ var EngineLoader = {
// move resize callback setup here to make possible to override callback
// from outside of dmloader.js
if (typeof CUSTOM_PARAMETERS["resize_window_callback"] === "function") {
var callback = CUSTOM_PARAMETERS["resize_window_callback"]
var callback = CUSTOM_PARAMETERS["resize_window_callback"];
callback();
window.addEventListener('resize', callback, false);
window.addEventListener('orientationchange', callback, false);
window.addEventListener('focus', callback, false);
}
}
}
};
/* ********************************************************************* */
@@ -367,7 +388,7 @@ var GameArchiveLoader = {
list.push(callback);
},
notifyListeners: function(list, data) {
for (i=0; i<list.length; ++i) {
for (let i=0; i<list.length; ++i) {
list[i](data);
}
},
@@ -375,8 +396,8 @@ var GameArchiveLoader = {
addFileDownloadErrorListener: function(callback) {
this.addListener(this._onFileDownloadErrorListeners, callback);
},
notifyFileDownloadError: function(url) {
this.notifyListeners(this._onFileDownloadErrorListeners, url);
notifyFileDownloadError: function(error) {
this.notifyListeners(this._onFileDownloadErrorListeners, error);
},
addFileLoadedListener: function(callback) {
@@ -404,14 +425,29 @@ var GameArchiveLoader = {
loadArchiveDescription: function(descriptionUrl) {
FileLoader.load(
this._archiveLocationFilter(descriptionUrl),
"json",
"text",
function (delta) { },
function (error) { GameArchiveLoader.notifyFileDownloadError(descriptionUrl); },
function (json) { GameArchiveLoader.onReceiveDescription(json); },
function (text) { GameArchiveLoader.onReceiveDescription(text); },
function (loadedDelta, currentAttempt) { });
},
onReceiveDescription: function(json) {
onReceiveDescription: async function(text) {
let json;
try {
json = JSON.parse(text);
if (EngineLoader.arc_sha1) {
const digest = await window.crypto.subtle.digest("SHA-1", (new TextEncoder()).encode(text));
const sha1 = Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join('');
if (sha1 != EngineLoader.arc_sha1) {
throw new Error(`Unexpected hash ${sha1} wanted ${EngineLoader.arc_sha1}`);
}
}
} catch (e) {
GameArchiveLoader.notifyFileDownloadError(e.toString());
return;
}
var totalSize = json.total_size;
var exeName = CUSTOM_PARAMETERS['exe_name'];
this._files = json.content;
@@ -424,7 +460,11 @@ var GameArchiveLoader = {
EngineLoader.loadAsmJsAsync(exeName);
totalSize += EngineLoader.asmjs_size;
}
this.downloadContent();
if (!Module['isDMFSSupported']) {
// we can download in parallel here because we will not rely on FS, otherwise
// we have to wait until after the [w]asm is loaded.
this.downloadContent();
}
ProgressUpdater.resetCurrent();
if (isWASMSupported) {
EngineLoader.updateWasmInstantiateProgress(totalSize);
@@ -432,18 +472,45 @@ var GameArchiveLoader = {
ProgressUpdater.setupTotal(totalSize + EngineLoader.wasm_instantiate_progress);
},
downloadContent: function() {
downloadContent: async function() {
var file = this._files[this._fileIndex];
// if the file consists of more than one piece we prepare an array to store the pieces in
if (file.pieces.length > 1) {
file.data = new Uint8Array(file.size);
if (Module['isDMFSSupported']) {
const path = `${DMSYS.GetUserPersistentDataRoot()}/${file.name}`;
try { // see if already and stored
const stat = FS.stat(path);
if (stat) {
let matches = (file.size == stat.size);
if (matches && file.sha1) {
const stream = FS.open(path, "r");
if (stream) {
try {
const mmap = FS.mmap(stream, stat.size, 0, 0x01, 0x01); //PROT_READ, MAP_SHARED
if (mmap) {
const digest = await window.crypto.subtle.digest("SHA-1", mmap);
matches = Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join('') == file.sha1;
}
} catch(e) { }
FS.close(stream);
} else {
matches = false;
}
}
if (matches) {
this.onFileLoaded(file);
return;
}
}
} catch(_e) { }
file.stream = FS.open(path, "w+");
}
// how many pieces to download at a time
var limit = file.pieces.length;
if (typeof this.MAX_CONCURRENT_XHR !== 'undefined') {
limit = Math.min(limit, this.MAX_CONCURRENT_XHR);
}
// download pieces
for (var i=0; i<limit; ++i) {
this.downloadPiece(file, i);
}
@@ -486,9 +553,14 @@ var GameArchiveLoader = {
},
addPieceToFile: function(file, piece) {
if (1 == file.pieces.length) {
if (file.stream !== undefined) {
FS.write(file.stream, piece.data, 0, piece.data.length, piece.offset);
} else if (1 == file.pieces.length) {
file.data = piece.data;
} else {
if (!file.data) {
file.data = new Uint8Array(file.size);
}
var start = piece.offset;
var end = start + piece.data.length;
if (0 > start) {
@@ -507,7 +579,16 @@ var GameArchiveLoader = {
++file.totalLoadedPieces;
// is all pieces of the file loaded?
if (file.totalLoadedPieces == file.pieces.length) {
this.onFileLoaded(file);
this.verifyFile(file).then(() => {
if (file.stream !== undefined) {
FS.close(file.stream);
file.stream = undefined;
}
this.onFileLoaded(file);
}).catch((e) => {
console.log('file verification failed! ' + e);
throw e;
});
}
// continue loading more pieces of the file
// if not all pieces are already in progress
@@ -526,7 +607,7 @@ var GameArchiveLoader = {
actualSize += file.pieces[i].dataLength;
}
if (actualSize != file.size) {
throw "Unexpected data size: " + file.name + ", expected size: " + file.size + ", actual size: " + actualSize;
return Promise.reject(new Error("Unexpected data size: " + file.name + ", expected size: " + file.size + ", actual size: " + actualSize));
}
// verify the pieces
@@ -540,21 +621,35 @@ var GameArchiveLoader = {
if (0 < i) {
var previous = pieces[i - 1];
if (previous.offset + previous.dataLength > start) {
throw RangeError("Segment underflow in file: " + file.name + ", offset: " + (previous.offset + previous.dataLength) + " , start: " + start);
return Promise.reject(new RangeError("Segment underflow in file: " + file.name + ", offset: " + (previous.offset + previous.dataLength) + " , start: " + start));
}
}
if (pieces.length - 2 > i) {
var next = pieces[i + 1];
if (end > next.offset) {
throw RangeError("Segment overflow in file: " + file.name + ", offset: " + next.offset + ", end: " + end);
return Promise.reject(new RangeError("Segment overflow in file: " + file.name + ", offset: " + next.offset + ", end: " + end));
}
}
}
}
if (file.sha1) {
let data = file.data;
if (file.stream) {
try {
data = FS.mmap(file.stream, file.size, 0, 0x01, 0x01); //PROT_READ, MAP_SHARED
} catch(e) { }
}
return window.crypto.subtle.digest("SHA-1", data).then((digest) => {
const sha1 = Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join('');
if (sha1 !== file.sha1)
return Promise.reject(new Error(`Unexpected hash ${sha1} wanted ${file.sha1}`));
return;
});
}
return Promise.resolve();
},
onFileLoaded: function(file) {
this.verifyFile(file);
this.notifyFileLoaded(file);
++this._fileIndex;
if (this._fileIndex == this._files.length) {
@@ -597,7 +692,7 @@ var ProgressView = {
// Remove any background/splash image that was set in runApp().
// Workaround for Safari bug DEF-3061.
Module.canvas.style.background = "";
Module.canvas.style.background = "none";
}
}
};
@@ -691,6 +786,7 @@ var Module = {
_archiveLoaded: false,
_preLoadDone: false,
_isEngineLoaded: false,
_isMainCalled: false,
// Persistent storage
persistentStorage: true,
@@ -707,6 +803,13 @@ var Module = {
setStatus: function(text) { console.log(text); },
isDMFSSupported: (function() {
// DMFS is meant as a mount for FS to provide another way to acess resources, by default we just use IDBFS
if (typeof DMFS === "undefined")
return false;
return true;
})(),
isWASMSupported: (function() {
try {
if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") {
@@ -755,9 +858,27 @@ var Module = {
return { stack:stack, message:message };
},
hasWebGPUSupport: function() {
var webgpu_support = false;
try {
var canvas = document.createElement("canvas");
var webgpu = canvas.getContext("webgpu");
if (webgpu && webgpu instanceof GPUCanvasContext) {
webgpu_support = true;
}
} catch (error) {
console.log("An error occurred while detecting WebGPU support: " + error);
webgpu_support = false;
}
return webgpu_support;
},
hasWebGLSupport: function() {
var webgl_support = false;
try {
// create canvas to simply check is rendering context supported
// real render context created by glfw
var canvas = document.createElement("canvas");
var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
if (gl && gl instanceof WebGLRenderingContext) {
@@ -782,6 +903,10 @@ var Module = {
* Module.runApp - Starts the application given a canvas element id
**/
runApp: function(appCanvasId, _) {
window.addEventListener("error", (errorEvent) => {
var errorObject = Module.prepareErrorObject(errorEvent.message, errorEvent.filename, errorEvent.lineno, errorEvent.colno, errorEvent.error);
Module.ccall('JSWriteDump', 'null', ['string'], [JSON.stringify(errorObject.stack)]);
});
Module._isEngineLoaded = true;
Module.setupCanvas(appCanvasId);
@@ -793,9 +918,16 @@ var Module = {
}
Module.fullScreenContainer = fullScreenContainer || Module.canvas;
if (Module.hasWebGLSupport()) {
if (Module.hasWebGLSupport() || Module.hasWebGPUSupport()) {
Module.canvas.focus();
Module.canvas.addEventListener("webglcontextlost", function(event) {
event.preventDefault();
dmRenderer.rendererContextEvent(dmRenderer.CONTEXT_LOST_EVENT);
}, false);
Module.canvas.addEventListener("webglcontextrestored", function(event) {
dmRenderer.rendererContextEvent(dmRenderer.CONTEXT_RESTORED_EVENT);
}, false);
// Add context menu hide-handler if requested
if (CUSTOM_PARAMETERS["disable_context_menu"])
{
@@ -818,7 +950,9 @@ var Module = {
},
onArchiveFileLoaded: function(file) {
Module._filesToPreload.push({path: file.name, data: file.data});
if (file.data) {
Module._filesToPreload.push({path: file.name, data: file.data});
}
},
onArchiveLoaded: function() {
@@ -844,14 +978,17 @@ var Module = {
FS.syncfs(true, function(err) {
if (err) {
Module._syncTries += 1;
console.warn("Unable to synchronize mounted file systems: " + err);
console.info(`Unable to synchronize mounted file systems (attempt ${Module._syncTries} of ${Module._syncMaxTries}): `, err);
if (Module._syncMaxTries > Module._syncTries) {
Module.preSync(done);
} else {
console.warn("Mounted system wasn't synchronized. Retry count was exceeded.");
Module._syncTries = 0;
Module._syncInitial = true;
done();
}
} else {
Module._syncTries = 0;
Module._syncInitial = true;
if (done !== undefined) {
done();
@@ -902,18 +1039,25 @@ var Module = {
return;
}
// If IndexedDB is supported we mount the persistent data root as IDBFS,
// then try to do a IDB->MEM sync before we start the engine to get
// previously saved data before boot.
try {
FS.mount(IDBFS, {}, dir);
if (Module['isDMFSSupported']) {
// In DMFS mode we will use that as our mountpoint and make sure that all
// relative paths point into there.
FS.mount(new DMFS(CUSTOM_PARAMETERS['exe_name']), {}, dir);
FS.chdir(dir);
} else {
// If IndexedDB is supported we mount the persistent data root as IDBFS,
// then try to do a IDB->MEM sync before we start the engine to get
// previously saved data before boot.
FS.mount(IDBFS, {}, dir);
}
// Patch FS.close so it will try to sync MEM->IDB
var _close = FS.close;
FS.close = function(stream) {
var r = _close(stream);
Module.persistentSync();
return r;
}
};
}
catch (error) {
Module.persistentStorage = false;
@@ -929,14 +1073,17 @@ var Module = {
preRun: [function() {
/* If archive is loaded, preload all its files */
if(Module._archiveLoaded) {
if (Module._archiveLoaded) {
Module.preloadAll();
}
}],
postRun: [function() {
if(Module._archiveLoaded) {
if (Module._archiveLoaded) {
ProgressView.removeProgress();
} else if (Module['isDMFSSupported']) {
// kick off the content download now that we have FS access
GameArchiveLoader.downloadContent();
}
}],
@@ -955,12 +1102,17 @@ var Module = {
}
},
_callMain: function() {
ProgressView.removeProgress();
if (Module.callMain === undefined) {
Module.noInitialRun = false;
_callMain: function(_, _) {
if (!Module._isMainCalled) {
Module._isMainCalled = true;
ProgressView.removeProgress();
if (Module.callMain === undefined) {
Module.noInitialRun = false;
} else {
Module.callMain(Module.arguments);
}
} else {
Module.callMain(Module.arguments);
console.warn("Main was called several times!");
}
},
// Wrap IDBFS syncfs call with logic to avoid multiple syncs
@@ -973,8 +1125,10 @@ var Module = {
Module._syncInProgress = false;
if (err) {
console.warn("Unable to synchronize mounted file systems: " + err);
console.info(`Unable to synchronize mounted file systems (attempt ${Module._syncTries} of ${Module._syncMaxTries}): `, err);
Module._syncTries += 1;
} else {
Module._syncTries = 0;
}
if (Module._syncNeeded) {
@@ -983,6 +1137,9 @@ var Module = {
}
});
} else {
console.warn("Mounted system wasn't synchronized. Retry count was exceeded.");
Module._syncTries = 0;
}
},
};
@@ -1001,19 +1158,15 @@ Module["locateFile"] = function(path, scriptDirectory)
// dmengine*.wasm is hardcoded in the built JS loader for WASM,
// we need to replace it here with the correct project name.
if (path == "dmengine.wasm" || path == "dmengine_release.wasm" || path == "dmengine_headless.wasm") {
path = "druid.wasm";
path = "Druid.wasm";
}
return scriptDirectory + path;
};
window.onerror = function(err, url, line, column, errObj) {
if (typeof Module.ccall !== 'undefined') {
var errorObject = Module.prepareErrorObject(err, url, line, column, errObj);
Module.ccall('JSWriteDump', 'null', ['string'], [JSON.stringify(errorObject.stack)]);
}
window.addEventListener("error", (errorEvent) => {
Module.setStatus('Exception thrown, see JavaScript console');
Module.setStatus = function(text) {
if (text) Module.printErr('[post-exception status] ' + text);
};
};
});