diff --git a/asm/mouse.s b/asm/mouse.s new file mode 100644 index 0000000..75199b7 --- /dev/null +++ b/asm/mouse.s @@ -0,0 +1,88 @@ +; +; Minimal mouse support. Only firmware routines are supported, no +; I/O hooks or softswitches. This is enough to work with titles +; that follow the documentation's recommendations to use the +; firmware routines like Dazzle Draw and Apple II DeskTop +; + ORG $C700 + +; Constants for future reference + +CLAMP_X_LOW EQU $478 +CLAMP_Y_LOW EQU $4F8 +CLAMP_X_HIGH EQU $578 +CLAMP_Y_HIGH EQU $5F8 + +X_LOW EQU $478 +Y_LOW EQU $4F8 +X_HIGH EQU $578 +Y_HIGH EQU $5F8 +RESERVED1 EQU $678 +RESERVED2 EQU $67F +STATUS EQU $778 +MODE EQU $7F8 + +STATUS_DOWN EQU $80 +STATUS_LAST EQU $40 +INT_SCREEN EQU $08 +INT_BUTTON EQU $04 +INT_MOUSE EQU $02 + +ROMRTS EQU $FF58 + + DFB $00 ; $00 + DFB $00 ; $01 + DFB $00 ; $02 + DFB $00 ; $03 + DFB $00 ; $04 +; Cx05 - Pascal ID byte + DFB $38 ; $05 + DFB $00 ; $06 +; Cx07 - Pascal ID byte + DFB $18 ; $07 + DFB $00 ; $08 + DFB $00 ; $09 + DFB $00 ; $0A +; Cx0B - Generic signature byte of firmware cards + DFB $01 ; $0B +; Cx0C - 2 = X-Y pointing device; 0 = identification code +ID1 DFB $20 ; $0C + DFB $00 ; $0D + DFB $00 ; $0E + DFB $00 ; $0F + DFB $00 ; $10 + DFB $00 ; $11 +; The firmware routines point to individual RTS opcodes +; that are intercepted by the card implementation which +; manipulates memory and processor state directly + DFB $20 ; $12 SETMOUSE + DFB $21 ; $13 SERVEMOUSE + DFB $22 ; $14 READMOUSE + DFB $23 ; $15 CLEARMOUSE + DFB $24 ; $16 POSMOUSE + DFB $25 ; $17 CLAMPMOUSE + DFB $26 ; $18 HOMEMOUSE + DFB $27 ; $19 INITMOUSE + DFB $00 ; $1A + DFB $00 ; $1B + DFB $00 ; $1C + DFB $00 ; $1D + DFB $00 ; $1E + DFB $00 ; $1F + RTS ; $20 SETMOUSE + RTS ; $21 SERVEMOUSE + RTS ; $22 READMOUSE + RTS ; $23 CLEARMOUSE + RTS ; $24 POSMOUSE + RTS ; $25 CLAMPMOUSE + RTS ; $26 HOMEMOUSE + RTS ; $27 INITMOUSE +PADDING DS $C7FB - PADDING + ORG $C7FB +; CxFB - A mouse identification byte +ID2 DFB $D6 ; $FB + DFB $00 ; $FC + DFB $00 ; $FD + DFB $00 ; $FE + DFB $00 ; $FF + END diff --git a/css/apple2.css b/css/apple2.css index 90184b5..3952ebc 100644 --- a/css/apple2.css +++ b/css/apple2.css @@ -215,6 +215,10 @@ canvas { height: 416px; } +#screen.mouseMode { + cursor: none; +} + #screen:-webkit-full-screen { background-color: black; top: 0; diff --git a/js/cards/mouse.ts b/js/cards/mouse.ts new file mode 100644 index 0000000..cc7083b --- /dev/null +++ b/js/cards/mouse.ts @@ -0,0 +1,322 @@ +import { Card, byte, word, Restorable } from '../types'; +import CPU6502, { CpuState } from '../cpu6502'; +import { debug } from '../util'; +import { rom } from '../roms/cards/mouse'; + +const CLAMP_MIN_LOW = 0x478; +const CLAMP_MAX_LOW = 0x4F8; +const CLAMP_MIN_HIGH = 0x578; +const CLAMP_MAX_HIGH = 0x5F8; + +const X_LOW = 0x478; +const Y_LOW = 0x4F8; +const X_HIGH = 0x578; +const Y_HIGH = 0x5F8; +const STATUS = 0x778; +const MODE = 0x7F8; + +const STATUS_DOWN = 0x80; +const STATUS_LAST = 0x40; +const STATUS_MOVED = 0x20; +const INT_SCREEN = 0x08; +const INT_PRESS = 0x04; +const INT_MOVE = 0x02; + +const MODE_ON = 0x01; +const MODE_INT_MOVE = 0x02; +const MODE_INT_PRESS = 0x04; +const MODE_INT_VBL = 0x08; + +/** + * Firmware routine offset pointers + */ +const ENTRIES = { + SET_MOUSE: 0x12, + SERVE_MOUSE: 0x13, + READ_MOUSE: 0x14, + CLEAR_MOUSE: 0x15, + POS_MOUSE: 0x16, + CLAMP_MOUSE: 0x17, + HOME_MOUSE: 0x18, + INIT_MOUSE: 0x19 +}; + +interface MouseState { + clampXMin: word; + clampYMin: word; + clampXMax: word + clampYMax: word; + x: word; + y: word; + mode: byte; + down: boolean; + lastDown: boolean; + lastX: word; + lastY: word; + serve: byte; + shouldIntMove: boolean; + shouldIntPress: boolean; + slot: byte; +} + +export default class Mouse implements Card, Restorable { + /** Lowest mouse X */ + private clampXMin: word = 0; + /** Lowest mouse Y */ + private clampYMin: word = 0; + /** Highest mouse X */ + private clampXMax: word = 0x3FF; + /** Highest mouse Y */ + private clampYMax: word = 0x3FF; + /** Mouse X position */ + private x: word = 0; + /** Mouse Y position */ + private y: word = 0; + /** Mouse mode */ + private mode: byte = 0; + /** Mouse button down state */ + private down = false; + /** Last mouse button down state */ + private lastDown = false; + /** Last mouse Y Position */ + private lastX: word = 0; + /** Last mouse X position */ + private lastY: word = 0; + /** Interrupt service flags */ + private serve: byte = 0; + /** Move happened since last refresh */ + private shouldIntMove = false; + /** Button press happened since last refresh */ + private shouldIntPress = false; + /** Slot for screen hole indexing */ + private slot = 0; + + constructor( + private cpu: CPU6502, + private cbs: { + setMouse: (mouse: Mouse) => void, + mouseMode: (on: boolean) => void + } + ) { + this.cbs.setMouse(this); + } + + ioSwitch(_off: byte, _val?: byte) { + return undefined; + } + + read(_page: byte, off: byte) { + let state = this.cpu.getState(); + + const holeWrite = (addr: word, val: byte) => { + this.cpu.write(addr >> 8, (addr & 0xff) + this.slot, val); + }; + + const holeRead = (addr: word) => { + return this.cpu.read(addr >> 8, addr & 0xff); + }; + + const clearCarry = (state: CpuState) => { + state.s &= 0xFE; + return state; + }; + + if (this.cpu.getSync()) { + switch (off) { + case rom[ENTRIES.SET_MOUSE]: + { + this.mode = state.a; + this.cbs.mouseMode(!!(this.mode & MODE_ON)); + state = clearCarry(state); + // debug( + // 'setMouse ', + // (_mode & MODE_ON ? 'Mouse on ' : 'Mouse off '), + // (_mode & MODE_INT_MOVE ? 'Move interrupt ' : '') + + // (_mode & MODE_INT_PRESS ? 'Move press ' : '') + + // (_mode & MODE_INT_VBL ? 'Move VBL ' : '') + // ); + } + break; + case rom[ENTRIES.SERVE_MOUSE]: + // debug('serveMouse'); + holeWrite(STATUS, this.serve); + state = clearCarry(state); + this.serve = 0; + break; + case rom[ENTRIES.READ_MOUSE]: + { + const moved = (this.lastX !== this.x) || (this.lastY !== this.y); + const status = + (this.down ? STATUS_DOWN : 0) | + (this.lastDown ? STATUS_LAST : 0) | + (moved ? STATUS_MOVED : 0); + const mouseXLow = this.x & 0xff; + const mouseYLow = this.y & 0xff; + const mouseXHigh = this.x >> 8; + const mouseYHigh = this.y >> 8; + + // debug({ mouseXLow, mouseYLow, mouseXHigh, mouseYHigh }); + + holeWrite(X_LOW, mouseXLow); + holeWrite(Y_LOW, mouseYLow); + holeWrite(X_HIGH, mouseXHigh); + holeWrite(Y_HIGH, mouseYHigh); + holeWrite(STATUS, status); + holeWrite(MODE, this.mode); + + this.lastDown = this.down; + this.lastX = this.x; + this.lastY = this.y; + + state = clearCarry(state); + } + break; + case rom[ENTRIES.CLEAR_MOUSE]: + debug('clearMouse'); + state = clearCarry(state); + break; + case rom[ENTRIES.POS_MOUSE]: + debug('posMouse'); + state = clearCarry(state); + break; + case rom[ENTRIES.CLAMP_MOUSE]: + { + const clampY = state.a; + if (clampY) { + this.clampYMin = holeRead(CLAMP_MIN_LOW) | (holeRead(CLAMP_MIN_HIGH) << 8); + this.clampYMax = holeRead(CLAMP_MAX_LOW) | (holeRead(CLAMP_MAX_HIGH) << 8); + debug('clampMouse Y', this.clampYMin, this.clampYMax); + } else { + this.clampXMin = holeRead(CLAMP_MIN_LOW) | (holeRead(CLAMP_MIN_HIGH) << 8); + this.clampXMax = holeRead(CLAMP_MAX_LOW) | (holeRead(CLAMP_MAX_HIGH) << 8); + debug('clampMouse X', this.clampXMin, this.clampXMax); + } + state = clearCarry(state); + } + break; + case rom[ENTRIES.HOME_MOUSE]: + { + debug('homeMouse'); + this.x = this.clampXMin; + this.y = this.clampYMin; + state = clearCarry(state); + } + break; + case rom[ENTRIES.INIT_MOUSE]: + { + this.slot = state.y >> 4; + debug('initMouse slot', this.slot); + state = clearCarry(state); + } + break; + } + + this.cpu.setState(state); + } + + return rom[off]; + } + + write() {} + + /** + * Triggers interrupts based on activity since the last tick + */ + + tick() { + if (this.mode & MODE_INT_VBL) { + this.serve |= INT_SCREEN; + } + if ((this.mode & MODE_INT_PRESS) && this.shouldIntPress) { + this.serve |= INT_PRESS; + } + if ((this.mode & MODE_INT_MOVE) && this.shouldIntMove) { + this.serve |= INT_MOVE; + } + if (this.serve) { + this.cpu.irq(); + } + this.shouldIntMove = false; + this.shouldIntPress = false; + } + + /** + * Scales mouse position and clamps to min and max,and flags + * potential mouse state change interrupt + * + * @param x Client mouse X position + * @param y Client mouse Y position + * @param w Client width + * @param h Client height + */ + + setMouseXY(x: number, y: number, w: number, h: number) { + const rangeX = this.clampXMax - this.clampXMin; + const rangeY = this.clampYMax - this.clampYMin; + this.x = (x * rangeX / w + this.clampXMin) & 0xffff; + this.y = (y * rangeY / h + this.clampYMin) & 0xffff; + this.shouldIntMove = true; + } + + /** + * Tracks mouse button state and flags potential + * mouse state change interrupt + * + * @param down Mouse button down state + */ + + setMouseDown(down: boolean) { + this.shouldIntPress = this.down !== down; + this.down = down; + } + + /** + * Restores saved state + * + * @param state stored state + */ + + setState(state: MouseState) { + this.clampXMin = state.clampXMin; + this.clampYMin = state.clampYMin; + this.clampXMax = state.clampXMax; + this.clampYMax = state.clampYMax; + this.x = state.x; + this.y = state.y; + this.mode = state.mode; + this.down = state.down; + this.lastDown = state.lastDown; + this.lastX = state.lastX; + this.lastY = state.lastY; + this.serve = state.serve; + this.shouldIntMove = state.shouldIntMove; + this.shouldIntPress = state.shouldIntPress; + this.slot = state.slot; + } + + /** + * Saves state for restoration + * + * @returns restorable state + */ + + getState(): MouseState { + return { + clampXMin: this.clampXMin, + clampYMin: this.clampYMin, + clampXMax: this.clampXMax, + clampYMax: this.clampYMax, + x: this.x, + y: this.y, + mode: this.mode, + down: this.down, + lastDown: this.lastDown, + lastX: this.lastX, + lastY: this.lastY, + serve: this.serve, + shouldIntMove: this.shouldIntMove, + shouldIntPress: this.shouldIntPress, + slot: this.slot + }; + } +} diff --git a/js/main2e.ts b/js/main2e.ts index 70aaf82..0ebd082 100644 --- a/js/main2e.ts +++ b/js/main2e.ts @@ -2,12 +2,14 @@ import Prefs from './prefs'; import { driveLights, initUI, updateUI } from './ui/apple2'; import Printer from './ui/printer'; +import { MouseUI } from './ui/mouse'; import DiskII from './cards/disk2'; import Parallel from './cards/parallel'; import RAMFactor from './cards/ramfactor'; import SmartPort from './cards/smartport'; import Thunderclock from './cards/thunderclock'; +import Mouse from './cards/mouse'; import { Apple2 } from './apple2'; @@ -54,15 +56,18 @@ apple2.ready.then(() => { const cpu = apple2.getCPU(); const printer = new Printer('#printer-modal .paper'); + const mouseUI = new MouseUI('#screen'); const parallel = new Parallel(printer); const slinky = new RAMFactor(1024 * 1024); const disk2 = new DiskII(io, driveLights); const clock = new Thunderclock(); const smartport = new SmartPort(cpu, { block: !enhanced }); + const mouse = new Mouse(cpu, mouseUI); io.setSlot(1, parallel); io.setSlot(2, slinky); + io.setSlot(4, mouse); io.setSlot(5, clock); io.setSlot(6, disk2); io.setSlot(7, smartport); diff --git a/js/roms/cards/mouse.ts b/js/roms/cards/mouse.ts new file mode 100644 index 0000000..c9a9d09 --- /dev/null +++ b/js/roms/cards/mouse.ts @@ -0,0 +1,37 @@ +import { ReadonlyUint8Array } from '../../types'; + +export const rom: ReadonlyUint8Array = new Uint8Array([ + 0x00,0x00,0x00,0x00,0x00,0x38,0x00,0x18, + 0x00,0x00,0x00,0x01,0x20,0x00,0x00,0x00, + 0x00,0x00,0x20,0x21,0x22,0x23,0x24,0x25, + 0x26,0x27,0x00,0x00,0x00,0x00,0x00,0x00, + 0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0xd6,0x00,0x00,0x00,0x00, +]); + diff --git a/js/ui/joystick.ts b/js/ui/joystick.ts index be828d6..cd4d0b2 100644 --- a/js/ui/joystick.ts +++ b/js/ui/joystick.ts @@ -6,6 +6,12 @@ const JOYSTICK_FLIP_X_AXIS = 'flip_x'; const JOYSTICK_FLIP_Y_AXIS = 'flip_y'; const JOYSTICK_SWAP_AXIS = 'swap_x_y'; +let mouseMode = false; + +export function enableMouseMode(on: boolean) { + mouseMode = on; +} + export class JoyStick implements OptionHandler { private disableMouseJoystick = false; private flipX = false; @@ -17,13 +23,13 @@ export class JoyStick implements OptionHandler { document.addEventListener('mousemove', this.mousemove); document.querySelectorAll('canvas').forEach((canvas) => { canvas.addEventListener('mousedown', (evt) => { - if (!this.gamepad) { + if (!this.gamepad && !mouseMode) { io.buttonDown(evt.which == 1 ? 0 : 1); } evt.preventDefault(); }); canvas.addEventListener('mouseup', (evt) => { - if (!this.gamepad) { + if (!this.gamepad && !mouseMode) { io.buttonUp(evt.which == 1 ? 0 : 1); } }); @@ -83,7 +89,7 @@ export class JoyStick implements OptionHandler { } private mousemove = (evt: MouseEvent) => { - if (this.gamepad || this.disableMouseJoystick) { + if (this.gamepad || this.disableMouseJoystick || mouseMode) { return; } diff --git a/js/ui/mouse.ts b/js/ui/mouse.ts new file mode 100644 index 0000000..a671e8f --- /dev/null +++ b/js/ui/mouse.ts @@ -0,0 +1,45 @@ +import type Mouse from '../cards/mouse'; +import { enableMouseMode } from './joystick'; + +export class MouseUI { + private mouse: Mouse; + private canvas: HTMLCanvasElement; + + constructor(selector: string) { + this.canvas = document.querySelector(selector)!; + + this.canvas.addEventListener( + 'mousemove', + (event: MouseEvent & { target: HTMLCanvasElement} ) => { + const { offsetX, offsetY, target } = event; + this.mouse.setMouseXY( + offsetX, + offsetY, + target.clientWidth, + target.clientHeight + ); + } + ); + + this.canvas.addEventListener('mousedown', () => { + this.mouse.setMouseDown(true); + }); + + this.canvas.addEventListener('mouseup', () => { + this.mouse.setMouseDown(false); + }); + } + + setMouse = (mouse: Mouse) => { + this.mouse = mouse; + }; + + mouseMode = (on: boolean) => { + enableMouseMode(on); + if (on) { + this.canvas.classList.add('mouseMode'); + } else { + this.canvas.classList.remove('mouseMode'); + } + }; +} diff --git a/js/ui/options_modal.ts b/js/ui/options_modal.ts index db8fed0..a5e575a 100644 --- a/js/ui/options_modal.ts +++ b/js/ui/options_modal.ts @@ -101,16 +101,14 @@ export class OptionsModal { const list = document.createElement('ul'); for (const option of options) { const { name, label, defaultVal, type } = option; - const onChange = (evt: InputEvent) => { - if (evt.target) { - const inputElement = evt.target as HTMLInputElement; - switch (type) { - case BOOLEAN_OPTION: - this.setOption(name, inputElement.checked); - break; - default: - this.setOption(name, inputElement.value); - } + const onChange = (evt: InputEvent & { target: HTMLInputElement }) => { + const { target } = evt; + switch (type) { + case BOOLEAN_OPTION: + this.setOption(name, target.checked); + break; + default: + this.setOption(name, target.value); } };