Very crude mouse support. (#95)

This commit is contained in:
Will Scullin 2021-12-29 15:00:44 -08:00 committed by GitHub
parent 2c02567309
commit a9885dbfbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 518 additions and 13 deletions

88
asm/mouse.s Normal file
View File

@ -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

View File

@ -215,6 +215,10 @@ canvas {
height: 416px;
}
#screen.mouseMode {
cursor: none;
}
#screen:-webkit-full-screen {
background-color: black;
top: 0;

322
js/cards/mouse.ts Normal file
View File

@ -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<MouseState> {
/** 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
};
}
}

View File

@ -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);

37
js/roms/cards/mouse.ts Normal file
View File

@ -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,
]);

View File

@ -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;
}

45
js/ui/mouse.ts Normal file
View File

@ -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<HTMLCanvasElement>(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');
}
};
}

View File

@ -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);
}
};