matoya.js 22 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081
  1. // This Source Code Form is subject to the terms of the MIT License.
  2. // If a copy of the MIT License was not distributed with this file,
  3. // You can obtain one at https://spdx.org/licenses/MIT.html.
  4. // Global State
  5. let MTY_MEMORY;
  6. let MTY_CURRENT_SCRIPT;
  7. // Worker
  8. if (typeof importScripts == 'function') {
  9. MTY_CURRENT_SCRIPT = location;
  10. // Main thread
  11. } else {
  12. MTY_CURRENT_SCRIPT = new URL(document.currentScript.src);
  13. window.MTY = {
  14. wsIndex: 1,
  15. wsObj: {},
  16. cursorId: 0,
  17. threadId: 1,
  18. cursorCache: {},
  19. cursorClass: '',
  20. defaultCursor: false,
  21. synthesizeEsc: true,
  22. relative: false,
  23. gps: [false, false, false, false],
  24. };
  25. }
  26. // Memory
  27. function mty_encode(str) {
  28. return new TextEncoder().encode(str);
  29. }
  30. function mty_decode(buf) {
  31. return new TextDecoder().decode(buf);
  32. }
  33. function mty_strlen(buf) {
  34. let len = 0;
  35. for (; buf[len] != 0; len++);
  36. return len;
  37. }
  38. function mty_memcpy(ptr, buf) {
  39. new Uint8Array(MTY_MEMORY.buffer, ptr, buf.byteLength).set(buf);
  40. }
  41. function mty_strcpy(ptr, buf) {
  42. mty_memcpy(ptr, buf);
  43. mty_set_int8(ptr + buf.byteLength, 0);
  44. }
  45. function mty_dup(ptr, size) {
  46. return new Uint8Array(MTY_MEMORY.buffer, ptr).slice(0, size);
  47. }
  48. function mty_str_to_js(ptr) {
  49. const buf = new Uint8Array(MTY_MEMORY.buffer, ptr);
  50. return mty_decode(buf.slice(0, mty_strlen(buf)));
  51. }
  52. function mty_str_to_c(str, ptr, size) {
  53. const buf = mty_encode(str);
  54. if (buf.byteLength >= size)
  55. throw 'mty_str_to_c overflow'
  56. mty_strcpy(ptr, buf);
  57. }
  58. function mty_get_uint8(ptr) {
  59. return new DataView(MTY_MEMORY.buffer).getUint8(ptr);
  60. }
  61. function mty_set_int8(ptr, value) {
  62. new DataView(MTY_MEMORY.buffer).setInt8(ptr, value);
  63. }
  64. function mty_set_uint16(ptr, value) {
  65. new DataView(MTY_MEMORY.buffer).setUint16(ptr, value, true);
  66. }
  67. function mty_get_uint32(ptr) {
  68. return new DataView(MTY_MEMORY.buffer).getUint32(ptr, true);
  69. }
  70. function mty_set_uint32(ptr, value) {
  71. new DataView(MTY_MEMORY.buffer).setUint32(ptr, value, true);
  72. }
  73. function mty_get_uint64(ptr, value) {
  74. return new DataView(MTY_MEMORY.buffer).getBigUint64(ptr, true);
  75. }
  76. function mty_set_uint64(ptr, value) {
  77. new DataView(MTY_MEMORY.buffer).setBigUint64(ptr, BigInt(value), true);
  78. }
  79. function mty_set_float(ptr, value) {
  80. new DataView(MTY_MEMORY.buffer).setFloat32(ptr, value, true);
  81. }
  82. // Base64
  83. function mty_buf_to_b64(buf) {
  84. let bstr = '';
  85. for (let x = 0; x < buf.byteLength; x++)
  86. bstr += String.fromCharCode(buf[x]);
  87. return btoa(bstr);
  88. }
  89. function mty_b64_to_buf(b64) {
  90. const bstr = atob(b64);
  91. const buf = new Uint8Array(bstr.length);
  92. for (let x = 0; x < bstr.length; x++)
  93. buf[x] = bstr.charCodeAt(x);
  94. return buf;
  95. }
  96. // Synchronization
  97. function mty_wait(sync) {
  98. if (Atomics.compareExchange(sync, 0, 0, 1) == 0)
  99. Atomics.wait(sync, 0, 1);
  100. Atomics.store(sync, 0, 0);
  101. }
  102. function mty_signal(sync, allow_miss = false) {
  103. if (Atomics.compareExchange(sync, 0, 0, 1) != 0)
  104. while (Atomics.notify(sync, 0, 1) == 0 && !allow_miss);
  105. }
  106. function MTY_SignalPtr(csync) {
  107. mty_signal(new Int32Array(MTY_MEMORY.buffer, csync, 1));
  108. }
  109. // Input
  110. function mty_scaled(num) {
  111. return Math.round(num * window.devicePixelRatio);
  112. }
  113. function mty_correct_relative() {
  114. if (!document.pointerLockElement && MTY.relative)
  115. MTY.canvas.requestPointerLock();
  116. }
  117. function mty_get_mods(ev) {
  118. let mods = 0;
  119. if (ev.shiftKey) mods |= 0x01;
  120. if (ev.ctrlKey) mods |= 0x02;
  121. if (ev.altKey) mods |= 0x04;
  122. if (ev.metaKey) mods |= 0x08;
  123. if (ev.getModifierState("CapsLock")) mods |= 0x10;
  124. if (ev.getModifierState("NumLock") ) mods |= 0x20;
  125. return mods;
  126. }
  127. function mty_set_pointer_lock(enable) {
  128. if (enable && !document.pointerLockElement) {
  129. MTY.canvas.requestPointerLock();
  130. } else if (!enable && document.pointerLockElement) {
  131. MTY.synthesizeEsc = false;
  132. document.exitPointerLock();
  133. }
  134. MTY.relative = enable;
  135. }
  136. function mty_allow_default(ev) {
  137. // The "allowed" browser hotkey list. Copy/Paste, Refresh, fullscreen, developer console, and tab switching
  138. return ((ev.ctrlKey || ev.metaKey) && ev.code == 'KeyV') ||
  139. ((ev.ctrlKey || ev.metaKey) && ev.code == 'KeyC') ||
  140. ((ev.ctrlKey || ev.shiftKey) && ev.code == 'KeyI') ||
  141. (ev.ctrlKey && ev.code == 'KeyR') ||
  142. (ev.ctrlKey && ev.code == 'F5') ||
  143. (ev.ctrlKey && ev.code == 'Digit1') ||
  144. (ev.ctrlKey && ev.code == 'Digit2') ||
  145. (ev.ctrlKey && ev.code == 'Digit3') ||
  146. (ev.ctrlKey && ev.code == 'Digit4') ||
  147. (ev.ctrlKey && ev.code == 'Digit5') ||
  148. (ev.ctrlKey && ev.code == 'Digit6') ||
  149. (ev.ctrlKey && ev.code == 'Digit7') ||
  150. (ev.ctrlKey && ev.code == 'Digit8') ||
  151. (ev.ctrlKey && ev.code == 'Digit9') ||
  152. (ev.code == 'F5') ||
  153. (ev.code == 'F11') ||
  154. (ev.code == 'F12');
  155. }
  156. function mty_add_input_events(thread) {
  157. MTY.canvas.addEventListener('mousemove', (ev) => {
  158. let x = mty_scaled(ev.clientX);
  159. let y = mty_scaled(ev.clientY);
  160. if (MTY.relative) {
  161. x = ev.movementX;
  162. y = ev.movementY;
  163. }
  164. thread.postMessage({
  165. type: 'motion',
  166. relative: MTY.relative,
  167. x: x,
  168. y: y,
  169. });
  170. });
  171. document.addEventListener('pointerlockchange', (ev) => {
  172. // Left relative via the ESC key, which swallows a natural ESC keypress
  173. if (!document.pointerLockElement && MTY.synthesizeEsc) {
  174. const msg = {
  175. type: 'keyboard',
  176. pressed: true,
  177. code: 'Escape',
  178. key: 'Escape',
  179. mods: 0,
  180. };
  181. thread.postMessage(msg);
  182. msg.pressed = false;
  183. thread.postMessage(msg);
  184. }
  185. MTY.synthesizeEsc = true;
  186. });
  187. window.addEventListener('click', (ev) => {
  188. // Popup blockers can interfere with window.open if not called from within the 'click' listener
  189. mty_run_action();
  190. ev.preventDefault();
  191. });
  192. window.addEventListener('mousedown', (ev) => {
  193. mty_correct_relative();
  194. ev.preventDefault();
  195. thread.postMessage({
  196. type: 'button',
  197. pressed: true,
  198. button: ev.button,
  199. x: mty_scaled(ev.clientX),
  200. y: mty_scaled(ev.clientY),
  201. });
  202. });
  203. window.addEventListener('mouseup', (ev) => {
  204. ev.preventDefault();
  205. thread.postMessage({
  206. type: 'button',
  207. pressed: false,
  208. button: ev.button,
  209. x: mty_scaled(ev.clientX),
  210. y: mty_scaled(ev.clientY),
  211. });
  212. });
  213. MTY.canvas.addEventListener('contextmenu', (ev) => {
  214. ev.preventDefault();
  215. });
  216. MTY.canvas.addEventListener('dragover', (ev) => {
  217. ev.preventDefault();
  218. });
  219. MTY.canvas.addEventListener('wheel', (ev) => {
  220. let x = ev.deltaX > 0 ? 120 : ev.deltaX < 0 ? -120 : 0;
  221. let y = ev.deltaY > 0 ? 120 : ev.deltaY < 0 ? -120 : 0;
  222. thread.postMessage({
  223. type: 'scroll',
  224. x: x,
  225. y: y,
  226. });
  227. }, {passive: true});
  228. window.addEventListener('keydown', (ev) => {
  229. mty_correct_relative();
  230. thread.postMessage({
  231. type: 'keyboard',
  232. pressed: true,
  233. code: ev.code,
  234. key: ev.key,
  235. mods: mty_get_mods(ev),
  236. });
  237. if (MTY.kb_grab || !mty_allow_default(ev))
  238. ev.preventDefault();
  239. });
  240. window.addEventListener('keyup', (ev) => {
  241. thread.postMessage({
  242. type: 'keyboard',
  243. pressed: false,
  244. code: ev.code,
  245. key: '',
  246. mods: mty_get_mods(ev),
  247. });
  248. if (MTY.kb_grab || !mty_allow_default(ev))
  249. ev.preventDefault();
  250. });
  251. window.addEventListener('blur', (ev) => {
  252. thread.postMessage({
  253. type: 'focus',
  254. focus: false,
  255. });
  256. });
  257. window.addEventListener('focus', (ev) => {
  258. thread.postMessage({
  259. type: 'focus',
  260. focus: true,
  261. });
  262. });
  263. window.addEventListener('resize', (ev) => {
  264. const rect = mty_update_canvas(MTY.canvas);
  265. thread.postMessage({
  266. type: 'size',
  267. width: mty_scaled(rect.width),
  268. height: mty_scaled(rect.height),
  269. });
  270. });
  271. MTY.canvas.addEventListener('drop', (ev) => {
  272. ev.preventDefault();
  273. if (!ev.dataTransfer.items)
  274. return;
  275. for (let x = 0; x < ev.dataTransfer.items.length; x++) {
  276. if (ev.dataTransfer.items[x].kind == 'file') {
  277. let file = ev.dataTransfer.items[x].getAsFile();
  278. const reader = new FileReader();
  279. reader.addEventListener('loadend', (fev) => {
  280. if (reader.readyState == 2) {
  281. thread.postMessage({
  282. type: 'drop',
  283. name: file.name,
  284. data: reader.result,
  285. }, [reader.result]);
  286. }
  287. });
  288. reader.readAsArrayBuffer(file);
  289. break;
  290. }
  291. }
  292. });
  293. }
  294. // Dialog
  295. function mty_alert(title, msg) {
  296. window.alert(mty_str_to_js(title) + '\n\n' + mty_str_to_js(msg));
  297. }
  298. // URI opener
  299. function mty_run_action() {
  300. setTimeout(() => {
  301. if (MTY.action) {
  302. MTY.action();
  303. delete MTY.action;
  304. }
  305. }, 100);
  306. }
  307. function mty_set_action(action) {
  308. MTY.action = action;
  309. // In case click handler doesn't happen
  310. mty_run_action();
  311. }
  312. // Window
  313. function mty_is_visible() {
  314. if (document.hidden != undefined) {
  315. return !document.hidden;
  316. } else if (document.webkitHidden != undefined) {
  317. return !document.webkitHidden;
  318. }
  319. return true;
  320. }
  321. function mty_window_info() {
  322. const rect = MTY.canvas.getBoundingClientRect();
  323. return {
  324. posX: window.screenX,
  325. posY: window.screenY,
  326. relative: MTY.relative,
  327. devicePixelRatio: window.devicePixelRatio,
  328. hasFocus: document.hasFocus(),
  329. screenWidth: screen.width,
  330. screenHeight: screen.height,
  331. fullscreen: document.fullscreenElement != null,
  332. visible: mty_is_visible(),
  333. canvasWidth: mty_scaled(rect.width),
  334. canvasHeight: mty_scaled(rect.height),
  335. };
  336. }
  337. function mty_update_canvas(canvas) {
  338. const rect = canvas.getBoundingClientRect();
  339. canvas.width = rect.width;
  340. canvas.height = rect.height;
  341. return rect;
  342. }
  343. function mty_set_fullscreen(fullscreen) {
  344. if (fullscreen && !document.fullscreenElement) {
  345. if (navigator.keyboard)
  346. navigator.keyboard.lock(["Escape"]);
  347. document.documentElement.requestFullscreen();
  348. } else if (!fullscreen && document.fullscreenElement) {
  349. document.exitFullscreen();
  350. if (navigator.keyboard)
  351. navigator.keyboard.unlock();
  352. }
  353. }
  354. async function mty_wake_lock(enable) {
  355. try {
  356. if (enable && !MTY.wakeLock) {
  357. MTY.wakeLock = await navigator.wakeLock.request('screen');
  358. } else if (!enable && MTY.wakeLock) {
  359. MTY.wakeLock.release();
  360. delete MTY.wakeLock;
  361. }
  362. } catch (e) {
  363. delete MTY.wakeLock;
  364. }
  365. }
  366. // Cursor
  367. function mty_show_cursor(show) {
  368. MTY.canvas.style.cursor = show ? '': 'none';
  369. }
  370. function mty_use_default_cursor(use_default) {
  371. if (MTY.cursorClass.length > 0) {
  372. if (use_default) {
  373. MTY.canvas.classList.remove(MTY.cursorClass);
  374. } else {
  375. MTY.canvas.classList.add(MTY.cursorClass);
  376. }
  377. }
  378. MTY.defaultCursor = use_default;
  379. }
  380. function mty_set_cursor(url, hot_x, hot_y) {
  381. if (url) {
  382. if (!MTY.cursorCache[url]) {
  383. MTY.cursorCache[url] = `cursor-x-${MTY.cursorId}`;
  384. const style = document.createElement('style');
  385. style.type = 'text/css';
  386. style.innerHTML = `.cursor-x-${MTY.cursorId++} ` +
  387. `{cursor: url(${url}) ${hot_x} ${hot_y}, auto;}`;
  388. document.querySelector('head').appendChild(style);
  389. }
  390. if (MTY.cursorClass.length > 0)
  391. MTY.canvas.classList.remove(MTY.cursorClass);
  392. MTY.cursorClass = MTY.cursorCache[url];
  393. if (!MTY.defaultCursor)
  394. MTY.canvas.classList.add(MTY.cursorClass);
  395. } else {
  396. if (!MTY.defaultCursor && MTY.cursorClass.length > 0)
  397. MTY.canvas.classList.remove(MTY.cursorClass);
  398. MTY.cursorClass = '';
  399. }
  400. }
  401. function mty_set_png_cursor(buf, hot_x, hot_y) {
  402. const url = buf ? 'data:image/png;base64,' + mty_buf_to_b64(buf) : null;
  403. mty_set_cursor(url, hot_x, hot_y);
  404. }
  405. function mty_set_rgba_cursor(buf, width, height, hot_x, hot_y) {
  406. let url = null;
  407. if (buf) {
  408. if (!MTY.ccanvas) {
  409. MTY.ccanvas = document.createElement('canvas');
  410. MTY.cctx = MTY.ccanvas.getContext('2d');
  411. }
  412. MTY.ccanvas.width = width;
  413. MTY.ccanvas.height = height;
  414. const image = MTY.cctx.getImageData(0, 0, width, height);
  415. image.data.set(buf);
  416. MTY.cctx.putImageData(image, 0, 0);
  417. url = MTY.ccanvas.toDataURL();
  418. }
  419. mty_set_cursor(url, hot_x, hot_y);
  420. }
  421. // Gamepads
  422. function mty_rumble_gamepad(id, low, high) {
  423. const gps = navigator.getGamepads();
  424. const gp = gps[id];
  425. if (gp && gp.vibrationActuator)
  426. gp.vibrationActuator.playEffect('dual-rumble', {
  427. startDelay: 0,
  428. duration: 2000,
  429. weakMagnitude: low,
  430. strongMagnitude: high,
  431. });
  432. }
  433. function mty_poll_gamepads() {
  434. const gps = navigator.getGamepads();
  435. for (let x = 0; x < 4; x++) {
  436. const gp = gps[x];
  437. if (gp) {
  438. let state = 0;
  439. // Connected
  440. if (!MTY.gps[x]) {
  441. MTY.gps[x] = true;
  442. state = 1;
  443. }
  444. let lx = 0;
  445. let ly = 0;
  446. let rx = 0;
  447. let ry = 0;
  448. let lt = 0;
  449. let rt = 0;
  450. let buttons = 0;
  451. if (gp.buttons) {
  452. if (gp.buttons[6]) lt = gp.buttons[6].value;
  453. if (gp.buttons[7]) rt = gp.buttons[7].value;
  454. for (let i = 0; i < gp.buttons.length && i < 32; i++)
  455. if (gp.buttons[i].pressed)
  456. buttons |= 1 << i;
  457. }
  458. if (gp.axes) {
  459. if (gp.axes[0]) lx = gp.axes[0];
  460. if (gp.axes[1]) ly = gp.axes[1];
  461. if (gp.axes[2]) rx = gp.axes[2];
  462. if (gp.axes[3]) ry = gp.axes[3];
  463. }
  464. thread.postMessage({
  465. type: 'controller',
  466. id: x,
  467. state: state,
  468. buttons: buttons,
  469. lx: lx,
  470. ly: ly,
  471. rx: rx,
  472. ry: ry,
  473. lt: lt,
  474. rt: rt,
  475. });
  476. // Disconnected
  477. } else if (MTY.gps[x]) {
  478. thread.postMessage({
  479. type: 'controller-disconnect',
  480. id: x,
  481. state: 2,
  482. });
  483. MTY.gps[x] = false;
  484. }
  485. }
  486. }
  487. // Audio
  488. async function mty_audio_queue(ctx, sampleRate, minBuffer, maxBuffer, channels) {
  489. // Initialize on first queue otherwise the browser may complain about user interaction
  490. if (!MTY.audioCtx) {
  491. MTY.audioCtx = new AudioContext({sampleRate: sampleRate});
  492. const baseFile = MTY_CURRENT_SCRIPT.pathname;
  493. await MTY.audioCtx.audioWorklet.addModule(baseFile.replace('.js', '-worker.js'));
  494. const node = new AudioWorkletNode(MTY.audioCtx, 'MTY_Audio', {
  495. outputChannelCount: [channels],
  496. processorOptions: {
  497. minBuffer,
  498. maxBuffer,
  499. },
  500. });
  501. node.connect(MTY.audioCtx.destination);
  502. node.port.postMessage(MTY.audioObjs);
  503. }
  504. }
  505. // Image
  506. async function mty_decode_image(input) {
  507. const img = new Image();
  508. img.src = URL.createObjectURL(new Blob([input]));
  509. await img.decode();
  510. const width = img.naturalWidth;
  511. const height = img.naturalHeight;
  512. const canvas = new OffscreenCanvas(width, height);
  513. const ctx = canvas.getContext('2d');
  514. ctx.drawImage(img, 0, 0, width, height);
  515. return ctx.getImageData(0, 0, width, height);
  516. }
  517. // Net
  518. function mty_ws_new(obj) {
  519. MTY.wsObj[MTY.wsIndex] = obj;
  520. return MTY.wsIndex++;
  521. }
  522. function mty_ws_del(index) {
  523. let obj = MTY.wsObj[index];
  524. delete MTY.wsObj[index];
  525. return obj;
  526. }
  527. function mty_ws_obj(index) {
  528. return MTY.wsObj[index];
  529. }
  530. async function mty_http_request(url, method, headers, body, buf) {
  531. let error = false
  532. let size = 0;
  533. let status = 0;
  534. let data = null;
  535. try {
  536. const response = await fetch(url, {
  537. method: method,
  538. headers: headers,
  539. body: body,
  540. });
  541. const res_ab = await response.arrayBuffer();
  542. data = new Uint8Array(res_ab);
  543. status = response.status;
  544. size = data.byteLength;
  545. } catch (err) {
  546. console.error(err);
  547. error = true;
  548. }
  549. return {
  550. data,
  551. error,
  552. size,
  553. status,
  554. };
  555. }
  556. async function mty_ws_connect(url) {
  557. return new Promise((resolve, reject) => {
  558. const ws = new WebSocket(url);
  559. const sab = new SharedArrayBuffer(4);
  560. ws.sync = new Int32Array(sab, 0, 1);
  561. ws.closeCode = 0;
  562. ws.msgs = [];
  563. ws.onclose = (ev) => {
  564. ws.closeCode = ev.code == 1005 ? 1000 : ev.code;
  565. resolve(null);
  566. };
  567. ws.onerror = (err) => {
  568. console.error(err);
  569. resolve(null);
  570. };
  571. ws.onopen = () => {
  572. resolve(ws);
  573. };
  574. ws.onmessage = (ev) => {
  575. ws.msgs.push(ev.data);
  576. Atomics.notify(ws.sync, 0, 1);
  577. };
  578. });
  579. }
  580. async function mty_ws_read(ws, timeout) {
  581. let msg = ws.msgs.shift()
  582. if (!msg) {
  583. const r0 = Atomics.waitAsync(ws.sync, 0, 0, timeout);
  584. const r1 = await r0.value;
  585. if (r1 != 'timed-out')
  586. msg = ws.msgs.shift()
  587. }
  588. return msg ? mty_encode(msg) : null;
  589. }
  590. // Entry
  591. function mty_supports_web_gl() {
  592. try {
  593. return document.createElement('canvas').getContext('webgl2');
  594. } catch (e) {}
  595. return false;
  596. }
  597. function mty_update_interval(thread) {
  598. // Poll gamepads
  599. if (document.hasFocus())
  600. mty_poll_gamepads();
  601. // Poll position changes
  602. if (MTY.posX != window.screenX || MTY.posY != window.screenY) {
  603. MTY.posX = window.screenX;
  604. MTY.posY = window.screenY;
  605. thread.postMessage({
  606. type: 'move',
  607. });
  608. }
  609. // send rect event
  610. thread.postMessage({
  611. type: 'window-update',
  612. windowInfo: mty_window_info(),
  613. });
  614. }
  615. function mty_thread_start(threadId, bin, wasmBuf, memory, startArg, userEnv, kbMap, psync, audioObjs, name) {
  616. const baseFile = MTY_CURRENT_SCRIPT.pathname;
  617. const worker = new Worker(baseFile.replace('.js', '-worker.js'), {name: name});
  618. worker.onmessage = mty_thread_message;
  619. worker.postMessage({
  620. type: 'init',
  621. file: baseFile,
  622. bin: bin,
  623. wasmBuf: wasmBuf,
  624. psync: psync,
  625. windowInfo: mty_window_info(),
  626. args: window.location.search,
  627. hostname: window.location.hostname,
  628. userEnv: userEnv ? Object.keys(userEnv) : [],
  629. kbMap: kbMap,
  630. startArg: startArg,
  631. threadId: threadId,
  632. memory: memory,
  633. audioObjs,
  634. });
  635. return worker;
  636. }
  637. async function MTY_Start(bin, container, userEnv) {
  638. if (!mty_supports_web_gl())
  639. return false;
  640. MTY.bin = bin;
  641. MTY.userEnv = userEnv;
  642. MTY.psync = new Int32Array(new SharedArrayBuffer(4));
  643. MTY.audioObjs = {
  644. buf: new Int16Array(new SharedArrayBuffer(1024 * 1024)),
  645. control: new Int32Array(new SharedArrayBuffer(32)),
  646. };
  647. // Drawing surface
  648. MTY.canvas = document.createElement('canvas');
  649. MTY.renderer = MTY.canvas.getContext('bitmaprenderer');
  650. MTY.canvas.style.width = '100%';
  651. MTY.canvas.style.height = '100%';
  652. container.appendChild(MTY.canvas);
  653. mty_update_canvas(MTY.canvas);
  654. // WASM binary
  655. const wasmRes = await fetch(bin);
  656. MTY.wasmBuf = await wasmRes.arrayBuffer();
  657. // Shared global memory
  658. MTY_MEMORY = new WebAssembly.Memory({
  659. initial: 512, // 32 MB
  660. maximum: 16384, // 1 GB
  661. shared: true,
  662. });
  663. // Load keyboard map
  664. MTY.kbMap = {};
  665. if (navigator.keyboard) {
  666. const layout = await navigator.keyboard.getLayoutMap();
  667. layout.forEach((currentValue, index) => {
  668. MTY.kbMap[index] = currentValue;
  669. });
  670. }
  671. // Main thread
  672. MTY.mainThread = mty_thread_start(MTY.threadId, bin, MTY.wasmBuf, MTY_MEMORY,
  673. 0, userEnv, MTY.kbMap, MTY.psync, MTY.audioObjs, 'main');
  674. // Init position, update loop
  675. MTY.posX = window.screenX;
  676. MTY.posY = window.screenY;
  677. setInterval(() => {
  678. mty_update_interval(MTY.mainThread);
  679. }, 10);
  680. // Vsync
  681. const vsync = () => {
  682. mty_signal(MTY.psync, true);
  683. requestAnimationFrame(vsync);
  684. };
  685. requestAnimationFrame(vsync);
  686. // Add input events
  687. mty_add_input_events(MTY.mainThread);
  688. return true;
  689. }
  690. async function mty_thread_message(ev) {
  691. const msg = ev.data;
  692. switch (msg.type) {
  693. case 'user-env':
  694. msg.sab[0] = MTY.userEnv[msg.name](...msg.args);
  695. mty_signal(msg.sync);
  696. break;
  697. case 'thread': {
  698. MTY.threadId++;
  699. const worker = mty_thread_start(MTY.threadId, MTY.bin, MTY.wasmBuf, MTY_MEMORY,
  700. msg.startArg, MTY.userEnv, MTY.kbMap, MTY.psync, MTY.audioObjs, 'thread-' + MTY.threadId);
  701. msg.sab[0] = MTY.threadId;
  702. mty_signal(msg.sync);
  703. break;
  704. }
  705. case 'present':
  706. MTY.renderer.transferFromImageBitmap(msg.image);
  707. break;
  708. case 'decode-image': {
  709. const image = await mty_decode_image(msg.input);
  710. this.tmp = image.data;
  711. msg.sab[0] = image.width;
  712. msg.sab[1] = image.height;
  713. mty_signal(msg.sync);
  714. break;
  715. }
  716. case 'kb-grab':
  717. MTY.kb_grab = msg.grab;
  718. break;
  719. case 'title':
  720. document.title = msg.title;
  721. break;
  722. case 'get-ls': {
  723. const val = window.localStorage[msg.key];
  724. if (val) {
  725. this.tmp = mty_b64_to_buf(val);
  726. msg.sab[0] = this.tmp.byteLength;
  727. } else {
  728. msg.sab[0] = 0;
  729. }
  730. mty_signal(msg.sync);
  731. break;
  732. }
  733. case 'set-ls':
  734. window.localStorage[msg.key] = mty_buf_to_b64(msg.val);
  735. mty_signal(msg.sync);
  736. break;
  737. case 'alert':
  738. mty_alert(msg.title, msg.msg);
  739. break;
  740. case 'fullscreen':
  741. mty_set_fullscreen(msg.fullscreen);
  742. break;
  743. case 'wake-lock':
  744. mty_wake_lock(msg.enable);
  745. break;
  746. case 'rumble':
  747. mty_rumble_gamepad(msg.id, msg.low, msg.high);
  748. break;
  749. case 'show-cursor':
  750. mty_show_cursor(msg.show);
  751. break;
  752. case 'get-clip':
  753. // FIXME Unsupported on Firefox
  754. if (navigator.clipboard.readText) {
  755. const text = await navigator.clipboard.readText();
  756. this.tmp = mty_encode(text);
  757. msg.sab[0] = this.tmp.byteLength;
  758. } else {
  759. msg.sab[0] = 0;
  760. }
  761. mty_signal(msg.sync);
  762. break;
  763. case 'set-clip':
  764. navigator.clipboard.writeText(mty_str_to_js(msg.text));
  765. break;
  766. case 'pointer-lock':
  767. mty_set_pointer_lock(msg.enable);
  768. break;
  769. case 'cursor-default':
  770. mty_use_default_cursor(msg.use_default);
  771. break;
  772. case 'cursor-rgba':
  773. mty_set_rgba_cursor(msg.buf, msg.width, msg.height, msg.hot_x, msg.hot_y);
  774. break;
  775. case 'cursor-png':
  776. mty_set_png_cursor(msg.buf, msg.hot_x, msg.hot_y);
  777. break;
  778. case 'uri':
  779. mty_set_action(() => {
  780. window.open(mty_str_to_js(msg.uri), '_blank');
  781. });
  782. break;
  783. case 'http': {
  784. const res = await mty_http_request(msg.url, msg.method, msg.headers, msg.body);
  785. this.tmp = res.data;
  786. msg.sab[0] = res.error ? 1 : 0;
  787. msg.sab[1] = res.size;
  788. msg.sab[2] = res.status;
  789. mty_signal(msg.sync);
  790. break;
  791. }
  792. case 'ws-connect': {
  793. const ws = await mty_ws_connect(msg.url);
  794. msg.sab[0] = ws ? mty_ws_new(ws) : 0;
  795. mty_signal(msg.sync);
  796. break;
  797. }
  798. case 'ws-read': {
  799. msg.sab[0] = 3; // MTY_ASYNC_ERROR
  800. const ws = mty_ws_obj(msg.ctx);
  801. if (ws) {
  802. if (ws.closeCode != 0) {
  803. msg.sab[0] = 1; // MTY_ASYNC_DONE
  804. } else {
  805. const buf = await mty_ws_read(ws, msg.timeout);
  806. if (buf) {
  807. this.tmp = buf;
  808. msg.sab[0] = 0; // MTY_ASYNC_OK
  809. msg.sab[1] = buf.length;
  810. } else {
  811. msg.sab[0] = 2; // MTY_ASYNC_CONTINUE
  812. }
  813. }
  814. }
  815. mty_signal(msg.sync);
  816. break;
  817. }
  818. case 'ws-write': {
  819. const ws = mty_ws_obj(msg.ctx);
  820. if (ws)
  821. ws.send(msg.text)
  822. break;
  823. }
  824. case 'ws-close': {
  825. const ws = mty_ws_obj(msg.ctx);
  826. if (ws) {
  827. ws.close();
  828. mty_ws_del(msg.ctx);
  829. }
  830. break;
  831. }
  832. case 'ws-code': {
  833. msg.sab[0] = 0;
  834. const ws = mty_ws_obj(msg.ctx);
  835. if (ws)
  836. msg.sab[0] = ws.closeCode;
  837. mty_signal(msg.sync);
  838. break;
  839. }
  840. case 'audio-queue':
  841. mty_audio_queue(MTY.audio, msg.sampleRate, msg.minBuffer,
  842. msg.maxBuffer, msg.channels);
  843. break;
  844. case 'audio-destroy':
  845. if (MTY.audioCtx)
  846. MTY.audioCtx.close();
  847. delete MTY.audioCtx;
  848. break;
  849. case 'async-copy':
  850. msg.sab8.set(this.tmp);
  851. delete this.tmp;
  852. mty_signal(msg.sync);
  853. break;
  854. }
  855. }