xiongzhu 2 years ago
parent
commit
7637c1d928

+ 2 - 2
app.js

@@ -28,8 +28,8 @@ module.exports = async function (fastify, opts) {
     });
 
     fastify.register(fstatic, {
-        root: path.join(__dirname, "public"),
-        prefix: "/public/"
+        root: path.join(__dirname, "lib"),
+        prefix: "/lib/"
     });
 
     fastify.register(cors, {

+ 7 - 7
keyboard.html

@@ -21,13 +21,13 @@
         </g>
     </svg>
 </div>
-<script src="/public/jquery-3.7.0.min.js"></script>
-<script src="/public/jquery-ui.min.js"></script>
-<script src="/public/jquery.ui.touch-punch.min.js"></script>
-<script src="/public/simple-keyboard-3.7.2.js"></script>
-<script src="/public/keycode.js"></script>
-<script src="/public/eruda.js"></script>
-<link rel="stylesheet" href="/public/simple-keyboard-3.7.2.css" />
+<script src="/lib/jquery-3.7.0.min.js"></script>
+<script src="/lib/jquery-ui.min.js"></script>
+<script src="/lib/jquery.ui.touch-punch.min.js"></script>
+<script src="/lib/simple-keyboard-3.7.2.js"></script>
+<script src="/lib/keycode.js"></script>
+<script src="/lib/eruda.js"></script>
+<link rel="stylesheet" href="/lib/simple-keyboard-3.7.2.css" />
 <style>
     html,
     body {

+ 0 - 0
public/eruda.js → lib/eruda.js


+ 0 - 0
public/jquery-3.7.0.min.js → lib/jquery-3.7.0.min.js


+ 0 - 0
public/jquery-ui.min.js → lib/jquery-ui.min.js


+ 0 - 0
public/jquery.ui.touch-punch.min.js → lib/jquery.ui.touch-punch.min.js


+ 0 - 0
public/keycode.js → lib/keycode.js


+ 1241 - 0
lib/matoya-worker.js

@@ -0,0 +1,1241 @@
+// This Source Code Form is subject to the terms of the MIT License.
+// If a copy of the MIT License was not distributed with this file,
+// You can obtain one at https://spdx.org/licenses/MIT.html.
+
+
+// Worker State
+
+const MTY = {
+	keys: {},
+	keysRev: {},
+	glIndex: 0,
+	glObj: {},
+	fds: {},
+	fdIndex: 0,
+};
+
+
+// Allocation
+
+function mty_cfunc(ptr) {
+	return MTY.exports.__indirect_function_table.get(ptr);
+}
+
+function mty_alloc(size, el) {
+	return MTY.exports.mty_system_alloc(size, el ? el : 1);
+}
+
+function mty_free(ptr) {
+	MTY.exports.mty_system_free(ptr);
+}
+
+function mty_dup_c(buf) {
+	const ptr = mty_alloc(buf.byteLength + 1);
+	mty_memcpy(ptr, buf);
+
+	return ptr;
+}
+
+
+// window.localStorage
+
+function mty_get_ls(key) {
+	postMessage({
+		type: 'get-ls',
+		key: key,
+		sab: MTY.sab,
+		sync: MTY.sync,
+	});
+
+	mty_wait(MTY.sync);
+
+	const size = MTY.sab[0];
+	if (size == 0)
+		return 0;
+
+	const sab8 = new Uint8Array(new SharedArrayBuffer(size));
+
+	postMessage({
+		type: 'async-copy',
+		sab8: sab8,
+		sync: MTY.sync,
+	});
+
+	mty_wait(MTY.sync);
+
+	return sab8;
+}
+
+function mty_set_ls(key, val) {
+	postMessage({
+		type: 'set-ls',
+		key: key,
+		val: val,
+		sync: MTY.sync,
+	});
+
+	mty_wait(MTY.sync);
+}
+
+
+// <unistd.h> stubs
+
+const MTY_UNISTD_API = {
+	flock: function (fd, flags) {
+		return 0;
+	},
+};
+
+
+// GL
+
+function mty_gl_new(obj) {
+	MTY.glObj[MTY.glIndex] = obj;
+
+	return MTY.glIndex++;
+}
+
+function mty_gl_del(index) {
+	let obj = MTY.glObj[index];
+
+	delete MTY.glObj[index];
+
+	return obj;
+}
+
+function mty_gl_obj(index) {
+	return MTY.glObj[index];
+}
+
+const MTY_GL_API = {
+	glGenFramebuffers: function (n, ids) {
+		for (let x = 0; x < n; x++)
+			mty_set_uint32(ids + x * 4, mty_gl_new(MTY.gl.createFramebuffer()));
+	},
+	glDeleteFramebuffers: function (n, ids) {
+		for (let x = 0; x < n; x++)
+			MTY.gl.deleteFramebuffer(mty_gl_del(mty_get_uint32(ids + x * 4)));
+	},
+	glBindFramebuffer: function (target, fb) {
+		MTY.gl.bindFramebuffer(target, fb ? mty_gl_obj(fb) : null);
+	},
+	glFramebufferTexture2D: function (target, attachment, textarget, texture, level) {
+		MTY.gl.framebufferTexture2D(target, attachment, textarget, mty_gl_obj(texture), level);
+	},
+	glEnable: function (cap) {
+		MTY.gl.enable(cap);
+	},
+	glDisable: function (cap) {
+		MTY.gl.disable(cap);
+	},
+	glViewport: function (x, y, width, height) {
+		MTY.gl.viewport(x, y, width, height);
+	},
+	glBindTexture: function (target, texture) {
+		MTY.gl.bindTexture(target, texture ? mty_gl_obj(texture) : null);
+	},
+	glDeleteTextures: function (n, ids) {
+		for (let x = 0; x < n; x++)
+			MTY.gl.deleteTexture(mty_gl_del(mty_get_uint32(ids + x * 4)));
+	},
+	glTexParameteri: function (target, pname, param) {
+		MTY.gl.texParameteri(target, pname, param);
+	},
+	glGenTextures: function (n, ids) {
+		for (let x = 0; x < n; x++)
+			mty_set_uint32(ids + x * 4, mty_gl_new(MTY.gl.createTexture()));
+	},
+	glTexImage2D: function (target, level, internalformat, width, height, border, format, type, data) {
+		MTY.gl.texImage2D(target, level, internalformat, width, height, border, format, type,
+			new Uint8Array(MTY_MEMORY.buffer, data));
+	},
+	glTexSubImage2D: function (target, level, xoffset, yoffset, width, height, format, type, pixels) {
+		MTY.gl.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type,
+			new Uint8Array(MTY_MEMORY.buffer, pixels));
+	},
+	glDrawElements: function (mode, count, type, indices) {
+		MTY.gl.drawElements(mode, count, type, indices);
+	},
+	glGetAttribLocation: function (program, c_name) {
+		return MTY.gl.getAttribLocation(mty_gl_obj(program), mty_str_to_js(c_name));
+	},
+	glShaderSource: function (shader, count, c_strings, c_len) {
+		let source = '';
+		for (let x = 0; x < count; x++)
+			source += mty_str_to_js(mty_get_uint32(c_strings + x * 4));
+
+		MTY.gl.shaderSource(mty_gl_obj(shader), source);
+	},
+	glBindBuffer: function (target, buffer) {
+		MTY.gl.bindBuffer(target, buffer ? mty_gl_obj(buffer) : null);
+	},
+	glVertexAttribPointer: function (index, size, type, normalized, stride, pointer) {
+		MTY.gl.vertexAttribPointer(index, size, type, normalized, stride, pointer);
+	},
+	glCreateProgram: function () {
+		return mty_gl_new(MTY.gl.createProgram());
+	},
+	glUniform1i: function (loc, v0) {
+		MTY.gl.uniform1i(mty_gl_obj(loc), v0);
+	},
+	glUniform1f: function (loc, v0) {
+		MTY.gl.uniform1f(mty_gl_obj(loc), v0);
+	},
+	glUniform4i: function (loc, v0, v1, v2, v3) {
+		MTY.gl.uniform4i(mty_gl_obj(loc), v0, v1, v2, v3);
+	},
+	glUniform4f: function (loc, v0, v1, v2, v3) {
+		MTY.gl.uniform4f(mty_gl_obj(loc), v0, v1, v2, v3);
+	},
+	glActiveTexture: function (texture) {
+		MTY.gl.activeTexture(texture);
+	},
+	glDeleteBuffers: function (n, ids) {
+		for (let x = 0; x < n; x++)
+			MTY.gl.deleteBuffer(mty_gl_del(mty_get_uint32(ids + x * 4)));
+	},
+	glEnableVertexAttribArray: function (index) {
+		MTY.gl.enableVertexAttribArray(index);
+	},
+	glBufferData: function (target, size, data, usage) {
+		MTY.gl.bufferData(target, new Uint8Array(MTY_MEMORY.buffer, data, size), usage);
+	},
+	glDeleteShader: function (shader) {
+		MTY.gl.deleteShader(mty_gl_del(shader));
+	},
+	glGenBuffers: function (n, ids) {
+		for (let x = 0; x < n; x++)
+			mty_set_uint32(ids + x * 4, mty_gl_new(MTY.gl.createBuffer()));
+	},
+	glCompileShader: function (shader) {
+		MTY.gl.compileShader(mty_gl_obj(shader));
+	},
+	glLinkProgram: function (program) {
+		MTY.gl.linkProgram(mty_gl_obj(program));
+	},
+	glGetUniformLocation: function (program, name) {
+		return mty_gl_new(MTY.gl.getUniformLocation(mty_gl_obj(program), mty_str_to_js(name)));
+	},
+	glCreateShader: function (type) {
+		return mty_gl_new(MTY.gl.createShader(type));
+	},
+	glAttachShader: function (program, shader) {
+		MTY.gl.attachShader(mty_gl_obj(program), mty_gl_obj(shader));
+	},
+	glUseProgram: function (program) {
+		MTY.gl.useProgram(program ? mty_gl_obj(program) : null);
+	},
+	glGetShaderiv: function (shader, pname, params) {
+		if (pname == 0x8B81) {
+			let ok = MTY.gl.getShaderParameter(mty_gl_obj(shader), MTY.gl.COMPILE_STATUS);
+			mty_set_uint32(params, ok);
+
+			if (!ok)
+				console.warn(MTY.gl.getShaderInfoLog(mty_gl_obj(shader)));
+
+		} else {
+			mty_set_uint32(params, 0);
+		}
+	},
+	glDetachShader: function (program, shader) {
+		MTY.gl.detachShader(mty_gl_obj(program), mty_gl_obj(shader));
+	},
+	glDeleteProgram: function (program) {
+		MTY.gl.deleteProgram(mty_gl_del(program));
+	},
+	glClear: function (mask) {
+		MTY.gl.clear(mask);
+	},
+	glClearColor: function (red, green, blue, alpha) {
+		MTY.gl.clearColor(red, green, blue, alpha);
+	},
+	glGetError: function () {
+		return MTY.gl.getError();
+	},
+	glGetShaderInfoLog: function (shader, maxLength, length, infoLog) {
+		const log = gl.getShaderInfoLog(mty_gl_obj(shader));
+		const buf = mty_encode(log);
+
+		if (buf.length < maxLength) {
+			mty_set_uint32(length);
+			mty_strcpy(infoLog, buf);
+		}
+	},
+	glFinish: function () {
+		MTY.gl.finish();
+	},
+	glScissor: function (x, y, width, height) {
+		MTY.gl.scissor(x, y, width, height);
+	},
+	glBlendFunc: function (sfactor, dfactor) {
+		MTY.gl.blendFunc(sfactor, dfactor);
+	},
+	glBlendEquation: function (mode) {
+		MTY.gl.blendEquation(mode);
+	},
+	glUniformMatrix4fv: function (loc, count, transpose, value) {
+		MTY.gl.uniformMatrix4fv(mty_gl_obj(loc), transpose,
+			new Float32Array(MTY_MEMORY.buffer, value, 4 * 4 * count));
+	},
+	glGetProgramiv: function (program, pname, params) {
+		mty_set_uint32(params, MTY.gl.getProgramParameter(mty_gl_obj(program), pname));
+	},
+	glPixelStorei: function (pname, param) {
+		MTY.gl.pixelStorei(pname, param);
+	},
+	web_gl_flush: function () {
+		MTY.gl.flush();
+	},
+};
+
+
+// Audio
+
+function mty_mutex_lock_nb(mutex, index)
+{
+	return Atomics.compareExchange(mutex, index, 0, 1) == 0;
+}
+
+function mty_mutex_lock(mutex, index) {
+	while (true) {
+		if (mty_mutex_lock_nb(mutex, index))
+			return;
+
+		Atomics.wait(mutex, index, 1);
+	}
+}
+
+function mty_mutex_unlock(mutex, index, notify) {
+	Atomics.compareExchange(mutex, index, 1, 0);
+
+	if (notify)
+		Atomics.notify(mutex, index, 1);
+}
+
+const MTY_AUDIO_API = {
+	MTY_AudioCreate: function (sampleRate, minBuffer, maxBuffer, channels, deviceID, fallback) {
+		MTY.audio = {
+			sampleRate,
+			minBuffer,
+			maxBuffer,
+			channels,
+		};
+
+		return 0xCDD;
+	},
+	MTY_AudioDestroy: function (audio) {
+		if (!audio || !mty_get_uint32(audio))
+			return;
+
+		postMessage({type: 'audio-destroy'});
+		mty_set_uint32(audio, 0);
+	},
+	MTY_AudioQueue: function (ctx, frames, count) {
+		const buf = new Int16Array(MTY_MEMORY.buffer, frames, count * MTY.audio.channels);
+
+		mty_mutex_lock(MTY.audioObjs.control, 0);
+
+		if (buf.length <= MTY.audioObjs.buf.length - MTY.audioObjs.control[1]) {
+			MTY.audioObjs.buf.set(buf, MTY.audioObjs.control[1]);
+			MTY.audioObjs.control[1] += buf.length;
+		}
+
+		mty_mutex_unlock(MTY.audioObjs.control, 0, false);
+
+		postMessage({type: 'audio-queue', ...MTY.audio});
+	},
+	MTY_AudioReset: function (ctx) {
+		mty_mutex_lock(MTY.audioObjs.control, 0);
+
+		MTY.audioObjs.control[1] = 0;
+
+		mty_mutex_unlock(MTY.audioObjs.control, 0, false);
+	},
+	MTY_AudioGetQueued: function (ctx) {
+		return Atomics.load(MTY.audioObjs.control, 2);
+	},
+};
+
+
+// Audio Worklet
+
+if (typeof AudioWorkletGlobalScope != 'undefined') {
+
+function mty_int16_to_float(i) {
+	return i < 0 ? i / 32768 : i / 32767;
+}
+
+class MTY_Audio extends AudioWorkletProcessor {
+	constructor(options) {
+		super();
+
+		const frames_per_ms = Math.round(sampleRate / 1000.0);
+
+		this.minBuffer = Math.round(options.processorOptions.minBuffer * frames_per_ms);
+		this.maxBuffer = Math.round(options.processorOptions.maxBuffer * frames_per_ms);
+		this.channels = options.outputChannelCount[0];
+		this.playing = false;
+
+		this.ibuf = new Int16Array(new ArrayBuffer(1024 * 1024));
+		this.ibufLen = 0;
+
+		this.port.onmessage = (evt) => {
+			this.sbuf = evt.data.buf;
+			this.control = evt.data.control;
+		};
+	}
+
+	process(inputs, outputs, parameters) {
+		// Copy from staging buffer to internal buffer
+		if (mty_mutex_lock_nb(this.control, 0)) {
+			if (this.control[1] <= this.ibuf.length - this.ibufLen) {
+				this.ibuf.set(new Int16Array(this.sbuf.buffer, 0, this.control[1]), this.ibufLen);
+				this.ibufLen += this.control[1];
+				this.control[1] = 0;
+			}
+
+			mty_mutex_unlock(this.control, 0, true);
+		}
+
+		let queued = this.ibufLen / this.channels;
+
+		// Queued audio has reached the min buffer, begin playing
+		if (!this.playing && queued >= this.minBuffer)
+			this.playing = true;
+
+		// No audio left, pause and let buffer refill
+		if (this.playing && this.ibufLen == 0)
+			this.playing = false;
+
+		// Fill output with buffered audio
+		if (this.playing) {
+			const l = outputs[0][0];
+			const r = outputs[0][1];
+
+			let x = 0;
+			for (let o = 0; x < this.ibufLen && o < l.length && o < r.length; x += this.channels, o++) {
+				l[o] = mty_int16_to_float(this.ibuf[x]);
+				r[o] = mty_int16_to_float(this.ibuf[x + 1]);
+			}
+
+			// Essentially a 'memmove' to bring remaining audio to front of the buffer
+			this.ibufLen -= x;
+			this.ibuf.set(new Int16Array(this.ibuf.buffer, x * 2, this.ibufLen));
+		}
+
+		queued = this.ibufLen / this.channels;
+
+		// If buffer has exceeded the max, reset
+		if (this.playing && queued > this.maxBuffer) {
+			this.playing = false;
+			this.ibufLen = 0;
+		}
+
+		// Store queued frames
+		Atomics.store(this.control, 2, queued);
+
+		return true;
+	}
+}
+
+registerProcessor('MTY_Audio', MTY_Audio);
+
+}
+
+
+// Net
+
+function mty_net_headers(cheaders) {
+	const headers_str = mty_str_to_js(cheaders);
+
+	const headers = {};
+	const headers_nl = headers_str.split('\n');
+	for (let x = 0; x < headers_nl.length; x++) {
+		const pair = headers_nl[x];
+		const pair_split = pair.split(':');
+
+		if (pair_split[0] && pair_split[1])
+			headers[pair_split[0]] = pair_split[1];
+	}
+
+	return headers;
+}
+
+const MTY_NET_API = {
+	MTY_HttpRequest: function (curl, cmethod, cheaders, cbody, bodySize, proxy, timeout,
+		response, responseSize, cstatus)
+	{
+		// FIXME timeout is currently ignored
+		// FIXME proxy is currently ignored
+
+		const body = cbody ? mty_dup(cbody, bodySize) : null;
+
+		postMessage({
+			type: 'http',
+			url: mty_str_to_js(curl),
+			method: mty_str_to_js(cmethod),
+			headers: mty_net_headers(cheaders),
+			body: body,
+			sync: MTY.sync,
+			sab: MTY.sab,
+		}, body ? [body.buffer] : []);
+
+		mty_wait(MTY.sync);
+
+		const error = MTY.sab[0];
+		if (error)
+			return false;
+
+		const size = MTY.sab[1];
+		mty_set_uint32(responseSize, size);
+
+		const status = MTY.sab[2];
+		mty_set_uint16(cstatus, status);
+
+		if (size > 0) {
+			const buf = mty_alloc(size + 1);
+			mty_set_uint32(response, buf);
+
+			postMessage({
+				type: 'async-copy',
+				sync: MTY.sync,
+				sab8: new Uint8Array(MTY_MEMORY.buffer, buf, size + 1),
+			});
+
+			mty_wait(MTY.sync);
+		}
+
+		return true;
+	},
+	MTY_WebSocketConnect: function (curl, cheaders, proxy, timeout, upgrade_status_out) {
+		// FIXME headers are currently ignored
+		// FIXME proxy is currently ignored
+		// FIXME timeout is currently ignored
+		// FIXME upgrade_status_out currently unsupported
+
+		postMessage({
+			type: 'ws-connect',
+			url: mty_str_to_js(curl),
+			sync: MTY.sync,
+			sab: MTY.sab,
+		});
+
+		mty_wait(MTY.sync);
+
+		return MTY.sab[0];
+	},
+	MTY_WebSocketDestroy: function (ctx_out) {
+		if (!ctx_out)
+			return;
+
+		postMessage({
+			type: 'ws-close',
+			ctx: mty_get_uint32(ctx_out),
+		});
+	},
+	MTY_WebSocketRead: function (ctx, timeout, msg_out, size) {
+		postMessage({
+			type: 'ws-read',
+			ctx: ctx,
+			timeout: timeout,
+			sab: MTY.sab,
+			sync: MTY.sync,
+		});
+
+		mty_wait(MTY.sync);
+
+		if (MTY.sab[0] == 0) { // MTY_ASYNC_OK
+			const rsize = MTY.sab[1];
+
+			if (rsize < size) {
+				const buf = mty_alloc(rsize);
+				const sab8 = new Uint8Array(MTY_MEMORY.buffer, buf, rsize);
+
+				postMessage({
+					type: 'async-copy',
+					sync: MTY.sync,
+					sab8: sab8,
+				});
+
+				mty_wait(MTY.sync);
+
+				mty_strcpy(msg_out, sab8);
+				mty_free(buf);
+
+			} else {
+				MTY.sab[0] = 3 // MTY_ASYNC_ERROR
+			}
+		}
+
+		return MTY.sab[0]; // MTY_Async
+	},
+	MTY_WebSocketWrite: function (ctx, msg_c) {
+		postMessage({
+			type: 'ws-write',
+			ctx: ctx,
+			text: mty_str_to_js(msg_c),
+		});
+
+		return true;
+	},
+	MTY_WebSocketGetCloseCode: function (ctx) {
+		postMessage({
+			type: 'ws-code',
+			ctx: ctx,
+			sab: MTY.sab,
+			sync: MTY.sync,
+		});
+
+		mty_wait(MTY.sync);
+
+		return MTY.sab[0];
+	},
+};
+
+
+// Image
+
+const MTY_IMAGE_API = {
+	MTY_DecompressImage: function (input, size, cwidth, cheight) {
+		const jinput = mty_dup(input, size);
+
+		postMessage({
+			type: 'decode-image',
+			input: jinput.buffer,
+			sync: MTY.sync,
+			sab: MTY.sab,
+		}, [jinput.buffer]);
+
+		mty_wait(MTY.sync);
+
+		const width = MTY.sab[0];
+		mty_set_uint32(cwidth, width);
+
+		const height = MTY.sab[1];
+		mty_set_uint32(cheight, height);
+
+		const buf_size = width * height * 4;
+		const buf = mty_alloc(buf_size);
+
+		postMessage({
+			type: 'async-copy',
+			sync: MTY.sync,
+			sab8: new Uint8Array(MTY_MEMORY.buffer, buf, buf_size),
+		});
+
+		mty_wait(MTY.sync);
+
+		return buf;
+	},
+	MTY_CompressImage: function (method, input, width, height, outputSize) {
+	},
+	MTY_GetProgramIcon: function (path, width, height) {
+	},
+};
+
+
+// Crypto
+
+const MTY_CRYPTO_API = {
+	MTY_CryptoHash: function (algo, input, inputSize, key, keySize, output, outputSize) {
+	},
+	MTY_GetRandomBytes: function (buf, size) {
+		mty_memcpy(buf, crypto.getRandomValues(new Uint8Array(size)));
+	},
+	MTY_BytesToBase64: function (bytes, size, base64, base64Size) {
+		const jbytes = new Uint8Array(MTY_MEMORY.buffer, bytes, size);
+
+		try {
+			mty_str_to_c(mty_buf_to_b64(jbytes), base64, base64Size);
+
+		} catch (e) {
+			console.error("'base64Size' is too small");
+		}
+	},
+};
+
+
+// System
+
+const MTY_SYSTEM_API = {
+	MTY_HandleProtocol: function (uri, token) {
+		postMessage({type: 'uri', uri});
+	},
+};
+
+
+// Web API (mostly used in app.c)
+
+function mty_update_window(app, info) {
+	MTY.exports.mty_window_update_position(app, info.posX, info.posY);
+	MTY.exports.mty_window_update_screen(app, info.screenWidth, info.screenHeight);
+	MTY.exports.mty_window_update_size(app, info.canvasWidth, info.canvasHeight);
+	MTY.exports.mty_window_update_focus(app, info.hasFocus);
+	MTY.exports.mty_window_update_fullscreen(app, info.fullscreen);
+	MTY.exports.mty_window_update_visibility(app, info.visible);
+	MTY.exports.mty_window_update_pixel_ratio(app, info.devicePixelRatio);
+	MTY.exports.mty_window_update_relative_mouse(app, info.relative);
+}
+
+const MTY_WEB_API = {
+	web_alert: function (title, msg) {
+		postMessage({type: 'alert', title, msg});
+	},
+	web_set_fullscreen: function (fullscreen) {
+		postMessage({type: 'fullscreen', fullscreen});
+	},
+	web_wake_lock: function (enable) {
+		postMessage({type: 'wake-lock', enable});
+	},
+	web_rumble_gamepad: function (id, low, high) {
+		postMessage({type: 'rumble', id, low, high});
+	},
+	web_show_cursor: function (show) {
+		postMessage({type: 'show-cursor', show});
+	},
+	web_get_clipboard: function () {
+		postMessage({type: 'get-clip', sync: MTY.sync, sab: MTY.sab});
+		mty_wait(MTY.sync);
+
+		const size = MTY.sab[0];
+		const buf = mty_alloc(size + 1);
+
+		if (size > 0) {
+			postMessage({
+				type: 'async-copy',
+				sync: MTY.sync,
+				sab8: new Uint8Array(MTY_MEMORY.buffer, buf, size + 1),
+			});
+
+			mty_wait(MTY.sync);
+		}
+
+		return buf;
+	},
+	web_set_clipboard: function (text) {
+		postMessage({type: 'set-clip', text});
+	},
+	web_set_pointer_lock: function (enable) {
+		postMessage({type: 'pointer-lock', enable});
+	},
+	web_use_default_cursor: function (use_default) {
+		postMessage({type: 'cursor-default', use_default});
+	},
+	web_set_rgba_cursor: function (buffer, width, height, hot_x, hot_y) {
+		const buf = buffer ? mty_dup(buffer, width * height * 4) : null
+		postMessage({type: 'cursor-rgba', buf, width, height, hot_x, hot_y}, buf ? [buf.buffer] : []);
+	},
+	web_set_png_cursor: function (buffer, size, hot_x, hot_y) {
+		const buf = buffer ? mty_dup(buffer, size) : null
+		postMessage({type: 'cursor-png', buf, hot_x, hot_y}, buf ? [buf.buffer] : []);
+	},
+	web_set_kb_grab: function (grab) {
+		postMessage({type: 'kb-grab', grab});
+	},
+	web_get_hostname: function () {
+		return mty_dup_c(mty_encode(MTY.hostname));
+	},
+	web_platform: function (platform, size) {
+		mty_str_to_c(navigator.platform, platform, size);
+	},
+	web_set_key: function (reverse, code, key) {
+		const str = mty_str_to_js(code);
+		MTY.keys[str] = key;
+
+		if (reverse)
+			MTY.keysRev[key] = str;
+	},
+	web_get_key: function (key, buf, len) {
+		const code = MTY.keysRev[key];
+
+		if (code != undefined) {
+			const text = MTY.kbMap[code];
+			if (text) {
+				mty_str_to_c(text.toUpperCase(), buf, len);
+
+			} else {
+				mty_str_to_c(code, buf, len);
+			}
+
+			return true;
+		}
+
+		return false;
+	},
+	web_set_title: function (title) {
+		postMessage({
+			type: 'title',
+			title: mty_str_to_js(title),
+		});
+	},
+	web_set_gfx: function () {
+		const info = MTY.initWindowInfo;
+		const canvas = new OffscreenCanvas(info.canvasWidth, info.canvasHeight);
+
+		MTY.gl = canvas.getContext('webgl2', {
+			depth: false,
+			antialias: false,
+			powerPreference: 'high-performance',
+		});
+	},
+	web_set_canvas_size: function (width, height) {
+		MTY.gl.canvas.width = width;
+		MTY.gl.canvas.height = height;
+	},
+	web_present: function (wait) {
+		const image = MTY.gl.canvas.transferToImageBitmap();
+
+		postMessage({
+			type: 'present',
+			image: image,
+		}, [image]);
+
+		if (wait)
+			mty_wait(MTY.psync);
+	},
+
+	// Synchronization from C
+	MTY_WaitPtr: function (csync) {
+		mty_wait(new Int32Array(MTY_MEMORY.buffer, csync, 1));
+	},
+
+	// Should be called on main thread only
+	web_set_app: function (app) {
+		MTY.app = app;
+		mty_update_window(app, MTY.initWindowInfo);
+	},
+	web_run_and_yield: function (iter, opaque) {
+		MTY.exports.mty_app_set_keys();
+
+		const step = () => {
+			if (mty_cfunc(iter)(opaque))
+				setTimeout(step, 0);
+		};
+
+		setTimeout(step, 0);
+		throw 'MTY_RunAndYield halted execution';
+	},
+};
+
+
+// WASI API
+
+// github.com/WebAssembly/wasi-libc/blob/main/libc-bottom-half/headers/public/wasi/api.h
+
+const __WASI_ERRNO_SUCCESS = 0;
+const __WASI_ERRNO_BADF = 8;
+const __WASI_ERRNO_INVAL = 28;
+
+function mty_append_buf(cur_buf, buf) {
+	// FIXME This is a crude way to handle appending to an open file,
+	// complex seek operations will break this
+
+	const new_buf = new Uint8Array(cur_buf.length + buf.length);
+
+	new_buf.set(cur_buf);
+	new_buf.set(buf, cur_buf.length);
+
+	return new_buf;
+}
+
+function mty_arg_list(bin, args) {
+	let plist = [mty_encode(bin)];
+
+	const params = new URLSearchParams(args);
+	const qs = params.toString();
+
+	if (qs)
+		plist.push(mty_encode(qs));
+
+	return plist;
+}
+
+const MTY_WASI_SNAPSHOT_PREVIEW1_API = {
+	// Command line arguments
+	args_get: function (argv, argv_buf) {
+		const args = mty_arg_list(MTY.bin, MTY.queryString);
+
+		for (let x = 0; x < args.length; x++) {
+			mty_strcpy(argv_buf, args[x]);
+			mty_set_uint32(argv + x * 4, argv_buf);
+			argv_buf += args[x].length + 1;
+		}
+
+		return __WASI_ERRNO_SUCCESS;
+	},
+	args_sizes_get: function (retptr0, retptr1) {
+		const args = mty_arg_list(MTY.bin, MTY.queryString);
+
+		let len = 0;
+		for (let x = 0; x < args.length; x++)
+			len += args[x].length + 1;
+
+		mty_set_uint32(retptr0, args.length);
+		mty_set_uint32(retptr1, len);
+
+		return __WASI_ERRNO_SUCCESS;
+	},
+
+	// WASI preopened directory (/)
+	fd_prestat_get: function (fd, retptr0) {
+		if (MTY.preopen == undefined) {
+			mty_set_int8(retptr0, 0);
+			mty_set_uint64(retptr0 + 4, 1);
+			MTY.preopen = fd;
+
+			return __WASI_ERRNO_SUCCESS;
+		}
+
+		return __WASI_ERRNO_BADF;
+	},
+	fd_prestat_dir_name: function (fd, path, path_len) {
+		if (MTY.preopen == fd) {
+			mty_strcpy(path, mty_encode('/'));
+			return __WASI_ERRNO_SUCCESS;
+		}
+
+		return __WASI_ERRNO_INVAL;
+	},
+
+	// Paths
+	path_filestat_get: function (fd, flags, path, path_size, retptr0) {
+		const jpath = mty_str_to_js(path);
+		const buf = mty_get_ls(jpath);
+
+		// We only need to return the size
+		if (buf)
+			mty_set_uint64(retptr0 + 32, buf.byteLength);
+
+		return __WASI_ERRNO_SUCCESS;
+	},
+	path_open: function (fd, dirflags, path, path_size, oflags, fs_rights_base,
+		fs_rights_inheriting, fdflags, retptr0)
+	{
+		const new_fd = MTY.fdIndex++;
+		mty_set_uint32(retptr0, new_fd);
+
+		MTY.fds[new_fd] = {
+			path: mty_str_to_js(path),
+			append: fdflags == 1,
+			offset: 0,
+		};
+
+		return __WASI_ERRNO_SUCCESS;
+	},
+	path_create_directory: function (fd, path) {
+		return __WASI_ERRNO_SUCCESS;
+	},
+	path_remove_directory: function (fd, path) {
+		return __WASI_ERRNO_SUCCESS;
+	},
+	path_unlink_file: function (fd, path) {
+		return __WASI_ERRNO_SUCCESS;
+	},
+	path_readlink: function (fd, path, buf, buf_len, retptr0) {
+	},
+	path_rename: function (fd, old_path, new_fd, new_path) {
+		return __WASI_ERRNO_SUCCESS;
+	},
+
+	// File descriptors
+	fd_close: function (fd) {
+		delete MTY.fds[fd];
+	},
+	fd_fdstat_get: function (fd, retptr0) {
+		return __WASI_ERRNO_SUCCESS;
+	},
+	fd_fdstat_set_flags: function (fd, flags) {
+	},
+	fd_readdir: function (fd, buf, buf_len, cookie, retptr0) {
+		return __WASI_ERRNO_BADF;
+	},
+	fd_seek: function (fd, offset, whence, retptr0) {
+		return __WASI_ERRNO_SUCCESS;
+	},
+	fd_read: function (fd, iovs, iovs_len, retptr0) {
+		const finfo = MTY.fds[fd];
+		const file_buf = mty_get_ls(finfo.path);
+
+		if (finfo && file_buf) {
+			let offset = 0;
+
+			for (let x = 0; x < iovs_len; x++) {
+				let ptr = iovs + x * 8;
+				let buf = mty_get_uint32(ptr);
+				let buf_len = mty_get_uint32(ptr + 4);
+				let len = buf_len < file_buf.length - offset ? buf_len : file_buf.length - offset;
+
+				mty_memcpy(buf, new Uint8Array(file_buf.buffer, offset, len));
+
+				offset += len;
+			}
+
+			mty_set_uint32(retptr0, offset);
+		}
+
+		return __WASI_ERRNO_SUCCESS;
+	},
+	fd_write: function (fd, iovs, iovs_len, retptr0) {
+		// Calculate full write size
+		let len = 0;
+		for (let x = 0; x < iovs_len; x++)
+			len += mty_get_uint32(iovs + x * 8 + 4);
+
+		mty_set_uint32(retptr0, len);
+
+		// Create a contiguous buffer
+		let offset = 0;
+		let file_buf = new Uint8Array(len);
+		for (let x = 0; x < iovs_len; x++) {
+			let ptr = iovs + x * 8;
+			let buf = mty_get_uint32(ptr);
+			let buf_len = mty_get_uint32(ptr + 4);
+
+			file_buf.set(new Uint8Array(MTY_MEMORY.buffer, buf, buf_len), offset);
+			offset += buf_len;
+		}
+
+		// stdout
+		if (fd == 1) {
+			const str = mty_decode(file_buf);
+			if (str != '\n')
+				console.log(str);
+
+		// stderr
+		} else if (fd == 2) {
+			const str = mty_decode(file_buf)
+			if (str != '\n')
+				console.error(str);
+
+		// Filesystem
+		} else if (MTY.fds[fd]) {
+			const finfo = MTY.fds[fd];
+			const cur_buf = mty_get_ls(finfo.path);
+
+			if (cur_buf && finfo.append) {
+				mty_set_ls(finfo.path, mty_append_buf(cur_buf, file_buf));
+
+			} else {
+				mty_set_ls(finfo.path, file_buf);
+			}
+
+			finfo.offet += len;
+		}
+
+		return __WASI_ERRNO_SUCCESS;
+	},
+
+	// Misc
+	clock_time_get: function (id, precision, retptr0) {
+		mty_set_uint64(retptr0, Math.round(performance.now() * 1000.0 * 1000.0));
+		return __WASI_ERRNO_SUCCESS;
+	},
+	poll_oneoff: function (_in, out, nsubscriptions, retptr0) {
+		// __WASI_EVENTTYPE_CLOCK
+		if (mty_get_uint8(_in + 8) == 0)
+			Atomics.wait(MTY.sleeper, 0, 0, Number(mty_get_uint64(_in + 24)) / 1000000);
+
+		mty_set_uint32(out + 8, 0);
+		return __WASI_ERRNO_SUCCESS;
+	},
+	proc_exit: function (rval) {
+	},
+	environ_get: function (environ, environ_buf) {
+	},
+	environ_sizes_get: function (retptr0, retptr1) {
+	},
+	sched_yield: function () {
+	},
+};
+
+const MTY_WASI_API = {
+	'thread-spawn': function (start_arg) {
+		postMessage({
+			type: 'thread',
+			startArg: start_arg,
+			sab: MTY.sab,
+			sync: MTY.sync,
+		});
+
+		mty_wait(MTY.sync);
+
+		return MTY.sab[0];
+	},
+};
+
+
+// Entry
+
+if (typeof WorkerGlobalScope != 'undefined') {
+
+async function mty_instantiate_wasm(wasmBuf, userEnv) {
+	// Imports
+	const imports = {
+		env: {
+			memory: MTY_MEMORY,
+			...MTY_UNISTD_API,
+			...MTY_GL_API,
+			...MTY_AUDIO_API,
+			...MTY_NET_API,
+			...MTY_IMAGE_API,
+			...MTY_CRYPTO_API,
+			...MTY_SYSTEM_API,
+			...MTY_WEB_API,
+		},
+		wasi_snapshot_preview1: {
+			...MTY_WASI_SNAPSHOT_PREVIEW1_API,
+		},
+		wasi: {
+			...MTY_WASI_API,
+		},
+	}
+
+	// Add userEnv to imports, run on the main thread
+	for (let x = 0; x < userEnv.length; x++) {
+		const key = userEnv[x];
+
+		imports.env[key] = function () {
+			const args = [];
+			for (let y = 0; y < arguments.length; y++)
+				args.push(arguments[y]);
+
+			postMessage({
+				type: 'user-env',
+				name: key,
+				args: args,
+				sab: MTY.sab,
+				sync: MTY.sync,
+			});
+
+			mty_wait(MTY.sync);
+
+			return MTY.sab[0];
+		};
+	}
+
+	return await WebAssembly.instantiate(wasmBuf, imports);
+}
+
+onmessage = async (ev) => {
+	const msg = ev.data;
+
+	switch (msg.type) {
+		case 'init':
+			importScripts(msg.file);
+
+			MTY_MEMORY = msg.memory;
+
+			MTY.queryString = msg.args;
+			MTY.hostname = msg.hostname;
+			MTY.bin = msg.bin;
+			MTY.fdIndex = 64;
+			MTY.kbMap = msg.kbMap;
+			MTY.psync = msg.psync;
+			MTY.audioObjs = msg.audioObjs;
+			MTY.initWindowInfo = msg.windowInfo;
+			MTY.sync = new Int32Array(new SharedArrayBuffer(4));
+			MTY.sleeper = new Int32Array(new SharedArrayBuffer(4));
+			MTY.module = await mty_instantiate_wasm(msg.wasmBuf, msg.userEnv);
+			MTY.exports = MTY.module.instance.exports;
+			MTY.sab = new Uint32Array(new SharedArrayBuffer(128));
+
+			// WASI will buffer stdout and stderr by default, disable it
+			MTY.exports.mty_system_disable_buffering();
+
+			try {
+				// Additional thread
+				if (msg.startArg) {
+					MTY.exports.wasi_thread_start(msg.threadId, msg.startArg);
+
+				// Main thread
+				} else {
+					MTY.exports._start();
+				}
+
+				close();
+
+			} catch (e) {
+				if (e.toString().search('MTY_RunAndYield') == -1)
+					console.error(e);
+			}
+			break;
+
+		// "Main" thread only
+		case 'window-update':
+			if (MTY.app)
+				mty_update_window(MTY.app, msg.windowInfo);
+			break;
+		case 'keyboard': {
+			if (!MTY.app)
+				return;
+
+			const key = MTY.keys[msg.code];
+
+			if (key != undefined) {
+				let packed = 0;
+
+				if (msg.key.length == 1) {
+					const buf = mty_encode(msg.key);
+
+					for (let x = 0; x < buf.length; x++)
+						packed |= buf[x] << x * 8;
+				}
+
+				MTY.exports.mty_window_keyboard(MTY.app, msg.pressed, key, packed, msg.mods);
+			}
+			break;
+		}
+		case 'motion':
+			if (MTY.app)
+				MTY.exports.mty_window_motion(MTY.app, msg.relative, msg.x, msg.y);
+			break;
+		case 'button':
+			if (MTY.app)
+				MTY.exports.mty_window_button(MTY.app, msg.pressed, msg.button, msg.x, msg.y);
+			break;
+		case 'scroll':
+			if (MTY.app)
+				MTY.exports.mty_window_scroll(MTY.app, msg.x, msg.y);
+			break;
+		case 'move':
+			if (MTY.app)
+				MTY.exports.mty_window_move(MTY.app);
+			break;
+		case 'size':
+			if (MTY.app) {
+				MTY.exports.mty_window_update_size(MTY.app, msg.width, msg.height);
+				MTY.exports.mty_window_size(MTY.app);
+			}
+			break;
+		case 'focus':
+			if (MTY.app) {
+				MTY.exports.mty_window_update_focus(MTY.app, msg.focus);
+				MTY.exports.mty_window_focus(MTY.app, msg.focus);
+			}
+			break;
+		case 'controller':
+			if (MTY.app)
+				MTY.exports.mty_window_controller(MTY.app, msg.id, msg.state, msg.buttons, msg.lx,
+					msg.ly, msg.rx, msg.ry, msg.lt, msg.rt);
+			break;
+		case 'controller-disconnect':
+			if (MTY.app)
+				MTY.exports.mty_window_controller(MTY.app, msg.id, msg.state, 0, 0, 0, 0, 0, 0, 0);
+			break;
+		case 'drop': {
+			if (!MTY.app)
+				return;
+
+			const cmem = mty_dup_c(new Uint8Array(msg.data));
+			const cname = mty_dup_c(mty_encode(msg.name));
+
+			MTY.exports.mty_window_drop(MTY.app, cname, cmem, buf.length);
+
+			mty_free(cname);
+			mty_free(cmem);
+			break;
+		}
+	}
+};
+
+}

+ 1081 - 0
lib/matoya.js

@@ -0,0 +1,1081 @@
+// This Source Code Form is subject to the terms of the MIT License.
+// If a copy of the MIT License was not distributed with this file,
+// You can obtain one at https://spdx.org/licenses/MIT.html.
+
+
+// Global State
+
+let MTY_MEMORY;
+let MTY_CURRENT_SCRIPT;
+
+// Worker
+if (typeof importScripts == 'function') {
+	MTY_CURRENT_SCRIPT = location;
+
+// Main thread
+} else {
+	MTY_CURRENT_SCRIPT = new URL(document.currentScript.src);
+
+	window.MTY = {
+		wsIndex: 1,
+		wsObj: {},
+		cursorId: 0,
+		threadId: 1,
+		cursorCache: {},
+		cursorClass: '',
+		defaultCursor: false,
+		synthesizeEsc: true,
+		relative: false,
+		gps: [false, false, false, false],
+	};
+}
+
+
+// Memory
+
+function mty_encode(str) {
+	return new TextEncoder().encode(str);
+}
+
+function mty_decode(buf) {
+	return new TextDecoder().decode(buf);
+}
+
+function mty_strlen(buf) {
+	let len = 0;
+	for (; buf[len] != 0; len++);
+
+	return len;
+}
+
+function mty_memcpy(ptr, buf) {
+	new Uint8Array(MTY_MEMORY.buffer, ptr, buf.byteLength).set(buf);
+}
+
+function mty_strcpy(ptr, buf) {
+	mty_memcpy(ptr, buf);
+	mty_set_int8(ptr + buf.byteLength, 0);
+}
+
+function mty_dup(ptr, size) {
+	return new Uint8Array(MTY_MEMORY.buffer, ptr).slice(0, size);
+}
+
+function mty_str_to_js(ptr) {
+	const buf = new Uint8Array(MTY_MEMORY.buffer, ptr);
+
+	return mty_decode(buf.slice(0, mty_strlen(buf)));
+}
+
+function mty_str_to_c(str, ptr, size) {
+	const buf = mty_encode(str);
+
+	if (buf.byteLength >= size)
+		throw 'mty_str_to_c overflow'
+
+	mty_strcpy(ptr, buf);
+}
+
+function mty_get_uint8(ptr) {
+	return new DataView(MTY_MEMORY.buffer).getUint8(ptr);
+}
+
+function mty_set_int8(ptr, value) {
+	new DataView(MTY_MEMORY.buffer).setInt8(ptr, value);
+}
+
+function mty_set_uint16(ptr, value) {
+	new DataView(MTY_MEMORY.buffer).setUint16(ptr, value, true);
+}
+
+function mty_get_uint32(ptr) {
+	return new DataView(MTY_MEMORY.buffer).getUint32(ptr, true);
+}
+
+function mty_set_uint32(ptr, value) {
+	new DataView(MTY_MEMORY.buffer).setUint32(ptr, value, true);
+}
+
+function mty_get_uint64(ptr, value) {
+	return new DataView(MTY_MEMORY.buffer).getBigUint64(ptr, true);
+}
+
+function mty_set_uint64(ptr, value) {
+	new DataView(MTY_MEMORY.buffer).setBigUint64(ptr, BigInt(value), true);
+}
+
+function mty_set_float(ptr, value) {
+	new DataView(MTY_MEMORY.buffer).setFloat32(ptr, value, true);
+}
+
+
+// Base64
+
+function mty_buf_to_b64(buf) {
+	let bstr = '';
+	for (let x = 0; x < buf.byteLength; x++)
+		bstr += String.fromCharCode(buf[x]);
+
+	return btoa(bstr);
+}
+
+function mty_b64_to_buf(b64) {
+	const bstr = atob(b64);
+	const buf = new Uint8Array(bstr.length);
+
+	for (let x = 0; x < bstr.length; x++)
+		buf[x] = bstr.charCodeAt(x);
+
+	return buf;
+}
+
+
+// Synchronization
+
+function mty_wait(sync) {
+	if (Atomics.compareExchange(sync, 0, 0, 1) == 0)
+		Atomics.wait(sync, 0, 1);
+
+	Atomics.store(sync, 0, 0);
+}
+
+function mty_signal(sync, allow_miss = false) {
+	if (Atomics.compareExchange(sync, 0, 0, 1) != 0)
+		while (Atomics.notify(sync, 0, 1) == 0 && !allow_miss);
+}
+
+function MTY_SignalPtr(csync) {
+	mty_signal(new Int32Array(MTY_MEMORY.buffer, csync, 1));
+}
+
+
+// Input
+
+function mty_scaled(num) {
+	return Math.round(num * window.devicePixelRatio);
+}
+
+function mty_correct_relative() {
+	if (!document.pointerLockElement && MTY.relative)
+		MTY.canvas.requestPointerLock();
+}
+
+function mty_get_mods(ev) {
+	let mods = 0;
+
+	if (ev.shiftKey) mods |= 0x01;
+	if (ev.ctrlKey)  mods |= 0x02;
+	if (ev.altKey)   mods |= 0x04;
+	if (ev.metaKey)  mods |= 0x08;
+
+	if (ev.getModifierState("CapsLock")) mods |= 0x10;
+	if (ev.getModifierState("NumLock") ) mods |= 0x20;
+
+	return mods;
+}
+
+function mty_set_pointer_lock(enable) {
+	if (enable && !document.pointerLockElement) {
+		MTY.canvas.requestPointerLock();
+
+	} else if (!enable && document.pointerLockElement) {
+		MTY.synthesizeEsc = false;
+		document.exitPointerLock();
+	}
+
+	MTY.relative = enable;
+}
+
+function mty_allow_default(ev) {
+	// The "allowed" browser hotkey list. Copy/Paste, Refresh, fullscreen, developer console, and tab switching
+
+	return ((ev.ctrlKey || ev.metaKey) && ev.code == 'KeyV') ||
+		((ev.ctrlKey || ev.metaKey) && ev.code == 'KeyC') ||
+		((ev.ctrlKey || ev.shiftKey) && ev.code == 'KeyI') ||
+		(ev.ctrlKey && ev.code == 'KeyR') ||
+		(ev.ctrlKey && ev.code == 'F5') ||
+		(ev.ctrlKey && ev.code == 'Digit1') ||
+		(ev.ctrlKey && ev.code == 'Digit2') ||
+		(ev.ctrlKey && ev.code == 'Digit3') ||
+		(ev.ctrlKey && ev.code == 'Digit4') ||
+		(ev.ctrlKey && ev.code == 'Digit5') ||
+		(ev.ctrlKey && ev.code == 'Digit6') ||
+		(ev.ctrlKey && ev.code == 'Digit7') ||
+		(ev.ctrlKey && ev.code == 'Digit8') ||
+		(ev.ctrlKey && ev.code == 'Digit9') ||
+		(ev.code == 'F5') ||
+		(ev.code == 'F11') ||
+		(ev.code == 'F12');
+}
+
+function mty_add_input_events(thread) {
+	MTY.canvas.addEventListener('mousemove', (ev) => {
+		let x = mty_scaled(ev.clientX);
+		let y = mty_scaled(ev.clientY);
+
+		if (MTY.relative) {
+			x = ev.movementX;
+			y = ev.movementY;
+		}
+
+		thread.postMessage({
+			type: 'motion',
+			relative: MTY.relative,
+			x: x,
+			y: y,
+		});
+	});
+
+	document.addEventListener('pointerlockchange', (ev) => {
+		// Left relative via the ESC key, which swallows a natural ESC keypress
+		if (!document.pointerLockElement && MTY.synthesizeEsc) {
+			const msg = {
+				type: 'keyboard',
+				pressed: true,
+				code: 'Escape',
+				key: 'Escape',
+				mods: 0,
+			};
+
+			thread.postMessage(msg);
+
+			msg.pressed = false;
+			thread.postMessage(msg);
+		}
+
+		MTY.synthesizeEsc = true;
+	});
+
+	window.addEventListener('click', (ev) => {
+		// Popup blockers can interfere with window.open if not called from within the 'click' listener
+		mty_run_action();
+		ev.preventDefault();
+	});
+
+	window.addEventListener('mousedown', (ev) => {
+		mty_correct_relative();
+		ev.preventDefault();
+
+		thread.postMessage({
+			type: 'button',
+			pressed: true,
+			button: ev.button,
+			x: mty_scaled(ev.clientX),
+			y: mty_scaled(ev.clientY),
+		});
+	});
+
+	window.addEventListener('mouseup', (ev) => {
+		ev.preventDefault();
+
+		thread.postMessage({
+			type: 'button',
+			pressed: false,
+			button: ev.button,
+			x: mty_scaled(ev.clientX),
+			y: mty_scaled(ev.clientY),
+		});
+	});
+
+	MTY.canvas.addEventListener('contextmenu', (ev) => {
+		ev.preventDefault();
+	});
+
+	MTY.canvas.addEventListener('dragover', (ev) => {
+		ev.preventDefault();
+	});
+
+	MTY.canvas.addEventListener('wheel', (ev) => {
+		let x = ev.deltaX > 0 ? 120 : ev.deltaX < 0 ? -120 : 0;
+		let y = ev.deltaY > 0 ? 120 : ev.deltaY < 0 ? -120 : 0;
+
+		thread.postMessage({
+			type: 'scroll',
+			x: x,
+			y: y,
+		});
+	}, {passive: true});
+
+	window.addEventListener('keydown', (ev) => {
+		mty_correct_relative();
+
+		thread.postMessage({
+			type: 'keyboard',
+			pressed: true,
+			code: ev.code,
+			key: ev.key,
+			mods: mty_get_mods(ev),
+		});
+
+		if (MTY.kb_grab || !mty_allow_default(ev))
+			ev.preventDefault();
+	});
+
+	window.addEventListener('keyup', (ev) => {
+		thread.postMessage({
+			type: 'keyboard',
+			pressed: false,
+			code: ev.code,
+			key: '',
+			mods: mty_get_mods(ev),
+		});
+
+		if (MTY.kb_grab || !mty_allow_default(ev))
+			ev.preventDefault();
+	});
+
+	window.addEventListener('blur', (ev) => {
+		thread.postMessage({
+			type: 'focus',
+			focus: false,
+		});
+	});
+
+	window.addEventListener('focus', (ev) => {
+		thread.postMessage({
+			type: 'focus',
+			focus: true,
+		});
+	});
+
+	window.addEventListener('resize', (ev) => {
+		const rect = mty_update_canvas(MTY.canvas);
+
+		thread.postMessage({
+			type: 'size',
+			width: mty_scaled(rect.width),
+			height: mty_scaled(rect.height),
+		});
+	});
+
+	MTY.canvas.addEventListener('drop', (ev) => {
+		ev.preventDefault();
+
+		if (!ev.dataTransfer.items)
+			return;
+
+		for (let x = 0; x < ev.dataTransfer.items.length; x++) {
+			if (ev.dataTransfer.items[x].kind == 'file') {
+				let file = ev.dataTransfer.items[x].getAsFile();
+
+				const reader = new FileReader();
+				reader.addEventListener('loadend', (fev) => {
+					if (reader.readyState == 2) {
+						thread.postMessage({
+							type: 'drop',
+							name: file.name,
+							data: reader.result,
+						}, [reader.result]);
+					}
+				});
+
+				reader.readAsArrayBuffer(file);
+				break;
+			}
+		}
+	});
+}
+
+
+// Dialog
+
+function mty_alert(title, msg) {
+	window.alert(mty_str_to_js(title) + '\n\n' + mty_str_to_js(msg));
+}
+
+
+// URI opener
+
+function mty_run_action() {
+	setTimeout(() => {
+		if (MTY.action) {
+			MTY.action();
+			delete MTY.action;
+		}
+	}, 100);
+}
+
+function mty_set_action(action) {
+	MTY.action = action;
+
+	// In case click handler doesn't happen
+	mty_run_action();
+}
+
+
+// Window
+
+function mty_is_visible() {
+	if (document.hidden != undefined) {
+		return !document.hidden;
+
+	} else if (document.webkitHidden != undefined) {
+		return !document.webkitHidden;
+	}
+
+	return true;
+}
+
+function mty_window_info() {
+	const rect = MTY.canvas.getBoundingClientRect();
+
+	return {
+		posX: window.screenX,
+		posY: window.screenY,
+		relative: MTY.relative,
+		devicePixelRatio: window.devicePixelRatio,
+		hasFocus: document.hasFocus(),
+		screenWidth: screen.width,
+		screenHeight: screen.height,
+		fullscreen: document.fullscreenElement != null,
+		visible: mty_is_visible(),
+		canvasWidth: mty_scaled(rect.width),
+		canvasHeight: mty_scaled(rect.height),
+	};
+}
+
+function mty_update_canvas(canvas) {
+	const rect = canvas.getBoundingClientRect();
+	canvas.width = rect.width;
+	canvas.height = rect.height;
+
+	return rect;
+}
+
+function mty_set_fullscreen(fullscreen) {
+	if (fullscreen && !document.fullscreenElement) {
+		if (navigator.keyboard)
+			navigator.keyboard.lock(["Escape"]);
+
+		document.documentElement.requestFullscreen();
+
+	} else if (!fullscreen && document.fullscreenElement) {
+		document.exitFullscreen();
+
+		if (navigator.keyboard)
+			navigator.keyboard.unlock();
+	}
+}
+
+async function mty_wake_lock(enable) {
+	try {
+		if (enable && !MTY.wakeLock) {
+			MTY.wakeLock = await navigator.wakeLock.request('screen');
+
+		} else if (!enable && MTY.wakeLock) {
+			MTY.wakeLock.release();
+			delete MTY.wakeLock;
+		}
+	} catch (e) {
+		delete MTY.wakeLock;
+	}
+}
+
+
+// Cursor
+
+function mty_show_cursor(show) {
+	MTY.canvas.style.cursor = show ? '': 'none';
+}
+
+function mty_use_default_cursor(use_default) {
+	if (MTY.cursorClass.length > 0) {
+		if (use_default) {
+			MTY.canvas.classList.remove(MTY.cursorClass);
+
+		} else {
+			MTY.canvas.classList.add(MTY.cursorClass);
+		}
+	}
+
+	MTY.defaultCursor = use_default;
+}
+
+function mty_set_cursor(url, hot_x, hot_y) {
+	if (url) {
+		if (!MTY.cursorCache[url]) {
+			MTY.cursorCache[url] = `cursor-x-${MTY.cursorId}`;
+
+			const style = document.createElement('style');
+			style.type = 'text/css';
+			style.innerHTML = `.cursor-x-${MTY.cursorId++} ` +
+				`{cursor: url(${url}) ${hot_x} ${hot_y}, auto;}`;
+			document.querySelector('head').appendChild(style);
+		}
+
+		if (MTY.cursorClass.length > 0)
+			MTY.canvas.classList.remove(MTY.cursorClass);
+
+		MTY.cursorClass = MTY.cursorCache[url];
+
+		if (!MTY.defaultCursor)
+			MTY.canvas.classList.add(MTY.cursorClass);
+
+	} else {
+		if (!MTY.defaultCursor && MTY.cursorClass.length > 0)
+			MTY.canvas.classList.remove(MTY.cursorClass);
+
+		MTY.cursorClass = '';
+	}
+}
+
+function mty_set_png_cursor(buf, hot_x, hot_y) {
+	const url = buf ? 'data:image/png;base64,' + mty_buf_to_b64(buf) : null;
+	mty_set_cursor(url, hot_x, hot_y);
+}
+
+function mty_set_rgba_cursor(buf, width, height, hot_x, hot_y) {
+	let url = null;
+
+	if (buf) {
+		if (!MTY.ccanvas) {
+			MTY.ccanvas = document.createElement('canvas');
+			MTY.cctx = MTY.ccanvas.getContext('2d');
+		}
+
+		MTY.ccanvas.width = width;
+		MTY.ccanvas.height = height;
+
+		const image = MTY.cctx.getImageData(0, 0, width, height);
+		image.data.set(buf);
+
+		MTY.cctx.putImageData(image, 0, 0);
+
+		url = MTY.ccanvas.toDataURL();
+	}
+
+	mty_set_cursor(url, hot_x, hot_y);
+}
+
+
+// Gamepads
+
+function mty_rumble_gamepad(id, low, high) {
+	const gps = navigator.getGamepads();
+	const gp = gps[id];
+
+	if (gp && gp.vibrationActuator)
+		gp.vibrationActuator.playEffect('dual-rumble', {
+			startDelay: 0,
+			duration: 2000,
+			weakMagnitude: low,
+			strongMagnitude: high,
+		});
+}
+
+function mty_poll_gamepads() {
+	const gps = navigator.getGamepads();
+
+	for (let x = 0; x < 4; x++) {
+		const gp = gps[x];
+
+		if (gp) {
+			let state = 0;
+
+			// Connected
+			if (!MTY.gps[x]) {
+				MTY.gps[x] = true;
+				state = 1;
+			}
+
+			let lx = 0;
+			let ly = 0;
+			let rx = 0;
+			let ry = 0;
+			let lt = 0;
+			let rt = 0;
+			let buttons = 0;
+
+			if (gp.buttons) {
+				if (gp.buttons[6]) lt = gp.buttons[6].value;
+				if (gp.buttons[7]) rt = gp.buttons[7].value;
+
+				for (let i = 0; i < gp.buttons.length && i < 32; i++)
+					if (gp.buttons[i].pressed)
+						buttons |= 1 << i;
+			}
+
+			if (gp.axes) {
+				if (gp.axes[0]) lx = gp.axes[0];
+				if (gp.axes[1]) ly = gp.axes[1];
+				if (gp.axes[2]) rx = gp.axes[2];
+				if (gp.axes[3]) ry = gp.axes[3];
+			}
+
+			thread.postMessage({
+				type: 'controller',
+				id: x,
+				state: state,
+				buttons: buttons,
+				lx: lx,
+				ly: ly,
+				rx: rx,
+				ry: ry,
+				lt: lt,
+				rt: rt,
+			});
+
+		// Disconnected
+		} else if (MTY.gps[x]) {
+			thread.postMessage({
+				type: 'controller-disconnect',
+				id: x,
+				state: 2,
+			});
+
+			MTY.gps[x] = false;
+		}
+	}
+}
+
+
+// Audio
+
+async function mty_audio_queue(ctx, sampleRate, minBuffer, maxBuffer, channels) {
+	// Initialize on first queue otherwise the browser may complain about user interaction
+	if (!MTY.audioCtx) {
+		MTY.audioCtx = new AudioContext({sampleRate: sampleRate});
+
+		const baseFile = MTY_CURRENT_SCRIPT.pathname;
+		await MTY.audioCtx.audioWorklet.addModule(baseFile.replace('.js', '-worker.js'));
+
+		const node = new AudioWorkletNode(MTY.audioCtx, 'MTY_Audio', {
+			outputChannelCount: [channels],
+			processorOptions: {
+				minBuffer,
+				maxBuffer,
+			},
+		});
+
+		node.connect(MTY.audioCtx.destination);
+		node.port.postMessage(MTY.audioObjs);
+	}
+}
+
+
+// Image
+
+async function mty_decode_image(input) {
+	const img = new Image();
+	img.src = URL.createObjectURL(new Blob([input]));
+
+	await img.decode();
+
+	const width = img.naturalWidth;
+	const height = img.naturalHeight;
+
+	const canvas = new OffscreenCanvas(width, height);
+	const ctx = canvas.getContext('2d');
+	ctx.drawImage(img, 0, 0, width, height);
+
+	return ctx.getImageData(0, 0, width, height);
+}
+
+
+// Net
+
+function mty_ws_new(obj) {
+	MTY.wsObj[MTY.wsIndex] = obj;
+
+	return MTY.wsIndex++;
+}
+
+function mty_ws_del(index) {
+	let obj = MTY.wsObj[index];
+
+	delete MTY.wsObj[index];
+
+	return obj;
+}
+
+function mty_ws_obj(index) {
+	return MTY.wsObj[index];
+}
+
+async function mty_http_request(url, method, headers, body, buf) {
+	let error = false
+	let size = 0;
+	let status = 0;
+	let data = null;
+
+	try {
+		const response = await fetch(url, {
+			method: method,
+			headers: headers,
+			body: body,
+		});
+
+		const res_ab = await response.arrayBuffer();
+		data = new Uint8Array(res_ab);
+
+		status = response.status;
+		size = data.byteLength;
+
+	} catch (err) {
+		console.error(err);
+		error = true;
+	}
+
+	return {
+		data,
+		error,
+		size,
+		status,
+	};
+}
+
+async function mty_ws_connect(url) {
+	return new Promise((resolve, reject) => {
+		const ws = new WebSocket(url);
+		const sab = new SharedArrayBuffer(4);
+		ws.sync = new Int32Array(sab, 0, 1);
+		ws.closeCode = 0;
+		ws.msgs = [];
+
+		ws.onclose = (ev) => {
+			ws.closeCode = ev.code == 1005 ? 1000 : ev.code;
+			resolve(null);
+		};
+
+		ws.onerror = (err) => {
+			console.error(err);
+			resolve(null);
+		};
+
+		ws.onopen = () => {
+			resolve(ws);
+		};
+
+		ws.onmessage = (ev) => {
+			ws.msgs.push(ev.data);
+			Atomics.notify(ws.sync, 0, 1);
+		};
+	});
+}
+
+async function mty_ws_read(ws, timeout) {
+	let msg = ws.msgs.shift()
+
+	if (!msg) {
+		const r0 = Atomics.waitAsync(ws.sync, 0, 0, timeout);
+		const r1 = await r0.value;
+
+		if (r1 != 'timed-out')
+			msg = ws.msgs.shift()
+	}
+
+	return msg ? mty_encode(msg) : null;
+}
+
+
+// Entry
+
+function mty_supports_web_gl() {
+	try {
+		return document.createElement('canvas').getContext('webgl2');
+	} catch (e) {}
+
+	return false;
+}
+
+function mty_update_interval(thread) {
+	// Poll gamepads
+	if (document.hasFocus())
+		mty_poll_gamepads();
+
+	// Poll position changes
+	if (MTY.posX != window.screenX || MTY.posY != window.screenY) {
+		MTY.posX = window.screenX;
+		MTY.posY = window.screenY;
+
+		thread.postMessage({
+			type: 'move',
+		});
+	}
+
+	// send rect event
+	thread.postMessage({
+		type: 'window-update',
+		windowInfo: mty_window_info(),
+	});
+}
+
+function mty_thread_start(threadId, bin, wasmBuf, memory, startArg, userEnv, kbMap, psync, audioObjs, name) {
+	const baseFile = MTY_CURRENT_SCRIPT.pathname;
+	const worker = new Worker(baseFile.replace('.js', '-worker.js'), {name: name});
+
+	worker.onmessage = mty_thread_message;
+
+	worker.postMessage({
+		type: 'init',
+		file: baseFile,
+		bin: bin,
+		wasmBuf: wasmBuf,
+		psync: psync,
+		windowInfo: mty_window_info(),
+		args: window.location.search,
+		hostname: window.location.hostname,
+		userEnv: userEnv ? Object.keys(userEnv) : [],
+		kbMap: kbMap,
+		startArg: startArg,
+		threadId: threadId,
+		memory: memory,
+		audioObjs,
+	});
+
+	return worker;
+}
+
+async function MTY_Start(bin, container, userEnv) {
+	if (!mty_supports_web_gl())
+		return false;
+
+	MTY.bin = bin;
+	MTY.userEnv = userEnv;
+	MTY.psync = new Int32Array(new SharedArrayBuffer(4));
+	MTY.audioObjs = {
+		buf: new Int16Array(new SharedArrayBuffer(1024 * 1024)),
+		control: new Int32Array(new SharedArrayBuffer(32)),
+	};
+
+	// Drawing surface
+	MTY.canvas = document.createElement('canvas');
+	MTY.renderer = MTY.canvas.getContext('bitmaprenderer');
+	MTY.canvas.style.width = '100%';
+	MTY.canvas.style.height = '100%';
+	container.appendChild(MTY.canvas);
+	mty_update_canvas(MTY.canvas);
+
+	// WASM binary
+	const wasmRes = await fetch(bin);
+	MTY.wasmBuf = await wasmRes.arrayBuffer();
+
+	// Shared global memory
+	MTY_MEMORY = new WebAssembly.Memory({
+		initial: 512,   // 32 MB
+		maximum: 16384, // 1 GB
+		shared: true,
+	});
+
+	// Load keyboard map
+	MTY.kbMap = {};
+	if (navigator.keyboard) {
+		const layout = await navigator.keyboard.getLayoutMap();
+
+		layout.forEach((currentValue, index) => {
+			MTY.kbMap[index] = currentValue;
+		});
+	}
+
+	// Main thread
+	MTY.mainThread = mty_thread_start(MTY.threadId, bin, MTY.wasmBuf, MTY_MEMORY,
+		0, userEnv, MTY.kbMap, MTY.psync, MTY.audioObjs, 'main');
+
+	// Init position, update loop
+	MTY.posX = window.screenX;
+	MTY.posY = window.screenY;
+	setInterval(() => {
+		mty_update_interval(MTY.mainThread);
+	}, 10);
+
+	// Vsync
+	const vsync = () => {
+		mty_signal(MTY.psync, true);
+		requestAnimationFrame(vsync);
+	};
+	requestAnimationFrame(vsync);
+
+	// Add input events
+	mty_add_input_events(MTY.mainThread);
+
+	return true;
+}
+
+async function mty_thread_message(ev) {
+	const msg = ev.data;
+
+	switch (msg.type) {
+		case 'user-env':
+			msg.sab[0] = MTY.userEnv[msg.name](...msg.args);
+			mty_signal(msg.sync);
+			break;
+		case 'thread': {
+			MTY.threadId++;
+
+			const worker = mty_thread_start(MTY.threadId, MTY.bin, MTY.wasmBuf, MTY_MEMORY,
+				msg.startArg, MTY.userEnv, MTY.kbMap, MTY.psync, MTY.audioObjs, 'thread-' + MTY.threadId);
+
+			msg.sab[0] = MTY.threadId;
+			mty_signal(msg.sync);
+			break;
+		}
+		case 'present':
+			MTY.renderer.transferFromImageBitmap(msg.image);
+			break;
+		case 'decode-image': {
+			const image = await mty_decode_image(msg.input);
+
+			this.tmp = image.data;
+			msg.sab[0] = image.width;
+			msg.sab[1] = image.height;
+
+			mty_signal(msg.sync);
+			break;
+		}
+		case 'kb-grab':
+			MTY.kb_grab = msg.grab;
+			break;
+		case 'title':
+			document.title = msg.title;
+			break;
+		case 'get-ls': {
+			const val = window.localStorage[msg.key];
+
+			if (val) {
+				this.tmp = mty_b64_to_buf(val);
+				msg.sab[0] = this.tmp.byteLength;
+
+			} else {
+				msg.sab[0] = 0;
+			}
+
+			mty_signal(msg.sync);
+			break;
+		}
+		case 'set-ls':
+			window.localStorage[msg.key] = mty_buf_to_b64(msg.val);
+			mty_signal(msg.sync);
+			break;
+		case 'alert':
+			mty_alert(msg.title, msg.msg);
+			break;
+		case 'fullscreen':
+			mty_set_fullscreen(msg.fullscreen);
+			break;
+		case 'wake-lock':
+			mty_wake_lock(msg.enable);
+			break;
+		case 'rumble':
+			mty_rumble_gamepad(msg.id, msg.low, msg.high);
+			break;
+		case 'show-cursor':
+			mty_show_cursor(msg.show);
+			break;
+		case 'get-clip':
+			// FIXME Unsupported on Firefox
+			if (navigator.clipboard.readText) {
+				const text = await navigator.clipboard.readText();
+
+				this.tmp = mty_encode(text);
+				msg.sab[0] = this.tmp.byteLength;
+
+			} else {
+				msg.sab[0] = 0;
+			}
+
+			mty_signal(msg.sync);
+			break;
+		case 'set-clip':
+			navigator.clipboard.writeText(mty_str_to_js(msg.text));
+			break;
+		case 'pointer-lock':
+			mty_set_pointer_lock(msg.enable);
+			break;
+		case 'cursor-default':
+			mty_use_default_cursor(msg.use_default);
+			break;
+		case 'cursor-rgba':
+			mty_set_rgba_cursor(msg.buf, msg.width, msg.height, msg.hot_x, msg.hot_y);
+			break;
+		case 'cursor-png':
+			mty_set_png_cursor(msg.buf, msg.hot_x, msg.hot_y);
+			break;
+		case 'uri':
+			mty_set_action(() => {
+				window.open(mty_str_to_js(msg.uri), '_blank');
+			});
+			break;
+		case 'http': {
+			const res = await mty_http_request(msg.url, msg.method, msg.headers, msg.body);
+
+			this.tmp = res.data;
+			msg.sab[0] = res.error ? 1 : 0;
+			msg.sab[1] = res.size;
+			msg.sab[2] = res.status;
+
+			mty_signal(msg.sync);
+			break;
+		}
+		case 'ws-connect': {
+			const ws = await mty_ws_connect(msg.url);
+			msg.sab[0] = ws ? mty_ws_new(ws) : 0;
+			mty_signal(msg.sync);
+			break;
+		}
+		case 'ws-read': {
+			msg.sab[0] = 3; // MTY_ASYNC_ERROR
+
+			const ws = mty_ws_obj(msg.ctx);
+
+			if (ws) {
+				if (ws.closeCode != 0) {
+					msg.sab[0] = 1; // MTY_ASYNC_DONE
+
+				} else {
+					const buf = await mty_ws_read(ws, msg.timeout);
+
+					if (buf) {
+						this.tmp = buf;
+						msg.sab[0] = 0; // MTY_ASYNC_OK
+						msg.sab[1] = buf.length;
+
+					} else {
+						msg.sab[0] = 2; // MTY_ASYNC_CONTINUE
+					}
+				}
+			}
+
+			mty_signal(msg.sync);
+			break;
+		}
+		case 'ws-write': {
+			const ws = mty_ws_obj(msg.ctx);
+			if (ws)
+				ws.send(msg.text)
+			break;
+		}
+		case 'ws-close': {
+			const ws = mty_ws_obj(msg.ctx);
+			if (ws) {
+				ws.close();
+				mty_ws_del(msg.ctx);
+			}
+			break;
+		}
+		case 'ws-code': {
+			msg.sab[0] = 0;
+
+			const ws = mty_ws_obj(msg.ctx);
+			if (ws)
+				msg.sab[0] = ws.closeCode;
+
+			mty_signal(msg.sync);
+			break;
+		}
+		case 'audio-queue':
+			mty_audio_queue(MTY.audio, msg.sampleRate, msg.minBuffer,
+				msg.maxBuffer, msg.channels);
+			break;
+		case 'audio-destroy':
+			if (MTY.audioCtx)
+				MTY.audioCtx.close();
+			delete MTY.audioCtx;
+			break;
+		case 'async-copy':
+			msg.sab8.set(this.tmp);
+			delete this.tmp;
+
+			mty_signal(msg.sync);
+			break;
+	}
+}

+ 36 - 0
lib/parsec.js

@@ -0,0 +1,36 @@
+'use strict';var h;function n(a){var b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}}function q(a){var b="undefined"!=typeof Symbol&&Symbol.iterator&&a[Symbol.iterator];return b?b.call(a):{next:n(a)}}var r="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global&&null!=global?global:this,u="function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,d){a!=Array.prototype&&a!=Object.prototype&&(a[b]=d.value)};
+function w(a,b){if(b){var d=r;a=a.split(".");for(var f=0;f<a.length-1;f++){var g=a[f];g in d||(d[g]={});d=d[g]}a=a[a.length-1];f=d[a];b=b(f);b!=f&&null!=b&&u(d,a,{configurable:!0,writable:!0,value:b})}}
+w("Promise",function(a){function b(c){this.c=0;this.h=void 0;this.b=[];var e=this.f();try{c(e.resolve,e.reject)}catch(k){e.reject(k)}}function d(){this.b=null}function f(c){return c instanceof b?c:new b(function(e){e(c)})}if(a)return a;d.prototype.c=function(c){if(null==this.b){this.b=[];var e=this;this.f(function(){e.h()})}this.b.push(c)};var g=r.setTimeout;d.prototype.f=function(c){g(c,0)};d.prototype.h=function(){for(;this.b&&this.b.length;){var c=this.b;this.b=[];for(var e=0;e<c.length;++e){var k=
+c[e];c[e]=null;try{k()}catch(m){this.g(m)}}}this.b=null};d.prototype.g=function(c){this.f(function(){throw c;})};b.prototype.f=function(){function c(m){return function(p){k||(k=!0,m.call(e,p))}}var e=this,k=!1;return{resolve:c(this.s),reject:c(this.g)}};b.prototype.s=function(c){if(c===this)this.g(new TypeError("A Promise cannot resolve to itself"));else if(c instanceof b)this.u(c);else{a:switch(typeof c){case "object":var e=null!=c;break a;case "function":e=!0;break a;default:e=!1}e?this.o(c):this.i(c)}};
+b.prototype.o=function(c){var e=void 0;try{e=c.then}catch(k){this.g(k);return}"function"==typeof e?this.v(e,c):this.i(c)};b.prototype.g=function(c){this.j(2,c)};b.prototype.i=function(c){this.j(1,c)};b.prototype.j=function(c,e){if(0!=this.c)throw Error("Cannot settle("+c+", "+e+"): Promise already settled in state"+this.c);this.c=c;this.h=e;this.l()};b.prototype.l=function(){if(null!=this.b){for(var c=0;c<this.b.length;++c)l.c(this.b[c]);this.b=null}};var l=new d;b.prototype.u=function(c){var e=this.f();
+c.w(e.resolve,e.reject)};b.prototype.v=function(c,e){var k=this.f();try{c.call(e,k.resolve,k.reject)}catch(m){k.reject(m)}};b.prototype.then=function(c,e){function k(t,v){return"function"==typeof t?function(B){try{m(t(B))}catch(C){p(C)}}:v}var m,p,D=new b(function(t,v){m=t;p=v});this.w(k(c,m),k(e,p));return D};b.prototype.catch=function(c){return this.then(void 0,c)};b.prototype.w=function(c,e){function k(){switch(m.c){case 1:c(m.h);break;case 2:e(m.h);break;default:throw Error("Unexpected state: "+
+m.c);}}var m=this;null==this.b?l.c(k):this.b.push(k)};b.resolve=f;b.reject=function(c){return new b(function(e,k){k(c)})};b.race=function(c){return new b(function(e,k){for(var m=q(c),p=m.next();!p.done;p=m.next())f(p.value).w(e,k)})};b.all=function(c){var e=q(c),k=e.next();return k.done?f([]):new b(function(m,p){function D(B){return function(C){t[B]=C;v--;0==v&&m(t)}}var t=[],v=0;do t.push(void 0),v++,f(k.value).w(D(t.length-1),p),k=e.next();while(!k.done)})};return b});
+function x(){x=function(){};r.Symbol||(r.Symbol=y)}function z(a,b){this.b=a;u(this,"description",{configurable:!0,writable:!0,value:b})}z.prototype.toString=function(){return this.b};var y=function(){function a(d){if(this instanceof a)throw new TypeError("Symbol is not a constructor");return new z("jscomp_symbol_"+(d||"")+"_"+b++,d)}var b=0;return a}();
+function A(){x();var a=r.Symbol.iterator;a||(a=r.Symbol.iterator=r.Symbol("Symbol.iterator"));"function"!=typeof Array.prototype[a]&&u(Array.prototype,a,{configurable:!0,writable:!0,value:function(){return E(n(this))}});A=function(){}}function E(a){A();a={next:a};a[r.Symbol.iterator]=function(){return this};return a}function F(){this.g=!1;this.c=null;this.i=void 0;this.b=1;this.l=this.h=0;this.f=null}function G(a){if(a.g)throw new TypeError("Generator is already running");a.g=!0}
+F.prototype.j=function(a){this.i=a};function H(a,b){a.f={F:b,Y:!0};a.b=a.h||a.l}F.prototype.return=function(a){this.f={return:a};this.b=this.l};function I(a,b,d){a.b=d;return{value:b}}function aa(a){this.b=new F;this.c=a}function ba(a,b){G(a.b);var d=a.b.c;if(d)return J(a,"return"in d?d["return"]:function(f){return{value:f,done:!0}},b,a.b.return);a.b.return(b);return K(a)}
+function J(a,b,d,f){try{var g=b.call(a.b.c,d);if(!(g instanceof Object))throw new TypeError("Iterator result "+g+" is not an object");if(!g.done)return a.b.g=!1,g;var l=g.value}catch(c){return a.b.c=null,H(a.b,c),K(a)}a.b.c=null;f.call(a.b,l);return K(a)}function K(a){for(;a.b.b;)try{var b=a.c(a.b);if(b)return a.b.g=!1,{value:b.value,done:!1}}catch(d){a.b.i=void 0,H(a.b,d)}a.b.g=!1;if(a.b.f){b=a.b.f;a.b.f=null;if(b.Y)throw b.F;return{value:b.return,done:!0}}return{value:void 0,done:!0}}
+function ca(a){this.next=function(b){G(a.b);a.b.c?b=J(a,a.b.c.next,b,a.b.j):(a.b.j(b),b=K(a));return b};this.throw=function(b){G(a.b);a.b.c?b=J(a,a.b.c["throw"],b,a.b.j):(H(a.b,b),b=K(a));return b};this.return=function(b){return ba(a,b)};A();this[Symbol.iterator]=function(){return this}}function da(a){function b(f){return a.next(f)}function d(f){return a.throw(f)}return new Promise(function(f,g){function l(c){c.done?f(c.value):Promise.resolve(c.value).then(b,d).then(l,g)}l(a.next())})}
+function L(a){return da(new ca(new aa(a)))}w("Object.entries",function(a){return a?a:function(b){var d=[],f;for(f in b)Object.prototype.hasOwnProperty.call(b,f)&&d.push([f,b[f]]);return d}});var M={PARSEC_OK:0,PARSEC_NOT_RUNNING:-3,PARSEC_CONNECTING:20,PARSEC_WRN_BROWSER:30};function N(a,b,d){a.addEventListener(b,d);return[a,b,d]}function O(a,b,d,f,g){a=new DataView(a);a.setInt32(0,d);a.setInt32(4,f);a.setInt32(8,g);a.setInt8(12,b)}
+function P(a,b,d,f){var g=new ArrayBuffer(13);O(g,a,b,d,f);return g}function Q(a,b,d){var f=new ArrayBuffer(13+d.length+1);O(f,a,d.length+1,b,0);a=(new TextEncoder).encode(d);b=new Int8Array(f,13);for(var g=0;g<d.length;g++)b[g]=a[g];return f}function ea(a,b){a=JSON.stringify({_version:1,_max_w:6E4,_max_h:6E4,_flags:0,resolutionX:a,resolutionY:b,refreshRate:60,mediaContainer:0});return Q(11,0,a)}
+function fa(a,b,d,f,g){switch(a.type){case 4:if(!a.relative){var l=Math.min(f/b,g/d),c=b*l;l*=d;g=Math.max((g-l)/2,0);f=Math.round(b/c*(a.x-Math.max((f-c)/2,0)));f===b-1&&(f=b);f>b&&(f=b);0>f&&(f=0);a.x=f;b=Math.round(d/l*(a.y-g));b===d-1&&(b=d);b>d&&(b=d);0>b&&(b=0);a.y=b}return P(3,a.relative?1:0,a.x,a.y);case 8:return d=new ArrayBuffer(28),O(d,23,a.id,0,0),b=new DataView(d),b.setUint16(16,a.buttons),b.setInt16(18,a.thumbLX),b.setInt16(20,a.thumbLY),b.setInt16(22,a.thumbRX),b.setInt16(24,a.thumbRY),
+b.setUint8(26,a.leftTrigger),b.setUint8(27,a.rightTrigger),d;case 2:return P(1,a.button,a.pressed?1:0,0);case 1:return P(0,a.code,a.mod,a.pressed?1:0);case 3:return P(2,a.x,a.y,0);case 5:return P(4,a.button,a.pressed?1:0,a.id);case 6:return P(5,a.axis,a.value,a.id);case 7:return P(6,0,0,a.id);case 9:return P(24,0,0,0)}}var R={},S=1;function T(a){var b=S++;R[b]=a;return b}
+function ha(a){var b=a.getInt16(32),d=a.getInt32(16),f=0<d?new Uint8Array(a.buffer,34,d):null;f=f?T(f):0;return{type:1,cursor:{size:d,positionX:a.getInt16(24),positionY:a.getInt16(26),width:a.getInt16(20),height:a.getInt16(22),hotX:a.getInt16(28),hotY:a.getInt16(30),imageUpdate:0<f,relative:!!(b&256),hidden:!!(b&512),stream:0},key:f}}function ia(a,b){b=T(new Uint8Array(b.buffer,13,a.m));return{type:3,id:a.A,key:b}}
+function ja(){var a=new Uint8Array(16);crypto.getRandomValues(a);return a.map(function(b){return b%10}).join("")}
+function U(a){var b=this;this.l=a;this.i=!1;this.j="";this.h=!1;this.b=this.sdp=null;this.c={};this.g=[];this.f=null;this.b=new RTCPeerConnection({iceServers:[{urls:"stun:47.114.174.231:3478"}]});this.b.onicecandidate=function(d){d.candidate&&(d=d.candidate.candidate.replace("candidate:","").split(" "),8<=d.length&&"udp"===d[2].toLowerCase()&&b.l(d[4],parseInt(d[5],10),!1,"srflx"===d[7],"host"===d[7]))}}
+U.prototype.close=function(){for(var a=q(Object.entries(this.c)),b=a.next();!b.done;b=a.next())b.value[1].close();this.b.close()};function V(a,b,d,f,g){a.c[d]=a.b.createDataChannel(b,{negotiated:!0,id:d});a.c[d].binaryType="arraybuffer";a.c[d].onopen=f;a.c[d].onmessage=g}
+function ka(a){var b;return L(function(d){if(1==d.b)return b=a,I(d,a.b.createOffer(),2);b.f=d.i;for(var f=a.f.sdp.split("\n"),g={},l=0;l<f.length;l++){var c=f[l].split("="),e=c[0];c=c[1];e&&("a"===e?(g.a||(g.a={}),e=c.split(/:(.+)/),g.a[e[0]]=e[1]):g[e]=c)}a.sdp=g;return d.return({ice_ufrag:a.sdp.a["ice-ufrag"],ice_pwd:a.sdp.a["ice-pwd"],fingerprint:a.sdp.a.fingerprint})})}U.prototype.send=function(a,b){"open"==this.c[b].readyState&&this.c[b].send(a)};
+function W(a){for(;0<a.g.length;){var b=a.g.shift();a.b.addIceCandidate(new RTCIceCandidate({candidate:"candidate:2395300328 1 udp 2113937151 "+b.ip+" "+(b.port+" typ "+(b.from_stun?"srflx":"host")+" generation 0 ufrag "+a.j+" network-cost 50"),sdpMid:a.sdp.a.mid,sdpMLineIndex:0}))}}
+function la(a,b,d,f){var g,l;L(function(c){switch(c.b){case 1:if(!a.f)throw"Offer is not set";if(a.h){c.b=0;break}a.j=b;return I(c,a.b.setLocalDescription(a.f),3);case 3:c.h=4;var e=a.sdp.a.mid;g="v=0\r\no=- "+(ja()+" 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE ")+(e+"\r\na=msid-semantic: WMS *\r\nm=application 9 DTLS/SCTP 5000\r\nc=IN IP4 0.0.0.0\r\nb=AS:30\r\na=ice-ufrag:")+(b+"\r\na=ice-pwd:")+(d+"\r\na=ice-options:trickle\r\na=fingerprint:")+(f+"\r\na=setup:active\r\na=mid:")+(e+"\r\na=sendrecv\r\na=sctpmap:5000 webrtc-datachannel 256\r\na=max-message-size:1073741823\r\n");
+return I(c,a.b.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:g})),6);case 6:c.b=5;c.h=0;break;case 4:c.h=0,e=c.f.F,c.f=null,l=e,console.log(l);case 5:a.i&&W(a),a.h=!0,c.b=0}})}function ma(a,b,d,f,g){f?(a.i=!0,setTimeout(function(){a.l("1.2.3.4",1234,!0,!1,!1)},500)):a.g.push({ip:b.replace("::ffff:",""),port:d,from_stun:g});a.h&&a.i&&W(a)}
+function X(a){var b=this;this.status=M.PARSEC_NOT_RUNNING;this.G=performance.now();this.f={encodeLatency:0,decodeLatency:0,networkLatency:0,frameWidth:0,frameHeight:0,fullRange:!1,444:!1};this.h=[];this.b=null;this.B=[];this.j="";this.c=a;this.l=a.getContext("2d");this.l.imageSmoothingEnabled=!0;this.l.Z="high";this.u=[];this.s={};this.v=0;this.o=[];this.i=this.g=null;this.B.push(N(window,"beforeunload",function(){b.D();return null}))}
+function na(a){a.g||(a.g=new AudioDecoder({output:function(b){if("f32"==b.format){var d=new ArrayBuffer(b.allocationSize({planeIndex:0}));b.copyTo(d,{planeIndex:0});a.o.push(new Float32Array(d));b.close()}},error:function(b){console.log("Audio decode error:",b)}}));a.i||(a.i=new VideoDecoder({output:function(b){window.requestAnimationFrame(function(){a.f.frameWidth=b.displayWidth;a.f.frameHeight=b.displayHeight;a.f.fullRange=b.colorSpace.fullRange;a.f["444"]="I444"==b.format;a.f.decodeLatency=.9*
+a.f.decodeLatency+.1*(performance.now()-b.timestamp/1E3);a.c.width=Math.round(document.body.clientWidth*window.devicePixelRatio);a.c.height=Math.round(document.body.clientHeight*window.devicePixelRatio);var d=b.displayWidth/b.displayHeight,f=0,g=0;if((0<a.c.height?a.c.width/a.c.height:1.77777777)>d){var l=a.c.height*d;d=a.c.height;f=(a.c.width-l)/2}else l=a.c.width,d=a.c.width/d,g=(a.c.height-d)/2;a.l.drawImage(b,0,0,a.f.frameWidth,a.f.frameHeight,f,g,l,d);b.close()})},error:function(b){console.log("Video decode error:",
+b)}}))}function Y(a){a.i&&a.i.reset();a.g&&a.g.reset();a.o=[];a.l.clearRect(0,0,a.c.width,a.c.height)}function Z(a,b){b!=M.PARSEC_OK&&(a.b&&a.status==M.PARSEC_OK&&a.b.send(P(10,0,0,0),0),a.b&&(a.b.close(),a.b=null),Y(a));a.status=b;a.u=[];a.s={};a.v=0}h=X.prototype;h.D=function(){for(var a=q(this.B),b=a.next();!b.done;b=a.next())b=b.value,b[0].removeEventListener(b[1],b[2]);this.C(M.PARSEC_NOT_RUNNING)};h.X=function(a){a=R[a];return void 0!=a?a.length:0};
+h.W=function(a){var b=R[a];void 0!=b&&delete R[a];return void 0==b?null:b};
+h.P=function(a){var b=this,d,f,g;return L(function(l){if(1==l.b){d=/Chrome/.test(navigator.userAgent)&&/Google Inc/.test(navigator.vendor);if(!d)return b.status=M.PARSEC_WRN_BROWSER,l.return(null);if(b.status!=M.PARSEC_NOT_RUNNING)return l.return(null);b.status=M.PARSEC_CONNECTING;R={};S=1;na(b);Y(b);b.j=a;b.h=[];f=function(c,e,k,m,p){b.h.push({type:8,attemptID:b.j,ip:c,port:e,lan:p,fromStun:m,sync:k})};g=function(){b.B.push(N(document,"visibilitychange",function(){document.hidden?b.b&&b.b.send(P(19,
+0,0,0),0):b.b&&b.b.send(P(13,0,0,0),0)}));var c=window.screen.width,e=window.screen.height;if(800>c||600>e||1920<c||1080<e)c=1920,e=1080;b.b&&b.b.send(ea(c,e),0);Z(b,M.PARSEC_OK)};b.b=new U(f);V(b.b,"control",0,g,function(c){b.G=performance.now();var e=new DataView(c.data);c={m:e.getInt32(0),A:e.getInt32(4),V:e.getInt32(8),type:e.getInt8(12)};switch(c.type){case 10:b.status=c.m;break;case 21:b.f.encodeLatency=parseFloat(c.A)/1E3;b.f.networkLatency=0;break;case 20:b.h.push({type:2,gamepadID:c.m,motorBig:c.A,
+motorSmall:c.V});break;case 16:b.h.push({type:c.m?4:5});break;case 28:b.v=c.m;break;case 17:b.h.push(ia(c,e));break;case 9:b.h.push(ha(e));break;case 25:e=JSON.parse((new TextDecoder("utf-8")).decode(new Uint8Array(e.buffer,13,c.m-1)));for(var k={},m=0;m<e.length;m++)e[m].id==c.A&&(k=e[m]);c={list:e,me:k};b.u=c.list;b.s=c.me}});V(b.b,"video",1,null,function(c){"configured"!=b.i.state&&b.i.configure({codec:"avc1.42001e",hardwareAcceleration:"prefer-hardware",optimizeForLatency:!0});c=new EncodedVideoChunk({type:"key",
+data:c.data,timestamp:1E3*performance.now(),duration:0});b.i.decode(c)});V(b.b,"audio",2,null,function(c){"configured"!=b.g.state&&b.g.configure({codec:"opus",sampleRate:48E3,numberOfChannels:2});c=new EncodedAudioChunk({type:"key",data:c.data,timestamp:1E3*performance.now(),duration:0});b.g.decode(c)});return I(l,ka(b.b),2)}return l.return(l.i)})};h.I=function(a,b,d,f,g){this.j==a&&this.b&&la(this.b,d,f,g)};h.H=function(a,b,d,f,g){this.j==a&&this.b&&ma(this.b,b,d,f,g)};h.N=function(){return this.status};
+h.M=function(){return this.s};h.K=function(){return this.v};h.J=function(){return this.u};h.C=function(a){Z(this,a)};h.U=function(a,b){this.b&&this.status==M.PARSEC_OK&&this.b.send(Q(17,a,b),0)};h.T=function(a){this.b&&this.status==M.PARSEC_OK&&this.b.send(fa(a,this.f.frameWidth,this.f.frameHeight,this.c.width,this.c.height),0)};h.S=function(){return this.h.shift()};h.L=function(){return this.f};h.O=function(){return this.status==M.PARSEC_OK&&5E3<performance.now()-this.G};h.R=function(){return this.o.shift()};
+X.prototype.destroy=X.prototype.D;X.prototype.getBufferSize=X.prototype.X;X.prototype.getBuffer=X.prototype.W;X.prototype.clientNewAttempt=X.prototype.P;X.prototype.clientBeginP2P=X.prototype.I;X.prototype.clientAddCandidate=X.prototype.H;X.prototype.clientGetStatus=X.prototype.N;X.prototype.clientGetSelf=X.prototype.M;X.prototype.clientGetHostMode=X.prototype.K;X.prototype.clientGetGuests=X.prototype.J;X.prototype.clientDisconnect=X.prototype.C;X.prototype.clientSendUserData=X.prototype.U;
+X.prototype.clientSendMessage=X.prototype.T;X.prototype.clientPollEvents=X.prototype.S;X.prototype.clientGetMetrics=X.prototype.L;X.prototype.clientNetworkFailure=X.prototype.O;X.prototype.clientPollAudio=X.prototype.R;X.prototype.Status=M;window.Parsec=X;

+ 0 - 0
public/simple-keyboard-3.7.2.css → lib/simple-keyboard-3.7.2.css


+ 0 - 0
public/simple-keyboard-3.7.2.js → lib/simple-keyboard-3.7.2.js


+ 195 - 0
lib/weblib.js

@@ -0,0 +1,195 @@
+let PARSEC;
+
+function float_to_int(fval) {
+	let ival = fval < 0 ? fval * 32768 : fval * 32767;
+
+	if (ival < -32768) {
+		ival = -32768;
+
+	} else if (ival > 32767) {
+		ival = 32767;
+	}
+
+	return ival;
+}
+
+const PARSEC_ENV = {
+	// user.bin
+	bin_user_bin_get: function (asset_dir_c, session_id_c, size) {
+		try {
+			const cookies = document.cookie.split(';');
+
+			for (let x = 0; x < cookies.length; x++) {
+				const cookie = cookies[x].trim();
+				const name = 'parsec_login=';
+
+				if (cookie.indexOf(name) == 0) {
+					const auth = JSON.parse(cookie.substring(name.length, cookie.length));
+					mty_str_to_c(auth['token'], session_id_c, size);
+
+					return 0;
+				}
+			}
+		} catch (e) {
+			console.error(e);
+		}
+
+		return -1;
+	},
+	bin_user_bin_set: function (asset_dir_c, session_id_c) {
+		const session_id = mty_str_to_js(session_id_c);
+
+		const value = JSON.stringify({
+			'token': session_id,
+			'userId': 0,
+		});
+
+		const hostname = window.location.hostname.replace(/.*?\./, '');
+		const secure = window.location.protocol == 'https:';
+		document.cookie = 'parsec_login=' + value + ';domain=' + hostname + ';path=/;' +
+			(secure ? 'secure' : '') + ';max-age=31536000;samesite=strict;';
+	},
+	bin_user_bin_delete: function (asset_dir_c) {
+		const hostname = window.location.hostname.replace(/.*?\./, '');
+		document.cookie = 'parsec_login=;domain=' + hostname + ';path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;';
+	},
+
+	// Parsec protocol
+	web_parsec_protocol: function (peer_id) {
+		window.location.assign('parsec://peer_id=' + mty_str_to_js(peer_id));
+	},
+
+	// parsec SDK
+	parsec_web_init: function () {
+		if (PARSEC)
+			return;
+
+		const container = document.createElement('div');
+		container.style.zIndex = -1;
+		container.style.background = 'black';
+		container.style.position = 'fixed';
+		container.style.top = 0;
+		container.style.right = 0;
+		container.style.bottom = 0;
+		container.style.left = 0;
+		document.body.appendChild(container);
+
+		const canvas = document.createElement('canvas');
+		canvas.style.width = '100%';
+		canvas.style.height = '100%';
+		container.appendChild(canvas);
+
+		const updateCanvas = (ev) => {
+			const rect = canvas.getBoundingClientRect();
+			canvas.width = rect.width;
+			canvas.height = rect.height;
+		};
+
+		updateCanvas(null);
+		window.addEventListener('resize', updateCanvas);
+
+		PARSEC = new Parsec(canvas);
+	},
+	parsec_web_destroy: function () {
+		if (!PARSEC)
+			return;
+
+		PARSEC.destroy();
+		PARSEC = undefined;
+	},
+	parsec_web_disconnect: function (e) {
+		PARSEC.clientDisconnect(e);
+	},
+	parsec_web_get_status: function () {
+		return PARSEC.clientGetStatus();
+	},
+	parsec_web_send_user_data: function (id, msg_c) {
+		PARSEC.clientSendUserData(id, mty_str_to_js(msg_c));
+	},
+	parsec_web_get_guests: function (jstr_c, len) {
+		mty_str_to_c(JSON.stringify(PARSEC.clientGetGuests()), jstr_c, len);
+	},
+	parsec_web_poll_events: function (event_str_c, len) {
+		const evt = PARSEC.clientPollEvents();
+
+		if (evt) {
+			mty_str_to_c(JSON.stringify(evt), event_str_c, len);
+			return true;
+		}
+
+		return false;
+	},
+	parsec_web_get_buffer_size: function (key) {
+		return PARSEC.getBufferSize(key);
+	},
+	parsec_web_get_buffer: function (key, ptr) {
+		const buffer = PARSEC.getBuffer(key);
+
+		if (buffer)
+			mty_memcpy(ptr, buffer);
+	},
+	parsec_web_send_message: function (msg_c) {
+		PARSEC.clientSendMessage(JSON.parse(mty_str_to_js(msg_c)));
+	},
+	parsec_web_get_metrics: function (frame_w, frame_h, color444, full_range, decode, encode, network) {
+		const metrics = PARSEC.clientGetMetrics();
+
+		mty_set_float(decode, metrics['decodeLatency']);
+		mty_set_float(encode, metrics['encodeLatency']);
+		mty_set_float(network, metrics['networkLatency']);
+
+		mty_set_uint32(frame_w, metrics['frameWidth']);
+		mty_set_uint32(frame_h, metrics['frameHeight']);
+
+		mty_set_int8(color444, metrics['444']);
+		mty_set_int8(full_range, metrics['fullRange']);
+	},
+	parsec_web_get_network_failure: function () {
+		return PARSEC.clientNetworkFailure();
+	},
+	parsec_web_get_self: function (owner_ptr, id_ptr) {
+		const me = PARSEC.clientGetSelf();
+
+		mty_set_int8(owner_ptr, me['owner']);
+		mty_set_uint32(id_ptr, me['id']);
+	},
+	parsec_web_get_host_mode: function () {
+		return PARSEC.clientGetHostMode();
+	},
+	parsec_web_new_attempt: async function (attempt_id, ufrag_c, pwd_c, fingerprint_c, buf_size, csync, err_c) {
+		const creds = await PARSEC.clientNewAttempt(mty_str_to_js(attempt_id));
+
+		if (creds) {
+			mty_set_uint32(err_c, 0);
+			mty_str_to_c(creds['ice_ufrag'], ufrag_c, buf_size);
+			mty_str_to_c(creds['ice_pwd'], pwd_c, buf_size);
+			mty_str_to_c(creds['fingerprint'], fingerprint_c, buf_size);
+
+		} else {
+			mty_set_uint32(err_c, 1);
+		}
+
+		MTY_SignalPtr(csync);
+	},
+	parsec_web_begin_p2p: function (attempt_id, port, ufrag, pwd, fingerprint) {
+		PARSEC.clientBeginP2P(mty_str_to_js(attempt_id), port, mty_str_to_js(ufrag),
+			mty_str_to_js(pwd), mty_str_to_js(fingerprint));
+	},
+	parsec_web_add_candidate: function (attempt_id, ip, port, sync, from_stun) {
+		PARSEC.clientAddCandidate(mty_str_to_js(attempt_id), mty_str_to_js(ip), port, sync, from_stun);
+	},
+	parsec_web_poll_audio: function (cbuf, len) {
+		const fbuf = PARSEC.clientPollAudio();
+
+		if (fbuf) {
+			const buf = new Int16Array(MTY_MEMORY.buffer, cbuf, len);
+
+			for (let x = 0; x < fbuf.length; x++)
+				buf[x] = float_to_int(fbuf[x]);
+
+			return fbuf.length / 2;
+		}
+
+		return 0;
+	},
+};