2018-08-22 18:50:10 +00:00
|
|
|
|
2019-08-13 19:43:41 +00:00
|
|
|
import { Platform, BasePlatform, EmuState, EmuControlsState, EmuRecorder } from "./baseplatform";
|
2019-06-09 15:13:25 +00:00
|
|
|
import { BaseDebugPlatform, EmuProfiler, ProfilerOutput } from "./baseplatform";
|
2018-08-23 20:02:13 +00:00
|
|
|
import { getNoiseSeed, setNoiseSeed } from "./emu";
|
|
|
|
|
2019-06-09 15:13:25 +00:00
|
|
|
// RECORDER
|
|
|
|
|
2018-08-23 20:02:13 +00:00
|
|
|
type FrameRec = {controls:EmuControlsState, seed:number};
|
2018-08-22 18:50:10 +00:00
|
|
|
|
|
|
|
export class StateRecorderImpl implements EmuRecorder {
|
|
|
|
checkpointInterval : number = 60;
|
|
|
|
callbackStateChanged : () => void;
|
2019-04-30 17:44:29 +00:00
|
|
|
callbackNewCheckpoint : (state:EmuState) => void;
|
2018-08-22 18:50:10 +00:00
|
|
|
maxCheckpoints : number = 120;
|
|
|
|
|
|
|
|
platform : Platform;
|
2018-08-23 20:02:13 +00:00
|
|
|
checkpoints : EmuState[];
|
|
|
|
framerecs : FrameRec[];
|
2018-08-22 18:50:10 +00:00
|
|
|
frameCount : number;
|
2018-08-25 02:55:16 +00:00
|
|
|
lastSeekFrame : number;
|
2018-08-22 18:50:10 +00:00
|
|
|
|
|
|
|
constructor(platform : Platform) {
|
|
|
|
this.reset();
|
|
|
|
this.platform = platform;
|
|
|
|
}
|
|
|
|
|
|
|
|
reset() {
|
2018-08-23 20:02:13 +00:00
|
|
|
this.checkpoints = [];
|
|
|
|
this.framerecs = [];
|
2018-08-22 18:50:10 +00:00
|
|
|
this.frameCount = 0;
|
2018-08-23 22:52:56 +00:00
|
|
|
this.lastSeekFrame = 0;
|
|
|
|
if (this.callbackStateChanged) this.callbackStateChanged();
|
2018-08-22 18:50:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
frameRequested() : boolean {
|
2018-08-23 22:52:56 +00:00
|
|
|
var controls = {
|
|
|
|
controls:this.platform.saveControlsState(),
|
|
|
|
seed:getNoiseSeed()
|
|
|
|
};
|
|
|
|
var requested = false;
|
|
|
|
// are we replaying? then we don't need to save a frame, just replace controls
|
|
|
|
if (this.lastSeekFrame < this.frameCount) {
|
|
|
|
this.loadControls(this.lastSeekFrame);
|
|
|
|
} else {
|
|
|
|
// record the control state, if available
|
|
|
|
if (this.platform.saveControlsState) {
|
|
|
|
this.framerecs.push(controls);
|
|
|
|
}
|
|
|
|
// time to save next frame?
|
|
|
|
requested = (this.frameCount++ % this.checkpointInterval) == 0;
|
2018-08-22 18:50:10 +00:00
|
|
|
}
|
2018-08-23 22:52:56 +00:00
|
|
|
this.lastSeekFrame++;
|
|
|
|
if (this.callbackStateChanged) this.callbackStateChanged();
|
|
|
|
return requested;
|
2018-08-22 18:50:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
numFrames() : number {
|
|
|
|
return this.frameCount;
|
|
|
|
}
|
|
|
|
|
2018-08-23 22:52:56 +00:00
|
|
|
currentFrame() : number {
|
|
|
|
return this.lastSeekFrame;
|
|
|
|
}
|
|
|
|
|
2018-08-22 18:50:10 +00:00
|
|
|
recordFrame(state : EmuState) {
|
2018-08-23 20:02:13 +00:00
|
|
|
this.checkpoints.push(state);
|
2019-04-30 17:44:29 +00:00
|
|
|
if (this.callbackNewCheckpoint) this.callbackNewCheckpoint(state);
|
2018-08-25 02:55:16 +00:00
|
|
|
// checkpoints full?
|
|
|
|
if (this.checkpoints.length > this.maxCheckpoints) {
|
|
|
|
this.checkpoints.shift(); // remove 1st checkpoint
|
|
|
|
this.framerecs = this.framerecs.slice(this.checkpointInterval);
|
|
|
|
this.lastSeekFrame -= this.checkpointInterval;
|
|
|
|
this.frameCount -= this.checkpointInterval;
|
|
|
|
if (this.callbackStateChanged) this.callbackStateChanged();
|
|
|
|
}
|
2018-08-22 18:50:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
getStateAtOrBefore(frame : number) : {frame : number, state : EmuState} {
|
|
|
|
var bufidx = Math.floor(frame / this.checkpointInterval);
|
2018-08-23 20:02:13 +00:00
|
|
|
var foundidx = bufidx < this.checkpoints.length ? bufidx : this.checkpoints.length-1;
|
2018-08-22 18:50:10 +00:00
|
|
|
var foundframe = foundidx * this.checkpointInterval;
|
2018-08-23 20:02:13 +00:00
|
|
|
return {frame:foundframe, state:this.checkpoints[foundidx]};
|
2018-08-22 18:50:10 +00:00
|
|
|
}
|
|
|
|
|
2018-08-23 20:02:13 +00:00
|
|
|
loadFrame(seekframe : number) : number {
|
|
|
|
if (seekframe == this.lastSeekFrame)
|
|
|
|
return seekframe; // already set to this frame
|
|
|
|
// TODO: what if < 1?
|
|
|
|
let {frame,state} = this.getStateAtOrBefore(seekframe-1);
|
2018-08-22 18:50:10 +00:00
|
|
|
if (state) {
|
|
|
|
this.platform.pause();
|
|
|
|
this.platform.loadState(state);
|
|
|
|
while (frame < seekframe) {
|
2018-08-23 20:02:13 +00:00
|
|
|
if (frame < this.framerecs.length) {
|
2018-08-23 22:52:56 +00:00
|
|
|
this.loadControls(frame);
|
2018-08-22 18:50:10 +00:00
|
|
|
}
|
|
|
|
frame++;
|
2018-08-23 20:02:13 +00:00
|
|
|
this.platform.advance(frame < seekframe); // TODO: infinite loop?
|
2018-08-22 18:50:10 +00:00
|
|
|
}
|
|
|
|
this.lastSeekFrame = seekframe;
|
2018-08-23 20:02:13 +00:00
|
|
|
return seekframe;
|
|
|
|
} else {
|
|
|
|
return 0;
|
2018-08-22 18:50:10 +00:00
|
|
|
}
|
|
|
|
}
|
2018-08-23 22:52:56 +00:00
|
|
|
|
|
|
|
loadControls(frame : number) {
|
|
|
|
if (this.platform.loadControlsState)
|
|
|
|
this.platform.loadControlsState(this.framerecs[frame].controls);
|
|
|
|
setNoiseSeed(this.framerecs[frame].seed);
|
|
|
|
}
|
2019-04-30 17:44:29 +00:00
|
|
|
|
|
|
|
getLastCheckpoint() : EmuState {
|
|
|
|
return this.checkpoints.length && this.checkpoints[this.checkpoints.length-1];
|
|
|
|
}
|
2018-08-22 18:50:10 +00:00
|
|
|
}
|
2019-06-09 15:13:25 +00:00
|
|
|
|
|
|
|
// PROFILER
|
|
|
|
|
2019-08-13 19:43:41 +00:00
|
|
|
const PROFOP_READ = 0x100000;
|
|
|
|
const PROFOP_WRITE = 0x200000;
|
|
|
|
const PROFOP_INTERRUPT = 0x400000;
|
|
|
|
|
2019-06-09 15:13:25 +00:00
|
|
|
export class EmuProfilerImpl implements EmuProfiler {
|
|
|
|
|
|
|
|
platform : Platform;
|
2019-08-13 19:43:41 +00:00
|
|
|
frame = null;
|
|
|
|
output = {frame:null};
|
|
|
|
i = 0;
|
|
|
|
lastsl = 9999;
|
|
|
|
starti = 0;
|
2019-06-09 15:13:25 +00:00
|
|
|
|
|
|
|
constructor(platform : Platform) {
|
|
|
|
this.platform = platform;
|
|
|
|
}
|
|
|
|
|
|
|
|
start() : ProfilerOutput {
|
2019-08-13 19:43:41 +00:00
|
|
|
if (this.platform instanceof BasePlatform) this.platform.profiler = this;
|
2019-06-09 15:13:25 +00:00
|
|
|
this.platform.setBreakpoint('profile', () => {
|
|
|
|
var c = this.platform.getCPUState();
|
2019-08-13 19:43:41 +00:00
|
|
|
this.log(c.EPC || c.PC);
|
2019-06-09 15:13:25 +00:00
|
|
|
return false; // profile forever
|
|
|
|
});
|
2019-08-13 19:43:41 +00:00
|
|
|
this.output = {frame:null};
|
|
|
|
return this.output;
|
2019-06-09 15:13:25 +00:00
|
|
|
}
|
2019-08-13 19:43:41 +00:00
|
|
|
|
|
|
|
log(op : number) {
|
|
|
|
var sl = this.platform.getRasterScanline();
|
|
|
|
if (sl != this.lastsl) {
|
|
|
|
if (this.frame) {
|
|
|
|
this.frame.lines.push({start:this.starti, end:this.i-1});
|
|
|
|
}
|
|
|
|
if (sl < this.lastsl) {
|
|
|
|
this.output.frame = this.frame;
|
|
|
|
this.frame = {iptab:new Uint32Array(0x8000), lines:[]}; // TODO: const
|
|
|
|
this.i = 0;
|
|
|
|
}
|
|
|
|
this.starti = this.i;
|
|
|
|
this.lastsl = sl;
|
|
|
|
}
|
|
|
|
this.frame.iptab[this.i++] = op;
|
|
|
|
}
|
|
|
|
|
2019-06-09 15:13:25 +00:00
|
|
|
stop() {
|
|
|
|
this.platform.clearBreakpoint('profile');
|
2019-08-13 19:43:41 +00:00
|
|
|
if (this.platform instanceof BasePlatform) this.platform.profiler = null;
|
|
|
|
}
|
|
|
|
// TODO?
|
|
|
|
logRead(a : number) {
|
|
|
|
this.log(a | PROFOP_READ);
|
|
|
|
}
|
|
|
|
logWrite(a : number) {
|
|
|
|
this.log(a | PROFOP_WRITE);
|
|
|
|
}
|
|
|
|
logInterrupt(a : number) {
|
|
|
|
this.log(a | PROFOP_INTERRUPT);
|
2019-06-09 15:13:25 +00:00
|
|
|
}
|
|
|
|
}
|
2019-08-24 15:28:04 +00:00
|
|
|
|
|
|
|
/////
|
|
|
|
|
|
|
|
import { Probeable, ProbeAll } from "./devices";
|
|
|
|
|
|
|
|
export enum ProbeFlags {
|
|
|
|
CLOCKS = 0x00000000,
|
|
|
|
EXECUTE = 0x01000000,
|
|
|
|
MEM_READ = 0x02000000,
|
2019-08-27 03:17:07 +00:00
|
|
|
MEM_WRITE = 0x03000000,
|
|
|
|
IO_READ = 0x04000000,
|
|
|
|
IO_WRITE = 0x05000000,
|
|
|
|
VRAM_READ = 0x06000000,
|
|
|
|
VRAM_WRITE = 0x07000000,
|
|
|
|
INTERRUPT = 0x08000000,
|
2019-08-24 15:28:04 +00:00
|
|
|
SCANLINE = 0x7e000000,
|
|
|
|
FRAME = 0x7f000000,
|
|
|
|
}
|
|
|
|
|
|
|
|
class ProbeFrame {
|
|
|
|
data : Uint32Array;
|
|
|
|
len : number;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class ProbeRecorder implements ProbeAll {
|
|
|
|
|
|
|
|
buf = new Uint32Array(0x100000);
|
|
|
|
idx = 0;
|
|
|
|
fclk = 0;
|
|
|
|
sl = 0;
|
|
|
|
m : Probeable;
|
2019-08-25 14:45:36 +00:00
|
|
|
singleFrame : boolean = true;
|
2019-08-24 15:28:04 +00:00
|
|
|
|
|
|
|
constructor(m:Probeable) {
|
|
|
|
this.m = m;
|
|
|
|
}
|
|
|
|
start() {
|
|
|
|
this.m.connectProbe(this);
|
|
|
|
}
|
|
|
|
stop() {
|
|
|
|
this.m.connectProbe(null);
|
|
|
|
}
|
|
|
|
reset() {
|
|
|
|
this.idx = 0;
|
|
|
|
}
|
|
|
|
log(a:number) {
|
|
|
|
// TODO: coalesce READ and EXECUTE
|
|
|
|
if (this.idx >= this.buf.length) return;
|
|
|
|
this.buf[this.idx++] = a;
|
|
|
|
}
|
2019-08-24 22:44:54 +00:00
|
|
|
relog(a:number) {
|
|
|
|
this.buf[this.idx-1] = a;
|
|
|
|
}
|
|
|
|
lastOp() {
|
|
|
|
if (this.idx > 0)
|
|
|
|
return this.buf[this.idx-1] & 0xff000000;
|
|
|
|
else
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
lastAddr() {
|
|
|
|
if (this.idx > 0)
|
|
|
|
return this.buf[this.idx-1] & 0xffffff;
|
|
|
|
else
|
|
|
|
return -1;
|
|
|
|
}
|
2019-08-24 15:28:04 +00:00
|
|
|
logClocks(clocks:number) {
|
2019-08-24 22:44:54 +00:00
|
|
|
if (clocks > 0) {
|
2019-08-24 15:28:04 +00:00
|
|
|
this.fclk += clocks;
|
2019-08-24 22:44:54 +00:00
|
|
|
if (this.lastOp() == ProbeFlags.CLOCKS)
|
|
|
|
this.relog((this.lastAddr() + clocks) | ProbeFlags.CLOCKS); // coalesce clocks
|
|
|
|
else
|
|
|
|
this.log(clocks | ProbeFlags.CLOCKS);
|
2019-08-24 15:28:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
logNewScanline() {
|
|
|
|
this.log(ProbeFlags.SCANLINE);
|
|
|
|
this.sl++;
|
|
|
|
}
|
|
|
|
logNewFrame() {
|
|
|
|
this.log(ProbeFlags.FRAME);
|
|
|
|
this.sl = 0;
|
2019-08-25 14:45:36 +00:00
|
|
|
if (this.singleFrame) this.reset();
|
2019-08-24 15:28:04 +00:00
|
|
|
}
|
|
|
|
logExecute(address:number) {
|
|
|
|
this.log(address | ProbeFlags.EXECUTE);
|
|
|
|
}
|
|
|
|
logInterrupt(type:number) {
|
|
|
|
this.log(type | ProbeFlags.INTERRUPT);
|
|
|
|
}
|
|
|
|
logRead(address:number, value:number) {
|
|
|
|
this.log(address | ProbeFlags.MEM_READ);
|
|
|
|
}
|
|
|
|
logWrite(address:number, value:number) {
|
|
|
|
this.log(address | ProbeFlags.MEM_WRITE);
|
|
|
|
}
|
|
|
|
logIORead(address:number, value:number) {
|
|
|
|
this.log(address | ProbeFlags.IO_READ);
|
|
|
|
}
|
|
|
|
logIOWrite(address:number, value:number) {
|
|
|
|
this.log(address | ProbeFlags.IO_WRITE);
|
|
|
|
}
|
2019-08-27 03:17:07 +00:00
|
|
|
logVRAMRead(address:number, value:number) {
|
|
|
|
this.log(address | ProbeFlags.VRAM_READ);
|
|
|
|
}
|
|
|
|
logVRAMWrite(address:number, value:number) {
|
|
|
|
this.log(address | ProbeFlags.VRAM_WRITE);
|
|
|
|
}
|
2019-08-24 15:28:04 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-08-27 03:31:39 +00:00
|
|
|
// TODO: handle runToVsync() without erasing entire frame
|
|
|
|
|