Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/src/proxyWorker.js
4128 views
1
/**
2
* @license
3
* Copyright 2013 The Emscripten Authors
4
* SPDX-License-Identifier: MIT
5
*/
6
7
/*
8
* Implements the server/worker side of proxyClient.js.
9
* This code gets included in the main emscripten output
10
* when PROXY_TO_WORKER is used. The resulting code then
11
* needs to be run in a worker and receive events from
12
* proxyClient.js running on the main thread.
13
*/
14
15
#if !PROXY_TO_WORKER
16
#error "proxyClient.js should only be included in PROXY_TO_WORKER mode"
17
#endif
18
19
#if ENVIRONMENT_MAY_BE_NODE
20
if (!ENVIRONMENT_IS_NODE) {
21
#endif
22
23
function FPSTracker(text) {
24
var last = 0;
25
var mean = 0;
26
var counter = 0;
27
this.tick = () => {
28
var now = Date.now();
29
if (last > 0) {
30
var diff = now - last;
31
mean = 0.99*mean + 0.01*diff;
32
if (counter++ === 60) {
33
counter = 0;
34
dump(text + ' fps: ' + (1000/mean).toFixed(2) + '\n');
35
}
36
}
37
last = now;
38
}
39
}
40
41
function Element() { throw 'TODO: Element' }
42
function HTMLCanvasElement() { throw 'TODO: HTMLCanvasElement' }
43
function HTMLVideoElement() { throw 'TODO: HTMLVideoElement' }
44
45
var KeyboardEvent = {
46
'DOM_KEY_LOCATION_RIGHT': 2,
47
};
48
49
function PropertyBag() {
50
this.addProperty = () => {};
51
this.removeProperty = () => {};
52
this.setProperty = () => {};
53
};
54
55
var IndexedObjects = {
56
nextId: 1,
57
cache: {},
58
add(object) {
59
object.id = this.nextId++;
60
this.cache[object.id] = object;
61
}
62
};
63
64
function EventListener() {
65
this.listeners = {};
66
67
this.addEventListener = function addEventListener(event, func) {
68
this.listeners[event] ||= [];
69
this.listeners[event].push(func);
70
};
71
72
this.removeEventListener = function(event, func) {
73
var list = this.listeners[event];
74
if (!list) return;
75
var me = list.indexOf(func);
76
if (me < 0) return;
77
list.splice(me, 1);
78
};
79
80
this.fireEvent = function(event) {
81
event.preventDefault = () => {};
82
83
if (event.type in this.listeners) {
84
this.listeners[event.type].forEach((listener) => listener(event));
85
}
86
}
87
}
88
89
function Image() {
90
IndexedObjects.add(this);
91
EventListener.call(this);
92
var src = '';
93
Object.defineProperty(this, 'src', {
94
set: (value) => {
95
src = value;
96
assert(this.id);
97
postMessage({ target: 'Image', method: 'src', src, id: this.id });
98
},
99
get: () => src
100
});
101
}
102
Image.prototype.onload = () => {};
103
Image.prototype.onerror = () => {};
104
105
var HTMLImageElement = Image;
106
107
var window = this;
108
var windowExtra = new EventListener();
109
for (var x in windowExtra) window[x] = windowExtra[x];
110
111
window.close = () => {
112
postMessage({ target: 'window', method: 'close' });
113
};
114
115
window.alert = (text) => {
116
err(`alert forever: ${text}`);
117
while (1) {};
118
};
119
120
window.scrollX = window.scrollY = 0; // TODO: proxy these
121
122
window.WebGLRenderingContext = WebGLWorker;
123
124
window.requestAnimationFrame = (() => {
125
// similar to Browser.requestAnimationFrame
126
var nextRAF = 0;
127
return (func) => {
128
// try to keep 60fps between calls to here
129
var now = Date.now();
130
if (nextRAF === 0) {
131
nextRAF = now + 1000/60;
132
} else {
133
while (now + 2 >= nextRAF) { // fudge a little, to avoid timer jitter causing us to do lots of delay:0
134
nextRAF += 1000/60;
135
}
136
}
137
var delay = Math.max(nextRAF - now, 0);
138
setTimeout(func, delay);
139
};
140
})();
141
142
var webGLWorker = new WebGLWorker();
143
144
var document = new EventListener();
145
146
document.createElement = (what) => {
147
switch (what) {
148
case 'canvas': {
149
var canvas = new EventListener();
150
canvas.ensureData = () => {
151
if (!canvas.data || canvas.data.width !== canvas.width || canvas.data.height !== canvas.height) {
152
canvas.data = {
153
width: canvas.width,
154
height: canvas.height,
155
data: new Uint8Array(canvas.width*canvas.height*4)
156
};
157
if (canvas === Module['canvas']) {
158
postMessage({ target: 'canvas', op: 'resize', width: canvas.width, height: canvas.height });
159
}
160
}
161
};
162
canvas.getContext = (type, attributes) => {
163
if (canvas === Module['canvas']) {
164
postMessage({ target: 'canvas', op: 'getContext', type, attributes });
165
}
166
if (type === '2d') {
167
return {
168
getImageData: (x, y, w, h) => {
169
assert(x == 0 && y == 0 && w == canvas.width && h == canvas.height);
170
canvas.ensureData();
171
return {
172
width: canvas.data.width,
173
height: canvas.data.height,
174
data: new Uint8Array(canvas.data.data) // TODO: can we avoid this copy?
175
};
176
},
177
putImageData: (image, x, y) => {
178
canvas.ensureData();
179
assert(x == 0 && y == 0 && image.width == canvas.width && image.height == canvas.height);
180
canvas.data.data.set(image.data); // TODO: can we avoid this copy?
181
if (canvas === Module['canvas']) {
182
postMessage({ target: 'canvas', op: 'render', image: canvas.data });
183
}
184
},
185
drawImage: (image, x, y, w, h, ox, oy, ow, oh) => {
186
assert (!x && !y && !ox && !oy);
187
assert(w === ow && h === oh);
188
assert(canvas.width === w || w === undefined);
189
assert(canvas.height === h || h === undefined);
190
assert(image.width === canvas.width && image.height === canvas.height);
191
canvas.ensureData();
192
canvas.data.data.set(image.data.data); // TODO: can we avoid this copy?
193
if (canvas === Module['canvas']) {
194
postMessage({ target: 'canvas', op: 'render', image: canvas.data });
195
}
196
}
197
};
198
} else {
199
return webGLWorker;
200
}
201
};
202
canvas.boundingClientRect = {};
203
canvas.getBoundingClientRect = () => ({
204
width: canvas.boundingClientRect.width,
205
height: canvas.boundingClientRect.height,
206
top: canvas.boundingClientRect.top,
207
left: canvas.boundingClientRect.left,
208
bottom: canvas.boundingClientRect.bottom,
209
right: canvas.boundingClientRect.right
210
});
211
canvas.style = new PropertyBag();
212
213
canvas.width_ ||= 0;
214
canvas.height_ ||= 0;
215
Object.defineProperty(canvas, 'width', {
216
set: (value) => {
217
canvas.width_ = value;
218
if (canvas === Module['canvas']) {
219
postMessage({ target: 'canvas', op: 'resize', width: canvas.width_, height: canvas.height_ });
220
}
221
},
222
get: () => canvas.width_
223
});
224
Object.defineProperty(canvas, 'height', {
225
set: (value) => {
226
canvas.height_ = value;
227
if (canvas === Module['canvas']) {
228
postMessage({ target: 'canvas', op: 'resize', width: canvas.width_, height: canvas.height_ });
229
}
230
},
231
get: () => canvas.height_
232
});
233
234
var style = {
235
parentCanvas: canvas,
236
removeProperty: () => {},
237
setProperty: () => {},
238
};
239
240
Object.defineProperty(style, 'cursor', {
241
set: (value) => {
242
if (!style.cursor_ || style.cursor_ !== value) {
243
style.cursor_ = value;
244
if (style.parentCanvas === Module['canvas']) {
245
postMessage({ target: 'canvas', op: 'setObjectProperty', object: 'style', property: 'cursor', value: style.cursor_ });
246
}
247
}
248
},
249
get: () => style.cursor,
250
});
251
252
canvas.style = style;
253
return canvas;
254
}
255
default: {
256
throw 'document.createElement ' + what;
257
}
258
}
259
};
260
261
document.getElementById = (id) => {
262
if (id === 'canvas' || id === 'application-canvas') {
263
return Module['canvas'];
264
}
265
throw 'document.getElementById failed on ' + id;
266
};
267
268
document.querySelector = (id) => {
269
if (id === '#canvas' || id === '#application-canvas' || id === 'canvas' || id === 'application-canvas') {
270
return Module['canvas'];
271
}
272
throw 'document.querySelector failed on ' + id;
273
};
274
275
document.documentElement = {};
276
277
document.styleSheets = [{
278
cssRules: [], // TODO: forward to client
279
insertRule(rule, i) {
280
this.cssRules.splice(i, 0, rule);
281
}
282
}];
283
284
document.URL = 'http://worker.not.yet.ready.wait.for.window.onload?fake';
285
286
document.exitPointerLock = () => {};
287
288
function Audio() {
289
warnOnce('faking Audio elements, no actual sound will play');
290
}
291
Audio.prototype = new EventListener();
292
Object.defineProperty(Audio.prototype, 'src', {
293
set(value) {
294
if (value[0] === 'd') return; // ignore data urls
295
this.onerror();
296
},
297
});
298
299
Audio.prototype.play = () => {};
300
Audio.prototype.pause = () => {};
301
302
Audio.prototype.cloneNode = () => new Audio;
303
304
function AudioContext() {
305
warnOnce('faking WebAudio elements, no actual sound will play');
306
var makeNode = () => {
307
return {
308
connect: () => {},
309
disconnect: () => {},
310
}
311
};
312
this.listener = {
313
setPosition: () => {},
314
setOrientation: () => {},
315
};
316
this.decodeAudioData = () => {}; // ignore callbacks
317
this.createBuffer = makeNode;
318
this.createBufferSource = makeNode;
319
this.createGain = makeNode;
320
this.createPanner = makeNode;
321
}
322
323
var screen = {
324
width: 0,
325
height: 0
326
};
327
328
Module['canvas'] = document.createElement('canvas');
329
330
Module.setStatus = () => {};
331
332
out = (x) => {
333
//dump('OUT: ' + x + '\n');
334
postMessage({ target: 'stdout', content: x });
335
};
336
err = (x) => {
337
//dump('ERR: ' + x + '\n');
338
postMessage({ target: 'stderr', content: x });
339
};
340
341
// Frame throttling
342
343
var frameId = 0;
344
var clientFrameId = 0;
345
346
var postMainLoop = Module['postMainLoop'];
347
Module['postMainLoop'] = () => {
348
postMainLoop?.();
349
// frame complete, send a frame id
350
postMessage({ target: 'tick', id: frameId++ });
351
commandBuffer = [];
352
};
353
354
// Wait to start running until we receive some info from the client
355
356
#if PTHREADS
357
if (!ENVIRONMENT_IS_PTHREAD) {
358
#endif
359
addRunDependency('gl-prefetch');
360
addRunDependency('worker-init');
361
#if PTHREADS
362
}
363
#endif
364
365
#if ENVIRONMENT_MAY_BE_NODE
366
}
367
#endif
368
369
// buffer messages until the program starts to run
370
371
var messageBuffer = null;
372
var messageResenderTimeout = null;
373
var calledMain = false;
374
375
// Set calledMain to true during postRun which happens once main returns
376
Module['postRun'] ||= [];
377
if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']];
378
Module['postRun'].push(() => { calledMain = true; });
379
380
function messageResender() {
381
if (calledMain) {
382
assert(messageBuffer && messageBuffer.length > 0);
383
messageResenderTimeout = null;
384
messageBuffer.forEach(onmessage);
385
messageBuffer = null;
386
} else {
387
messageResenderTimeout = setTimeout(messageResender, 100);
388
}
389
}
390
391
function onMessageFromMainEmscriptenThread(message) {
392
if (!calledMain && !message.data.preMain) {
393
if (!messageBuffer) {
394
messageBuffer = [];
395
messageResenderTimeout = setTimeout(messageResender, 100);
396
}
397
messageBuffer.push(message);
398
return;
399
}
400
if (calledMain && messageResenderTimeout) {
401
clearTimeout(messageResenderTimeout);
402
messageResender();
403
}
404
//dump('worker got ' + JSON.stringify(message.data).slice(0, 150) + '\n');
405
switch (message.data.target) {
406
case 'document': {
407
document.fireEvent(message.data.event);
408
break;
409
}
410
case 'window': {
411
window.fireEvent(message.data.event);
412
break;
413
}
414
case 'canvas': {
415
if (message.data.event) {
416
Module['canvas'].fireEvent(message.data.event);
417
} else if (message.data.boundingClientRect) {
418
Module['canvas'].boundingClientRect = message.data.boundingClientRect;
419
} else throw 'ey?';
420
break;
421
}
422
case 'gl': {
423
webGLWorker.onmessage(message.data);
424
break;
425
}
426
case 'tock': {
427
clientFrameId = message.data.id;
428
break;
429
}
430
case 'Image': {
431
var img = IndexedObjects.cache[message.data.id];
432
switch (message.data.method) {
433
case 'onload': {
434
img.width = message.data.width;
435
img.height = message.data.height;
436
img.data = { width: img.width, height: img.height, data: message.data.data };
437
img.complete = true;
438
img.onload();
439
break;
440
}
441
case 'onerror': {
442
img.onerror({ srcElement: img });
443
break;
444
}
445
}
446
break;
447
}
448
case 'IDBStore': {
449
assert(message.data.method === 'response');
450
assert(IDBStore.pending);
451
IDBStore.pending(message.data);
452
break;
453
}
454
case 'worker-init': {
455
Module['canvas'] = document.createElement('canvas');
456
screen.width = Module['canvas'].width_ = message.data.width;
457
screen.height = Module['canvas'].height_ = message.data.height;
458
Module['canvas'].boundingClientRect = message.data.boundingClientRect;
459
#if ENVIRONMENT_MAY_BE_NODE
460
if (ENVIRONMENT_IS_NODE)
461
#endif
462
document.URL = message.data.URL;
463
#if PTHREADS
464
currentScriptUrl = message.data.currentScriptUrl;
465
#endif
466
window.fireEvent({ type: 'load' });
467
removeRunDependency('worker-init');
468
break;
469
}
470
case 'custom': {
471
if (Module['onCustomMessage']) {
472
Module['onCustomMessage'](message);
473
} else {
474
throw 'Custom message received but worker Module.onCustomMessage not implemented.';
475
}
476
break;
477
}
478
case 'setimmediate': {
479
Module['setImmediates']?.shift()();
480
break;
481
}
482
default: throw 'wha? ' + message.data.target;
483
}
484
};
485
486
#if PTHREADS
487
if (!ENVIRONMENT_IS_PTHREAD) {
488
#endif
489
onmessage = onMessageFromMainEmscriptenThread;
490
#if PTHREADS
491
}
492
#endif
493
494
// proxyWorker.js has defined 'document' and 'window' objects above, so need to
495
// initialize them for library_html5.js explicitly here.
496
if (typeof specialHTMLTargets != 'undefined') {
497
specialHTMLTargets = [0, document, window];
498
}
499
500
function postCustomMessage(data) {
501
postMessage({ target: 'custom', userData: data });
502
}
503
504