From 4fd4b5f788193ac58e151d2b723d355fa015d1b9 Mon Sep 17 00:00:00 2001
From: Adrian Siekierka <kontakt@asie.pl>
Date: Sun, 30 Oct 2022 10:51:32 +0100
Subject: [PATCH] preliminary v86 support

---
 example_v86.html |  75 ++++++++++++++++++
 loader.js        | 192 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 267 insertions(+)
 create mode 100644 example_v86.html

diff --git a/example_v86.html b/example_v86.html
new file mode 100644
index 00000000..75ba0c28
--- /dev/null
+++ b/example_v86.html
@@ -0,0 +1,75 @@
+<!--
+ The Emularity: An Example Computer Loader
+ For use with The Emularity, downloadable at http://www.emularity.com/
+
+ SIMPLE STEPS for trying an emulated computer (FreeDOS).
+
+ * Check out this repository in your browser-accessible directory;
+   this file as well as es6-promise.js, browserfs.min.js and loader.js
+   are required. The logo and images directories are optional, but the
+   splash screen looks quite a lot better when they're available.
+
+ * Clone: https://github.com/asiekierka/v86/tree/emularity
+   and run "make all".
+
+ * Copy bios/seabios.bin, bios/vgabios.bin, build/libv86.js,
+   build/v86.wasm to "emulators/v86".
+
+ * Optionally, acquire an 8x14 EGA font in WOFF format from:
+   https://int10h.org/oldschool-pc-fonts/fontlist/ and copy it to
+   "emulators/v86/ega437.woff".
+
+ * Download FreeDOS from: https://k.copy.sh/freedos722.img
+
+ * Place disk image in an "examples" subdirectory.
+
+ * Visit your example_v86.html file with a modern
+   Javascript-capable browser.
+-->
+
+<html>
+
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+  <title>example computer program</title>
+  <style type="text/css">
+@font-face {
+  font-family: 'Ega437';
+  src: url('emulators/v86/ega437.woff') format('woff');
+}
+.emularity-v86-screen-text {
+  font-family: 'Ega437', monospace !important;
+}
+  </style>
+</head>
+
+<body>
+  <canvas id="canvas" style="width: 50%; height: 50%; display: block; margin: 0 auto;" />
+  <script type="text/javascript" src="es6-promise.js"></script>
+  <script type="text/javascript" src="browserfs.min.js"></script>
+  <script type="text/javascript" src="loader.js"></script>
+  <script type="text/javascript">
+    var emulator = new Emulator(document.querySelector("#canvas"),
+                                null,
+                                new V86Loader(V86Loader.nativeResolution(800, 600),
+                                              V86Loader.scale(1),
+                                              V86Loader.emulatorJS("emulators/v86/libv86.js"),
+                                              V86Loader.emulatorWASM("emulators/v86/v86.wasm"),
+                                              V86Loader.mountFile("seabios.bin",
+                                                                  V86Loader.fetchFile("BIOS",
+                                                                                      "emulators/v86/seabios.bin")),
+                                              V86Loader.mountFile("vgabios.bin",
+                                                                  V86Loader.fetchFile("VGA BIOS",
+                                                                                      "emulators/v86/vgabios.bin")),
+                                              V86Loader.mountFile("freedos.img",
+                                                                 V86Loader.fetchFile("FreeDOS",
+                                                                                      "examples/freedos722.img")),
+                                              V86Loader.bios("seabios.bin"),
+                                              V86Loader.vgaBios("vgabios.bin"),
+                                              V86Loader.fda("freedos.img"),
+                                             ));
+    emulator.start({ waitAfterDownloading: true });
+  </script>
+</body>
+
+</html>
diff --git a/loader.js b/loader.js
index 1e304381..5acde595 100644
--- a/loader.js
+++ b/loader.js
@@ -64,6 +64,7 @@ var Module = null;
                   xmil: img("/images/xmillenium_logo.jpg"),
                   vmac: img("/images/vmac.png"),
                   ruffle: img("/images/ruffle.png"),
+                  v86: img("/images/v86.png"),
                 };
      } else {
        images = { ia: img("other_logos/ia-logo-150x150.png"),
@@ -77,6 +78,7 @@ var Module = null;
                   xmil: img("other_logos/xmillenium_logo.jpg"),
                   vmac: img("other_logos/vmac.png"),
                   ruffle: img("/other_logos/ruffle.png"),
+                  v86: img("other_logos/v86.png"),
                 };
      }
 
@@ -188,6 +190,11 @@ var Module = null;
                                              cfgr = NP2Loader;
                                              get_files = get_vmac_files;
                                            }
+                                           else if (module && module.indexOf("v86") === 0) {
+                                             emulator_logo = images.v86;
+                                             cfgr = V86Loader;
+                                             get_files = get_v86_files;
+                                           }
                                            else if (module) {
                                              emulator_logo = images.mame;
                                              cfgr = MAMELoader;
@@ -605,6 +612,49 @@ var Module = null;
        return files;
      }
 
+     function get_v86_files(cfgr, metadata, modulecfg, filelist) {
+       var files = [];
+
+       if (modulecfg['bios_filename']) {
+         files.push(cfgr.mountFile('/' + modulecfg['bios_filename'], cfgr.fetchFile("BIOS File", get_bios_url(modulecfg['bios_filename']))));
+         files.push(cfgr.bios(modulecfg['bios_filename']));
+       }
+       if (modulecfg['vga_bios_filename']) {
+         files.push(cfgr.mountFile('/' + modulecfg['vga_bios_filename'], cfgr.fetchFile("VGA BIOS File", get_bios_url(modulecfg['vga_bios_filename']))));
+         files.push(cfgr.vgaBios(modulecfg['vga_bios_filename']));
+       }
+
+       var meta = dict_from_xml(metadata),
+           game_files_counter = {};
+       files_with_ext_from_filelist(filelist, meta.emulator_ext).forEach(function (file, i) {
+                                                                           if (modulecfg.peripherals && modulecfg.peripherals[i]) {
+                                                                             game_files_counter[file.name] = modulecfg.peripherals[i];
+                                                                           }
+                                                                         });
+       meta_props_matching(meta, /^v86_drive_([a-zA-Z0-9]+)$/).forEach(function (result) {
+                                                                         var key = result[0], periph = result[1][1];
+                                                                         game_files_counter[meta[key]] = periph;
+                                                                       });
+
+       var game_files = Object.keys(game_files_counter),
+           len = game_files.length;
+       game_files.forEach(function (filename, i) {
+                            var title = "Game File ("+ (i+1) +" of "+ len +")",
+                                ext = filename.match(/\.([^.]*)$/)[1],
+                                url = (filename.includes("/")) ? get_zip_url(filename)
+                                                               : get_zip_url(filename, get_item_name(game)),
+                                periph = game_files_counter[filename],
+                                path = '/' + periph + '.' + ext,
+                                periph_cfg = {};
+                            periph_cfg[periph] = {"path": path};
+                            files.push(cfgr.mountFile(path,
+                                                      cfgr.fetchFile(title, url)));
+                            files.push(periph_cfg);
+                          });
+
+      return files;
+     }
+
      var get_item_name = function (game_path) {
        return game_path.split('/').shift();
      };
@@ -949,6 +999,52 @@ var Module = null;
      return { extra_np2_args: args };
    };
 
+   /**
+    * V86Loader
+    */
+    function V86Loader() {
+      var config = Array.prototype.reduce.call(arguments, extend);
+      config.runner = V86Runner;
+      return config;
+    }
+    V86Loader.__proto__ = BaseLoader;
+ 
+    V86Loader.bios = function (filename) {
+      return {"bios": {"path": filename}}
+    };
+
+    V86Loader.vgaBios = function (filename) {
+      return {"vga_bios": {"path": filename}}
+    };
+
+    V86Loader.fda = function (filename) {
+      return {"fda": {"path": filename}}
+    };
+
+    V86Loader.fdb = function (filename) {
+      return {"fdb": {"path": filename}}
+    };
+
+    V86Loader.hda = function (filename) {
+      return {"hda": {"path": filename}}
+    };
+
+    V86Loader.hdb = function (filename) {
+      return {"hda": {"path": filename}}
+    };
+
+    V86Loader.cdrom = function (filename) {
+      return {"cdrom": {"path": filename}}
+    };
+
+    V86Loader.memorySize = function (amount) {
+      return {"memory_size": amount};
+    };
+
+    V86Loader.vgaMemorySize = function (amount) {
+      return {"vga_memory_size": amount};
+    };
+
    var build_mame_arguments = function (muted, driver, native_resolution, sample_rate, peripheral, autoboot, extra_args, keepaspect, scale) {
      scale = scale || 1;
      var args = [driver,
@@ -1276,6 +1372,101 @@ var Module = null;
      getfullscreenenabler().call(this._canvas);
    };
 
+   /*
+    * V86Runner
+    */
+   function V86Runner(canvas, game_data) {
+     // v86 needs a specific DOM structure instead of a canvas
+     var screenContainerOuterElt = document.createElement("div");
+     screenContainerOuterElt.id = canvas.id;
+     screenContainerOuterElt.classList = canvas.classList;
+     screenContainerOuterElt.style = canvas.style;
+
+     var screenContainerInnerElt = document.createElement("div");
+     screenContainerInnerElt.classList = ["emularity-v86-screen-container"];
+     screenContainerInnerElt.style = "display:flex;justify-content:center;align-items:center;background-color:#000;";
+
+     var textDivElt = document.createElement("div");
+     textDivElt.classList = ["emularity-v86-screen-text"];
+     textDivElt.style = "font-size:14px;font-family:monospace;line-height:14px;white-space:pre;";
+     var canvasElt = document.createElement("canvas");
+     canvasElt.classList = ["emularity-v86-screen-canvas"];
+     canvasElt.style = "display:none;";
+
+     screenContainerInnerElt.appendChild(textDivElt);
+     screenContainerInnerElt.appendChild(canvasElt);
+     screenContainerOuterElt.appendChild(screenContainerInnerElt);
+     canvas.parentNode.replaceChild(screenContainerOuterElt, canvas);
+
+     var cfg = {};
+     cfg.screen_container = screenContainerInnerElt;
+     cfg.memory_size = game_data.memory_size || 32 << 20;
+     cfg.vga_memory_size = game_data.vga_memory_size || 2 << 20;
+     cfg.autostart = true; // FIXME
+
+     cfg.wasm_fn = env => {
+       return new Promise(async resolve => {
+         const wasm = await WebAssembly.instantiate(game_data.wasmBinary, env);
+         resolve(wasm.instance.exports);
+       });
+     };
+     ["bios", "vga_bios", "fda", "fdb", "cdrom", "hda", "hdb"].forEach(key => {
+       if (game_data[key] && game_data[key]["path"]) {
+         cfg[key] = {"buffer": game_data.fs.readFileSync('/'+game_data[key]["path"], null, flag_r).buffer};
+       }
+     });
+
+     var emu = new V86Starter(cfg);
+     this._emulator = emu;
+     this.ready = null;
+
+     if (game_data["scale"]) {
+       emu.screen_set_scale(game_data["scale"], game_data["scale"]);
+     }
+
+     screenContainerInnerElt.addEventListener('click', function (e) {
+       emu.lock_mouse();
+     });
+   }
+
+   V86Runner.prototype.start = function () {
+     // this._emulator.run(); // FIXME
+   };
+
+   V86Runner.prototype.pause = function () {
+     this._emulator.stop();
+   };
+
+   V86Runner.prototype.stop = function () {
+     this._emulator.stop();
+   };
+
+   V86Runner.prototype.mute = function () {
+     if (this._emulator.is_muted) {
+       this._emulator.speaker_adapter.mixer.set_volume(1, undefined);
+       this._emulator.is_muted = false;
+     }
+   };
+
+   V86Runner.prototype.unmute = function () {
+     if (!this._emulator.is_muted) {
+       this._emulator.speaker_adapter.mixer.set_volume(0, undefined);
+       this._emulator.is_muted = true;
+     }
+   };
+
+   V86Runner.prototype.onStarted = function (func) {
+     this._emulator.add_listener("emulator-started", func);
+   };
+
+   V86Runner.prototype.onReset = function (func) {
+     // not supported
+   };
+
+   V86Runner.prototype.requestFullScreen = function () {
+     getfullscreenenabler().call(this._canvas);
+   };
+
    /*
     * RuffleRunner
     */
@@ -2202,6 +2393,7 @@ var Module = null;
    window.PCELoader = PCELoader;
    window.VICELoader = VICELoader;
    window.NP2Loader = NP2Loader;
+   window.V86Loader = V86Loader;
    window.RuffleLoader = RuffleLoader;
    window.Emulator = Emulator;
    window._SDL_CreateRGBSurfaceFrom = _SDL_CreateRGBSurfaceFrom;