@dcl-regenesislabs/bevy-explorer-web 0.1.0-35932535123.commit-9883f71 → 0.1.0-36033403701.commit-19347eb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/{Showcase-C5rqjC5_.js → Showcase-BUhkfRgq.js} +1 -1
- package/assets/{design-tpXIcp9S.js → design-Fh4VSWdp.js} +1 -1
- package/assets/{index-D2KtSwXa.css → index-B_oaXFz0.css} +1 -1
- package/assets/{index-lFIF2gi0.js → index-DX1YuW5C.js} +3 -3
- package/assets/{menus-DAAES5v_.js → menus-CSgILZ7j.js} +1 -1
- package/assets/{vendor-C7oQlkoF.css → vendor-BtKTmWnJ.css} +1 -1
- package/assets/{vendor-BcGk5WfL.js → vendor-C2yMCOI1.js} +1 -1
- package/bridge-scene/static/BevyExplorerUI/about +1 -1
- package/bridge-scene/static/{bafkreiagin33u6tsytrf7mrhd42h3pezggv4xnhvujhviws2jmee72ul7u → bafkreibr7lbrs4iu6lr3z5thdguk6ze7vslm7f4irlihbi4aoxdhj532m4} +1 -1
- package/engine/boot.js +14 -5
- package/engine/engine.js +117 -48
- package/engine/gpu_cache.js +49 -28
- package/engine/pkg/manifest.json +1 -1
- package/engine/pkg/sandbox_worker.bundle.js +834 -289
- package/engine/pkg/snippets/bevy_internal-2d8ddab1173ada7c/src/web_worker.js +121 -0
- package/engine/pkg/snippets/comms-53217a45365bb5fa/livekit_web_bindings.js +47 -13
- package/engine/pkg/snippets/decentra-bevy-f9c23e535711925a/deploy/web/engine/gpu_cache.js +439 -0
- package/engine/pkg/webgpu_build.d.ts +97 -67
- package/engine/pkg/webgpu_build.js +413 -328
- package/engine/pkg/webgpu_build_bg.wasm +2 -2
- package/engine/pkg/webgpu_build_bg.wasm.d.ts +26 -25
- package/engine/sandbox_worker.js +3 -3
- package/index.html +17 -17
- package/package.json +3 -3
- package/engine/asset_loader.js +0 -30
- package/engine/asset_processor.js +0 -32
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// The worker script of `bevy::web_worker`, and its page-side spawner. The same file runs on the
|
|
2
|
+
// page, where the wasm-bindgen glue imports it, and as the script of every worker it spawns.
|
|
3
|
+
//
|
|
4
|
+
// A worker is told, in its init message, the url of the glue and the exports to call, in order,
|
|
5
|
+
// once its wasm instance is up (over the shared module and memory the page passes). The page functions
|
|
6
|
+
// the engine calls (extern blocks marked `#[page_functions]`, listed by the glue exports that
|
|
7
|
+
// attribute generates) each become a function on the worker's global that posts the call to the
|
|
8
|
+
// page and resolves with the page function's result. Console output is sent to the page to
|
|
9
|
+
// print. When the last export returns (or its promise resolves) the worker posts
|
|
10
|
+
// `{ __bevy_ready: tag }`; if importing the glue or an export fails it posts
|
|
11
|
+
// `{ __bevy_ready: tag, error }` instead.
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Page side: spawns a worker and posts it its init message.
|
|
15
|
+
* @param {{ glueUrl: string, calls: { entry: string, args: any[] }[], tag: string,
|
|
16
|
+
* module: WebAssembly.Module, memory: WebAssembly.Memory, stackSize?: number,
|
|
17
|
+
* transfer: Transferable[] }} init
|
|
18
|
+
* @returns {Worker}
|
|
19
|
+
*/
|
|
20
|
+
export function spawnWorker(init) {
|
|
21
|
+
const worker = new Worker(new URL(import.meta.url), { type: "module" });
|
|
22
|
+
worker.addEventListener("message", async ({ data }) => {
|
|
23
|
+
if (!data) return;
|
|
24
|
+
if (data.__bevy_log) {
|
|
25
|
+
console[data.__bevy_log](...data.args);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (data.__bevy_call !== undefined) {
|
|
29
|
+
const fn = globalThis[data.__bevy_call];
|
|
30
|
+
let value, error;
|
|
31
|
+
try {
|
|
32
|
+
if (typeof fn !== "function") throw new Error(`${data.__bevy_call} is not defined on the page`);
|
|
33
|
+
value = await fn(...data.args);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
error = err?.message ?? String(err);
|
|
36
|
+
console.error(`[bevy] ${data.tag} worker: ${data.__bevy_call} failed`, err);
|
|
37
|
+
}
|
|
38
|
+
worker.postMessage({ __bevy_result: data.__bevy_id, value, error });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
const { transfer = [], ...message } = init;
|
|
42
|
+
worker.postMessage({ __bevy_init: message }, transfer);
|
|
43
|
+
return worker;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Worker side.
|
|
47
|
+
if (typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope) {
|
|
48
|
+
const pending = new Map();
|
|
49
|
+
let nextId = 1;
|
|
50
|
+
self.addEventListener("message", ({ data }) => {
|
|
51
|
+
if (!data || data.__bevy_result === undefined) return;
|
|
52
|
+
const call = pending.get(data.__bevy_result);
|
|
53
|
+
if (!call) return;
|
|
54
|
+
pending.delete(data.__bevy_result);
|
|
55
|
+
if (data.error !== undefined) call.reject(new Error(String(data.error)));
|
|
56
|
+
else call.resolve(data.value);
|
|
57
|
+
});
|
|
58
|
+
self.addEventListener("message", async ({ data }) => {
|
|
59
|
+
const init = data?.__bevy_init;
|
|
60
|
+
if (!init) return;
|
|
61
|
+
const { glueUrl, calls, tag, module, memory, stackSize } = init;
|
|
62
|
+
forwardConsole(tag);
|
|
63
|
+
try {
|
|
64
|
+
const glue = await import(glueUrl);
|
|
65
|
+
await glue.default({ module_or_path: module, memory, thread_stack_size: stackSize });
|
|
66
|
+
for (const key of Object.keys(glue)) {
|
|
67
|
+
if (!key.startsWith("__bevy_page_functions_")) continue;
|
|
68
|
+
for (const name of glue[key]()) {
|
|
69
|
+
self[name] = (...args) =>
|
|
70
|
+
new Promise((resolve, reject) => {
|
|
71
|
+
const id = nextId++;
|
|
72
|
+
pending.set(id, { resolve, reject });
|
|
73
|
+
postMessage({ __bevy_call: name, __bevy_id: id, args, tag });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const { entry, args = [] } of calls) {
|
|
78
|
+
const entryFn = glue[entry];
|
|
79
|
+
if (typeof entryFn !== "function") throw new Error(`${entry} is not exported by ${glueUrl}`);
|
|
80
|
+
await entryFn(...args);
|
|
81
|
+
}
|
|
82
|
+
postMessage({ __bevy_ready: tag });
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.error(`[bevy] ${tag} worker failed to start`, e);
|
|
85
|
+
postMessage({ __bevy_ready: tag, error: String(e?.stack ?? e) });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Sends this worker's console to the page, which prints it (and whose console hooks see it).
|
|
91
|
+
// It is not printed here as well: DevTools shows a worker's console alongside the page's, so
|
|
92
|
+
// every line would appear twice. Arguments are passed through as-is where they can be cloned, so
|
|
93
|
+
// styled (`%c`) lines render the same on the page.
|
|
94
|
+
function forwardConsole(tag) {
|
|
95
|
+
for (const level of ["log", "info", "warn", "error", "debug"]) {
|
|
96
|
+
const original = console[level].bind(console);
|
|
97
|
+
console[level] = (...args) => {
|
|
98
|
+
try {
|
|
99
|
+
postMessage({ __bevy_log: level, tag, args: args.map(cloneable) });
|
|
100
|
+
} catch (_) {
|
|
101
|
+
// not cloneable: print it here instead
|
|
102
|
+
original(...args);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
self.addEventListener("error", (e) => console.error(`[${tag} worker] uncaught: ${e.message}`));
|
|
107
|
+
self.addEventListener("unhandledrejection", (e) =>
|
|
108
|
+
console.error(`[${tag} worker] unhandled rejection: ${e.reason?.stack ?? e.reason}`)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function cloneable(value) {
|
|
113
|
+
if (value instanceof Error) return value.stack || value.message;
|
|
114
|
+
const t = typeof value;
|
|
115
|
+
if (t === "string" || t === "number" || t === "boolean" || t === "bigint" || value == null) return value;
|
|
116
|
+
try {
|
|
117
|
+
return JSON.stringify(value);
|
|
118
|
+
} catch (_) {
|
|
119
|
+
return String(value);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -11,12 +11,17 @@ function error(...args) {
|
|
|
11
11
|
var audioContext = null;
|
|
12
12
|
var microphonePermission = "denied";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* @param {function} on_change called with the permission state now and whenever it changes
|
|
16
|
+
*/
|
|
17
|
+
export function setupMicrophonePermission(on_change) {
|
|
15
18
|
navigator.permissions.query({ name: "microphone" }).then((permissionState) => {
|
|
16
19
|
microphonePermission = permissionState.state;
|
|
20
|
+
on_change(microphonePermission);
|
|
17
21
|
|
|
18
22
|
permissionState.onchange = () => {
|
|
19
23
|
microphonePermission = permissionState.state;
|
|
24
|
+
on_change(microphonePermission);
|
|
20
25
|
};
|
|
21
26
|
});
|
|
22
27
|
}
|
|
@@ -60,7 +65,13 @@ export async function room_connect(url, token, room_options, room_connect_option
|
|
|
60
65
|
|
|
61
66
|
set_room_event_handler(room, handler);
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
try {
|
|
69
|
+
await room.connect(url, token, room_connect_options);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
// the handler does not outlive a failed connect
|
|
72
|
+
room.removeAllListeners();
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
64
75
|
|
|
65
76
|
return room;
|
|
66
77
|
}
|
|
@@ -73,6 +84,14 @@ export async function room_close(room) {
|
|
|
73
84
|
await room.disconnect();
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Releases a room the engine dropped: its event handler is gone.
|
|
89
|
+
* @param {livekit.Room} room
|
|
90
|
+
*/
|
|
91
|
+
export function room_drop(room) {
|
|
92
|
+
room.removeAllListeners();
|
|
93
|
+
}
|
|
94
|
+
|
|
76
95
|
/**
|
|
77
96
|
*
|
|
78
97
|
* @param {livekit.Room} room
|
|
@@ -439,6 +458,30 @@ export function remote_track_publication_track(remote_track_publication) {
|
|
|
439
458
|
return remote_track_publication.track;
|
|
440
459
|
}
|
|
441
460
|
|
|
461
|
+
/**
|
|
462
|
+
* @param {livekit.LocalTrackPublication} local_track_publication
|
|
463
|
+
* @returns string
|
|
464
|
+
*/
|
|
465
|
+
export function local_track_publication_sid(local_track_publication) {
|
|
466
|
+
return local_track_publication.trackSid;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* @param {livekit.LocalTrackPublication} local_track_publication
|
|
471
|
+
* @returns string
|
|
472
|
+
*/
|
|
473
|
+
export function local_track_publication_kind(local_track_publication) {
|
|
474
|
+
return local_track_publication.kind;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* @param {livekit.LocalTrackPublication} local_track_publication
|
|
479
|
+
* @returns string
|
|
480
|
+
*/
|
|
481
|
+
export function local_track_publication_source(local_track_publication) {
|
|
482
|
+
return local_track_publication.source;
|
|
483
|
+
}
|
|
484
|
+
|
|
442
485
|
/**
|
|
443
486
|
*
|
|
444
487
|
* @param {livekit.AudioCaptureOptions} options
|
|
@@ -452,15 +495,6 @@ export async function local_audio_track_new(options) {
|
|
|
452
495
|
}
|
|
453
496
|
}
|
|
454
497
|
|
|
455
|
-
/**
|
|
456
|
-
*
|
|
457
|
-
* @param {livekit.LocalAudioTrack} local_audio_track
|
|
458
|
-
* @returns livekit.TrackSid
|
|
459
|
-
*/
|
|
460
|
-
export function local_audio_track_sid(local_audio_track) {
|
|
461
|
-
return local_audio_track.sid;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
498
|
/**
|
|
465
499
|
*
|
|
466
500
|
* @param {livekit.RemoteTrack} remote_track
|
|
@@ -522,8 +556,8 @@ function track_rig_drop(remote_track) {
|
|
|
522
556
|
export function remote_track_pan_and_volume(remote_track, pan, volume) {
|
|
523
557
|
// log(`Setting pan and volume for track ${remote_track.sid}.`);
|
|
524
558
|
const track_rig = remote_track.trackRig;
|
|
525
|
-
//
|
|
526
|
-
track_rig.pannerNode.pan.value = Math.max(-1, Math.min(1, pan));
|
|
559
|
+
// kira's 0 (left) .. 1 (right) to the stereo panner's -1 .. 1
|
|
560
|
+
track_rig.pannerNode.pan.value = Math.max(-1, Math.min(1, pan * 2 - 1));
|
|
527
561
|
// Volume should be between 0 and 1 (or higher for boost)
|
|
528
562
|
track_rig.gainNode.gain.value = Math.max(0, volume);
|
|
529
563
|
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
var count = 0;
|
|
2
|
+
|
|
3
|
+
function simpleHash(s) {
|
|
4
|
+
var h = 0x811c9dc5;
|
|
5
|
+
|
|
6
|
+
for (let i = 0; i < s.length; i++) {
|
|
7
|
+
h ^= s.charCodeAt(i);
|
|
8
|
+
h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return h >>> 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
var gpuSessionState = {};
|
|
15
|
+
var requiredItemTypes = new Map();
|
|
16
|
+
var precaching = false;
|
|
17
|
+
|
|
18
|
+
const dbConfig = {
|
|
19
|
+
name: "GpuCacheDB",
|
|
20
|
+
version: 1,
|
|
21
|
+
onUpgrade: (db) => {
|
|
22
|
+
if (!db.objectStoreNames.contains("shader")) db.createObjectStore("shader");
|
|
23
|
+
if (!db.objectStoreNames.contains("bindgroup"))
|
|
24
|
+
db.createObjectStore("bindgroup");
|
|
25
|
+
if (!db.objectStoreNames.contains("layout")) db.createObjectStore("layout");
|
|
26
|
+
if (!db.objectStoreNames.contains("pipeline"))
|
|
27
|
+
db.createObjectStore("pipeline");
|
|
28
|
+
if (!db.objectStoreNames.contains("requiredItems"))
|
|
29
|
+
db.createObjectStore("requiredItems");
|
|
30
|
+
if (!db.objectStoreNames.contains("deviceConfig"))
|
|
31
|
+
db.createObjectStore("deviceConfig");
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
var dbPromise;
|
|
36
|
+
function openDB() {
|
|
37
|
+
if (dbPromise) {
|
|
38
|
+
return dbPromise;
|
|
39
|
+
}
|
|
40
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
41
|
+
const request = indexedDB.open(dbConfig.name, dbConfig.version);
|
|
42
|
+
request.onupgradeneeded = (event) => {
|
|
43
|
+
dbConfig.onUpgrade(event.target.result);
|
|
44
|
+
};
|
|
45
|
+
request.onsuccess = (event) => resolve(event.target.result);
|
|
46
|
+
request.onerror = (event) => reject(event.target.error);
|
|
47
|
+
});
|
|
48
|
+
return dbPromise;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function clearDatabase() {
|
|
52
|
+
const db = await openDB();
|
|
53
|
+
|
|
54
|
+
const tableNames = ["shader", "bindgroup", "layout", "pipeline", "requiredItems"];
|
|
55
|
+
|
|
56
|
+
const tx = db.transaction(tableNames, "readwrite");
|
|
57
|
+
|
|
58
|
+
const clearPromises = tableNames.map(name => {
|
|
59
|
+
const store = tx.objectStore(name);
|
|
60
|
+
const request = store.clear();
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
request.onsuccess = () => resolve(request.result);
|
|
63
|
+
request.onerror = () => reject(request.error);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await Promise.all(clearPromises);
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
69
|
+
tx.oncomplete = resolve;
|
|
70
|
+
tx.onabort = reject;
|
|
71
|
+
tx.onerror = reject;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function fetchRequiredItems() {
|
|
76
|
+
const db = await openDB();
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const tx = db.transaction("requiredItems", "readonly");
|
|
79
|
+
const request = tx.objectStore("requiredItems").get("it");
|
|
80
|
+
request.onsuccess = () => {
|
|
81
|
+
resolve(request.result ?? new Map());
|
|
82
|
+
};
|
|
83
|
+
request.onerror = () => {
|
|
84
|
+
resolve(new Map());
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function storeRequiredItems() {
|
|
90
|
+
const db = await openDB();
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const tx = db.transaction("requiredItems", "readwrite");
|
|
93
|
+
tx.oncomplete = () => {
|
|
94
|
+
resolve();
|
|
95
|
+
};
|
|
96
|
+
tx.onerror = () => {
|
|
97
|
+
console.log("[GPU Cache] failed to store requiredItems");
|
|
98
|
+
reject(tx.error);
|
|
99
|
+
};
|
|
100
|
+
tx.objectStore("requiredItems").put(requiredItemTypes, "it");
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The small settings that lived in localStorage, which a worker has no access to.
|
|
105
|
+
async function getConfig(key) {
|
|
106
|
+
const db = await openDB();
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
const request = db.transaction("deviceConfig", "readonly").objectStore("deviceConfig").get(key);
|
|
109
|
+
request.onsuccess = () => resolve(request.result);
|
|
110
|
+
request.onerror = () => resolve(undefined);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function setConfig(key, value) {
|
|
115
|
+
const db = await openDB();
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const tx = db.transaction("deviceConfig", "readwrite");
|
|
118
|
+
tx.oncomplete = () => resolve();
|
|
119
|
+
tx.onerror = () => reject(tx.error);
|
|
120
|
+
tx.objectStore("deviceConfig").put(value, key);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function fetchInstance(type, hash) {
|
|
125
|
+
const db = await openDB();
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const tx = db.transaction(type, "readonly");
|
|
128
|
+
const request = tx.objectStore(type).get(hash);
|
|
129
|
+
request.onsuccess = () => {
|
|
130
|
+
if (request.result) {
|
|
131
|
+
resolve(JSON.parse(request.result));
|
|
132
|
+
} else {
|
|
133
|
+
reject("undefined");
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
request.onerror = () => {
|
|
137
|
+
reject("error");
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function storeInstance(type, hash, value) {
|
|
143
|
+
const db = await openDB();
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
const tx = db.transaction(type, "readwrite");
|
|
146
|
+
tx.oncomplete = () => {
|
|
147
|
+
resolve();
|
|
148
|
+
};
|
|
149
|
+
tx.onerror = () => {
|
|
150
|
+
console.log("[GPU Cache] failed to store ${type} instance");
|
|
151
|
+
reject(tx.error);
|
|
152
|
+
};
|
|
153
|
+
tx.objectStore(type).put(JSON.stringify(value), hash);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function initGpuCache(key, fakeAsync) {
|
|
158
|
+
console.log(`[GPU Cache] key: ${key}`);
|
|
159
|
+
patchWebgpuAdapter(fakeAsync);
|
|
160
|
+
await createGpuCache(key);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function patchWebgpuAdapter(fakeAsync) {
|
|
164
|
+
const originalRequestDevice = GPUAdapter.prototype.requestDevice;
|
|
165
|
+
GPUAdapter.prototype.requestDevice = async function (descriptor) {
|
|
166
|
+
const jsonDescriptor = JSON.stringify(descriptor || {});
|
|
167
|
+
if (gpuSessionState.deviceDescriptor === jsonDescriptor) {
|
|
168
|
+
console.log("[GPU Cache] using precached device");
|
|
169
|
+
return gpuSessionState.device;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!precaching) {
|
|
173
|
+
console.log(
|
|
174
|
+
"[GPU Cache] device params are different: creating device and clearing cache"
|
|
175
|
+
);
|
|
176
|
+
clearDatabase();
|
|
177
|
+
}
|
|
178
|
+
gpuSessionState = {
|
|
179
|
+
shader: new Map(),
|
|
180
|
+
bindgroup: new Map(),
|
|
181
|
+
layout: new Map(),
|
|
182
|
+
pipeline: new Map(),
|
|
183
|
+
};
|
|
184
|
+
gpuSessionState.deviceDescriptor = jsonDescriptor;
|
|
185
|
+
var device = undefined;
|
|
186
|
+
try {
|
|
187
|
+
device = await originalRequestDevice.apply(this, [descriptor]);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.warn("Failed to request a device with descriptor, requesting device with default parameters.");
|
|
190
|
+
device = await originalRequestDevice.apply(this, []);
|
|
191
|
+
}
|
|
192
|
+
gpuSessionState.device = device;
|
|
193
|
+
await setConfig("deviceDescriptor", jsonDescriptor);
|
|
194
|
+
|
|
195
|
+
function wrapDeviceFunction(itemType, originalFunction) {
|
|
196
|
+
return (...args) => {
|
|
197
|
+
const jsonArgs = JSON.stringify(args);
|
|
198
|
+
const hash = simpleHash(jsonArgs);
|
|
199
|
+
const cachedItem = gpuSessionState[itemType].get(hash);
|
|
200
|
+
if (cachedItem !== undefined) {
|
|
201
|
+
return cachedItem;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const item = originalFunction.apply(device, args);
|
|
205
|
+
item.__gpu_item_type = itemType;
|
|
206
|
+
item.__gpu_hash = hash;
|
|
207
|
+
gpuSessionState[itemType].set(hash, item);
|
|
208
|
+
|
|
209
|
+
if (!precaching) {
|
|
210
|
+
console.log(`[GPU Cache] no cached ${itemType} for ${hash}`);
|
|
211
|
+
if (!requiredItemTypes.has(itemType)) {
|
|
212
|
+
requiredItemTypes.set(itemType, new Set());
|
|
213
|
+
}
|
|
214
|
+
const requiredItems = requiredItemTypes.get(itemType);
|
|
215
|
+
if (!requiredItems.has(hash)) {
|
|
216
|
+
requiredItems.add(hash);
|
|
217
|
+
storeRequiredItems();
|
|
218
|
+
storeInstance(itemType, hash, args);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return item;
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
device.createShaderModule = wrapDeviceFunction(
|
|
226
|
+
"shader",
|
|
227
|
+
device.createShaderModule
|
|
228
|
+
);
|
|
229
|
+
device.createBindGroupLayout = wrapDeviceFunction(
|
|
230
|
+
"bindgroup",
|
|
231
|
+
device.createBindGroupLayout
|
|
232
|
+
);
|
|
233
|
+
device.createPipelineLayout = wrapDeviceFunction(
|
|
234
|
+
"layout",
|
|
235
|
+
device.createPipelineLayout
|
|
236
|
+
);
|
|
237
|
+
if (fakeAsync) {
|
|
238
|
+
device.createRenderPipeline = wrapDeviceFunction(
|
|
239
|
+
"pipeline",
|
|
240
|
+
device.createRenderPipeline
|
|
241
|
+
);
|
|
242
|
+
} else {
|
|
243
|
+
let inline_function = wrapDeviceFunction("pipeline", device.createRenderPipeline);
|
|
244
|
+
|
|
245
|
+
globalThis.pendingAsyncPipelineCount = 0;
|
|
246
|
+
globalThis.lastPipelineWasValidFlag = false;
|
|
247
|
+
globalThis.wgpuResolveIdle = [];
|
|
248
|
+
const itemType = "pipeline";
|
|
249
|
+
const placeholderPipeline = getPlaceholder(device);
|
|
250
|
+
|
|
251
|
+
device.createRenderPipeline = (...args) => {
|
|
252
|
+
const jsonArgs = JSON.stringify(args);
|
|
253
|
+
const hash = simpleHash(jsonArgs);
|
|
254
|
+
const cachedItem = gpuSessionState[itemType].get(hash);
|
|
255
|
+
if (cachedItem !== undefined) {
|
|
256
|
+
globalThis.nextPipelineCanFail = false;
|
|
257
|
+
globalThis.lastPipelineWasValidFlag = true;
|
|
258
|
+
return cachedItem;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log(`[GPU Cache] (async) no cached ${itemType} for ${hash}`);
|
|
262
|
+
|
|
263
|
+
if (!globalThis.nextPipelineCanFail) {
|
|
264
|
+
return inline_function.apply(device, args);
|
|
265
|
+
}
|
|
266
|
+
globalThis.__setShaderCompiling?.(true);
|
|
267
|
+
globalThis.nextPipelineCanFail = false;
|
|
268
|
+
globalThis.lastPipelineWasValidFlag = false;
|
|
269
|
+
globalThis.pendingAsyncPipelineCount++;
|
|
270
|
+
|
|
271
|
+
const promise = device.createRenderPipelineAsync(args[0]).then(async (item) => {
|
|
272
|
+
item.__gpu_item_type = itemType;
|
|
273
|
+
item.__gpu_hash = hash;
|
|
274
|
+
gpuSessionState[itemType].set(hash, item);
|
|
275
|
+
globalThis.pendingAsyncPipelineCount--;
|
|
276
|
+
|
|
277
|
+
if (!requiredItemTypes.has(itemType)) {
|
|
278
|
+
requiredItemTypes.set(itemType, new Set());
|
|
279
|
+
}
|
|
280
|
+
const requiredItems = requiredItemTypes.get(itemType);
|
|
281
|
+
if (!requiredItems.has(hash)) {
|
|
282
|
+
requiredItems.add(hash);
|
|
283
|
+
await storeRequiredItems();
|
|
284
|
+
await storeInstance(itemType, hash, args);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (globalThis.pendingAsyncPipelineCount === 0) {
|
|
288
|
+
while (globalThis.wgpuResolveIdle.length > 0) {
|
|
289
|
+
globalThis.wgpuResolveIdle.pop()();
|
|
290
|
+
}
|
|
291
|
+
globalThis.__setShaderCompiling?.(false);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return item;
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
return placeholderPipeline;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return device;
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function createGpuCache(key) {
|
|
306
|
+
const cachedKey = await getConfig("gpuCacheKey");
|
|
307
|
+
if (cachedKey != key) {
|
|
308
|
+
console.log("shaders updated, clearing db");
|
|
309
|
+
await clearDatabase();
|
|
310
|
+
await setConfig("gpuCacheKey", key);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const cachedDeviceDescriptor = await getConfig("deviceDescriptor");
|
|
315
|
+
if (cachedDeviceDescriptor === undefined) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
precaching = true;
|
|
319
|
+
const started = performance.now();
|
|
320
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
321
|
+
const device = await adapter.requestDevice(
|
|
322
|
+
JSON.parse(cachedDeviceDescriptor)
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
requiredItemTypes = await fetchRequiredItems();
|
|
326
|
+
try {
|
|
327
|
+
await createItemType("shader", async (args) => {
|
|
328
|
+
return device.createShaderModule(args);
|
|
329
|
+
});
|
|
330
|
+
await createItemType("bindgroup", async (args) => {
|
|
331
|
+
return device.createBindGroupLayout(args);
|
|
332
|
+
});
|
|
333
|
+
await createItemType("layout", async (args) => {
|
|
334
|
+
return device.createPipelineLayout(args);
|
|
335
|
+
});
|
|
336
|
+
await createItemType("pipeline", async (args) => {
|
|
337
|
+
return await device.createRenderPipelineAsync(args);
|
|
338
|
+
});
|
|
339
|
+
} finally {
|
|
340
|
+
precaching = false;
|
|
341
|
+
}
|
|
342
|
+
storeRequiredItems();
|
|
343
|
+
const stats = Object.keys(gpuSessionState).map(
|
|
344
|
+
(k) => `\n${k}: ${gpuSessionState[k].size ?? "ok"}`
|
|
345
|
+
);
|
|
346
|
+
console.log(`[GPU Cache]: preloaded in ${Math.round(performance.now() - started)} ms ${stats}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function createItemType(itemType, asyncCreateFunction) {
|
|
350
|
+
const storedItems = requiredItemTypes.get(itemType) ?? new Set();
|
|
351
|
+
|
|
352
|
+
await Promise.all(
|
|
353
|
+
[...storedItems].map(async (hash) => {
|
|
354
|
+
try {
|
|
355
|
+
const args = await fetchInstance(itemType, hash);
|
|
356
|
+
rehydrateItem(args);
|
|
357
|
+
const item = await asyncCreateFunction(args[0]);
|
|
358
|
+
gpuSessionState[itemType].set(hash, item);
|
|
359
|
+
} catch (e) {
|
|
360
|
+
console.warn(`[GPU Cache] failed to precreate ${itemType}: ${e}`);
|
|
361
|
+
storedItems.delete(hash);
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function rehydrateItem(currentObject) {
|
|
368
|
+
if (typeof currentObject !== "object" || currentObject === null) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (Array.isArray(currentObject)) {
|
|
373
|
+
for (let i = 0; i < currentObject.length; i++) {
|
|
374
|
+
const item = currentObject[i];
|
|
375
|
+
if (item && item.__gpu_hash) {
|
|
376
|
+
const type = item.__gpu_item_type;
|
|
377
|
+
const hash = item.__gpu_hash;
|
|
378
|
+
currentObject[i] = gpuSessionState[type].get(hash);
|
|
379
|
+
if (!currentObject[i]) {
|
|
380
|
+
throw `failed to rehydrate: missing ${type} with hash ${hash}`;
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
rehydrateItem(item);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
for (const key in currentObject) {
|
|
388
|
+
if (Object.prototype.hasOwnProperty.call(currentObject, key)) {
|
|
389
|
+
const value = currentObject[key];
|
|
390
|
+
if (value && value.__gpu_hash) {
|
|
391
|
+
const type = value.__gpu_item_type;
|
|
392
|
+
const hash = value.__gpu_hash;
|
|
393
|
+
currentObject[key] = gpuSessionState[type].get(hash);
|
|
394
|
+
if (!currentObject[key]) {
|
|
395
|
+
throw `failed to rehydrate: missing ${type} with hash ${hash}`;
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
rehydrateItem(value);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function getPlaceholder(device) {
|
|
406
|
+
// create a trivial shader and pipeline
|
|
407
|
+
const module = device.createShaderModule({
|
|
408
|
+
code: `
|
|
409
|
+
@vertex fn vs_main() -> @builtin(position) vec4f { return vec4f(0.0, 0.0, 0.0, 1.0); }
|
|
410
|
+
@fragment fn fs_main() -> @location(0) vec4f { return vec4f(0.0, 1.0, 0.0, 1.0); }
|
|
411
|
+
`
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
return device.createRenderPipeline({
|
|
415
|
+
layout: device.createPipelineLayout({
|
|
416
|
+
bindGroupLayouts: []
|
|
417
|
+
}),
|
|
418
|
+
vertex: { module, entryPoint: 'vs_main' },
|
|
419
|
+
fragment: { module, entryPoint: 'fs_main', targets: [{ format: 'bgra8unorm' }] }, // Adjust format if needed
|
|
420
|
+
primitive: { topology: 'triangle-list' }
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
globalThis.allowADummyPipeline = function() {
|
|
425
|
+
globalThis.nextPipelineCanFail = true;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
globalThis.lastPipelineWasValid = function() {
|
|
429
|
+
return globalThis.lastPipelineWasValidFlag;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
globalThis.waitForPipelines = function() {
|
|
433
|
+
if (globalThis.pendingAsyncPipelineCount === 0) {
|
|
434
|
+
return Promise.resolve();
|
|
435
|
+
}
|
|
436
|
+
return new Promise((resolve) => {
|
|
437
|
+
globalThis.wgpuResolveIdle.push(resolve);
|
|
438
|
+
});
|
|
439
|
+
};
|