diff --git a/Makefile b/Makefile index a01ca1e729..340ca9bcc4 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,7 @@ CARGO_FLAGS_SAFE=\ CARGO_FLAGS=$(CARGO_FLAGS_SAFE) -C target-feature=+bulk-memory -C target-feature=+multivalue -C target-feature=+simd128 CORE_FILES=cjs.js const.js io.js main.js lib.js buffer.js ide.js pci.js floppy.js \ - dma.js pit.js vga.js ps2.js rtc.js uart.js \ + dma.js pit.js vga.js ps2.js rtc.js uart.js vmware.js \ acpi.js iso9660.js \ state.js ne2k.js sb16.js virtio.js virtio_console.js virtio_net.js virtio_balloon.js \ bus.js log.js cpu.js \ diff --git a/docs/windows-9x.md b/docs/windows-9x.md index d72f080e61..1d2c76149f 100644 --- a/docs/windows-9x.md +++ b/docs/windows-9x.md @@ -101,6 +101,18 @@ The default VGA display driver only supports 640x480x4 video mode, to fix this, 5. Select "VESA ISA" adapter and press "OK". 6. After installing, restart Windows. +## Enabling absolute mouse positioning (VBMOUSE) + +v86 emulates the VMware absolute pointing device. With an absolute mouse driver installed in the guest, the guest cursor follows the host cursor directly, without having to lock the mouse. + +[VBADOS](https://git.javispedro.com/cgit/vbados.git/about/) provides VBMOUSE, an open-source DOS mouse driver with VMware mouse support, together with a 16-bit Windows mouse driver on top of it that Windows 9x can use. + +1. Download [vbados.flp](https://depot.javispedro.com/vbox/vbados/vbados.flp) and mount it as a floppy image (or copy its contents into the guest in some other way). +2. Copy `VBMOUSE.EXE` from the floppy to the hard disk (for example to `C:\VBADOS`) and `VBMOUSE.DRV` to `C:\WINDOWS\SYSTEM`. +3. Add `C:\VBADOS\VBMOUSE.EXE` to `C:\AUTOEXEC.BAT`, so the DOS part of the driver is loaded before Windows starts. +4. In `C:\WINDOWS\SYSTEM.INI`, change the `mouse.drv` line in the `[boot]` section to `mouse.drv=vbmouse.drv`. +5. Restart Windows. + ## CPU idling on Windows 95 See about [installing AmnHLT](cpu-idling.md#windows-9x-using-amnhlt). diff --git a/src/browser/main.js b/src/browser/main.js index a1bb3b906a..94c2ce7579 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -2681,6 +2681,7 @@ function init_ui(profile, settings, emulator) var last_instr_counter = 0; var interval = null; var os_uses_mouse = false; + var os_uses_absolute_mouse = false; var total_instructions = 0; function update_info() @@ -2828,6 +2829,11 @@ function init_ui(profile, settings, emulator) $("info_mouse_enabled").textContent = is_enabled ? "Yes" : "No"; }); + emulator.add_listener("vmware-absolute-mouse", function(is_enabled) + { + os_uses_absolute_mouse = is_enabled; + }); + emulator.add_listener("screen-set-size", function(args) { const [w, h, bpp] = args; @@ -3216,7 +3222,11 @@ function init_ui(profile, settings, emulator) emulator.speaker_adapter.audio_context.resume(); } - if(mouse_is_enabled && os_uses_mouse) + // No need to lock the mouse if the guest tracks the host cursor + // through the absolute pointing device. The "Lock mouse" button can + // still be used, e.g. for games (movement is then sent as relative + // deltas). + if(mouse_is_enabled && os_uses_mouse && !os_uses_absolute_mouse) { emulator.lock_mouse(); } diff --git a/src/browser/mouse.js b/src/browser/mouse.js index 5fa4a17ce3..3c54e8f318 100644 --- a/src/browser/mouse.js +++ b/src/browser/mouse.js @@ -58,6 +58,7 @@ export function MouseAdapter(bus, screen_container) window.removeEventListener("mousedown", mousedown_handler, false); window.removeEventListener("mouseup", mouseup_handler, false); window.removeEventListener("wheel", mousewheel_handler, { passive: false }); + document.removeEventListener("pointerlockchange", pointerlockchange_handler, false); }; this.init = function() @@ -75,9 +76,15 @@ export function MouseAdapter(bus, screen_container) window.addEventListener("mousedown", mousedown_handler, false); window.addEventListener("mouseup", mouseup_handler, false); window.addEventListener("wheel", mousewheel_handler, { passive: false }); + document.addEventListener("pointerlockchange", pointerlockchange_handler, false); }; this.init(); + function pointerlockchange_handler() + { + mouse.bus.send("mouse-pointer-lock", !!document.pointerLockElement); + } + function is_child(child, parent) { while(child.parentNode) @@ -225,7 +232,9 @@ export function MouseAdapter(bus, screen_container) mouse.bus.send("mouse-delta", [delta_x, delta_y]); - if(screen_container) + // Under pointer lock the page coordinates don't change, so no + // meaningful absolute position can be reported + if(screen_container && !document.pointerLockElement) { const absolute_x = e.pageX - screen_container.offsetLeft; const absolute_y = e.pageY - screen_container.offsetTop; diff --git a/src/browser/starter.js b/src/browser/starter.js index b91d3a7f2c..4793bf3816 100644 --- a/src/browser/starter.js +++ b/src/browser/starter.js @@ -288,6 +288,20 @@ V86.prototype.continue_init = async function(emulator, options) this.mouse_adapter = new MouseAdapter(this.bus, screen_options.container); } + // Pointer lock is not needed while the guest uses absolute pointer + // positions (the guest cursor follows the host cursor), so release it + // when the guest driver enables absolute positioning + this.absolute_pointer_enabled = false; + this.bus.register("vmware-absolute-mouse", function(enabled) + { + if(enabled && !this.absolute_pointer_enabled && + typeof document !== "undefined" && document.pointerLockElement) + { + document.exitPointerLock(); + } + this.absolute_pointer_enabled = enabled; + }, this); + if(screen_options.container) { this.screen_adapter = new ScreenAdapter(screen_options, () => this.v86.cpu.devices.vga && this.v86.cpu.devices.vga.screen_fill_buffer()); diff --git a/src/cpu.js b/src/cpu.js index bd8687571d..6a5f39338f 100644 --- a/src/cpu.js +++ b/src/cpu.js @@ -24,6 +24,7 @@ import { IO } from "./io.js"; import { VirtioConsole } from "./virtio_console.js"; import { PCI } from "./pci.js"; import { PS2 } from "./ps2.js"; +import { VMwareMouse } from "./vmware.js"; import { read_elf } from "./elf.js"; import { FloppyController } from "./floppy.js"; @@ -570,6 +571,7 @@ CPU.prototype.get_state = function() state[86] = this.last_result; state[87] = this.fpu_status_word; state[88] = this.mxcsr; + state[89] = this.devices.vmware; return state; }; @@ -738,6 +740,7 @@ CPU.prototype.set_state = function(state) this.devices.virtio_console && this.devices.virtio_console.set_state(state[82]); this.devices.virtio_net && this.devices.virtio_net.set_state(state[83]); this.devices.virtio_balloon && this.devices.virtio_balloon.set_state(state[84]); + this.devices.vmware && state[89] && this.devices.vmware.set_state(state[89]); this.fw_value = state[62]; @@ -1176,6 +1179,7 @@ CPU.prototype.init = function(settings, device_bus) this.devices.vga = new VGAScreen(this, device_bus, settings.screen, settings.vga_memory_size || 8 * 1024 * 1024); this.devices.ps2 = new PS2(this, device_bus); + this.devices.vmware = new VMwareMouse(this, device_bus); this.devices.uart0 = new UART(this, 0x3F8, device_bus); diff --git a/src/vmware.js b/src/vmware.js new file mode 100644 index 0000000000..bc85fbd3b0 --- /dev/null +++ b/src/vmware.js @@ -0,0 +1,282 @@ +import { REG_EAX, REG_EBX, REG_ECX, REG_EDX, LOG_OTHER } from "./const.js"; +import { dbg_log } from "./log.js"; + +// For Types Only +import { CPU } from "./cpu.js"; +import { BusConnector } from "./bus.js"; + +const VMWARE_PORT = 0x5658; +const VMWARE_MAGIC = 0x564D5868; + +const CMD_GETVERSION = 10; +const CMD_ABSPOINTER_DATA = 39; +const CMD_ABSPOINTER_STATUS = 40; +const CMD_ABSPOINTER_COMMAND = 41; + +const ABSPOINTER_ENABLE = 0x45414552; +const ABSPOINTER_DISABLE = 0x000000F5; +const ABSPOINTER_RELATIVE = 0x4C455252; +const ABSPOINTER_ABSOLUTE = 0x53424152; + +const READ_ID = 0x3442554A; + +const BUTTON_LEFT = 0x20; +const BUTTON_RIGHT = 0x10; +const BUTTON_MIDDLE = 0x08; + +// Flag in the status dword marking a packet whose x/y are signed deltas +// rather than absolute positions (VMMOUSE_RELATIVE_PACKET in the guest +// drivers). Used while the host pointer is locked and no meaningful absolute +// position exists. +const RELATIVE_PACKET = 0x00010000; + +const QUEUE_MAX = 1024; + +/** + * VMware mouse backdoor (port 0x5658). Lets a guest driver read absolute + * pointer position so the guest cursor can track the host cursor 1:1 without + * pointer lock. PS/2 still supplies the IRQ; the driver reads this port on + * each IRQ12. While the host pointer is locked (e.g. for games), movement is + * reported as relative packets instead, since no meaningful absolute position + * exists. + * + * @constructor + * @param {CPU} cpu + * @param {BusConnector} bus + */ +export function VMwareMouse(cpu, bus) +{ + /** @const @type {CPU} */ + this.cpu = cpu; + + /** @const @type {BusConnector} */ + this.bus = bus; + + /** @type {boolean} */ + this.enabled = false; + + /** @type {boolean} */ + this.absolute = false; + + /** @type {!Array} */ + this.queue = []; + + this.buttons = 0; + this.last_x = -1; + this.last_y = -1; + this.tail_is_move = false; + + /** + * Whether the host pointer is currently locked (browser pointer lock). + * While locked, the host cursor position is meaningless, so movement is + * reported as relative packets instead of absolute ones. + * @type {boolean} + */ + this.host_pointer_locked = false; + + // sub-pixel remainders of relative movement + this.rel_dx = 0; + this.rel_dy = 0; + + this.bus.register("mouse-absolute", function(data) + { + const x = Math.max(0, Math.min(0xFFFF, Math.round(data[0] / data[2] * 0xFFFF))); + const y = Math.max(0, Math.min(0xFFFF, Math.round(data[1] / data[3] * 0xFFFF))); + if(x === this.last_x && y === this.last_y) + { + return; + } + this.last_x = x; + this.last_y = y; + this.push_absolute(0, true); + }, this); + + this.bus.register("mouse-delta", function(data) + { + if(!this.host_pointer_locked) + { + return; + } + this.rel_dx += data[0]; + this.rel_dy += data[1]; + const dx = this.rel_dx | 0; + const dy = this.rel_dy | 0; + if(!dx && !dy) + { + return; + } + this.rel_dx -= dx; + this.rel_dy -= dy; + this.push_relative(dx, dy, 0, true); + }, this); + + this.bus.register("mouse-pointer-lock", function(locked) + { + this.host_pointer_locked = locked; + this.rel_dx = 0; + this.rel_dy = 0; + }, this); + + this.bus.register("mouse-click", function(data) + { + this.buttons = + (data[0] ? BUTTON_LEFT : 0) | + (data[1] ? BUTTON_MIDDLE : 0) | + (data[2] ? BUTTON_RIGHT : 0); + if(this.host_pointer_locked) + { + this.push_relative(0, 0, 0, false); + } + else + { + this.push_absolute(0, false); + } + }, this); + + this.bus.register("mouse-wheel", function(data) + { + if(this.host_pointer_locked) + { + this.push_relative(0, 0, -data[0] | 0, false); + } + else + { + this.push_absolute(-data[0] | 0, false); + } + }, this); + + // The backdoor protocol is 32-bit only, but guests probe the port at + // narrower widths during detection — answer those as an empty port. + const nop = function() {}; + cpu.io.register_read(VMWARE_PORT, this, + function() { return 0xFF; }, function() { return 0xFFFF; }, this.port_read32); + cpu.io.register_write(VMWARE_PORT, this, nop, nop, nop); +} + +VMwareMouse.prototype.push_absolute = function(wheel, move_only) +{ + if(!this.enabled || !this.absolute || this.last_x < 0) + { + return; + } + // Absolute pointing has no use for move history — if the guest hasn't + // drained the previous move yet, overwrite it in place. Clicks and wheel + // are never coalesced. This keeps the guest cursor at most one frame + // behind regardless of how slowly it drains, and makes overflow + // unreachable in practice. + if(move_only && this.tail_is_move && this.queue.length >= 4 && + !(this.queue[this.queue.length - 4] & RELATIVE_PACKET)) + { + this.queue[this.queue.length - 3] = this.last_x; + this.queue[this.queue.length - 2] = this.last_y; + return; + } + this.push_packet(this.buttons, this.last_x, this.last_y, wheel, move_only); +}; + +// Relative fallback, used while the host pointer is locked (e.g. for games). +// The sign convention follows the Linux vmmouse driver: positive x is right, +// positive y is up, like PS/2. +VMwareMouse.prototype.push_relative = function(dx, dy, wheel, move_only) +{ + if(!this.enabled) + { + return; + } + // Same idea as in push_absolute, but pending deltas accumulate instead of + // being overwritten. + if(move_only && this.tail_is_move && this.queue.length >= 4 && + (this.queue[this.queue.length - 4] & RELATIVE_PACKET)) + { + this.queue[this.queue.length - 3] += dx; + this.queue[this.queue.length - 2] += dy; + return; + } + this.push_packet(this.buttons | RELATIVE_PACKET, dx, dy, wheel, move_only); +}; + +VMwareMouse.prototype.push_packet = function(status, x, y, wheel, move_only) +{ + if(this.queue.length + 4 > QUEUE_MAX) + { + this.enabled = false; + this.queue.length = 0; + dbg_log("vmware mouse: queue overflow, disabling", LOG_OTHER); + return; + } + this.queue.push(status, x, y, wheel); + this.tail_is_move = move_only; +}; + +VMwareMouse.prototype.port_read32 = function() +{ + const reg32 = this.cpu.reg32; + if(reg32[REG_EAX] !== VMWARE_MAGIC) + { + return 0xFFFFFFFF | 0; + } + + switch(reg32[REG_ECX] & 0xFFFF) + { + case CMD_GETVERSION: + reg32[REG_EBX] = VMWARE_MAGIC; + return 6; + + case CMD_ABSPOINTER_STATUS: + return this.enabled ? this.queue.length : 0xFFFF0000 | 0; + + case CMD_ABSPOINTER_DATA: + { + const n = Math.min(reg32[REG_EBX] >>> 0, 4, this.queue.length); + const v = [0, 0, 0, 0]; + for(let i = 0; i < n; i++) + { + v[i] = this.queue.shift(); + } + reg32[REG_EBX] = v[1]; + reg32[REG_ECX] = v[2]; + reg32[REG_EDX] = v[3]; + return v[0]; + } + + case CMD_ABSPOINTER_COMMAND: + switch(reg32[REG_EBX]) + { + case ABSPOINTER_ENABLE: + this.enabled = true; + this.queue.length = 0; + this.tail_is_move = false; + this.queue.push(READ_ID); + break; + case ABSPOINTER_DISABLE: + this.enabled = false; + this.absolute = false; + this.queue.length = 0; + this.bus.send("vmware-absolute-mouse", false); + break; + case ABSPOINTER_ABSOLUTE: + this.absolute = true; + this.bus.send("vmware-absolute-mouse", true); + break; + case ABSPOINTER_RELATIVE: + this.absolute = false; + this.bus.send("vmware-absolute-mouse", false); + break; + } + return 0; + } + + return 0xFFFFFFFF | 0; +}; + +VMwareMouse.prototype.get_state = function() +{ + return [this.enabled, this.absolute]; +}; + +VMwareMouse.prototype.set_state = function(state) +{ + this.enabled = state[0]; + this.absolute = state[1]; + this.bus.send("vmware-absolute-mouse", this.absolute); +}; diff --git a/v86.d.ts b/v86.d.ts index d63ddbc5aa..54c2bdd598 100644 --- a/v86.d.ts +++ b/v86.d.ts @@ -224,6 +224,7 @@ export interface Event { "virtio-console0-output-bytes": Uint8Array; "virtio-console0-input-bytes": Uint8Array; "virtio-console0-resize": [cols: number, rows: number]; + "vmware-absolute-mouse": boolean; } /**