mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2025-02-20 14:29:16 +00:00
395 lines
8.9 KiB
TypeScript
395 lines
8.9 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 ClockBased {
|
|
advanceClock() : void;
|
|
}
|
|
|
|
export interface InstructionBased {
|
|
advanceInsn() : number;
|
|
}
|
|
|
|
export type TrapCondition = () => boolean;
|
|
|
|
export interface FrameBased {
|
|
advanceFrame(maxClocks:number, trap:TrapCondition) : number;
|
|
}
|
|
|
|
export interface VideoSource {
|
|
getVideoParams() : VideoParams;
|
|
connectVideo(pixels:Uint32Array) : void;
|
|
}
|
|
|
|
export interface RasterFrameBased extends FrameBased, VideoSource {
|
|
getRasterY() : number;
|
|
getRasterX() : number;
|
|
}
|
|
|
|
export interface VideoParams {
|
|
width : number;
|
|
height : number;
|
|
overscan? : boolean;
|
|
rotate? : 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 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;
|
|
}
|
|
|
|
// TODO: joystick
|
|
export interface AcceptsKeyInput {
|
|
setKeyInput(key:number, code:number, flags:number) : void;
|
|
}
|
|
|
|
export interface AcceptsPaddleInput {
|
|
setPaddleInput(controller:number, value:number) : void;
|
|
}
|
|
|
|
export interface Probeable {
|
|
connectProbe(probe: ProbeAll) : void;
|
|
}
|
|
|
|
export function xorshift32(x : number) : number {
|
|
x ^= x << 13;
|
|
x ^= x >> 17;
|
|
x ^= x << 5;
|
|
return x;
|
|
}
|
|
|
|
/// HOOKS
|
|
|
|
export interface Hook<T> {
|
|
//target : T;
|
|
unhook();
|
|
}
|
|
|
|
export class BusHook implements Hook<Bus> {
|
|
//target : Bus;
|
|
constructor(bus : Bus, profiler : ProbeBus) {
|
|
//this.target = bus;
|
|
var oldread = bus.read.bind(bus);
|
|
var oldwrite = bus.write.bind(bus);
|
|
bus.read = (a:number):number => {
|
|
var val = oldread(a);
|
|
profiler.logRead(a,val);
|
|
return val;
|
|
}
|
|
bus.write = (a:number,v:number) => {
|
|
profiler.logWrite(a,v);
|
|
oldwrite(a,v);
|
|
}
|
|
this.unhook = () => {
|
|
bus.read = oldread;
|
|
bus.write = oldwrite;
|
|
}
|
|
}
|
|
unhook : () => void;
|
|
}
|
|
|
|
export class CPUClockHook implements Hook<CPU&ClockBased> {
|
|
//target : CPU&ClockBased;
|
|
constructor(cpu : CPU&ClockBased, profiler : ProbeCPU) {
|
|
//this.target = cpu;
|
|
var oldclock = cpu.advanceClock.bind(cpu);
|
|
cpu.advanceClock = () => {
|
|
profiler.logExecute(cpu.getPC());
|
|
return oldclock();
|
|
}
|
|
this.unhook = () => {
|
|
cpu.advanceClock = oldclock;
|
|
}
|
|
}
|
|
unhook : () => void;
|
|
}
|
|
|
|
export class CPUInsnHook implements Hook<CPU&InstructionBased> {
|
|
//target : CPU&InstructionBased;
|
|
constructor(cpu : CPU&InstructionBased, profiler : ProbeCPU) {
|
|
//this.target = cpu;
|
|
var oldinsn = cpu.advanceInsn.bind(cpu);
|
|
cpu.advanceInsn = () => {
|
|
profiler.logExecute(cpu.getPC());
|
|
return oldinsn();
|
|
}
|
|
this.unhook = () => {
|
|
cpu.advanceInsn = oldinsn;
|
|
}
|
|
}
|
|
unhook : () => void;
|
|
}
|
|
|
|
/// PROFILER
|
|
|
|
export interface ProbeTime {
|
|
logClocks(clocks:number);
|
|
logNewScanline();
|
|
logNewFrame();
|
|
}
|
|
|
|
export interface ProbeCPU {
|
|
logExecute(address:number);
|
|
logInterrupt(type:number);
|
|
}
|
|
|
|
export interface ProbeBus {
|
|
logRead(address:number, value:number);
|
|
logWrite(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 {
|
|
}
|
|
|
|
export class NullProbe implements ProbeAll {
|
|
logClocks() {}
|
|
logNewScanline() {}
|
|
logNewFrame() {}
|
|
logExecute() {}
|
|
logInterrupt() {}
|
|
logRead() {}
|
|
logWrite() {}
|
|
logIORead() {}
|
|
logIOWrite() {}
|
|
logVRAMRead() {}
|
|
logVRAMWrite() {}
|
|
}
|
|
|
|
/// 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);
|
|
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()); }
|
|
if (c.advanceClock) { c.advanceClock(); }
|
|
else if (c.advanceInsn) { n = c.advanceInsn(1); }
|
|
this.probe.logClocks(n);
|
|
return n;
|
|
}
|
|
probeMemoryBus(membus:Bus) : Bus {
|
|
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);
|
|
}
|
|
};
|
|
}
|
|
connectCPUMemoryBus(membus:Bus) : void {
|
|
this.cpu.connectMemoryBus(this.probeMemoryBus(membus));
|
|
}
|
|
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);
|
|
}
|
|
};
|
|
}
|
|
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;
|
|
|
|
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, 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(maxClocks:number, trap) : number {
|
|
this.preFrame();
|
|
var clock = 0;
|
|
var endLineClock = 0;
|
|
this.probe.logNewFrame();
|
|
for (var sl=0; sl<this.numTotalScanlines; sl++) {
|
|
endLineClock += this.cpuCyclesPerLine;
|
|
this.scanline = sl;
|
|
this.frameCycles = clock;
|
|
this.startScanline();
|
|
while (clock < endLineClock) {
|
|
if (trap && trap()) {
|
|
sl = 999;
|
|
break;
|
|
}
|
|
clock += this.advanceCPU();
|
|
}
|
|
this.drawScanline();
|
|
this.probe.logNewScanline();
|
|
this.probe.logClocks(clock-endLineClock);
|
|
}
|
|
this.postFrame();
|
|
return clock;
|
|
}
|
|
preFrame() { }
|
|
postFrame() { }
|
|
getRasterY() { return this.scanline; }
|
|
getRasterX() { return this.frameCycles % this.cpuCyclesPerLine; }
|
|
}
|