diff --git a/src/baseplatform.ts b/src/baseplatform.ts index dc1e41bf..af482549 100644 --- a/src/baseplatform.ts +++ b/src/baseplatform.ts @@ -121,6 +121,9 @@ export interface Platform { getCPUState?() : CpuState; debugSymbols? : DebugSymbols; + + startProbing?() : ProbeRecorder; + stopProbing?() : void; } export interface Preset { @@ -1070,6 +1073,7 @@ export function lookupSymbol(platform:Platform, addr:number, extra:boolean) { var sym = addr2symbol[addr]; return extra ? (sym + " + $" + hex(start-addr)) : sym; } + if (!extra) break; addr--; } return ""; @@ -1198,8 +1202,9 @@ export abstract class BasicZ80ScanlinePlatform extends BaseZ80Platform { /// new style import { Bus, Resettable, FrameBased, VideoSource, SampledAudioSource, AcceptsROM, AcceptsKeyInput, SavesState, SavesInputState, HasCPU } from "./devices"; -import { CPUClockHook, RasterFrameBased } from "./devices"; +import { Probeable, RasterFrameBased } from "./devices"; import { SampledAudio } from "./audio"; +import { ProbeRecorder } from "./recorder"; interface Machine extends Bus, Resettable, FrameBased, AcceptsROM, HasCPU, SavesState, SavesInputState { } @@ -1216,6 +1221,9 @@ function hasKeyInput(arg:any): arg is AcceptsKeyInput { function isRaster(arg:any): arg is RasterFrameBased { return typeof arg.getRasterY === 'function'; } +function hasProbe(arg:any): arg is Probeable { + return typeof arg.connectProbe == 'function'; +} export abstract class BaseMachinePlatform extends BaseDebugPlatform implements Platform { machine : T; @@ -1224,6 +1232,9 @@ export abstract class BaseMachinePlatform extends BaseDebugPl video : RasterVideo; audio : SampledAudio; poller : ControllerPoller; + probeRecorder : ProbeRecorder; + startProbing; + stopProbing; abstract newMachine() : T; abstract getToolForFilename(s:string) : string; @@ -1246,7 +1257,7 @@ export abstract class BaseMachinePlatform extends BaseDebugPl saveControlsState() { return this.machine.saveControlsState(); } start() { - var m = this.machine; + const m = this.machine; this.timer = new AnimationTimer(60, this.nextFrame.bind(this)); if (hasVideo(m)) { var vp = m.getVideoParams(); @@ -1264,6 +1275,16 @@ export abstract class BaseMachinePlatform extends BaseDebugPl this.video.setKeyboardEvents(m.setKeyInput.bind(m)); this.poller = new ControllerPoller(m.setKeyInput.bind(m)); } + if (hasProbe(m)) { + this.probeRecorder = new ProbeRecorder(m); + this.startProbing = () => { + m.connectProbe(this.probeRecorder); + return this.probeRecorder; + }; + this.stopProbing = () => { + m.connectProbe(null); + }; + } } loadROM(title, data) { diff --git a/src/devices.ts b/src/devices.ts index 711327ae..500dc266 100644 --- a/src/devices.ts +++ b/src/devices.ts @@ -124,12 +124,12 @@ export class BusHook implements Hook { var oldread = bus.read.bind(bus); var oldwrite = bus.write.bind(bus); bus.read = (a:number):number => { - profiler.logRead(a); var val = oldread(a); + profiler.logRead(a,val); return val; } bus.write = (a:number,v:number) => { - profiler.logWrite(a); + profiler.logWrite(a,v); oldwrite(a,v); } this.unhook = () => { @@ -174,25 +174,34 @@ export class CPUInsnHook implements Hook { /// PROFILER +export interface ProbeTime { + logClocks(clocks:number); + logNewScanline(); + logNewFrame(); +} + export interface ProbeCPU { logExecute(address:number); logInterrupt(type:number); } export interface ProbeBus { - logRead(address:number); - logWrite(address:number); + logRead(address:number, value:number); + logWrite(address:number, value:number); } export interface ProbeIO { - logIORead(address:number); - logIOWrite(address:number); + logIORead(address:number, value:number); + logIOWrite(address:number, value:number); } -export interface ProbeAll extends ProbeCPU, ProbeBus, ProbeIO { +export interface ProbeAll extends ProbeTime, ProbeCPU, ProbeBus, ProbeIO { } export class NullProbe implements ProbeAll { + logClocks() {} + logNewScanline() {} + logNewFrame() {} logExecute() {} logInterrupt() {} logRead() {} @@ -204,15 +213,15 @@ export class NullProbe implements ProbeAll { /// CONVENIENCE export interface BasicMachineControlsState { - in: Uint8Array; + inputs: Uint8Array; } export interface BasicMachineState extends BasicMachineControlsState { c: any; // TODO - b: Uint8Array; + ram: Uint8Array; } -export abstract class BasicMachine implements HasCPU, Bus, SampledAudioSource, AcceptsROM, +export abstract class BasicMachine implements HasCPU, Bus, SampledAudioSource, AcceptsROM, Probeable, SavesState, SavesInputState { abstract cpuFrequency : number; @@ -234,6 +243,9 @@ export abstract class BasicMachine implements HasCPU, Bus, SampledAudioSource, A scanline : number; frameCycles : number; + nullProbe = new NullProbe(); + probe : ProbeAll = this.nullProbe; + abstract read(a:number) : number; abstract write(a:number, v:number) : void; abstract startScanline() : void; @@ -251,6 +263,9 @@ export abstract class BasicMachine implements HasCPU, Bus, SampledAudioSource, A connectVideo(pixels:Uint32Array) : void { this.pixels = pixels; } + connectProbe(probe: ProbeAll) : void { + this.probe = probe || this.nullProbe; + } reset() { this.cpu.reset(); } @@ -260,33 +275,63 @@ export abstract class BasicMachine implements HasCPU, Bus, SampledAudioSource, A } loadState(state) { this.cpu.loadState(state.c); - this.ram.set(state.b); - this.inputs.set(state.in); + this.ram.set(state.ram); + this.inputs.set(state.inputs); } saveState() { return { c:this.cpu.saveState(), - b:this.ram.slice(0), - in:this.inputs.slice(0), + ram:this.ram.slice(0), + inputs:this.inputs.slice(0), }; } loadControlsState(state) { - this.inputs.set(state.in); + this.inputs.set(state.inputs); } saveControlsState() { return { - in:this.inputs.slice(0) + inputs:this.inputs.slice(0) }; } - advance(cycles : number) : number { - for (var i=0; i { + let val = membus.read(a); + this.probe.logRead(a,val); + return val; + }, + write: (a,v) => { + this.probe.logWrite(a,v); + membus.write(a,v); + } + }); + } + connectCPUIOBus(iobus:Bus) : void { + this.cpu['connectIOBus']({ + 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); + } + }); } } @@ -298,6 +343,7 @@ export abstract class BasicScanlineMachine extends BasicMachine implements Raste advanceFrame(maxClocks:number, trap) : number { var clock = 0; var endLineClock = 0; + this.probe.logNewFrame(); for (var sl=0; sl, SavesInputState { +export class AppleII extends BasicScanlineMachine { + + cpuFrequency = 1023000; + sampleRate = this.cpuFrequency; + cpuCyclesPerLine = 65; // approx: http://www.cs.columbia.edu/~sedwards/apple2fpga/ + cpuCyclesPerFrame = 65*262; + canvasWidth = 280; + numVisibleScanlines = 192; + numTotalScanlines = 262; + defaultROMSize = 0xbf00-0x803; // TODO ram = new Uint8Array(0x13000); // 64K + 16K LC RAM - 4K hardware + 12K ROM - rom : Uint8Array; + bios : Uint8Array; cpu = new MOS6502(); - audio : SampledAudioSink; - pixels : Uint32Array; grdirty = new Array(0xc000 >> 7); grparams = {dirty:this.grdirty, grswitch:GR_TXMODE, mem:this.ram}; ap2disp; - pgmbin : Uint8Array; rnd = 1; kbdlatch = 0; soundstate = 0; @@ -52,14 +58,14 @@ export class AppleII implements HasCPU, Bus, RasterFrameBased, SampledAudioSourc // bank 1 is E000-FFFF, bank 2 is D000-DFFF bank2rdoffset=0; bank2wroffset=0; - lastFrameCycles=0; constructor() { - this.rom = new lzgmini().decode(APPLEIIGO_LZG); - this.ram.set(this.rom, 0xd000); + super(); + this.bios = new lzgmini().decode(APPLEIIGO_LZG); + this.ram.set(this.bios, 0xd000); this.ram[0xbf00] = 0x4c; // fake DOS detect for C this.ram[0xbf6f] = 0x01; // fake DOS detect for C - this.cpu.connectMemoryBus(this); + this.connectCPUMemoryBus(this); } saveState() : AppleIIState { // TODO: automagic @@ -73,6 +79,7 @@ export class AppleII implements HasCPU, Bus, RasterFrameBased, SampledAudioSourc auxRAMselected: this.auxRAMselected, auxRAMbank: this.auxRAMbank, writeinhibit: this.writeinhibit, + inputs: null }; } loadState(s:AppleIIState) { @@ -89,13 +96,13 @@ export class AppleII implements HasCPU, Bus, RasterFrameBased, SampledAudioSourc this.ap2disp.invalidate(); // repaint entire screen } saveControlsState() : AppleIIControlsState { - return {kbdlatch:this.kbdlatch}; + return {inputs:null,kbdlatch:this.kbdlatch}; } loadControlsState(s:AppleIIControlsState) { this.kbdlatch = s.kbdlatch; } reset() { - this.cpu.reset(); + super.reset(); this.rnd = 1; // execute until $c600 boot for (var i=0; i<2000000; i++) { @@ -113,7 +120,7 @@ export class AppleII implements HasCPU, Bus, RasterFrameBased, SampledAudioSourc return this.ram[address]; } else if (address >= 0xd000) { if (!this.auxRAMselected) - return this.rom[address - 0xd000]; + return this.bios[address - 0xd000]; else if (address >= 0xe000) return this.ram[address]; else @@ -175,8 +182,8 @@ export class AppleII implements HasCPU, Bus, RasterFrameBased, SampledAudioSourc // JMP VM_BASE case 0xc600: { // load program into RAM - if (this.pgmbin) - this.ram.set(this.pgmbin.slice(HDR_SIZE), PGM_BASE); + if (this.rom) + this.ram.set(this.rom.slice(HDR_SIZE), PGM_BASE); return 0x4c; } case 0xc601: return VM_BASE&0xff; @@ -201,36 +208,26 @@ export class AppleII implements HasCPU, Bus, RasterFrameBased, SampledAudioSourc this.ram[address + this.bank2wroffset] = val; } } - loadROM(data:Uint8Array) { - this.pgmbin = data.slice(); - } - getVideoParams() { - return {width:280, height:192}; - } - getAudioParams() { - return {sampleRate:cpuFrequency, stereo:false}; - } + connectVideo(pixels:Uint32Array) { - this.pixels = pixels; - this.ap2disp = pixels && new Apple2Display(this.pixels, this.grparams); + super.connectVideo(pixels); + this.ap2disp = this.pixels && new Apple2Display(this.pixels, this.grparams); } - connectAudio(audio:SampledAudioSink) { - this.audio = audio; + startScanline() { } - advanceFrame(maxCycles, trap) : number { - maxCycles = Math.min(maxCycles, cpuCyclesPerFrame); - for (var i=0; i=0 && trap()) break; - this.cpu.advanceClock(); - this.audio.feedSample(this.soundstate, 1); - } + drawScanline() { + // TODO: draw scanline via ap2disp + } + advanceFrame(maxClocks:number, trap) : number { + var clocks = super.advanceFrame(maxClocks, trap); this.ap2disp && this.ap2disp.updateScreen(); - return (this.lastFrameCycles = i); + return clocks; } - - getRasterX() { return this.lastFrameCycles % cpuCyclesPerLine; } - getRasterY() { return Math.floor(this.lastFrameCycles / cpuCyclesPerLine); } - + advance() { + this.audio.feedSample(this.soundstate, 1); + return super.advance(); + } + setKeyInput(key:number, code:number, flags:number) : void { if (flags & KeyFlags.KeyPress) { // convert to uppercase for Apple ][ diff --git a/src/machine/vicdual.ts b/src/machine/vicdual.ts index 391ab0da..62fd6dc8 100644 --- a/src/machine/vicdual.ts +++ b/src/machine/vicdual.ts @@ -50,8 +50,8 @@ export class VicDual extends BasicScanlineMachine { constructor() { super(); - this.cpu.connectMemoryBus(this); - this.cpu.connectIOBus(this.newIOBus()); + this.connectCPUMemoryBus(this); + this.connectCPUIOBus(this.newIOBus()); this.inputs.set([0xff, 0xff, 0xff, 0xff ^ 0x8]); // most things active low this.display = new VicDualDisplay(); this.handler = newKeyboardHandler(this.inputs, CARNIVAL_KEYCODE_MAP, this.getKeyboardFunction()); diff --git a/src/recorder.ts b/src/recorder.ts index e9e3e66e..b27b94cd 100644 --- a/src/recorder.ts +++ b/src/recorder.ts @@ -177,3 +177,86 @@ export class EmuProfilerImpl implements EmuProfiler { this.log(a | PROFOP_INTERRUPT); } } + +///// + +import { Probeable, ProbeAll } from "./devices"; + +export enum ProbeFlags { + CLOCKS = 0x00000000, + EXECUTE = 0x01000000, + MEM_READ = 0x02000000, + MEM_WRITE = 0x04000000, + IO_READ = 0x08000000, + IO_WRITE = 0x10000000, + INTERRUPT = 0x20000000, + 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; + + constructor(m:Probeable) { + this.m = m; + } + start() { + this.m.connectProbe(this); + this.reset(); + } + 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; + } + logClocks(clocks:number) { + if (clocks) { + this.fclk += clocks; + this.log(clocks | ProbeFlags.CLOCKS); + } + } + logNewScanline() { + this.log(ProbeFlags.SCANLINE); + this.sl++; + } + logNewFrame() { + this.log(ProbeFlags.FRAME); + this.sl = 0; + } + 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); + } + +} + diff --git a/src/ui.ts b/src/ui.ts index f152c347..f6c34567 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -254,6 +254,14 @@ function refreshWindowList() { return new Views.ProfileView(); }); } + if (platform.startProbing) { + addWindowItem("#eventprobe", "Event Probe", () => { + return new Views.EventProbeView(); + }); + addWindowItem("#heatmap", "Heat Map", () => { + return new Views.HeatMapView(); + }); + } addWindowItem('#asseteditor', 'Asset Editor', () => { return new Views.AssetEditorView(); }); @@ -1251,7 +1259,7 @@ function updateDebugWindows() { projectWindows.tick(); debugTickPaused = true; } - setTimeout(updateDebugWindows, 200); + setTimeout(updateDebugWindows, 100); } function setWaitDialog(b : boolean) { diff --git a/src/views.ts b/src/views.ts index 294dafd9..c44f184c 100644 --- a/src/views.ts +++ b/src/views.ts @@ -7,7 +7,7 @@ import { Platform, EmuState, ProfilerOutput, lookupSymbol, BaseDebugPlatform } f import { hex, lpad, rpad, safeident, rgb2bgr } from "./util"; import { CodeAnalyzer } from "./analysis"; import { platform, platform_id, compparams, current_project, lastDebugState, projectWindows } from "./ui"; -import { EmuProfilerImpl } from "./recorder"; +import { EmuProfilerImpl, ProbeRecorder, ProbeFlags } from "./recorder"; import * as pixed from "./pixed/pixeleditor"; declare var Mousetrap; @@ -922,6 +922,178 @@ export class ProfileView implements ProjectView { /// +// TODO: clear buffer when scrubbing + +abstract class ProbeViewBase { + probe : ProbeRecorder; + maindiv : HTMLElement; + canvas : HTMLCanvasElement; + ctx : CanvasRenderingContext2D; + recreateOnResize = true; + + createCanvas(parent:HTMLElement, width:number, height:number) { + var div = document.createElement('div'); + var canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + parent.appendChild(div); + div.appendChild(canvas); + this.canvas = canvas; + this.ctx = canvas.getContext('2d'); + this.initCanvas(); + return this.maindiv = div; + } + initCanvas() { + } + + setVisible(showing : boolean) : void { + if (showing) { + this.probe = platform.startProbing(); + } else { + platform.stopProbing(); + this.probe = null; + } + } + + clear() { + var ctx = this.ctx; + ctx.globalCompositeOperation = 'source-over'; + ctx.globalAlpha = 0.5; + ctx.fillStyle = '#000000'; + ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + ctx.globalAlpha = 1.0; + ctx.globalCompositeOperation = 'lighter'; + } + + tick() { + var p = this.probe; + if (!p || !p.idx) return; // if no probe, or if empty + var row=0; + var col=0; + var ctx = this.ctx; + this.clear(); + for (var i=0; i> 8) & 0xff; + var col; + switch (op) { + case ProbeFlags.EXECUTE: col = 0x0f3f0f; break; + case ProbeFlags.MEM_READ: col = 0x3f0101; break; + case ProbeFlags.MEM_WRITE: col = 0x000f3f; break; + case ProbeFlags.IO_READ: col = 0x001f01; break; + case ProbeFlags.IO_WRITE: col = 0x003f3f; break; + case ProbeFlags.INTERRUPT: col = 0x3f3f00; break; + default: col = 0x1f1f1f; break; + } + var data = this.datau32[addr & 0xffff]; + data = (data & 0x7f7f7f) << 1; + data = data | col | 0xff000000; + this.datau32[addr & 0xffff] = data; + } + +} + +export class EventProbeView extends ProbeViewBase implements ProjectView { + symcache : Map = new Map(); + + createDiv(parent : HTMLElement) { + return this.createCanvas( parent, $(parent).width(), $(parent).height() ); + } + + drawEvent(op, addr, col, row) { + var ctx = this.ctx; + var xscale = this.canvas.width / 128; // TODO: pixels + var yscale = this.canvas.height / 262; // TODO: lines + var x = col * xscale; + var y = row * yscale; + var sym = this.getSymbol(addr); + if (!sym && op == ProbeFlags.IO_WRITE) sym = hex(addr,4); + //if (!sym && op == ProbeFlags.IO_READ) sym = hex(addr,4); + if (sym) { + this.setContextForOp(op); + ctx.fillText(sym, x, y); + } + } + + getSymbol(addr:number) : string { + var sym = this.symcache[addr]; + if (!sym) { + sym = lookupSymbol(platform, addr, false); + this.symcache[addr] = sym; + } + return sym; + } + + refresh() { + this.tick(); + this.symcache.clear(); + } +} + +/// + export class AssetEditorView implements ProjectView, pixed.EditorContext { maindiv : JQuery; cureditordiv : JQuery; diff --git a/test/cli/testplatforms.js b/test/cli/testplatforms.js index a860d2a8..6133cb5b 100644 --- a/test/cli/testplatforms.js +++ b/test/cli/testplatforms.js @@ -125,6 +125,7 @@ function testPlatform(platid, romname, maxframes, callback) { platform.reset(); // reset again var state0b = platform.saveState(); //TODO: vcs fails assert.deepEqual(state0a, state0b); + //if (platform.startProbing) platform.startProbing(); platform.resume(); // so that recorder works platform.setRecorder(rec); for (var i=0; i