index.html623 lines · 27.8 KB · raw
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>