1<!DOCTYPE html> 2<html lang="en"> 3 <head> 4 <title>jgenesis web</title> 5 <meta charset="UTF-8"> 6 <style> 7 .hidden { 8 visibility: hidden; 9 } 10 11 .cursor-hidden { 12 cursor: none; 13 } 14 15 body { 16 background-color: black; 17 color: white; 18 font-family: sans-serif; 19 } 20 21 /* File dialog has a white background, so font color needs to be not white */ 22 #rfd-card { 23 color: black; 24 } 25 26 canvas { 27 margin-top: 15px; 28 } 29 30 fieldset { 31 margin-top: 10px; 32 } 33 34 input[type="checkbox"] { 35 margin-top: 15px; 36 } 37 38 #loading-text { 39 font-size: 60px; 40 text-align: center; 41 } 42 43 #header-text { 44 font-size: 24px; 45 margin-left: 10px; 46 } 47 48 #jgenesis { 49 display: flex; 50 flex-direction: row; 51 } 52 53 #jgenesis-rom-title { 54 text-align: center; 55 margin-top: 10px; 56 } 57 58 .jgenesis-controls { 59 text-align: center; 60 margin-top: 10px; 61 } 62 63 .jgenesis-controls > input { 64 margin-left: auto; 65 margin-right: auto; 66 } 67 68 #jgenesis-config { 69 margin-left: 30px; 70 } 71 72 #footer { 73 text-align: center; 74 } 75 76 .darken { 77 opacity: 0.5; 78 } 79 </style> 80 </head> 81 <body> 82 <div id="loading-text">Loading...</div> 83 <div id="header-text" class="hidden hide-fullscreen"> 84 jgenesis web 85 </div> 86 <div id="jgenesis" class="hidden"> 87 <div id="jgenesis-wasm-and-controls"> 88 <div id="jgenesis-wasm"></div> 89 <div id="jgenesis-rom-title" class="hide-fullscreen">(No ROM loaded)</div> 90 <div class="jgenesis-controls hide-fullscreen"> 91 <input type="button" id="open-file" value="Open ROM file"> 92 <input type="button" id="reset-emulator" value="Reset"> 93 <input type="button" id="download-save-file" class="save-button" value="Download save file" disabled> 94 <input type="button" id="upload-save-file" class="save-button" value="Upload save file and reset" disabled> 95 </div> 96 <div class="jgenesis-controls hide-fullscreen"> 97 <input type="button" id="sega-cd-bios" value="Configure Sega CD BIOS"> 98 <input type="button" id="gba-bios" value="Configure GBA BIOS"> 99 </div> 100 </div> 101 <div id="jgenesis-config" class="hide-fullscreen"> 102 <div id="common-config"> 103 <fieldset> 104 <legend>Image filtering</legend> 105 106 <input type="radio" id="image-filter-nearest" name="image-filter" value="Nearest"> 107 <label for="image-filter-nearest">Nearest neighbor</label> 108 109 <input type="radio" id="image-filter-linear" name="image-filter" value="Linear" checked> 110 <label for="image-filter-linear">Linear interpolation</label> 111 </fieldset> 112 113 <fieldset> 114 <legend>Blending shader</legend> 115 116 <input type="radio" id="blend-shader-none" name="blend-shader" value="None" checked> 117 <label for="blend-shader-none">None</label> 118 119 <input type="radio" id="blend-shader-ntsc" name="blend-shader" value="NtscComposite"> 120 <label for="blend-shader-ntsc">NTSC composite</label> 121 122 <input type="radio" id="blend-shader-xbrz" name="blend-shader" value="Xbrz4x"> 123 <label for="blend-shader-xbrz">xBRZ</label> 124 125 <input type="radio" id="blend-shader-hblur-3px" name="blend-shader" value="HorizontalBlurThreePixels"> 126 <label for="blend-shader-hblur-3px">Horizontal blur</label> 127 </fieldset> 128 129 <input type="checkbox" id="auto-prescale" name="auto-prescale" checked> 130 <label for="auto-prescale">Prescaling enabled</label> 131 </div> 132 <div id="smsgg-config" hidden> 133 <fieldset> 134 <legend>Timing / display mode</legend> 135 136 <input type="radio" id="sms-timing-mode-ntsc" name="sms-timing-mode" value="Ntsc" checked> 137 <label for="sms-timing-mode-ntsc">NTSC</label> 138 139 <input type="radio" id="sms-timing-mode-pal" name="sms-timing-mode" value="Pal"> 140 <label for="sms-timing-mode-pal">PAL</label> 141 </fieldset> 142 143 <fieldset> 144 <legend>Sega Master System aspect ratio</legend> 145 146 <input type="radio" id="sms-aspect-ratio-ntsc" name="sms-aspect-ratio" value="Ntsc" checked> 147 <label for="sms-aspect-ratio-ntsc">NTSC</label> 148 149 <input type="radio" id="sms-aspect-ratio-pal" name="sms-aspect-ratio" value="Pal"> 150 <label for="sms-aspect-ratio-pal">PAL</label> 151 152 <input type="radio" id="sms-aspect-ratio-square-pixels" name="sms-aspect-ratio" value="SquarePixels"> 153 <label for="sms-aspect-ratio-square-pixels">Square pixels</label> 154 </fieldset> 155 156 <fieldset> 157 <legend>Game Gear aspect ratio</legend> 158 159 <input type="radio" id="gg-aspect-ratio-lcd" name="gg-aspect-ratio" value="GgLcd" checked> 160 <label for="gg-aspect-ratio-lcd">Game Gear LCD</label> 161 162 <input type="radio" id="gg-aspect-ratio-square-pixels" name="gg-aspect-ratio" value="SquarePixels"> 163 <label for="gg-aspect-ratio-square-pixels">Square pixels</label> 164 </fieldset> 165 166 <div> 167 <input style="margin-top: 20px;" type="checkbox" id="sms-crop-vertical-border" checked> 168 <label for="sms-crop-vertical-border">Crop SMS vertical border</label> 169 </div> 170 171 <div> 172 <input type="checkbox" id="sms-crop-left-border"> 173 <label for="sms-crop-left-border">Crop SMS left border</label> 174 </div> 175 176 <div> 177 <input type="checkbox" id="sms-fm-enabled" checked> 178 <label for="sms-fm-enabled">FM sound unit enabled (requires reset)</label> 179 </div> 180 181 <div> 182 <input type="checkbox" id="sms-remove-sprite-limit"> 183 <label for="sms-remove-sprite-limit">Remove sprite-per-scanline limit</label> 184 </div> 185 </div> 186 <div id="genesis-config" hidden> 187 <fieldset> 188 <legend>Aspect ratio</legend> 189 190 <input type="radio" id="gen-aspect-auto" name="gen-aspect-ratio" value="Auto"> 191 <label for="gen-aspect-auto">Auto</label> 192 193 <input type="radio" id="gen-aspect-ntsc" name="gen-aspect-ratio" value="Ntsc"> 194 <label for="gen-aspect-ntsc">NTSC</label> 195 196 <input type="radio" id="gen-aspect-pal" name="gen-aspect-ratio" value="Pal"> 197 <label for="gen-aspect-pal">PAL</label> 198 199 <input type="radio" id="gen-aspect-square-pixels" name="gen-aspect-ratio" value="SquarePixels"> 200 <label for="gen-aspect-square-pixels">Square pixels</label> 201 </fieldset> 202 203 <fieldset> 204 <legend>Main CPU speed</legend> 205 206 <input type="radio" id="gen-divider-7" name="gen-m68k-divider" value="7" checked> 207 <label for="gen-divider-7">100%</label> 208 209 <input type="radio" id="gen-divider-6" name="gen-m68k-divider" value="6"> 210 <label for="gen-divider-6">117%</label> 211 212 <input type="radio" id="gen-divider-5" name="gen-m68k-divider" value="5"> 213 <label for="gen-divider-5">140%</label> 214 215 <input type="radio" id="gen-divider-4" name="gen-m68k-divider" value="4"> 216 <label for="gen-divider-4">175%</label> 217 218 <input type="radio" id="gen-divider-3" name="gen-m68k-divider" value="3"> 219 <label for="gen-divider-3">233%</label> 220 </fieldset> 221 222 <div> 223 <input type="checkbox" id="genesis-non-linear-color-scale" checked> 224 <label for="genesis-non-linear-color-scale">Emulate non-linear VDP color scale</label> 225 </div> 226 227 <div> 228 <input type="checkbox" id="genesis-remove-sprite-limits"> 229 <label for="genesis-remove-sprite-limits">Remove sprite-per-scanline and sprite-pixel-per-scanline limits</label> 230 </div> 231 232 <div> 233 <input type="checkbox" id="genesis-emulate-low-pass" checked> 234 <label for="genesis-emulate-low-pass">Emulate 3.39 KHz low-pass filter</label> 235 </div> 236 237 <div> 238 <input type="checkbox" id="genesis-render-vertical-border"> 239 <label for="genesis-render-vertical-border">Render vertical border</label> 240 </div> 241 242 <div> 243 <input type="checkbox" id="genesis-render-horizontal-border"> 244 <label for="genesis-render-horizontal-border">Render horizontal border</label> 245 </div> 246 </div> 247 <div id="snes-config" hidden> 248 <fieldset> 249 <legend>Aspect ratio</legend> 250 251 <input type="radio" id="snes-aspect-ntsc" name="snes-aspect-ratio" value="Ntsc" checked> 252 <label for="snes-aspect-ntsc">NTSC</label> 253 254 <input type="radio" id="snes-aspect-pal" name="snes-aspect-ratio" value="Pal"> 255 <label for="snes-aspect-pal">PAL</label> 256 257 <input type="radio" id="snes-aspect-square-pixels" name="snes-aspect-ratio" value="SquarePixels"> 258 <label for="snes-aspect-square-pixels">Square pixels</label> 259 </fieldset> 260 261 <fieldset> 262 <legend>ADPCM sample interpolation</legend> 263 264 <input type="radio" id="snes-audio-gaussian" name="snes-audio-interpolation" value="Gaussian" checked> 265 <label for="snes-audio-gaussian">Gaussian</label> 266 267 <input type="radio" id="snes-audio-hermite" name="snes-audio-interpolation" value="Hermite"> 268 <label for="snes-audio-hermite">Cubic Hermite</label> 269 </fieldset> 270 </div> 271 <div id="gba-config" hidden> 272 <fieldset> 273 <legend>Color correction</legend> 274 275 <input type="radio" id="gba-color-correct-none" name="gba-color-correct" value="None"> 276 <label for="gba-color-correct-none">None</label> 277 278 <input type="radio" id="gba-color-correct-gbalcd" name="gba-color-correct" value="GbaLcd"> 279 <label for="gba-color-correct-gbalcd">Game Boy Advance LCD</label> 280 </fieldset> 281 282 <fieldset> 283 <legend>Audio interpolation</legend> 284 285 <input type="radio" id="gba-audio-interpolation-nearest" name="gba-audio-interpolation" value="NearestNeighbor"> 286 <label for="gba-audio-interpolation-nearest">Nearest neighbor</label> 287 288 <input type="radio" id="gba-audio-interpolation-cubic" name="gba-audio-interpolation" value="CubicHermite"> 289 <label for="gba-audio-interpolation-cubic">Cubic Hermite</label> 290 291 <input type="radio" id="gba-audio-interpolation-sinc" name="gba-audio-interpolation" value="WindowedSinc"> 292 <label for="gba-audio-interpolation-sinc">Windowed sinc</label> 293 </fieldset> 294 295 <div> 296 <input type="checkbox" id="gba-skip-bios-animation"> 297 <label for="gba-skip-bios-animation">Skip BIOS intro animation</label> 298 </div> 299 300 <div> 301 <input type="checkbox" id="gba-frame-blending"> 302 <label for="gba-frame-blending">Interframe blending</label> 303 </div> 304 305 <div> 306 <input type="checkbox" id="gba-psg-low-pass"> 307 <label for="gba-psg-low-pass">Apply low-pass filter to PSG (enhanced interpolation only)</label> 308 </div> 309 </div> 310 <div style="margin-top:10px;"> 311 <input type="button" id="restore-defaults" value="Restore default config"> 312 </div> 313 <div id="supported-files-info" style="margin-top:10px;"> 314 <p>Supported platforms in this interface:</p> 315 <ul> 316 <li>Sega Genesis (.md / .gen / .bin / .smd)</li> 317 <li>Sega CD (.chd)</li> 318 <li>Sega 32X (.32x)</li> 319 <li>Sega Master System (.sms)</li> 320 <li>Game Gear (.gg)</li> 321 <li>SNES (.sfc / .smc)</li> 322 <li>Game Boy Advance (.gba)</li> 323 </ul> 324 <p>Sega CD and GBA require a BIOS ROM to be configured</p> 325 <p>32X is extremely CPU-intensive and may not run at full speed</p> 326 </div> 327 <div id="input-config" hidden> 328 <p>Controls:</p> 329 <div id="controls"></div> 330 <div>Press F8 to toggle fullscreen</div> 331 </div> 332 </div> 333 </div> 334 <div id="footer" class="hide-fullscreen" hidden> 335 <p>Built from <span id="build-commit-link"></span></p> 336 <p>Source and native version: <a href="https://github.com/jsgroth/jgenesis">https://github.com/jsgroth/jgenesis</a></p> 337 </div> 338 <script type="module"> 339 import init, { 340 EmulatorChannel, 341 WebConfigRef, 342 build_commit_hash, 343 run_emulator, 344 } from "./pkg/jgenesis_web.js"; 345 346 await init(); 347 348 let config = new WebConfigRef(); 349 let channel = new EmulatorChannel(); 350 351 function setChecked(elem, value) { 352 elem.checked = value; 353 } 354 355 function initConfigUi() { 356 // Common UI 357 document.querySelectorAll("input[name='image-filter']").forEach((elem) => { 358 setChecked(elem, elem.getAttribute("value") === config.filter_mode()); 359 }); 360 361 document.querySelectorAll("input[name='blend-shader']").forEach((elem) => { 362 setChecked(elem, elem.getAttribute("value") === config.preprocess_shader()); 363 }); 364 365 setChecked(document.getElementById("auto-prescale"), config.auto_prescale()); 366 367 // SMS/GG UI 368 document.querySelectorAll("input[name='sms-timing-mode']").forEach((elem) => { 369 setChecked(elem, elem.getAttribute("value") === config.sms_timing_mode()); 370 }); 371 372 document.querySelectorAll("input[name='sms-aspect-ratio']").forEach((elem) => { 373 setChecked(elem, elem.getAttribute("value") === config.sms_aspect_ratio()); 374 }); 375 376 document.querySelectorAll("input[name='gg-aspect-ratio']").forEach((elem) => { 377 setChecked(elem, elem.getAttribute("value") === config.gg_aspect_ratio()); 378 }); 379 380 setChecked(document.getElementById("sms-crop-vertical-border"), config.sms_crop_vertical_border()); 381 setChecked(document.getElementById("sms-crop-left-border"), config.sms_crop_left_border()); 382 setChecked(document.getElementById("sms-fm-enabled"), config.sms_fm_enabled()); 383 setChecked(document.getElementById("sms-remove-sprite-limit"), config.sms_remove_sprite_limit()); 384 385 // Genesis UI 386 document.querySelectorAll("input[name='gen-aspect-ratio']").forEach((elem) => { 387 setChecked(elem, elem.getAttribute("value") === config.genesis_aspect_ratio()); 388 }); 389 390 document.querySelectorAll("input[name='gen-m68k-divider']").forEach((elem) => { 391 setChecked(elem, parseInt(elem.getAttribute("value")) === config.genesis_m68k_divider()); 392 }); 393 394 setChecked(document.getElementById("genesis-non-linear-color-scale"), config.genesis_non_linear_color_scale()); 395 setChecked(document.getElementById("genesis-remove-sprite-limits"), config.genesis_remove_sprite_limits()); 396 setChecked(document.getElementById("genesis-emulate-low-pass"), config.genesis_emulate_low_pass()); 397 setChecked(document.getElementById("genesis-render-vertical-border"), config.genesis_render_vertical_border()); 398 setChecked(document.getElementById("genesis-render-horizontal-border"), config.genesis_render_horizontal_border()); 399 400 // SNES UI 401 document.querySelectorAll("input[name='snes-aspect-ratio']").forEach((elem) => { 402 setChecked(elem, elem.getAttribute("value") === config.snes_aspect_ratio()); 403 }); 404 405 document.querySelectorAll("input[name='snes-audio-interpolation']").forEach((elem) => { 406 setChecked(elem, elem.getAttribute("value") === config.snes_audio_interpolation()); 407 }); 408 409 // GBA UI 410 document.querySelectorAll("input[name='gba-color-correct']").forEach((elem) => { 411 setChecked(elem, elem.getAttribute("value") === config.gba_color_correction()); 412 }); 413 414 document.querySelectorAll("input[name='gba-audio-interpolation']").forEach((elem) => { 415 setChecked(elem, elem.getAttribute("value") === config.gba_audio_interpolation()); 416 }); 417 418 setChecked(document.getElementById("gba-skip-bios-animation"), config.gba_skip_bios_animation()); 419 setChecked(document.getElementById("gba-frame-blending"), config.gba_frame_blending()); 420 setChecked(document.getElementById("gba-psg-low-pass"), config.gba_psg_low_pass()); 421 } 422 423 initConfigUi(); 424 425 function downloadSaveFile() { 426 const currentFileName = channel.current_file_name(); 427 428 const openReq = indexedDB.open("saves"); 429 openReq.onsuccess = (event) => { 430 const db = event.target.result; 431 const tx = db.transaction(["files"]); 432 const objectStore = tx.objectStore("files"); 433 434 const getReq = objectStore.get(currentFileName); 435 getReq.onsuccess = (event) => { 436 const value = event.target.result; 437 if (!value || !value["data"] || !value["data"]["sav"]) { 438 alert(`No save file found for ${currentFileName}`); 439 return; 440 } 441 442 const saveBytes = value["data"]["sav"]; 443 444 const saveFileName = currentFileName.replace(/\.[a-zA-Z]*$/, ".sav"); 445 446 const a = document.createElement("a"); 447 a.href = window.URL.createObjectURL(new Blob([saveBytes], {type: "application/octet-stream"})); 448 a.download = saveFileName; 449 450 document.body.appendChild(a); 451 a.click(); 452 document.body.removeChild(a); 453 }; 454 }; 455 } 456 457 document.getElementById("open-file").addEventListener("click", () => { 458 channel.request_open_file(); 459 }); 460 461 document.getElementById("sega-cd-bios").addEventListener("click", () => { 462 channel.request_open_sega_cd_bios(); 463 }); 464 465 document.getElementById("gba-bios").addEventListener("click", () => { 466 channel.request_open_gba_bios(); 467 }); 468 469 document.getElementById("reset-emulator").addEventListener("click", () => { 470 channel.request_reset(); 471 }); 472 473 document.getElementById("download-save-file").addEventListener("click", downloadSaveFile); 474 475 document.getElementById("upload-save-file").addEventListener("click", () => { 476 channel.request_upload_save_file(); 477 }); 478 479 document.querySelectorAll("input[name='image-filter']").forEach((element) => { 480 element.addEventListener("click", (event) => { 481 config.set_filter_mode(event.target.value); 482 }); 483 }); 484 485 document.querySelectorAll("input[name='blend-shader']").forEach((element) => { 486 element.addEventListener("click", (event) => { 487 config.set_preprocess_shader(event.target.value); 488 }); 489 }); 490 491 document.getElementById("auto-prescale").addEventListener("click", (event) => { 492 config.set_auto_prescale(event.target.checked); 493 }); 494 495 document.querySelectorAll("input[name='sms-timing-mode']").forEach((element) => { 496 element.addEventListener("click", (event) => { 497 config.set_sms_timing_mode(event.target.value); 498 }); 499 }); 500 501 document.querySelectorAll("input[name='sms-aspect-ratio']").forEach((element) => { 502 element.addEventListener("click", (event) => { 503 config.set_sms_aspect_ratio(event.target.value); 504 }); 505 }); 506 507 document.querySelectorAll("input[name='gg-aspect-ratio']").forEach((element) => { 508 element.addEventListener("click", (event) => { 509 config.set_gg_aspect_ratio(event.target.value); 510 }); 511 }); 512 513 document.getElementById("sms-crop-vertical-border").addEventListener("click", (event) => { 514 config.set_sms_crop_vertical_border(event.target.checked); 515 }); 516 517 document.getElementById("sms-crop-left-border").addEventListener("click", (event) => { 518 config.set_sms_crop_left_border(event.target.checked); 519 }); 520 521 document.getElementById("sms-fm-enabled").addEventListener("click", (event) => { 522 config.set_sms_fm_enabled(event.target.checked); 523 }); 524 525 document.getElementById("sms-remove-sprite-limit").addEventListener("click", (event) => { 526 config.set_sms_remove_sprite_limit(event.target.checked); 527 }); 528 529 document.querySelectorAll("input[name='gen-aspect-ratio']").forEach((element) => { 530 element.addEventListener("click", (event) => { 531 config.set_genesis_aspect_ratio(event.target.value); 532 }); 533 }); 534 535 document.querySelectorAll("input[name='gen-m68k-divider']").forEach((element) => { 536 element.addEventListener("click", (event) => { 537 config.set_genesis_m68k_divider(event.target.value); 538 }); 539 }); 540 541 document.getElementById("genesis-remove-sprite-limits").addEventListener("click", (event) => { 542 config.set_genesis_remove_sprite_limits(event.target.checked); 543 }); 544 545 document.getElementById("genesis-non-linear-color-scale").addEventListener("click", (event) => { 546 config.set_genesis_non_linear_color_scale(event.target.checked); 547 }); 548 549 document.getElementById("genesis-emulate-low-pass").addEventListener("click", (event) => { 550 config.set_genesis_emulate_low_pass(event.target.checked); 551 }); 552 553 document.getElementById("genesis-render-vertical-border").addEventListener("click", (event) => { 554 config.set_genesis_render_vertical_border(event.target.checked); 555 }); 556 557 document.getElementById("genesis-render-horizontal-border").addEventListener("click", (event) => { 558 config.set_genesis_render_horizontal_border(event.target.checked); 559 }); 560 561 document.querySelectorAll("input[name='snes-aspect-ratio']").forEach((element) => { 562 element.addEventListener("click", (event) => { 563 config.set_snes_aspect_ratio(event.target.value); 564 }); 565 }); 566 567 document.querySelectorAll("input[name='snes-audio-interpolation']").forEach((element) => { 568 element.addEventListener("click", (event) => { 569 config.set_snes_audio_interpolation(event.target.value); 570 }); 571 }); 572 573 document.querySelectorAll("input[name='gba-color-correct']").forEach((element) => { 574 element.addEventListener("click", (event) => { 575 config.set_gba_color_correction(event.target.value); 576 }); 577 }); 578 579 document.querySelectorAll("input[name='gba-audio-interpolation']").forEach((element) => { 580 element.addEventListener("click", (event) => { 581 config.set_gba_audio_interpolation(event.target.value); 582 }); 583 }); 584 585 document.getElementById("gba-skip-bios-animation").addEventListener("click", (event) => { 586 config.set_gba_skip_bios_animation(event.target.checked); 587 }); 588 589 document.getElementById("gba-frame-blending").addEventListener("click", (event) => { 590 config.set_gba_frame_blending(event.target.checked); 591 }); 592 593 document.getElementById("gba-psg-low-pass").addEventListener("click", (event) => { 594 config.set_gba_psg_low_pass(event.target.checked); 595 }); 596 597 window.inputClickListener = (event) => { 598 let inputName = event.target.getAttribute("data-name"); 599 channel.request_configure_input(inputName); 600 }; 601 602 document.getElementById("restore-defaults").addEventListener("click", () => { 603 config.restore_defaults(); 604 initConfigUi(); 605 }); 606 607 (() => { 608 let buildCommitHash = build_commit_hash(); 609 let commitLinkSpan = document.getElementById("build-commit-link"); 610 if (buildCommitHash) { 611 let link = document.createElement("a"); 612 link.href = `https://github.com/jsgroth/jgenesis/commit/${buildCommitHash}`; 613 link.innerText = buildCommitHash; 614 commitLinkSpan.append(link); 615 } else { 616 commitLinkSpan.innerText = "<unknown>"; 617 } 618 })(); 619 620 await run_emulator(config.clone(), channel.clone()); 621 </script> 622 </body> 623</html>