script.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386
  1. /*
  2. * Copyright (C) 2017 Ben Smith
  3. *
  4. * This software may be modified and distributed under the terms
  5. * of the MIT license. See the LICENSE file for details.
  6. */
  7. "use strict";
  8. // User configurable.
  9. const ROM_FILENAME = "rom/game.gb";
  10. const ENABLE_REWIND = true;
  11. const ENABLE_PAUSE = false;
  12. const ENABLE_SWITCH_PALETTES = true;
  13. const OSGP_DEADZONE = 0.1; // On screen gamepad deadzone range
  14. const CGB_COLOR_CURVE = 2; // 0: none, 1: Sameboy "Emulate Hardware" 2: Gambatte/Gameboy Online
  15. // List of DMG palettes to switch between. By default it includes all 84
  16. // built-in palettes. If you want to restrict this, change it to an array of
  17. // the palettes you want to use and change DEFAULT_PALETTE_IDX to the index of the
  18. // default palette in that list.
  19. //
  20. // Example: (only allow one palette with index 16):
  21. // const DEFAULT_PALETTE_IDX = 0;
  22. // const PALETTES = [16];
  23. //
  24. // Example: (allow three palettes, 16, 32, 64, with default 32):
  25. // const DEFAULT_PALETTE_IDX = 1;
  26. // const PALETTES = [16, 32, 64];
  27. //
  28. const DEFAULT_PALETTE_IDX = 83;
  29. const PALETTES = [
  30. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
  31. 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
  32. 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
  33. 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78,
  34. 79, 80, 81, 82, 83,
  35. ];
  36. const RESULT_OK = 0;
  37. const RESULT_ERROR = 1;
  38. const SCREEN_WIDTH = 160;
  39. const SCREEN_HEIGHT = 144;
  40. const SGB_SCREEN_WIDTH = 256;
  41. const SGB_SCREEN_HEIGHT = 224;
  42. const SGB_SCREEN_LEFT = (SGB_SCREEN_WIDTH - SCREEN_WIDTH) >> 1;
  43. const SGB_SCREEN_RIGHT = (SGB_SCREEN_WIDTH + SCREEN_WIDTH) >> 1;
  44. const SGB_SCREEN_TOP = (SGB_SCREEN_HEIGHT - SCREEN_HEIGHT) >> 1;
  45. const SGB_SCREEN_BOTTOM = (SGB_SCREEN_HEIGHT + SCREEN_HEIGHT) >> 1;
  46. const AUDIO_FRAMES = 4096;
  47. const AUDIO_LATENCY_SEC = 0.1;
  48. const MAX_UPDATE_SEC = 5 / 60;
  49. const CPU_TICKS_PER_SECOND = 4194304;
  50. const EVENT_NEW_FRAME = 1;
  51. const EVENT_AUDIO_BUFFER_FULL = 2;
  52. const EVENT_UNTIL_TICKS = 4;
  53. const REWIND_FRAMES_PER_BASE_STATE = 45;
  54. const REWIND_BUFFER_CAPACITY = 4 * 1024 * 1024;
  55. const REWIND_FACTOR = 1.5;
  56. const REWIND_UPDATE_MS = 16;
  57. const GAMEPAD_POLLING_INTERVAL = 1000 / 60 / 4; // When activated, poll for gamepad input about ~4 times per gameboy frame (~240 times second)
  58. const GAMEPAD_KEYMAP_STANDARD_STR = "standard"; // Try to use "standard" HTML5 mapping config if available
  59. const $ = document.querySelector.bind(document);
  60. let emulator = null;
  61. const controllerEl = $("#controller");
  62. const dpadEl = $("#controller_dpad");
  63. const selectEl = $("#controller_select");
  64. const startEl = $("#controller_start");
  65. const bEl = $("#controller_b");
  66. const aEl = $("#controller_a");
  67. const binjgbPromise = Binjgb();
  68. const sgbEnabled = window.location.href.includes("sgb=true");
  69. if (sgbEnabled) {
  70. $("canvas").width = SGB_SCREEN_WIDTH;
  71. $("canvas").height = SGB_SCREEN_HEIGHT;
  72. } else {
  73. $("canvas").width = SCREEN_WIDTH;
  74. $("canvas").height = SCREEN_HEIGHT;
  75. }
  76. // Extract stuff from the vue.js implementation in demo.js.
  77. class VM {
  78. constructor() {
  79. this.ticks = 0;
  80. this.extRamUpdated = false;
  81. this.paused_ = false;
  82. this.volume = 0.5;
  83. this.palIdx = DEFAULT_PALETTE_IDX;
  84. this.canvas = {
  85. show: true,
  86. useSgbBorder: sgbEnabled,
  87. scale: 3,
  88. };
  89. this.rewind = {
  90. minTicks: 0,
  91. maxTicks: 0,
  92. };
  93. setInterval(() => {
  94. if (this.extRamUpdated) {
  95. this.updateExtRam();
  96. this.extRamUpdated = false;
  97. }
  98. }, 1000);
  99. }
  100. get paused() {
  101. return this.paused_;
  102. }
  103. set paused(newPaused) {
  104. let oldPaused = this.paused_;
  105. this.paused_ = newPaused;
  106. if (!emulator) return;
  107. if (newPaused === oldPaused) return;
  108. if (newPaused) {
  109. emulator.pause();
  110. this.ticks = emulator.ticks;
  111. this.rewind.minTicks = emulator.rewind.oldestTicks;
  112. this.rewind.maxTicks = emulator.rewind.newestTicks;
  113. } else {
  114. emulator.resume();
  115. }
  116. }
  117. togglePause() {
  118. this.paused = !this.paused;
  119. }
  120. updateExtRam() {
  121. if (!emulator) return;
  122. const extram = emulator.getExtRam();
  123. localStorage.setItem("extram", JSON.stringify(Array.from(extram)));
  124. }
  125. }
  126. const vm = new VM();
  127. // Load a ROM.
  128. (async function go() {
  129. let response = await fetch(ROM_FILENAME);
  130. let romBuffer = await response.arrayBuffer();
  131. const extRam = new Uint8Array(JSON.parse(localStorage.getItem("extram")));
  132. Emulator.start(await binjgbPromise, romBuffer, extRam);
  133. emulator.setBuiltinPalette(vm.palIdx);
  134. })();
  135. function makeWasmBuffer(module, ptr, size) {
  136. return new Uint8Array(module.HEAP8.buffer, ptr, size);
  137. }
  138. class Emulator {
  139. static start(module, romBuffer, extRamBuffer) {
  140. Emulator.stop();
  141. emulator = new Emulator(module, romBuffer, extRamBuffer);
  142. emulator.run();
  143. }
  144. static stop() {
  145. if (emulator) {
  146. emulator.destroy();
  147. emulator = null;
  148. }
  149. }
  150. constructor(module, romBuffer, extRamBuffer) {
  151. this.module = module;
  152. this.romDataPtr = this.module._malloc(romBuffer.byteLength);
  153. makeWasmBuffer(this.module, this.romDataPtr, romBuffer.byteLength).set(
  154. new Uint8Array(romBuffer)
  155. );
  156. this.e = this.module._emulator_new_simple(
  157. this.romDataPtr,
  158. romBuffer.byteLength,
  159. Audio.ctx.sampleRate,
  160. AUDIO_FRAMES,
  161. CGB_COLOR_CURVE
  162. );
  163. if (this.e == 0) {
  164. throw new Error("Invalid ROM.");
  165. }
  166. this.gamepad = new Gamepad(module, this.e);
  167. this.audio = new Audio(module, this.e);
  168. this.video = new Video(module, this.e, $("canvas"));
  169. this.rewind = new Rewind(module, this.e);
  170. this.rewindIntervalId = 0;
  171. this.lastRafSec = 0;
  172. this.leftoverTicks = 0;
  173. this.fps = 60;
  174. if (extRamBuffer) {
  175. this.loadExtRam(extRamBuffer);
  176. }
  177. this.bindKeys();
  178. this.bindTouch();
  179. this.touchEnabled = "ontouchstart" in document.documentElement;
  180. this.updateOnscreenGamepad();
  181. this.gamepad.init();
  182. }
  183. destroy() {
  184. this.gamepad.shutdown();
  185. this.unbindTouch();
  186. this.unbindKeys();
  187. this.cancelAnimationFrame();
  188. clearInterval(this.rewindIntervalId);
  189. this.rewind.destroy();
  190. this.module._emulator_delete(this.e);
  191. this.module._free(this.romDataPtr);
  192. }
  193. withNewFileData(cb) {
  194. const fileDataPtr = this.module._ext_ram_file_data_new(this.e);
  195. const buffer = makeWasmBuffer(
  196. this.module,
  197. this.module._get_file_data_ptr(fileDataPtr),
  198. this.module._get_file_data_size(fileDataPtr)
  199. );
  200. const result = cb(fileDataPtr, buffer);
  201. this.module._file_data_delete(fileDataPtr);
  202. return result;
  203. }
  204. loadExtRam(extRamBuffer) {
  205. this.withNewFileData((fileDataPtr, buffer) => {
  206. if (buffer.byteLength === extRamBuffer.byteLength) {
  207. buffer.set(new Uint8Array(extRamBuffer));
  208. this.module._emulator_read_ext_ram(this.e, fileDataPtr);
  209. }
  210. });
  211. }
  212. getExtRam() {
  213. return this.withNewFileData((fileDataPtr, buffer) => {
  214. this.module._emulator_write_ext_ram(this.e, fileDataPtr);
  215. return new Uint8Array(buffer);
  216. });
  217. }
  218. get isPaused() {
  219. return this.rafCancelToken === null;
  220. }
  221. pause() {
  222. if (!this.isPaused) {
  223. this.cancelAnimationFrame();
  224. this.audio.pause();
  225. this.beginRewind();
  226. }
  227. }
  228. resume() {
  229. if (this.isPaused) {
  230. this.endRewind();
  231. this.requestAnimationFrame();
  232. this.audio.resume();
  233. }
  234. }
  235. setBuiltinPalette(palIdx) {
  236. this.module._emulator_set_builtin_palette(this.e, PALETTES[palIdx]);
  237. }
  238. get isRewinding() {
  239. return this.rewind.isRewinding;
  240. }
  241. beginRewind() {
  242. this.rewind.beginRewind();
  243. }
  244. rewindToTicks(ticks) {
  245. if (this.rewind.rewindToTicks(ticks)) {
  246. this.runUntil(ticks);
  247. this.video.renderTexture();
  248. }
  249. }
  250. endRewind() {
  251. this.rewind.endRewind();
  252. this.lastRafSec = 0;
  253. this.leftoverTicks = 0;
  254. this.audio.startSec = 0;
  255. }
  256. set autoRewind(enabled) {
  257. if (enabled) {
  258. this.rewindIntervalId = setInterval(() => {
  259. const oldest = this.rewind.oldestTicks;
  260. const start = this.ticks;
  261. const delta =
  262. ((REWIND_FACTOR * REWIND_UPDATE_MS) / 1000) * CPU_TICKS_PER_SECOND;
  263. const rewindTo = Math.max(oldest, start - delta);
  264. this.rewindToTicks(rewindTo);
  265. vm.ticks = emulator.ticks;
  266. }, REWIND_UPDATE_MS);
  267. } else {
  268. clearInterval(this.rewindIntervalId);
  269. this.rewindIntervalId = 0;
  270. }
  271. }
  272. requestAnimationFrame() {
  273. this.rafCancelToken = requestAnimationFrame(this.rafCallback.bind(this));
  274. }
  275. cancelAnimationFrame() {
  276. cancelAnimationFrame(this.rafCancelToken);
  277. this.rafCancelToken = null;
  278. }
  279. run() {
  280. this.requestAnimationFrame();
  281. }
  282. get ticks() {
  283. return this.module._emulator_get_ticks_f64(this.e);
  284. }
  285. runUntil(ticks) {
  286. while (true) {
  287. const event = this.module._emulator_run_until_f64(this.e, ticks);
  288. if (event & EVENT_NEW_FRAME) {
  289. this.rewind.pushBuffer();
  290. this.video.uploadTexture();
  291. }
  292. if (event & EVENT_AUDIO_BUFFER_FULL && !this.isRewinding) {
  293. this.audio.pushBuffer();
  294. }
  295. if (event & EVENT_UNTIL_TICKS) {
  296. break;
  297. }
  298. }
  299. if (this.module._emulator_was_ext_ram_updated(this.e)) {
  300. vm.extRamUpdated = true;
  301. }
  302. }
  303. rafCallback(startMs) {
  304. this.requestAnimationFrame();
  305. let deltaSec = 0;
  306. if (!this.isRewinding) {
  307. const startSec = startMs / 1000;
  308. deltaSec = Math.max(startSec - (this.lastRafSec || startSec), 0);
  309. const startTicks = this.ticks;
  310. const deltaTicks =
  311. Math.min(deltaSec, MAX_UPDATE_SEC) * CPU_TICKS_PER_SECOND;
  312. const runUntilTicks = startTicks + deltaTicks - this.leftoverTicks;
  313. this.runUntil(runUntilTicks);
  314. this.leftoverTicks = (this.ticks - runUntilTicks) | 0;
  315. this.lastRafSec = startSec;
  316. }
  317. const lerp = (from, to, alpha) => alpha * from + (1 - alpha) * to;
  318. this.fps = lerp(this.fps, Math.min(1 / deltaSec, 10000), 0.3);
  319. this.video.renderTexture();
  320. }
  321. updateOnscreenGamepad() {
  322. $("#controller").style.display = this.touchEnabled ? "block" : "none";
  323. }
  324. bindTouch() {
  325. this.touchFuncs = {
  326. controller_b: this.setJoypB.bind(this),
  327. controller_a: this.setJoypA.bind(this),
  328. controller_start: this.setJoypStart.bind(this),
  329. controller_select: this.setJoypSelect.bind(this),
  330. };
  331. this.boundButtonTouchStart = this.buttonTouchStart.bind(this);
  332. this.boundButtonTouchEnd = this.buttonTouchEnd.bind(this);
  333. selectEl.addEventListener("touchstart", this.boundButtonTouchStart);
  334. selectEl.addEventListener("touchend", this.boundButtonTouchEnd);
  335. startEl.addEventListener("touchstart", this.boundButtonTouchStart);
  336. startEl.addEventListener("touchend", this.boundButtonTouchEnd);
  337. bEl.addEventListener("touchstart", this.boundButtonTouchStart);
  338. bEl.addEventListener("touchend", this.boundButtonTouchEnd);
  339. aEl.addEventListener("touchstart", this.boundButtonTouchStart);
  340. aEl.addEventListener("touchend", this.boundButtonTouchEnd);
  341. this.boundDpadTouchStartMove = this.dpadTouchStartMove.bind(this);
  342. this.boundDpadTouchEnd = this.dpadTouchEnd.bind(this);
  343. dpadEl.addEventListener("touchstart", this.boundDpadTouchStartMove);
  344. dpadEl.addEventListener("touchmove", this.boundDpadTouchStartMove);
  345. dpadEl.addEventListener("touchend", this.boundDpadTouchEnd);
  346. this.boundTouchRestore = this.touchRestore.bind(this);
  347. window.addEventListener("touchstart", this.boundTouchRestore);
  348. }
  349. unbindTouch() {
  350. selectEl.removeEventListener("touchstart", this.boundButtonTouchStart);
  351. selectEl.removeEventListener("touchend", this.boundButtonTouchEnd);
  352. startEl.removeEventListener("touchstart", this.boundButtonTouchStart);
  353. startEl.removeEventListener("touchend", this.boundButtonTouchEnd);
  354. bEl.removeEventListener("touchstart", this.boundButtonTouchStart);
  355. bEl.removeEventListener("touchend", this.boundButtonTouchEnd);
  356. aEl.removeEventListener("touchstart", this.boundButtonTouchStart);
  357. aEl.removeEventListener("touchend", this.boundButtonTouchEnd);
  358. dpadEl.removeEventListener("touchstart", this.boundDpadTouchStartMove);
  359. dpadEl.removeEventListener("touchmove", this.boundDpadTouchStartMove);
  360. dpadEl.removeEventListener("touchend", this.boundDpadTouchEnd);
  361. window.removeEventListener("touchstart", this.boundTouchRestore);
  362. }
  363. buttonTouchStart(event) {
  364. if (event.currentTarget.id in this.touchFuncs) {
  365. this.touchFuncs[event.currentTarget.id](true);
  366. event.currentTarget.classList.add("btnPressed");
  367. event.preventDefault();
  368. }
  369. }
  370. buttonTouchEnd(event) {
  371. if (event.currentTarget.id in this.touchFuncs) {
  372. this.touchFuncs[event.currentTarget.id](false);
  373. event.currentTarget.classList.remove("btnPressed");
  374. event.preventDefault();
  375. }
  376. }
  377. dpadTouchStartMove(event) {
  378. const rect = event.currentTarget.getBoundingClientRect();
  379. const x =
  380. (2 * (event.targetTouches[0].clientX - rect.left)) / rect.width - 1;
  381. const y =
  382. (2 * (event.targetTouches[0].clientY - rect.top)) / rect.height - 1;
  383. if (Math.abs(x) > OSGP_DEADZONE) {
  384. if (y > x && y < -x) {
  385. this.setJoypLeft(true);
  386. this.setJoypRight(false);
  387. } else if (y < x && y > -x) {
  388. this.setJoypLeft(false);
  389. this.setJoypRight(true);
  390. }
  391. } else {
  392. this.setJoypLeft(false);
  393. this.setJoypRight(false);
  394. }
  395. if (Math.abs(y) > OSGP_DEADZONE) {
  396. if (x > y && x < -y) {
  397. this.setJoypUp(true);
  398. this.setJoypDown(false);
  399. } else if (x < y && x > -y) {
  400. this.setJoypUp(false);
  401. this.setJoypDown(true);
  402. }
  403. } else {
  404. this.setJoypUp(false);
  405. this.setJoypDown(false);
  406. }
  407. event.preventDefault();
  408. }
  409. dpadTouchEnd(event) {
  410. this.setJoypLeft(false);
  411. this.setJoypRight(false);
  412. this.setJoypUp(false);
  413. this.setJoypDown(false);
  414. event.preventDefault();
  415. }
  416. touchRestore() {
  417. this.touchEnabled = true;
  418. this.updateOnscreenGamepad();
  419. }
  420. bindKeys() {
  421. this.keyFuncs = {
  422. Backspace: this.keyRewind.bind(this),
  423. " ": this.keyPause.bind(this),
  424. "[": this.keyPrevPalette.bind(this),
  425. "]": this.keyNextPalette.bind(this),
  426. };
  427. if (customControls.down && customControls.down.length > 0) {
  428. customControls.down.forEach((k) => {
  429. this.keyFuncs[k] = this.setJoypDown.bind(this);
  430. });
  431. } else {
  432. this.keyFuncs["ArrowDown"] = this.setJoypDown.bind(this);
  433. this.keyFuncs["s"] = this.setJoypDown.bind(this);
  434. }
  435. if (customControls.left && customControls.left.length > 0) {
  436. customControls.left.forEach((k) => {
  437. this.keyFuncs[k] = this.setJoypLeft.bind(this);
  438. });
  439. } else {
  440. this.keyFuncs["ArrowLeft"] = this.setJoypLeft.bind(this);
  441. this.keyFuncs["a"] = this.setJoypLeft.bind(this);
  442. }
  443. if (customControls.right && customControls.right.length > 0) {
  444. customControls.right.forEach((k) => {
  445. this.keyFuncs[k] = this.setJoypRight.bind(this);
  446. });
  447. } else {
  448. this.keyFuncs["ArrowRight"] = this.setJoypRight.bind(this);
  449. this.keyFuncs["d"] = this.setJoypRight.bind(this);
  450. }
  451. if (customControls.up && customControls.up.length > 0) {
  452. customControls.up.forEach((k) => {
  453. this.keyFuncs[k] = this.setJoypUp.bind(this);
  454. });
  455. } else {
  456. this.keyFuncs["ArrowUp"] = this.setJoypUp.bind(this);
  457. this.keyFuncs["w"] = this.setJoypUp.bind(this);
  458. }
  459. if (customControls.a && customControls.a.length > 0) {
  460. customControls.a.forEach((k) => {
  461. this.keyFuncs[k] = this.setJoypA.bind(this);
  462. });
  463. } else {
  464. this.keyFuncs["z"] = this.setJoypA.bind(this);
  465. this.keyFuncs["j"] = this.setJoypA.bind(this);
  466. this.keyFuncs["Alt"] = this.setJoypA.bind(this);
  467. }
  468. if (customControls.b && customControls.b.length > 0) {
  469. customControls.b.forEach((k) => {
  470. this.keyFuncs[k] = this.setJoypB.bind(this);
  471. });
  472. } else {
  473. this.keyFuncs["x"] = this.setJoypB.bind(this);
  474. this.keyFuncs["k"] = this.setJoypB.bind(this);
  475. this.keyFuncs["Control"] = this.setJoypB.bind(this);
  476. }
  477. if (customControls.start && customControls.start.length > 0) {
  478. customControls.start.forEach((k) => {
  479. this.keyFuncs[k] = this.setJoypStart.bind(this);
  480. });
  481. } else {
  482. this.keyFuncs["Enter"] = this.setJoypStart.bind(this);
  483. }
  484. if (customControls.select && customControls.select.length > 0) {
  485. customControls.select.forEach((k) => {
  486. this.keyFuncs[k] = this.setJoypSelect.bind(this);
  487. });
  488. } else {
  489. this.keyFuncs["Shift"] = this.setJoypSelect.bind(this);
  490. }
  491. this.boundKeyDown = this.keyDown.bind(this);
  492. this.boundKeyUp = this.keyUp.bind(this);
  493. window.addEventListener("keydown", this.boundKeyDown);
  494. window.addEventListener("keyup", this.boundKeyUp);
  495. }
  496. unbindKeys() {
  497. window.removeEventListener("keydown", this.boundKeyDown);
  498. window.removeEventListener("keyup", this.boundKeyUp);
  499. }
  500. keyDown(event) {
  501. if (event.key === "w" && (event.metaKey || event.ctrlKey)) {
  502. return;
  503. }
  504. if (event.key in this.keyFuncs) {
  505. if (this.touchEnabled) {
  506. this.touchEnabled = false;
  507. this.updateOnscreenGamepad();
  508. }
  509. this.keyFuncs[event.key](true);
  510. event.preventDefault();
  511. }
  512. }
  513. keyUp(event) {
  514. if (event.key in this.keyFuncs) {
  515. this.keyFuncs[event.key](false);
  516. event.preventDefault();
  517. }
  518. }
  519. keyRewind(isKeyDown) {
  520. if (!ENABLE_REWIND) {
  521. return;
  522. }
  523. if (this.isRewinding !== isKeyDown) {
  524. if (isKeyDown) {
  525. vm.paused = true;
  526. this.autoRewind = true;
  527. } else {
  528. this.autoRewind = false;
  529. vm.paused = false;
  530. }
  531. }
  532. }
  533. keyPause(isKeyDown) {
  534. if (!ENABLE_PAUSE) {
  535. return;
  536. }
  537. if (isKeyDown) vm.togglePause();
  538. }
  539. keyPrevPalette(isKeyDown) {
  540. if (!ENABLE_SWITCH_PALETTES) {
  541. return;
  542. }
  543. if (isKeyDown) {
  544. vm.palIdx = (vm.palIdx + PALETTES.length - 1) % PALETTES.length;
  545. emulator.setBuiltinPalette(vm.palIdx);
  546. }
  547. }
  548. keyNextPalette(isKeyDown) {
  549. if (!ENABLE_SWITCH_PALETTES) {
  550. return;
  551. }
  552. if (isKeyDown) {
  553. vm.palIdx = (vm.palIdx + 1) % PALETTES.length;
  554. emulator.setBuiltinPalette(vm.palIdx);
  555. }
  556. }
  557. setJoypDown(set) {
  558. this.module._set_joyp_down(this.e, set);
  559. }
  560. setJoypUp(set) {
  561. this.module._set_joyp_up(this.e, set);
  562. }
  563. setJoypLeft(set) {
  564. this.module._set_joyp_left(this.e, set);
  565. }
  566. setJoypRight(set) {
  567. this.module._set_joyp_right(this.e, set);
  568. }
  569. setJoypSelect(set) {
  570. this.module._set_joyp_select(this.e, set);
  571. }
  572. setJoypStart(set) {
  573. this.module._set_joyp_start(this.e, set);
  574. }
  575. setJoypB(set) {
  576. this.module._set_joyp_B(this.e, set);
  577. }
  578. setJoypA(set) {
  579. this.module._set_joyp_A(this.e, set);
  580. }
  581. }
  582. class Gamepad {
  583. constructor(module, e) {
  584. this.module = module;
  585. this.e = e;
  586. }
  587. // Load a key map for gamepad-to-gameboy buttons
  588. bindKeys(strMapping) {
  589. this.GAMEPAD_KEYMAP_STANDARD = [
  590. {
  591. gb_key: "b",
  592. gp_button: 0,
  593. type: "button",
  594. gp_bind: this.module._set_joyp_B.bind(null, this.e),
  595. },
  596. {
  597. gb_key: "a",
  598. gp_button: 1,
  599. type: "button",
  600. gp_bind: this.module._set_joyp_A.bind(null, this.e),
  601. },
  602. {
  603. gb_key: "select",
  604. gp_button: 8,
  605. type: "button",
  606. gp_bind: this.module._set_joyp_select.bind(null, this.e),
  607. },
  608. {
  609. gb_key: "start",
  610. gp_button: 9,
  611. type: "button",
  612. gp_bind: this.module._set_joyp_start.bind(null, this.e),
  613. },
  614. {
  615. gb_key: "up",
  616. gp_button: 12,
  617. type: "button",
  618. gp_bind: this.module._set_joyp_up.bind(null, this.e),
  619. },
  620. {
  621. gb_key: "down",
  622. gp_button: 13,
  623. type: "button",
  624. gp_bind: this.module._set_joyp_down.bind(null, this.e),
  625. },
  626. {
  627. gb_key: "left",
  628. gp_button: 14,
  629. type: "button",
  630. gp_bind: this.module._set_joyp_left.bind(null, this.e),
  631. },
  632. {
  633. gb_key: "right",
  634. gp_button: 15,
  635. type: "button",
  636. gp_bind: this.module._set_joyp_right.bind(null, this.e),
  637. },
  638. ];
  639. this.GAMEPAD_KEYMAP_DEFAULT = [
  640. {
  641. gb_key: "a",
  642. gp_button: 0,
  643. type: "button",
  644. gp_bind: this.module._set_joyp_A.bind(null, this.e),
  645. },
  646. {
  647. gb_key: "b",
  648. gp_button: 1,
  649. type: "button",
  650. gp_bind: this.module._set_joyp_B.bind(null, this.e),
  651. },
  652. {
  653. gb_key: "select",
  654. gp_button: 2,
  655. type: "button",
  656. gp_bind: this.module._set_joyp_select.bind(null, this.e),
  657. },
  658. {
  659. gb_key: "start",
  660. gp_button: 3,
  661. type: "button",
  662. gp_bind: this.module._set_joyp_start.bind(null, this.e),
  663. },
  664. {
  665. gb_key: "up",
  666. gp_button: 2,
  667. type: "axis",
  668. gp_bind: this.module._set_joyp_up.bind(null, this.e),
  669. },
  670. {
  671. gb_key: "down",
  672. gp_button: 3,
  673. type: "axis",
  674. gp_bind: this.module._set_joyp_down.bind(null, this.e),
  675. },
  676. {
  677. gb_key: "left",
  678. gp_button: 0,
  679. type: "axis",
  680. gp_bind: this.module._set_joyp_left.bind(null, this.e),
  681. },
  682. {
  683. gb_key: "right",
  684. gp_button: 1,
  685. type: "axis",
  686. gp_bind: this.module._set_joyp_right.bind(null, this.e),
  687. },
  688. ];
  689. // Try to use the w3c "standard" gamepad mapping if available
  690. // (Chrome/V8 seems to do that better than Firefox)
  691. //
  692. // Otherwise use a default mapping that assigns
  693. // A/B/Select/Start to the first four buttons,
  694. // and U/D/L/R to the first two axes.
  695. if (strMapping === GAMEPAD_KEYMAP_STANDARD_STR) {
  696. this.gp.keybinds = this.GAMEPAD_KEYMAP_STANDARD;
  697. } else {
  698. this.gp.keybinds = this.GAMEPAD_KEYMAP_DEFAULT;
  699. }
  700. }
  701. cacheValues(gamepad) {
  702. // Read Buttons
  703. for (let k = 0; k < gamepad.buttons.length; k++) {
  704. // .value is for analog, .pressed is for boolean buttons
  705. this.gp.buttons.cur[k] =
  706. gamepad.buttons[k].value > 0 || gamepad.buttons[k].pressed == true;
  707. // Update state changed if not on first input pass
  708. if (this.gp.buttons.last !== undefined) {
  709. this.gp.buttons.changed[k] =
  710. this.gp.buttons.cur[k] != this.gp.buttons.last[k];
  711. }
  712. }
  713. // Read Axes
  714. for (let k = 0; k < gamepad.axes.length; k++) {
  715. // Decode each dpad axis into two buttons, one for each direction
  716. this.gp.axes.cur[k * 2] = gamepad.axes[k] < 0;
  717. this.gp.axes.cur[k * 2 + 1] = gamepad.axes[k] > 0;
  718. // Update state changed if not on first input pass
  719. if (this.gp.axes.last !== undefined) {
  720. this.gp.axes.changed[k * 2] =
  721. this.gp.axes.cur[k * 2] != this.gp.axes.last[k * 2];
  722. this.gp.axes.changed[k * 2 + 1] =
  723. this.gp.axes.cur[k * 2 + 1] != this.gp.axes.last[k * 2 + 1];
  724. }
  725. }
  726. // Save current state for comparison on next input
  727. this.gp.axes.last = this.gp.axes.cur.slice(0);
  728. this.gp.buttons.last = this.gp.buttons.cur.slice(0);
  729. }
  730. handleButton(keyBind) {
  731. let buttonCache;
  732. // Select button / axis cache based on key bind type
  733. if (keyBind.type === "button") {
  734. buttonCache = this.gp.buttons;
  735. } else if (keyBind.type === "axis") {
  736. buttonCache = this.gp.axes;
  737. }
  738. // Make sure the button exists in the cache array
  739. if (keyBind.gp_button < buttonCache.changed.length) {
  740. // Send the button state if it's changed
  741. if (buttonCache.changed[keyBind.gp_button]) {
  742. if (buttonCache.cur[keyBind.gp_button]) {
  743. // Gamepad Button Down
  744. keyBind.gp_bind(true);
  745. } else {
  746. // Gamepad Button Up
  747. keyBind.gp_bind(false);
  748. }
  749. }
  750. }
  751. }
  752. getCurrent() {
  753. // Chrome requires retrieving a new gamepad object
  754. // every time button state is queried (the existing object
  755. // will have stale button state). Just do that for all browsers
  756. let gamepad = navigator.getGamepads()[this.gp.apiID];
  757. if (gamepad) {
  758. if (gamepad.connected) {
  759. return gamepad;
  760. }
  761. }
  762. return undefined;
  763. }
  764. update() {
  765. let gamepad = this.getCurrent();
  766. if (gamepad !== undefined) {
  767. // Cache gamepad input values
  768. this.cacheValues(gamepad);
  769. // Loop through buttons and send changes if needed
  770. for (let i = 0; i < this.gp.keybinds.length; i++) {
  771. this.handleButton(this.gp.keybinds[i]);
  772. }
  773. } else {
  774. // Gamepad is no longer present, disconnect
  775. this.releaseGamepad();
  776. }
  777. }
  778. startGamepad(gamepad) {
  779. // Make sure it has enough buttons and axes
  780. if (
  781. gamepad.mapping === GAMEPAD_KEYMAP_STANDARD_STR ||
  782. (gamepad.axes.length >= 2 && gamepad.buttons.length >= 4)
  783. ) {
  784. // Save API index for polling (required by Chrome/V8)
  785. this.gp.apiID = gamepad.index;
  786. // Assign gameboy keys to the gamepad
  787. this.bindKeys(gamepad.mapping);
  788. // Start polling the gamepad for input
  789. this.gp.timerID = setInterval(
  790. () => this.update(),
  791. GAMEPAD_POLLING_INTERVAL
  792. );
  793. }
  794. }
  795. releaseGamepad() {
  796. // Stop polling the gamepad for input
  797. if (this.gp.timerID !== undefined) {
  798. clearInterval(this.gp.timerID);
  799. }
  800. // Clear previous button history and controller info
  801. this.gp.axes.last = undefined;
  802. this.gp.buttons.last = undefined;
  803. this.gp.keybinds = undefined;
  804. this.gp.apiID = undefined;
  805. }
  806. // If a gamepad was already connected on this page
  807. // and released, it won't fire another connect event.
  808. // So try to find any that might be present
  809. checkAlreadyConnected() {
  810. let gamepads = navigator.getGamepads();
  811. // If any gamepads are already attached to the page,
  812. // use the first one that is connected
  813. for (let idx = 0; idx < gamepads.length; idx++) {
  814. if (gamepads[idx] !== undefined && gamepads[idx] !== null) {
  815. if (gamepads[idx].connected === true) {
  816. this.startGamepad(gamepads[idx]);
  817. }
  818. }
  819. }
  820. }
  821. // Event handler for when a gamepad is connected
  822. eventConnected(event) {
  823. this.startGamepad(navigator.getGamepads()[event.gamepad.index]);
  824. }
  825. // Event handler for when a gamepad is disconnected
  826. eventDisconnected(event) {
  827. this.releaseGamepad();
  828. }
  829. // Register event connection handlers for gamepads
  830. init() {
  831. // gamepad related vars
  832. this.gp = {
  833. apiID: undefined,
  834. timerID: undefined,
  835. keybinds: undefined,
  836. axes: { last: undefined, cur: [], changed: [] },
  837. buttons: { last: undefined, cur: [], changed: [] },
  838. };
  839. // Check for previously attached gamepads that might
  840. // not emit a gamepadconnected() event
  841. this.checkAlreadyConnected();
  842. this.boundGamepadConnected = this.eventConnected.bind(this);
  843. this.boundGamepadDisconnected = this.eventDisconnected.bind(this);
  844. // When a gamepad connects, start polling it for input
  845. window.addEventListener("gamepadconnected", this.boundGamepadConnected);
  846. // When a gamepad disconnects, shut down polling for input
  847. window.addEventListener(
  848. "gamepaddisconnected",
  849. this.boundGamepadDisconnected
  850. );
  851. }
  852. // Release event connection handlers and settings
  853. shutdown() {
  854. this.releaseGamepad();
  855. window.removeEventListener("gamepadconnected", this.boundGamepadConnected);
  856. window.removeEventListener(
  857. "gamepaddisconnected",
  858. this.boundGamepadDisconnected
  859. );
  860. }
  861. }
  862. class Audio {
  863. constructor(module, e) {
  864. this.started = false;
  865. this.module = module;
  866. this.buffer = makeWasmBuffer(
  867. this.module,
  868. this.module._get_audio_buffer_ptr(e),
  869. this.module._get_audio_buffer_capacity(e)
  870. );
  871. this.startSec = 0;
  872. this.resume();
  873. this.boundStartPlayback = this.startPlayback.bind(this);
  874. window.addEventListener("keydown", this.boundStartPlayback, true);
  875. window.addEventListener("click", this.boundStartPlayback, true);
  876. window.addEventListener("touchend", this.boundStartPlayback, true);
  877. }
  878. startPlayback() {
  879. window.removeEventListener("touchend", this.boundStartPlayback, true);
  880. window.removeEventListener("keydown", this.boundStartPlayback, true);
  881. window.removeEventListener("click", this.boundStartPlayback, true);
  882. this.started = true;
  883. this.resume();
  884. }
  885. get sampleRate() {
  886. return Audio.ctx.sampleRate;
  887. }
  888. pushBuffer() {
  889. if (!this.started) {
  890. return;
  891. }
  892. const nowSec = Audio.ctx.currentTime;
  893. const nowPlusLatency = nowSec + AUDIO_LATENCY_SEC;
  894. const volume = vm.volume;
  895. this.startSec = this.startSec || nowPlusLatency;
  896. if (this.startSec >= nowSec) {
  897. const buffer = Audio.ctx.createBuffer(2, AUDIO_FRAMES, this.sampleRate);
  898. const channel0 = buffer.getChannelData(0);
  899. const channel1 = buffer.getChannelData(1);
  900. for (let i = 0; i < AUDIO_FRAMES; i++) {
  901. channel0[i] = (this.buffer[2 * i] * volume) / 255;
  902. channel1[i] = (this.buffer[2 * i + 1] * volume) / 255;
  903. }
  904. const bufferSource = Audio.ctx.createBufferSource();
  905. bufferSource.buffer = buffer;
  906. bufferSource.connect(Audio.ctx.destination);
  907. bufferSource.start(this.startSec);
  908. const bufferSec = AUDIO_FRAMES / this.sampleRate;
  909. this.startSec += bufferSec;
  910. } else {
  911. console.log(
  912. "Resetting audio (" +
  913. this.startSec.toFixed(2) +
  914. " < " +
  915. nowSec.toFixed(2) +
  916. ")"
  917. );
  918. this.startSec = nowPlusLatency;
  919. }
  920. }
  921. pause() {
  922. if (!this.started) {
  923. return;
  924. }
  925. Audio.ctx.suspend();
  926. }
  927. resume() {
  928. if (!this.started) {
  929. return;
  930. }
  931. Audio.ctx.resume();
  932. }
  933. }
  934. Audio.ctx = new AudioContext();
  935. class Video {
  936. constructor(module, e, el) {
  937. this.module = module;
  938. // Both iPhone and Desktop Safari dont't upscale using image-rendering: pixelated
  939. // on webgl canvases. See https://bugs.webkit.org/show_bug.cgi?id=193895.
  940. // For now, default to Canvas2D.
  941. if (window.navigator.userAgent.match(/iPhone|iPad|15.[0-9] Safari/)) {
  942. this.renderer = new Canvas2DRenderer(el);
  943. } else {
  944. try {
  945. this.renderer = new WebGLRenderer(el);
  946. } catch (error) {
  947. console.log(`Error creating WebGLRenderer: ${error}`);
  948. this.renderer = new Canvas2DRenderer(el);
  949. }
  950. }
  951. this.buffer = makeWasmBuffer(
  952. this.module,
  953. this.module._get_frame_buffer_ptr(e),
  954. this.module._get_frame_buffer_size(e)
  955. );
  956. this.sgbBuffer = makeWasmBuffer(
  957. this.module,
  958. this.module._get_sgb_frame_buffer_ptr(e),
  959. this.module._get_sgb_frame_buffer_size(e)
  960. );
  961. }
  962. uploadTexture() {
  963. this.renderer.uploadTextures(this.buffer, this.sgbBuffer);
  964. }
  965. renderTexture() {
  966. this.renderer.renderTextures();
  967. }
  968. }
  969. class Canvas2DRenderer {
  970. constructor(el) {
  971. this.ctx = el.getContext("2d");
  972. this.imageData = this.ctx.createImageData(SCREEN_WIDTH, SCREEN_HEIGHT);
  973. this.sgbImageData = this.ctx.createImageData(
  974. SGB_SCREEN_WIDTH,
  975. SGB_SCREEN_HEIGHT
  976. );
  977. this.overlayCanvas = document.createElement("canvas");
  978. this.overlayCanvas.width = SGB_SCREEN_WIDTH;
  979. this.overlayCanvas.height = SGB_SCREEN_HEIGHT;
  980. this.overlayCtx = this.overlayCanvas.getContext("2d");
  981. }
  982. uploadTextures(buffer, sgbBuffer) {
  983. this.imageData.data.set(buffer);
  984. this.sgbImageData.data.set(sgbBuffer);
  985. }
  986. renderTextures() {
  987. if (vm.canvas.useSgbBorder) {
  988. this.ctx.putImageData(this.imageData, SGB_SCREEN_LEFT, SGB_SCREEN_TOP);
  989. this.overlayCtx.putImageData(this.sgbImageData, 0, 0);
  990. this.ctx.drawImage(this.overlayCanvas, 0, 0);
  991. } else {
  992. this.ctx.putImageData(this.imageData, 0, 0);
  993. }
  994. }
  995. }
  996. class WebGLRenderer {
  997. constructor(el) {
  998. const gl = (this.gl = el.getContext("webgl", {
  999. preserveDrawingBuffer: true,
  1000. }));
  1001. if (gl === null) {
  1002. throw new Error("unable to create webgl context");
  1003. }
  1004. function compileShader(type, source) {
  1005. const shader = gl.createShader(type);
  1006. gl.shaderSource(shader, source);
  1007. gl.compileShader(shader);
  1008. if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
  1009. throw new Error(`compileShader failed: ${gl.getShaderInfoLog(shader)}`);
  1010. }
  1011. return shader;
  1012. }
  1013. const vertexShader = compileShader(
  1014. gl.VERTEX_SHADER,
  1015. `attribute vec2 aPos;
  1016. attribute vec2 aTexCoord;
  1017. varying highp vec2 vTexCoord;
  1018. void main(void) {
  1019. gl_Position = vec4(aPos, 0.0, 1.0);
  1020. vTexCoord = aTexCoord;
  1021. }`
  1022. );
  1023. const fragmentShader = compileShader(
  1024. gl.FRAGMENT_SHADER,
  1025. `varying highp vec2 vTexCoord;
  1026. uniform sampler2D uSampler;
  1027. void main(void) {
  1028. gl_FragColor = texture2D(uSampler, vTexCoord);
  1029. }`
  1030. );
  1031. const program = gl.createProgram();
  1032. gl.attachShader(program, vertexShader);
  1033. gl.attachShader(program, fragmentShader);
  1034. gl.linkProgram(program);
  1035. if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
  1036. throw new Error(`program link failed: ${gl.getProgramInfoLog(program)}`);
  1037. }
  1038. gl.useProgram(program);
  1039. this.aPos = gl.getAttribLocation(program, "aPos");
  1040. this.aTexCoord = gl.getAttribLocation(program, "aTexCoord");
  1041. this.uSampler = gl.getUniformLocation(program, "uSampler");
  1042. this.fbTexture = this.createTexture();
  1043. this.sgbFbTexture = this.createTexture();
  1044. const invLerpClipSpace = (x, max) => 2 * (x / max) - 1;
  1045. const l = invLerpClipSpace(SGB_SCREEN_LEFT, SGB_SCREEN_WIDTH);
  1046. const r = invLerpClipSpace(SGB_SCREEN_RIGHT, SGB_SCREEN_WIDTH);
  1047. const t = -invLerpClipSpace(SGB_SCREEN_TOP, SGB_SCREEN_HEIGHT);
  1048. const b = -invLerpClipSpace(SGB_SCREEN_BOTTOM, SGB_SCREEN_HEIGHT);
  1049. const w = SCREEN_WIDTH / 256,
  1050. sw = SGB_SCREEN_WIDTH / 256;
  1051. const h = SCREEN_HEIGHT / 256,
  1052. sh = SGB_SCREEN_HEIGHT / 256;
  1053. const verts = new Float32Array([
  1054. // fb only
  1055. -1,
  1056. -1,
  1057. 0,
  1058. h,
  1059. +1,
  1060. -1,
  1061. w,
  1062. h,
  1063. -1,
  1064. +1,
  1065. 0,
  1066. 0,
  1067. +1,
  1068. +1,
  1069. w,
  1070. 0,
  1071. // sgb fb
  1072. l,
  1073. b,
  1074. 0,
  1075. h,
  1076. r,
  1077. b,
  1078. w,
  1079. h,
  1080. l,
  1081. t,
  1082. 0,
  1083. 0,
  1084. r,
  1085. t,
  1086. w,
  1087. 0,
  1088. // sgb border
  1089. -1,
  1090. -1,
  1091. 0,
  1092. sh,
  1093. +1,
  1094. -1,
  1095. sw,
  1096. sh,
  1097. -1,
  1098. +1,
  1099. 0,
  1100. 0,
  1101. +1,
  1102. +1,
  1103. sw,
  1104. 0,
  1105. ]);
  1106. const buffer = gl.createBuffer();
  1107. this.gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  1108. gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);
  1109. gl.enableVertexAttribArray(this.aPos);
  1110. gl.enableVertexAttribArray(this.aTexCoord);
  1111. gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, gl.FALSE, 16, 0);
  1112. gl.vertexAttribPointer(this.aTexCoord, 2, gl.FLOAT, gl.FALSE, 16, 8);
  1113. gl.uniform1i(this.uSampler, 0);
  1114. }
  1115. createTexture() {
  1116. const gl = this.gl;
  1117. const texture = gl.createTexture();
  1118. gl.bindTexture(gl.TEXTURE_2D, texture);
  1119. gl.texImage2D(
  1120. gl.TEXTURE_2D,
  1121. 0,
  1122. gl.RGBA,
  1123. 256,
  1124. 256,
  1125. 0,
  1126. gl.RGBA,
  1127. gl.UNSIGNED_BYTE,
  1128. null
  1129. );
  1130. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  1131. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  1132. return texture;
  1133. }
  1134. uploadTextures(buffer, sgbBuffer) {
  1135. const gl = this.gl;
  1136. gl.bindTexture(gl.TEXTURE_2D, this.fbTexture);
  1137. gl.texSubImage2D(
  1138. gl.TEXTURE_2D,
  1139. 0,
  1140. 0,
  1141. 0,
  1142. SCREEN_WIDTH,
  1143. SCREEN_HEIGHT,
  1144. gl.RGBA,
  1145. gl.UNSIGNED_BYTE,
  1146. buffer
  1147. );
  1148. gl.bindTexture(gl.TEXTURE_2D, this.sgbFbTexture);
  1149. gl.texSubImage2D(
  1150. gl.TEXTURE_2D,
  1151. 0,
  1152. 0,
  1153. 0,
  1154. SGB_SCREEN_WIDTH,
  1155. SGB_SCREEN_HEIGHT,
  1156. gl.RGBA,
  1157. gl.UNSIGNED_BYTE,
  1158. sgbBuffer
  1159. );
  1160. }
  1161. renderTextures() {
  1162. const gl = this.gl;
  1163. gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
  1164. gl.clearColor(0.5, 0.5, 0.5, 1.0);
  1165. gl.clear(gl.COLOR_BUFFER_BIT);
  1166. if (vm.canvas.useSgbBorder) {
  1167. gl.bindTexture(gl.TEXTURE_2D, this.fbTexture);
  1168. gl.drawArrays(gl.TRIANGLE_STRIP, 4, 4);
  1169. gl.enable(gl.BLEND);
  1170. gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
  1171. gl.bindTexture(gl.TEXTURE_2D, this.sgbFbTexture);
  1172. gl.drawArrays(gl.TRIANGLE_STRIP, 8, 4);
  1173. gl.disable(gl.BLEND);
  1174. } else {
  1175. gl.bindTexture(gl.TEXTURE_2D, this.fbTexture);
  1176. gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  1177. }
  1178. }
  1179. }
  1180. class Rewind {
  1181. constructor(module, e) {
  1182. this.module = module;
  1183. this.e = e;
  1184. this.joypadBufferPtr = this.module._joypad_new();
  1185. this.statePtr = 0;
  1186. this.bufferPtr = this.module._rewind_new_simple(
  1187. e,
  1188. REWIND_FRAMES_PER_BASE_STATE,
  1189. REWIND_BUFFER_CAPACITY
  1190. );
  1191. this.module._emulator_set_default_joypad_callback(e, this.joypadBufferPtr);
  1192. }
  1193. destroy() {
  1194. this.module._rewind_delete(this.bufferPtr);
  1195. this.module._joypad_delete(this.joypadBufferPtr);
  1196. }
  1197. get oldestTicks() {
  1198. return this.module._rewind_get_oldest_ticks_f64(this.bufferPtr);
  1199. }
  1200. get newestTicks() {
  1201. return this.module._rewind_get_newest_ticks_f64(this.bufferPtr);
  1202. }
  1203. pushBuffer() {
  1204. if (!this.isRewinding) {
  1205. this.module._rewind_append(this.bufferPtr, this.e);
  1206. }
  1207. }
  1208. get isRewinding() {
  1209. return this.statePtr !== 0;
  1210. }
  1211. beginRewind() {
  1212. if (this.isRewinding) return;
  1213. this.statePtr = this.module._rewind_begin(
  1214. this.e,
  1215. this.bufferPtr,
  1216. this.joypadBufferPtr
  1217. );
  1218. }
  1219. rewindToTicks(ticks) {
  1220. if (!this.isRewinding) return;
  1221. return (
  1222. this.module._rewind_to_ticks_wrapper(this.statePtr, ticks) === RESULT_OK
  1223. );
  1224. }
  1225. endRewind() {
  1226. if (!this.isRewinding) return;
  1227. this.module._emulator_set_default_joypad_callback(
  1228. this.e,
  1229. this.joypadBufferPtr
  1230. );
  1231. this.module._rewind_end(this.statePtr);
  1232. this.statePtr = 0;
  1233. }
  1234. }