apple2js/js/cards/mouse.ts
2022-05-10 08:04:20 -07:00

323 lines
9.8 KiB
TypeScript

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