mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2024-12-21 06:29:48 +00:00
415 lines
9.6 KiB
TypeScript
415 lines
9.6 KiB
TypeScript
|
|
export interface SavesState<S> {
|
|
saveState(): S;
|
|
loadState(state: S): void;
|
|
}
|
|
|
|
export interface Bus {
|
|
read(a: number): number;
|
|
write(a: number, v: number): void;
|
|
readConst?(a: number): number;
|
|
}
|
|
|
|
export interface Bus32 {
|
|
read32(a: number): number;
|
|
write32(a: number, v: number): void;
|
|
readConst32?(a: number): number;
|
|
}
|
|
|
|
export interface ClockBased {
|
|
advanceClock(): void;
|
|
}
|
|
|
|
export interface InstructionBased {
|
|
advanceInsn(): number;
|
|
}
|
|
|
|
export type TrapCondition = () => boolean;
|
|
|
|
export interface FrameBased {
|
|
advanceFrame(trap: TrapCondition): number;
|
|
}
|
|
|
|
export interface VideoSource {
|
|
getVideoParams(): VideoParams;
|
|
connectVideo(pixels: Uint32Array): void;
|
|
}
|
|
|
|
export interface RasterFrameBased extends FrameBased, VideoSource {
|
|
getRasterY(): number;
|
|
getRasterX(): number;
|
|
getRasterCanvasPosition?(): { x: number, y: number };
|
|
}
|
|
|
|
export interface VideoParams {
|
|
width: number;
|
|
height: number;
|
|
overscan?: boolean;
|
|
rotate?: number;
|
|
videoFrequency?: number; // default = 60
|
|
aspect?: number;
|
|
}
|
|
|
|
// TODO: frame buffer optimization (apple2, etc)
|
|
|
|
export interface SampledAudioParams {
|
|
sampleRate: number;
|
|
stereo: boolean;
|
|
}
|
|
|
|
export interface SampledAudioSink {
|
|
feedSample(value: number, count: number): void;
|
|
//sendAudioFrame(samples:Uint16Array) : void;
|
|
}
|
|
|
|
export interface SampledAudioSource {
|
|
getAudioParams(): SampledAudioParams;
|
|
connectAudio(audio: SampledAudioSink): void;
|
|
}
|
|
|
|
export interface AcceptsROM {
|
|
loadROM(data: Uint8Array, title?: string): void;
|
|
}
|
|
|
|
export interface AcceptsBIOS {
|
|
loadBIOS(data: Uint8Array, title?: string): void;
|
|
}
|
|
|
|
export interface Resettable {
|
|
reset(): void;
|
|
}
|
|
|
|
export interface MemoryBusConnected {
|
|
connectMemoryBus(bus: Bus): void;
|
|
}
|
|
|
|
export interface IOBusConnected {
|
|
connectIOBus(bus: Bus): void;
|
|
}
|
|
|
|
export interface CPU extends MemoryBusConnected, Resettable, SavesState<any> {
|
|
getPC(): number;
|
|
getSP(): number;
|
|
isStable(): boolean;
|
|
}
|
|
|
|
export interface HasCPU extends Resettable {
|
|
cpu: CPU;
|
|
}
|
|
|
|
export interface Interruptable<IT> {
|
|
interrupt(type: IT): void;
|
|
}
|
|
|
|
export interface SavesInputState<CS> {
|
|
loadControlsState(cs: CS): void;
|
|
saveControlsState(): CS;
|
|
}
|
|
|
|
export interface AcceptsKeyInput {
|
|
setKeyInput(key: number, code: number, flags: number): void;
|
|
}
|
|
|
|
export interface AcceptsPaddleInput {
|
|
setPaddleInput(controller: number, value: number): void;
|
|
}
|
|
|
|
// TODO: interface not yet used (setKeyInput() handles joystick)
|
|
export interface AcceptsJoyInput {
|
|
setJoyInput(joy: number, bitmask: number): void;
|
|
}
|
|
|
|
// SERIAL I/O
|
|
|
|
export interface SerialEvent {
|
|
op: 'read' | 'write';
|
|
value: number;
|
|
nbits: number;
|
|
}
|
|
|
|
// TODO: all these needed?
|
|
export interface SerialIOInterface {
|
|
// from machine to platform
|
|
clearToSend(): boolean;
|
|
sendByte(b: number);
|
|
// from platform to machine
|
|
byteAvailable(): boolean;
|
|
recvByte(): number;
|
|
// implement these too
|
|
reset(): void;
|
|
advance(clocks: number): void;
|
|
// refresh() : void;
|
|
}
|
|
|
|
export interface HasSerialIO {
|
|
connectSerialIO(serial: SerialIOInterface);
|
|
serialOut?: SerialEvent[]; // outgoing event log
|
|
serialIn?: SerialEvent[]; // incoming queue
|
|
}
|
|
|
|
/// PROFILER
|
|
|
|
export interface Probeable {
|
|
connectProbe(probe: ProbeAll): void;
|
|
}
|
|
|
|
export interface ProbeTime {
|
|
logClocks(clocks: number);
|
|
logNewScanline();
|
|
logNewFrame();
|
|
}
|
|
|
|
export interface ProbeCPU {
|
|
logExecute(address: number, SP: number);
|
|
logInterrupt(type: number);
|
|
logIllegal(address: number);
|
|
logWait(address: number);
|
|
}
|
|
|
|
export interface ProbeBus {
|
|
logRead(address: number, value: number);
|
|
logWrite(address: number, value: number);
|
|
logDMARead(address: number, value: number);
|
|
logDMAWrite(address: number, value: number);
|
|
}
|
|
|
|
export interface ProbeIO {
|
|
logIORead(address: number, value: number);
|
|
logIOWrite(address: number, value: number);
|
|
}
|
|
|
|
export interface ProbeVRAM {
|
|
logVRAMRead(address: number, value: number);
|
|
logVRAMWrite(address: number, value: number);
|
|
}
|
|
|
|
export interface ProbeAll extends ProbeTime, ProbeCPU, ProbeBus, ProbeIO, ProbeVRAM {
|
|
logData(data: number); // entire 32 bits
|
|
addLogBuffer(src: Uint32Array);
|
|
}
|
|
|
|
export class NullProbe implements ProbeAll {
|
|
logClocks() { }
|
|
logNewScanline() { }
|
|
logNewFrame() { }
|
|
logExecute() { }
|
|
logInterrupt() { }
|
|
logRead() { }
|
|
logWrite() { }
|
|
logIORead() { }
|
|
logIOWrite() { }
|
|
logVRAMRead() { }
|
|
logVRAMWrite() { }
|
|
logIllegal() { }
|
|
logWait() { }
|
|
logDMARead() { }
|
|
logDMAWrite() { }
|
|
logData() { }
|
|
addLogBuffer(src: Uint32Array) { }
|
|
}
|
|
|
|
/// CONVENIENCE
|
|
|
|
export interface BasicMachineControlsState {
|
|
inputs: Uint8Array;
|
|
}
|
|
|
|
export interface BasicMachineState extends BasicMachineControlsState {
|
|
c: any; // TODO
|
|
ram: Uint8Array;
|
|
}
|
|
|
|
export abstract class BasicHeadlessMachine implements HasCPU, Bus, AcceptsROM, Probeable,
|
|
SavesState<BasicMachineState>, SavesInputState<BasicMachineControlsState> {
|
|
|
|
abstract cpuFrequency: number;
|
|
abstract defaultROMSize: number;
|
|
|
|
abstract cpu: CPU;
|
|
abstract ram: Uint8Array;
|
|
|
|
rom: Uint8Array;
|
|
inputs: Uint8Array = new Uint8Array(32);
|
|
handler: (key, code, flags) => void; // keyboard handler
|
|
|
|
nullProbe = new NullProbe();
|
|
probe: ProbeAll = this.nullProbe;
|
|
|
|
abstract read(a: number): number;
|
|
abstract write(a: number, v: number): void;
|
|
|
|
setKeyInput(key: number, code: number, flags: number): void {
|
|
this.handler && this.handler(key, code, flags);
|
|
}
|
|
connectProbe(probe: ProbeAll): void {
|
|
this.probe = probe || this.nullProbe;
|
|
}
|
|
reset() {
|
|
this.cpu.reset();
|
|
}
|
|
loadROM(data: Uint8Array, title?: string): void {
|
|
if (!this.rom) this.rom = new Uint8Array(this.defaultROMSize);
|
|
if (data.length > this.rom.length)
|
|
throw new Error(`ROM too big: ${data.length} > ${this.rom.length}}`);
|
|
this.rom.set(data);
|
|
}
|
|
loadState(state) {
|
|
this.cpu.loadState(state.c);
|
|
this.ram.set(state.ram);
|
|
this.inputs.set(state.inputs);
|
|
}
|
|
saveState() {
|
|
return {
|
|
c: this.cpu.saveState(),
|
|
ram: this.ram.slice(0),
|
|
inputs: this.inputs.slice(0),
|
|
};
|
|
}
|
|
loadControlsState(state) {
|
|
this.inputs.set(state.inputs);
|
|
}
|
|
saveControlsState() {
|
|
return {
|
|
inputs: this.inputs.slice(0)
|
|
};
|
|
}
|
|
advanceCPU() {
|
|
var c = this.cpu as any;
|
|
var n = 1;
|
|
if (this.cpu.isStable()) { this.probe.logExecute(this.cpu.getPC(), this.cpu.getSP()); }
|
|
if (c.advanceClock) { c.advanceClock(); }
|
|
else if (c.advanceInsn) { n = c.advanceInsn(1); }
|
|
this.probe.logClocks(n);
|
|
return n;
|
|
}
|
|
probeMemoryBus(membus: Bus & Partial<Bus32>): Bus & Partial<Bus32> {
|
|
return {
|
|
read: (a) => {
|
|
let val = membus.read(a);
|
|
this.probe.logRead(a, val);
|
|
return val;
|
|
},
|
|
write: (a, v) => {
|
|
this.probe.logWrite(a, v);
|
|
membus.write(a, v);
|
|
},
|
|
read32: (a) => {
|
|
let val = membus.read32(a);
|
|
this.probe.logRead(a, val);
|
|
return val;
|
|
},
|
|
write32: (a, v) => {
|
|
this.probe.logWrite(a, v);
|
|
membus.write32(a, v);
|
|
}
|
|
};
|
|
}
|
|
connectCPUMemoryBus(membus: Bus): void {
|
|
this.cpu.connectMemoryBus(this.probeMemoryBus(membus as Bus&Bus32));
|
|
}
|
|
probeIOBus(iobus: Bus): Bus {
|
|
return {
|
|
read: (a) => {
|
|
let val = iobus.read(a);
|
|
this.probe.logIORead(a, val);
|
|
return val;
|
|
},
|
|
write: (a, v) => {
|
|
this.probe.logIOWrite(a, v);
|
|
iobus.write(a, v);
|
|
},
|
|
};
|
|
}
|
|
probeDMABus(iobus: Bus): Bus {
|
|
return {
|
|
read: (a) => {
|
|
let val = iobus.read(a);
|
|
this.probe.logDMARead(a, val);
|
|
return val;
|
|
},
|
|
write: (a, v) => {
|
|
this.probe.logDMAWrite(a, v);
|
|
iobus.write(a, v);
|
|
}
|
|
};
|
|
}
|
|
connectCPUIOBus(iobus: Bus): void {
|
|
this.cpu['connectIOBus'](this.probeIOBus(iobus));
|
|
}
|
|
}
|
|
|
|
export abstract class BasicMachine extends BasicHeadlessMachine implements SampledAudioSource {
|
|
|
|
abstract canvasWidth: number;
|
|
abstract numVisibleScanlines: number;
|
|
abstract sampleRate: number;
|
|
overscan: boolean = false;
|
|
rotate: number = 0;
|
|
aspectRatio: number;
|
|
|
|
pixels: Uint32Array;
|
|
audio: SampledAudioSink;
|
|
|
|
scanline: number;
|
|
|
|
getAudioParams(): SampledAudioParams {
|
|
return { sampleRate: this.sampleRate, stereo: false };
|
|
}
|
|
connectAudio(audio: SampledAudioSink): void {
|
|
this.audio = audio;
|
|
}
|
|
getVideoParams(): VideoParams {
|
|
return {
|
|
width: this.canvasWidth,
|
|
height: this.numVisibleScanlines,
|
|
aspect: this.aspectRatio,
|
|
overscan: this.overscan,
|
|
rotate: this.rotate
|
|
};
|
|
}
|
|
connectVideo(pixels: Uint32Array): void {
|
|
this.pixels = pixels;
|
|
}
|
|
}
|
|
|
|
export abstract class BasicScanlineMachine extends BasicMachine implements RasterFrameBased {
|
|
|
|
abstract numTotalScanlines: number;
|
|
abstract cpuCyclesPerLine: number;
|
|
|
|
abstract startScanline(): void;
|
|
abstract drawScanline(): void;
|
|
|
|
frameCycles: number;
|
|
|
|
advanceFrame(trap: TrapCondition): number {
|
|
this.preFrame();
|
|
var endLineClock = 0;
|
|
var steps = 0;
|
|
this.probe.logNewFrame();
|
|
this.frameCycles = 0;
|
|
for (var sl = 0; sl < this.numTotalScanlines; sl++) {
|
|
endLineClock += this.cpuCyclesPerLine; // could be fractional
|
|
this.scanline = sl;
|
|
this.startScanline();
|
|
while (this.frameCycles < endLineClock) {
|
|
if (trap && trap()) {
|
|
sl = 999;
|
|
break;
|
|
}
|
|
this.frameCycles += this.advanceCPU();
|
|
steps++;
|
|
}
|
|
this.drawScanline();
|
|
this.probe.logNewScanline();
|
|
this.probe.logClocks(Math.floor(this.frameCycles - endLineClock)); // remainder of prev. line
|
|
}
|
|
this.postFrame();
|
|
return steps; // TODO: return steps, not clock? for recorder
|
|
}
|
|
preFrame() { }
|
|
postFrame() { }
|
|
getRasterY() { return this.scanline; }
|
|
getRasterX() { return this.frameCycles % this.cpuCyclesPerLine; }
|
|
}
|