parsec/webapp/static/lib/matoya.js
2023-06-02 15:41:51 +08:00

1513 lines
37 KiB
JavaScript

// 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
const MTY = {
module: null,
alloc: 0,
free: 0,
audio: null,
cbuf: null,
kbMap: null,
keysRev: {},
wakeLock: null,
reqs: {},
reqIndex: 0,
endFunc: () => {},
cursorId: 0,
cursorCache: {},
cursorClass: '',
defaultCursor: false,
synthesizeEsc: true,
relative: false,
gps: [false, false, false, false],
action: null,
lastX: 0,
lastY: 0,
keys: {},
clip: null,
// GL
gl: null,
glver: 'webgl',
glIndex: 0,
glObj: {},
// WASI
arg0: '',
fds: {},
fdIndex: 64,
preopen: false,
};
// Private helpers
function mty_mem() {
return MTY.module.instance.exports.memory.buffer;
}
function mty_mem_view() {
return new DataView(mty_mem());
}
function mty_buf_to_js_str(buf) {
return (new TextDecoder()).decode(buf);
}
function mty_b64_to_buf(str) {
return Uint8Array.from(atob(str), c => c.charCodeAt(0))
}
function mty_buf_to_b64(buf) {
let str = '';
for (let x = 0; x < buf.length; x++)
str += String.fromCharCode(buf[x]);
return btoa(str);
}
function mty_copy_str(ptr, buf) {
const heap = new Uint8Array(mty_mem(), ptr);
heap.set(buf);
heap[buf.length] = 0;
}
function mty_strlen(buf) {
let len = 0;
for (; len < 0x7FFFFFFF && buf[len] != 0; len++);
return len;
}
// WASM utility
function MTY_CFunc(ptr) {
return MTY.module.instance.exports.__indirect_function_table.get(ptr);
}
function MTY_Alloc(size, el) {
return MTY_CFunc(MTY.alloc)(size, el ? el : 1);
}
function MTY_Free(ptr) {
MTY_CFunc(MTY.free)(ptr);
}
function MTY_SetUint32(ptr, value) {
mty_mem_view().setUint32(ptr, value, true);
}
function MTY_SetUint16(ptr, value) {
mty_mem_view().setUint16(ptr, value, true);
}
function MTY_SetInt32(ptr, value) {
mty_mem_view().setInt32(ptr, value, true);
}
function MTY_SetInt8(ptr, value) {
mty_mem_view().setInt8(ptr, value);
}
function MTY_SetFloat(ptr, value) {
mty_mem_view().setFloat32(ptr, value, true);
}
function MTY_SetUint64(ptr, value) {
mty_mem_view().setBigUint64(ptr, BigInt(value), true);
}
function MTY_GetUint32(ptr) {
return mty_mem_view().getUint32(ptr, true);
}
function MTY_Memcpy(cptr, abuffer) {
const heap = new Uint8Array(mty_mem(), cptr, abuffer.length);
heap.set(abuffer);
}
function MTY_StrToJS(ptr) {
const len = mty_strlen(new Uint8Array(mty_mem(), ptr));
const slice = new Uint8Array(mty_mem(), ptr, len)
return (new TextDecoder()).decode(slice);
}
function MTY_StrToC(js_str, ptr, size) {
if (size == 0)
return;
const buf = (new TextEncoder()).encode(js_str);
const copy_size = buf.length < size ? buf.length : size - 1;
mty_copy_str(ptr, new Uint8Array(buf, 0, copy_size));
return ptr;
}
function MTY_StrToCD(js_str) {
const buf = (new TextEncoder()).encode(js_str);
const ptr = MTY_Alloc(buf.length);
mty_copy_str(ptr, buf);
return ptr;
}
// <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];
MTY.glObj[index] = undefined;
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_SetUint32(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_GetUint32(ids + x * 4)));
},
glBindFramebuffer: function (target, fb) {
MTY.gl.bindFramebuffer(target, fb ? mty_gl_obj(fb) : null);
},
glBlitFramebuffer: function (srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter) {
MTY.gl.blitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
},
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);
},
glIsEnabled: function (cap) {
return MTY.gl.isEnabled(cap);
},
glDisable: function (cap) {
MTY.gl.disable(cap);
},
glViewport: function (x, y, width, height) {
MTY.gl.viewport(x, y, width, height);
},
glGetIntegerv: function (name, data) {
const p = MTY.gl.getParameter(name);
switch (name) {
// object
case MTY.gl.READ_FRAMEBUFFER_BINDING:
case MTY.gl.DRAW_FRAMEBUFFER_BINDING:
case MTY.gl.ARRAY_BUFFER_BINDING:
case MTY.gl.TEXTURE_BINDING_2D:
case MTY.gl.CURRENT_PROGRAM:
MTY_SetUint32(data, mty_gl_new(p));
break;
// int32[4]
case MTY.gl.VIEWPORT:
case MTY.gl.SCISSOR_BOX:
for (let x = 0; x < 4; x++)
MTY_SetUint32(data + x * 4, p[x]);
break;
// int
case MTY.gl.ACTIVE_TEXTURE:
case MTY.gl.BLEND_SRC_RGB:
case MTY.gl.BLEND_DST_RGB:
case MTY.gl.BLEND_SRC_ALPHA:
case MTY.gl.BLEND_DST_ALPHA:
case MTY.gl.BLEND_EQUATION_RGB:
case MTY.gl.BLEND_EQUATION_ALPHA:
MTY_SetUint32(data, p);
break;
}
MTY_SetUint32(data, p);
},
glGetFloatv: function (name, data) {
switch (name) {
case MTY.gl.COLOR_CLEAR_VALUE:
const p = MTY.gl.getParameter(name);
for (let x = 0; x < 4; x++)
MTY_SetFloat(data + x * 4, p[x]);
break;
}
},
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_GetUint32(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_SetUint32(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_mem(), 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_mem(), 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_StrToJS(c_name));
},
glShaderSource: function (shader, count, c_strings, c_len) {
let source = '';
for (let x = 0; x < count; x++)
source += MTY_StrToJS(MTY_GetUint32(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_GetUint32(ids + x * 4)));
},
glEnableVertexAttribArray: function (index) {
MTY.gl.enableVertexAttribArray(index);
},
glBufferData: function (target, size, data, usage) {
MTY.gl.bufferData(target, new Uint8Array(mty_mem(), 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_SetUint32(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_StrToJS(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_SetUint32(params, ok);
if (!ok)
console.warn(MTY.gl.getShaderInfoLog(mty_gl_obj(shader)));
} else {
MTY_SetUint32(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 () {
// FIXME Logged automatically as part of glGetShaderiv
},
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_mem(), value, 4 * 4 * count));
},
glBlendEquationSeparate: function (modeRGB, modeAlpha) {
MTY.gl.blendEquationSeparate(modeRGB, modeAlpha);
},
glBlendFuncSeparate: function (srcRGB, dstRGB, srcAlpha, dstAlpha) {
MTY.gl.blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha);
},
glGetProgramiv: function (program, pname, params) {
MTY_SetUint32(params, MTY.gl.getProgramParameter(mty_gl_obj(program), pname));
},
glPixelStorei: function (pname, param) {
// GL_UNPACK_ROW_LENGTH is not compatible with WebGL 1
if (MTY.glver == 'webgl' && pname == 0x0CF2)
return;
MTY.gl.pixelStorei(pname, param);
},
web_gl_flush: function () {
MTY.gl.flush();
},
};
// Audio
function mty_audio_queued_ms() {
let queued_ms = Math.round((MTY.audio.next_time - MTY.audio.ctx.currentTime) * 1000.0);
let buffered_ms = Math.round((MTY.audio.offset / 4) / MTY.audio.frames_per_ms);
return (queued_ms < 0 ? 0 : queued_ms) + buffered_ms;
}
const MTY_AUDIO_API = {
MTY_AudioCreate: function (sampleRate, minBuffer, maxBuffer, channels, deviceID, fallback) {
MTY.audio = {};
MTY.audio.flushing = false;
MTY.audio.playing = false;
MTY.audio.sample_rate = sampleRate;
MTY.audio.channels = channels;
MTY.audio.frames_per_ms = Math.round(sampleRate / 1000.0);
MTY.audio.min_buffer = minBuffer * MTY.audio.frames_per_ms;
MTY.audio.max_buffer = maxBuffer * MTY.audio.frames_per_ms;
MTY.audio.offset = 0;
MTY.audio.buf = MTY_Alloc(sampleRate * 2 * MTY.audio.channels);
return 0xCDD;
},
MTY_AudioDestroy: function (audio) {
MTY_Free(MTY.audio.buf);
MTY_SetUint32(audio, 0);
MTY.audio = null;
},
MTY_AudioQueue: function (ctx, frames, count) {
// Initialize on first queue otherwise the browser may complain about user interaction
if (!MTY.audio.ctx)
MTY.audio.ctx = new AudioContext();
let queued_frames = MTY.audio.frames_per_ms * mty_audio_queued_ms();
// Stop playing and flush if we've exceeded the maximum buffer
if (queued_frames > MTY.audio.max_buffer) {
MTY.audio.playing = false;
MTY.audio.flushing = true;
}
// Stop flushing when the queue reaches zero
if (queued_frames == 0) {
MTY.audio.flushing = false;
MTY.audio.playing = false;
}
// Convert PCM int16_t to float
if (!MTY.audio.flushing) {
let size = count * 2 * MTY.audio.channels;
MTY_Memcpy(MTY.audio.buf + MTY.audio.offset, new Uint8Array(mty_mem(), frames, size));
MTY.audio.offset += size;
}
// Begin playing again if the buffer has accumulated past the min
if (!MTY.audio.playing && !MTY.audio.flushing &&
MTY.audio.offset / (2 * MTY.audio.channels) > MTY.audio.min_buffer)
{
MTY.audio.next_time = MTY.audio.ctx.currentTime;
MTY.audio.playing = true;
}
// Queue the audio if playing
if (MTY.audio.playing) {
const src = new Int16Array(mty_mem(), MTY.audio.buf);
const bcount = MTY.audio.offset / (2 * MTY.audio.channels);
const buf = MTY.audio.ctx.createBuffer(MTY.audio.channels, bcount, MTY.audio.sample_rate);
const chans = [];
for (let x = 0; x < MTY.audio.channels; x++)
chans[x] = buf.getChannelData(x);
let offset = 0;
for (let x = 0; x < bcount * MTY.audio.channels; x += MTY.audio.channels) {
for (y = 0; y < MTY.audio.channels; y++) {
chans[y][offset] = src[x + y] / 32768;
offset++;
}
}
const source = MTY.audio.ctx.createBufferSource();
source.buffer = buf;
source.connect(MTY.audio.ctx.destination);
source.start(MTY.audio.next_time);
MTY.audio.next_time += buf.duration;
MTY.audio.offset = 0;
}
},
MTY_AudioReset: function (ctx) {
MTY.audio.playing = false;
MTY.audio.flushing = false;
MTY.audio.offset = 0;
},
MTY_AudioGetQueued: function (ctx) {
if (MTY.audio.ctx)
return mty_audio_queued_ms();
return 0;
},
};
// Net
const MTY_ASYNC_OK = 0;
const MTY_ASYNC_DONE = 1;
const MTY_ASYNC_CONTINUE = 2;
const MTY_ASYNC_ERROR = 3;
function mty_decompress_image(input, func) {
const img = new Image();
img.src = URL.createObjectURL(new Blob([input]));
img.decode().then(() => {
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);
const imgData = ctx.getImageData(0, 0, width, height);
func(imgData.data, width, height);
});
}
const MTY_NET_API = {
MTY_HttpAsyncCreate: function (num_threads) {
},
MTY_HttpAsyncDestroy: function () {
},
MTY_HttpSetProxy: function (proxy) {
},
MTY_HttpParseUrl: function (url_c, host_c_out, host_size, path_c_out, path_size) {
const url = MTY_StrToJS(url_c);
try {
const url_obj = new URL(url);
const path = url_obj.pathname + url_obj.search;
MTY_StrToC(url_obj.host, host_c_out, host_size);
MTY_StrToC(path, path_c_out, path_size);
return true;
} catch (err) {
console.error(err);
}
return false;
},
MTY_HttpEncodeUrl: function(src, dst, dst_len) {
// No-op, automatically converted in fetch
MTY_StrToC(MTY_StrToJS(src), dst, dst_len);
},
MTY_HttpAsyncRequest: function(index, chost, port, secure, cmethod,
cpath, cheaders, cbody, bodySize, timeout, image)
{
const req = ++MTY.reqIndex;
MTY_SetUint32(index, req);
MTY.reqs[req] = {
async: MTY_ASYNC_CONTINUE,
image: image,
};
const jport = port != 0 ? ':' + port.toString() : '';
const scheme = secure ? 'https' : 'http';
const method = MTY_StrToJS(cmethod);
const host = MTY_StrToJS(chost);
const path = MTY_StrToJS(cpath);
const headers_str = MTY_StrToJS(cheaders);
const body = cbody ? MTY_StrToJS(cbody) : undefined;
const url = scheme + '://' + host + jport + path;
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];
}
fetch(url, {
method: method,
headers: headers,
body: body
}).then((response) => {
const data = MTY.reqs[req];
data.status = response.status;
return response.arrayBuffer();
}).then((body) => {
const data = MTY.reqs[req];
data.response = new Uint8Array(body);
data.async = MTY_ASYNC_OK;
}).catch((err) => {
const data = MTY.reqs[req];
console.error(err);
data.status = 0;
data.async = MTY_ASYNC_ERROR;
});
},
MTY_HttpAsyncPoll: function(index, response, responseSize, code) {
const data = MTY.reqs[index];
// Unknown index or request has already been polled
if (data == undefined || data.async == MTY_ASYNC_DONE)
return MTY_ASYNC_DONE;
// Request is in progress
if (data.async == MTY_ASYNC_CONTINUE)
return MTY_ASYNC_CONTINUE;
// Request is has completed asynchronously, check if there is a response
if (data.response != undefined) {
// Optionally decompress an image on a successful response
const res_ok = data.status >= 200 && data.status < 300;
const req_ok = data.async == MTY_ASYNC_OK;
if (data.image && req_ok && res_ok) {
data.async = MTY_ASYNC_CONTINUE;
data.image = false;
mty_decompress_image(data.response, (image, width, height) => {
data.width = width;
data.height = height;
data.response = image
data.async = MTY_ASYNC_OK;
});
return MTY_ASYNC_CONTINUE;
}
// Set C status code
MTY_SetUint32(code, data.status);
// Set C response size
if (data.width && data.height) {
MTY_SetUint32(responseSize, data.width | data.height << 16);
} else {
MTY_SetUint32(responseSize, data.response.length);
}
// Allocate C buffer and set return pointer
if (data.buf == undefined) {
data.buf = MTY_Alloc(data.response.length + 1);
MTY_Memcpy(data.buf, data.response);
}
MTY_SetUint32(response, data.buf);
}
const r = data.async;
data.async = MTY_ASYNC_DONE;
return r;
},
MTY_HttpAsyncClear: function (index) {
const req = MTY_GetUint32(index);
const data = MTY.reqs[req];
if (data == undefined)
return;
MTY_Free(data.buf);
delete MTY.reqs[req];
MTY_SetUint32(index, 0);
},
};
// Image
const MTY_IMAGE_API = {
MTY_DecompressImageAsync: function (input, size, func, opaque) {
const jinput = new Uint8Array(mty_mem(), input, size);
mty_decompress_image(jinput, (image, width, height) => {
const cimage = MTY_Alloc(width * height * 4);
MTY_Memcpy(cimage, image);
MTY_CFunc(func)(cimage, width, height, opaque);
});
},
};
// Crypto
const MTY_CRYPTO_API = {
MTY_CryptoHash: function (algo, input, inputSize, key, keySize, output, outputSize) {
},
MTY_GetRandomBytes: function (buf, size) {
const jbuf = new Uint8Array(mty_mem(), buf, size);
crypto.getRandomValues(jbuf);
},
};
// System
const MTY_SYSTEM_API = {
MTY_HandleProtocol: function (uri, token) {
MTY_SetAction(() => {
window.open(MTY_StrToJS(uri), '_blank');
});
},
};
// Web API (mostly used in app.c)
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_run_action() {
setTimeout(() => {
if (MTY.action) {
MTY.action();
MTY.action = null;
}
}, 100);
}
function MTY_SetAction(action) {
MTY.action = action;
// In case click handler doesn't happen
mty_run_action();
}
function mty_scaled(num) {
return Math.round(num * window.devicePixelRatio);
}
function mty_correct_relative() {
if (!document.pointerLockElement && MTY.relative)
MTY.gl.canvas.requestPointerLock();
}
function mty_poll_gamepads(app, controller) {
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];
}
MTY_CFunc(controller)(app, x, state, buttons, lx, ly, rx, ry, lt, rt);
// Disconnected
} else if (MTY.gps[x]) {
MTY_CFunc(controller)(app, x, 2, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
MTY.gps[x] = false;
}
}
}
const MTY_WEB_API = {
web_alert: function (title, msg) {
alert(MTY_StrToJS(title) + '\n\n' + MTY_StrToJS(msg));
},
web_platform: function (platform, size) {
MTY_StrToC(navigator.platform, platform, size);
},
web_set_fullscreen: function (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();
}
},
web_get_fullscreen: function () {
return document.fullscreenElement != null;
},
web_set_mem_funcs: function (alloc, free) {
MTY.alloc = alloc;
MTY.free = free;
// Global buffers for scratch heap space
MTY.cbuf = MTY_Alloc(1024);
},
web_set_key: function (reverse, code, key) {
const str = MTY_StrToJS(code);
MTY.keys[str] = key;
if (reverse)
MTY.keysRev[key] = str;
},
web_get_key: function (key, cbuf, len) {
const code = MTY.keysRev[key];
if (code != undefined) {
if (MTY.kbMap) {
const text = MTY.kbMap.get(code);
if (text) {
MTY_StrToC(text.toUpperCase(), cbuf, len);
return true;
}
}
MTY_StrToC(code, cbuf, len);
return true;
}
return false;
},
web_wake_lock: async function (enable) {
try {
if (enable && !MTY.wakeLock) {
MTY.wakeLock = await navigator.wakeLock.request('screen');
} else if (!enable && MTY.wakeLock) {
MTY.wakeLock.release();
MTY.wakeLock = undefined;
}
} catch (e) {
MTY.wakeLock = undefined;
}
},
web_rumble_gamepad: function (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,
});
},
web_show_cursor: function (show) {
MTY.gl.canvas.style.cursor = show ? '': 'none';
},
web_get_hostname: function () {
return MTY_StrToCD(location.hostname);
},
web_get_clipboard: function () {
MTY.clip.focus();
MTY.clip.select();
document.execCommand('paste');
return MTY_StrToCD(MTY.clip.value);
},
web_set_clipboard: function (text_c) {
MTY.clip.value = MTY_StrToJS(text_c);
MTY.clip.focus();
MTY.clip.select();
document.execCommand('copy');
},
web_set_pointer_lock: function (enable) {
if (enable && !document.pointerLockElement) {
MTY.gl.canvas.requestPointerLock();
} else if (!enable && document.pointerLockElement) {
MTY.synthesizeEsc = false;
document.exitPointerLock();
}
MTY.relative = enable;
},
web_get_relative: function () {
return MTY.relative;
},
web_has_focus: function () {
return document.hasFocus();
},
web_is_visible: function () {
if (document.hidden != undefined) {
return !document.hidden;
} else if (document.webkitHidden != undefined) {
return !document.webkitHidden;
}
return true;
},
web_get_size: function (c_width, c_height) {
MTY_SetUint32(c_width, MTY.gl.drawingBufferWidth);
MTY_SetUint32(c_height, MTY.gl.drawingBufferHeight);
},
web_get_position: function (c_x, c_y) {
MTY_SetInt32(c_x, MTY.lastX);
MTY_SetInt32(c_y, MTY.lastY);
},
web_get_screen_size: function (c_width, c_height) {
MTY_SetUint32(c_width, screen.width);
MTY_SetUint32(c_height, screen.height);
},
web_set_title: function (title) {
document.title = MTY_StrToJS(title);
},
web_use_default_cursor: function (use_default) {
if (MTY.cursorClass.length > 0) {
if (use_default) {
MTY.gl.canvas.classList.remove(MTY.cursorClass);
} else {
MTY.gl.canvas.classList.add(MTY.cursorClass);
}
}
MTY.defaultCursor = use_default;
},
web_set_png_cursor: function (buffer, size, hot_x, hot_y) {
if (buffer) {
const buf = new Uint8Array(mty_mem(), buffer, size);
const b64_png = mty_buf_to_b64(buf);
if (!MTY.cursorCache[b64_png]) {
MTY.cursorCache[b64_png] = `cursor-x-${MTY.cursorId}`;
const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = `.cursor-x-${MTY.cursorId++} ` +
`{cursor: url(data:image/png;base64,${b64_png}) ${hot_x} ${hot_y}, auto;}`;
document.querySelector('head').appendChild(style);
}
if (MTY.cursorClass.length > 0)
MTY.gl.canvas.classList.remove(MTY.cursorClass);
MTY.cursorClass = MTY.cursorCache[b64_png];
if (!MTY.defaultCursor)
MTY.gl.canvas.classList.add(MTY.cursorClass);
} else {
if (!MTY.defaultCursor && MTY.cursorClass.length > 0)
MTY.gl.canvas.classList.remove(MTY.cursorClass);
MTY.cursorClass = '';
}
},
web_get_pixel_ratio: function () {
return window.devicePixelRatio;
},
web_attach_events: function (app, mouse_motion, mouse_button, mouse_wheel, keyboard, focus, drop, resize) {
MTY.gl.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;
}
MTY_CFunc(mouse_motion)(app, MTY.relative, x, y);
});
document.addEventListener('pointerlockchange', (ev) => {
// Left relative via the ESC key, which swallows a natural ESC keypress
if (!document.pointerLockElement && MTY.synthesizeEsc) {
MTY_CFunc(keyboard)(app, true, MTY.keys['Escape'], 0, 0);
MTY_CFunc(keyboard)(app, false, MTY.keys['Escape'], 0, 0);
}
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();
MTY_CFunc(mouse_button)(app, true, ev.button, mty_scaled(ev.clientX), mty_scaled(ev.clientY));
});
window.addEventListener('mouseup', (ev) => {
ev.preventDefault();
MTY_CFunc(mouse_button)(app, false, ev.button, mty_scaled(ev.clientX), mty_scaled(ev.clientY));
});
MTY.gl.canvas.addEventListener('contextmenu', (ev) => {
ev.preventDefault();
});
MTY.gl.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;
MTY_CFunc(mouse_wheel)(app, x, y);
}, {passive: true});
window.addEventListener('keydown', (ev) => {
mty_correct_relative();
const key = MTY.keys[ev.code];
if (key != undefined) {
const text = ev.key.length == 1 ? MTY_StrToC(ev.key, MTY.cbuf, 1024) : 0;
if (MTY_CFunc(keyboard)(app, true, key, text, mty_get_mods(ev)))
ev.preventDefault();
}
});
window.addEventListener('keyup', (ev) => {
const key = MTY.keys[ev.code];
if (key != undefined)
if (MTY_CFunc(keyboard)(app, false, key, 0, mty_get_mods(ev)))
ev.preventDefault();
});
MTY.gl.canvas.addEventListener('dragover', (ev) => {
ev.preventDefault();
});
window.addEventListener('blur', (ev) => {
MTY_CFunc(focus)(app, false);
});
window.addEventListener('focus', (ev) => {
MTY_CFunc(focus)(app, true);
});
window.addEventListener('resize', (ev) => {
MTY_CFunc(resize)(app);
});
MTY.gl.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) {
let buf = new Uint8Array(reader.result);
let cmem = MTY_Alloc(buf.length);
MTY_Memcpy(cmem, buf);
MTY_CFunc(drop)(app, MTY_StrToC(file.name, MTY.cbuf, 1024), cmem, buf.length);
MTY_Free(cmem);
}
});
reader.readAsArrayBuffer(file);
break;
}
}
});
},
web_raf: function (app, func, controller, move, opaque) {
// Init position
MTY.lastX = window.screenX;
MTY.lastY = window.screenY;
const step = () => {
// Poll gamepads
if (document.hasFocus())
mty_poll_gamepads(app, controller);
// Poll position changes
if (MTY.lastX != window.screenX || MTY.lastY != window.screenY) {
MTY.lastX = window.screenX;
MTY.lastY = window.screenY;
MTY_CFunc(move)(app);
}
// Poll size changes and resize the canvas
const rect = MTY.gl.canvas.getBoundingClientRect();
MTY.gl.canvas.width = mty_scaled(rect.width);
MTY.gl.canvas.height = mty_scaled(rect.height);
// Keep looping recursively or end based on AppFunc return value
if (MTY_CFunc(func)(opaque)) {
window.requestAnimationFrame(step);
} else {
MTY.endFunc();
}
};
window.requestAnimationFrame(step);
throw 'MTY_AppRun halted execution';
},
};
// WASI API
// https://github.com/WebAssembly/WASI/blob/master/phases/snapshot/docs.md
function mty_append_buf_to_b64(b64, buf) {
// FIXME This is a crude way to handle appending to an open file,
// complex seek operations will break this
const cur_buf = mty_b64_to_buf(b64);
const new_buf = new Uint8Array(cur_buf.length + buf.length);
new_buf.set(cur_buf);
new_buf.set(buf, cur_buf.length);
return mty_buf_to_b64(new_buf);
}
function mty_arg_list() {
const params = new URLSearchParams(window.location.search);
const qs = params.toString();
let plist = [MTY.arg0];
// TODO This would put each key/val pair as a separate arg
// for (let p of params)
// plist.push(p[0] + '=' + p[1]);
//return plist;
// For now treat the entire query string as argv[1]
if (qs)
plist.push(qs);
return plist;
}
const MTY_WASI_API = {
// Command line arguments
args_get: function (argv, argv_buf) {
const args = mty_arg_list();
for (let x = 0; x < args.length; x++) {
MTY_StrToC(args[x], argv_buf, 32 * 1024); // FIXME what is the real size of this buffer
MTY_SetUint32(argv + x * 4, argv_buf);
argv_buf += args[x].length + 1;
}
return 0;
},
args_sizes_get: function (argc, argv_buf_size) {
const args = mty_arg_list();
MTY_SetUint32(argc, args.length);
MTY_SetUint32(argv_buf_size, args.join(' ').length + 1);
return 0;
},
// WASI preopened directory (/)
fd_prestat_get: function (fd, path) {
return !MTY.preopen ? 0 : 8;
},
fd_prestat_dir_name: function (fd, path, path_len) {
if (!MTY.preopen) {
MTY_StrToC('/', path, path_len);
MTY.preopen = true;
return 0;
}
return 28;
},
// Paths
path_filestat_get: function (fd, flags, cpath, _0, filestat_out) {
const path = MTY_StrToJS(cpath);
if (localStorage[path]) {
// We only need to return the size
const buf = mty_b64_to_buf(localStorage[path]);
MTY_SetUint64(filestat_out + 32, buf.byteLength);
}
return 0;
},
path_open: function (fd, dir_flags, path, o_flags, _0, _1, _2, mode, fd_out) {
const new_fd = MTY.fdIndex++;
MTY_SetUint32(fd_out, new_fd);
MTY.fds[new_fd] = {
path: MTY_StrToJS(path),
append: mode == 1,
offset: 0,
};
return 0;
},
path_create_directory: function () {
return 0;
},
path_remove_directory: function () {
return 0;
},
path_unlink_file: function () {
return 0;
},
path_readlink: function () {
},
path_rename: function () {
console.log('path_rename', arguments);
return 0;
},
// File descriptors
fd_close: function (fd) {
delete MTY.fds[fd];
},
fd_fdstat_get: function () {
return 0;
},
fd_fdstat_set_flags: function () {
},
fd_readdir: function () {
return 8;
},
fd_seek: function (fd, offset, whence, offset_out) {
return 0;
},
fd_read: function (fd, iovs, iovs_len, nread) {
const finfo = MTY.fds[fd];
if (finfo && localStorage[finfo.path]) {
const full_buf = mty_b64_to_buf(localStorage[finfo.path]);
let total = 0;
for (let x = 0; x < iovs_len; x++) {
let ptr = iovs + x * 8;
let cbuf = MTY_GetUint32(ptr);
let cbuf_len = MTY_GetUint32(ptr + 4);
let len = cbuf_len < full_buf.length - total ? cbuf_len : full_buf.length - total;
let view = new Uint8Array(mty_mem(), cbuf, cbuf_len);
let slice = new Uint8Array(full_buf.buffer, total, len);
view.set(slice);
total += len;
}
MTY_SetUint32(nread, total);
}
return 0;
},
fd_write: function (fd, iovs, iovs_len, nwritten) {
// Calculate full write size
let len = 0;
for (let x = 0; x < iovs_len; x++)
len += MTY_GetUint32(iovs + x * 8 + 4);
MTY_SetUint32(nwritten, len);
// Create a contiguous buffer
let offset = 0;
let full_buf = new Uint8Array(len);
for (let x = 0; x < iovs_len; x++) {
let ptr = iovs + x * 8;
let cbuf = MTY_GetUint32(ptr);
let cbuf_len = MTY_GetUint32(ptr + 4);
full_buf.set(new Uint8Array(mty_mem(), cbuf, cbuf_len), offset);
offset += cbuf_len;
}
// stdout
if (fd == 1) {
const str = mty_buf_to_js_str(full_buf);
if (str != '\n')
console.log(str);
// stderr
} else if (fd == 2) {
const str = mty_buf_to_js_str(full_buf)
if (str != '\n')
console.error(str);
// Filesystem
} else if (MTY.fds[fd]) {
const finfo = MTY.fds[fd];
const cur_b64 = localStorage[finfo.path];
if (cur_b64 && finfo.append) {
localStorage[finfo.path] = mty_append_buf_to_b64(cur_b64, full_buf);
} else {
localStorage[finfo.path] = mty_buf_to_b64(full_buf, len);
}
finfo.offet += len;
}
return 0;
},
// Misc
clock_time_get: function (id, precision, time_out) {
MTY_SetUint64(time_out, Math.round(performance.now() * 1000.0 * 1000.0));
return 0;
},
poll_oneoff: function (sin, sout, nsubscriptions, nevents) {
MTY_SetUint32(sout + 8, 0);
return 0;
},
proc_exit: function () {
},
environ_get: function () {
},
environ_sizes_get: function () {
},
};
// Entry
function mty_supports_wasm() {
try {
if (typeof WebAssembly == 'object' && typeof WebAssembly.instantiate == 'function') {
const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
if (module instanceof WebAssembly.Module)
return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
}
} catch (e) {}
return false;
}
function mty_supports_web_gl() {
try {
return document.createElement('canvas').getContext('webgl');
} catch (e) {}
return false;
}
async function MTY_Start(bin, userEnv, endFunc, glver) {
MTY.arg0 = bin;
if (!mty_supports_wasm() || !mty_supports_web_gl())
return false;
if (!userEnv)
userEnv = {};
if (endFunc)
MTY.endFunc = endFunc;
// Set up full window canvas and webgl context
const html = document.querySelector('html');
html.style.width = '100%';
html.style.height = '100%';
html.style.margin = 0;
const body = document.querySelector('body');
body.style.width = '100%';
body.style.height = '100%';
body.style.background = 'black';
body.style.overflow = 'hidden';
body.style.margin = 0;
const canvas = document.createElement('canvas');
canvas.style.width = '100%';
canvas.style.height = '100%';
document.body.appendChild(canvas);
if (glver)
MTY.glver = glver;
MTY.gl = canvas.getContext(MTY.glver, {
depth: false,
antialias: false,
premultipliedAlpha: true,
});
// Set up the clipboard
MTY.clip = document.createElement('textarea');
MTY.clip.style.position = 'absolute';
MTY.clip.style.left = '-9999px';
MTY.clip.autofocus = true;
document.body.appendChild(MTY.clip);
// Load keyboard map
if (navigator.keyboard)
MTY.kbMap = await navigator.keyboard.getLayoutMap();
// Fetch the wasm file as an ArrayBuffer
const res = await fetch(bin);
const buf = await res.arrayBuffer();
// Create wasm instance (module) from the ArrayBuffer
MTY.module = await WebAssembly.instantiate(buf, {
// Custom imports
env: {
...MTY_UNISTD_API,
...MTY_GL_API,
...MTY_AUDIO_API,
...MTY_NET_API,
...MTY_IMAGE_API,
...MTY_CRYPTO_API,
...MTY_SYSTEM_API,
...MTY_WEB_API,
...userEnv,
},
// Current version of WASI we're compiling against, 'wasi_snapshot_preview1'
wasi_snapshot_preview1: {
...MTY_WASI_API,
},
});
// Execute the '_start' entry point, this will fetch args and execute the 'main' function
try {
MTY.module.instance.exports._start();
// We expect to catch the 'MTY_AppRun halted execution' exception
// Otherwise look for an indication of unsupported WASM features
} catch (e) {
estr = e.toString();
if (estr.search('MTY_AppRun') == -1)
console.error(e);
// This probably means the browser does not support WASM 64
return estr.search('i64 not allowed') == -1;
}
return true;
}