import { newDiv, ProjectView } from "./baseviews"; import { Segment } from "../../common/workertypes"; import { platform, compparams, current_project, projectWindows, runToPC, setupBreakpoint } from "../ui"; import { hex, lpad, rpad } from "../../common/util"; import { VirtualList } from "../../common/vlist"; import { getMousePos, getVisibleEditorLineHeight, VirtualTextLine, VirtualTextScroller } from "../../common/emu"; import { ProbeFlags, ProbeRecorder } from "../../common/probe"; import { BaseZ80MachinePlatform, BaseZ80Platform } from "../../common/baseplatform"; /// function ignoreSymbol(sym:string) { return sym.endsWith('_SIZE__') || sym.endsWith('_LAST__') || sym.endsWith('STACKSIZE__') || sym.endsWith('FILEOFFS__') || sym.startsWith('l__') || sym.startsWith('s__') || sym.startsWith('.__.'); } // TODO: make it use debug state // TODO: make it safe (load/restore state?) // TODO: refactor w/ VirtualTextLine export class MemoryView implements ProjectView { memorylist; dumplines; maindiv : HTMLElement; recreateOnResize = true; totalRows = 0x1400; createDiv(parent : HTMLElement) { var div = document.createElement('div'); div.setAttribute("class", "memdump"); parent.appendChild(div); this.showMemoryWindow(parent, div); return this.maindiv = div; } showMemoryWindow(workspace:HTMLElement, parent:HTMLElement) { this.memorylist = new VirtualList({ w: $(workspace).width(), h: $(workspace).height(), itemHeight: getVisibleEditorLineHeight(), totalRows: this.totalRows, generatorFn: (row : number) => { var s = this.getMemoryLineAt(row); var linediv = document.createElement("div"); if (this.dumplines) { var dlr = this.dumplines[row]; if (dlr) linediv.classList.add('seg_' + this.getMemorySegment(this.dumplines[row].a)); } linediv.appendChild(document.createTextNode(s)); return linediv; } }); $(parent).append(this.memorylist.container); this.tick(); if (compparams && this.dumplines) this.scrollToAddress(compparams.data_start); } scrollToAddress(addr : number) { if (this.dumplines) { this.memorylist.scrollToItem(this.findMemoryWindowLine(addr)); } } refresh() { this.dumplines = null; this.tick(); } tick() { if (this.memorylist) { $(this.maindiv).find('[data-index]').each( (i,e) => { var div = $(e); var row = parseInt(div.attr('data-index')); var oldtext = div.text(); var newtext = this.getMemoryLineAt(row); if (oldtext != newtext) div.text(newtext); }); } } getMemoryLineAt(row : number) : string { var offset = row * 16; var n1 = 0; var n2 = 16; var sym; if (this.getDumpLines()) { var dl = this.dumplines[row]; if (dl) { offset = dl.a & 0xfff0; n1 = dl.a - offset; n2 = n1 + dl.l; sym = dl.s; } else { return '.'; } } var s = hex(offset+n1,4) + ' '; for (var i=0; i 8) s += ' '; for (var i=n1; i nextofs) ofs2 = nextofs; //if (ofs < 1000) console.log(ofs, ofs2, nextofs, sym); this.dumplines.push({a:ofs, l:ofs2-ofs, s:sym}); ofs = ofs2; } } sym = nextsym; } } return this.dumplines; } // TODO: use segments list? getMemorySegment(a:number) : string { if (compparams) { if (a >= compparams.data_start && a < compparams.data_start+compparams.data_size) { if (platform.getSP && a >= platform.getSP() - 15) return 'stack'; else return 'data'; } else if (a >= compparams.code_start && a < compparams.code_start+(compparams.code_size||compparams.rom_size)) return 'code'; } var segments = current_project.segments; if (segments) { for (var seg of segments) { if (a >= seg.start && a < seg.start+seg.size) { if (seg.type == 'rom') return 'code'; if (seg.type == 'ram') return 'data'; if (seg.type == 'io') return 'io'; } } } return 'unknown'; } findMemoryWindowLine(a:number) : number { for (var i=0; i= a) return i; } } export class VRAMMemoryView extends MemoryView { totalRows = 0x800; readAddress(n : number) { return platform.readVRAMAddress(n); } getMemorySegment(a:number) : string { return 'video'; } getDumpLines() { return null; } } /// export class BinaryFileView implements ProjectView { vlist : VirtualTextScroller; maindiv : HTMLElement; path:string; data:Uint8Array; recreateOnResize = true; constructor(path:string, data:Uint8Array) { this.path = path; this.data = data; } createDiv(parent : HTMLElement) { this.vlist = new VirtualTextScroller(parent); this.vlist.create(parent, ((this.data.length+15) >> 4), this.getMemoryLineAt.bind(this)); return this.vlist.maindiv; } getMemoryLineAt(row : number) : VirtualTextLine { var offset = row * 16; var n1 = 0; var n2 = 16; var s = hex(offset+n1,4) + ' '; for (var i=0; i 8) s += ' '; for (var i=n1; i=0?hex(read,2):' '); } return {text:s}; } refresh() { this.vlist.refresh(); } getPath() { return this.path; } } /// export class MemoryMapView implements ProjectView { maindiv : JQuery; createDiv(parent : HTMLElement) { this.maindiv = newDiv(parent, 'vertical-scroll'); this.maindiv.css('display', 'grid'); this.maindiv.css('grid-template-columns', '5em 40% 40%'); return this.maindiv[0]; } // TODO: overlapping segments (e.g. ROM + LC) addSegment(seg : Segment, newrow : boolean) { if (newrow) { var offset = $('
'); offset.text('$'+hex(seg.start,4)); this.maindiv.append(offset); } var segdiv = $('
'); if (!newrow) segdiv.css('grid-column-start', 3); // make sure it's on right side if (seg.last) segdiv.text(seg.name+" ("+(seg.last-seg.start)+" / "+seg.size+" bytes used)"); else segdiv.text(seg.name+" ("+seg.size+" bytes)"); if (seg.size >= 256) { var pad = (Math.log(seg.size) - Math.log(256)) * 0.5; segdiv.css('padding-top', pad+'em'); segdiv.css('padding-bottom', pad+'em'); } if (seg.type) { segdiv.addClass('segment-'+seg.type); } this.maindiv.append(segdiv); //var row = $('
').append(offset, segdiv); //var container = $('
').append(row); //this.maindiv.append(container); segdiv.click(() => { // TODO: what if memory browser does not exist? var memview = projectWindows.createOrShow('#memory') as MemoryView; memview.scrollToAddress(seg.start); }); } refresh() { this.maindiv.empty(); var segments = current_project.segments; if (segments) { var curofs = 0; var laststart = -1; for (var seg of segments) { //var used = seg.last ? (seg.last-seg.start) : seg.size; if (seg.start > curofs) this.addSegment({name:'',start:curofs, size:seg.start-curofs}, true); this.addSegment(seg, laststart != seg.start); laststart = seg.start; curofs = seg.start + seg.size; } } } } /// // TODO: clear buffer when scrubbing const OPAQUE_BLACK = 0xff000000; export abstract class ProbeViewBaseBase { probe : ProbeRecorder; tooldiv : HTMLElement; cumulativeData : boolean = false; cyclesPerLine : number; totalScanlines : number; sp : number; // stack pointer abstract tick() : void; constructor() { var width = 160; var height = 262; // TODO: PAL? try { width = Math.ceil(platform['machine']['cpuCyclesPerLine']) || width; // TODO height = Math.ceil(platform['machine']['numTotalScanlines']) || height; // TODO } catch (e) { } this.cyclesPerLine = width; this.totalScanlines = height; } addr2symbol(addr : number) : string { var _addr2sym = (platform.debugSymbols && platform.debugSymbols.addr2symbol) || {}; return _addr2sym[addr]; } addr2str(addr : number) : string { var sym = this.addr2symbol(addr); if (typeof sym === 'string') return '$' + hex(addr) + ' (' + sym + ')'; else return '$' + hex(addr); } showTooltip(s:string) { if (s) { if (!this.tooldiv) { this.tooldiv = document.createElement("div"); this.tooldiv.setAttribute("class", "tooltiptrack"); document.body.appendChild(this.tooldiv); } $(this.tooldiv).text(s).show(); } else { $(this.tooldiv).hide(); } } setVisible(showing : boolean) : void { if (showing) { this.probe = platform.startProbing(); this.probe.singleFrame = !this.cumulativeData; this.tick(); } else { if (this.probe) this.probe.singleFrame = true; platform.stopProbing(); this.probe = null; } } redraw( eventfn:(op,addr,col,row,clk,value) => void ) { var p = this.probe; if (!p || !p.idx) return; // if no probe, or if empty var row=0; var col=0; var clk=0; this.sp = 0; for (var i=0; i> 16) & 0xff; var op = word & OPAQUE_BLACK; switch (op) { case ProbeFlags.SCANLINE: row++; col=0; break; case ProbeFlags.FRAME: row=0; col=0; break; case ProbeFlags.CLOCKS: col += addr; clk += addr; break; case ProbeFlags.SP_PUSH: case ProbeFlags.SP_POP: this.sp = addr; default: eventfn(op, addr, col, row, clk, value); break; } } } opToString(op:number, addr?:number, value?:number) { var s = ""; switch (op) { case ProbeFlags.EXECUTE: s = "Exec"; break; case ProbeFlags.MEM_READ: s = "Read"; break; case ProbeFlags.MEM_WRITE: s = "Write"; break; case ProbeFlags.IO_READ: s = "IO Read"; break; case ProbeFlags.IO_WRITE: s = "IO Write"; break; case ProbeFlags.VRAM_READ: s = "VRAM Read"; break; case ProbeFlags.VRAM_WRITE: s = "VRAM Write"; break; case ProbeFlags.DMA_READ: s = "DMA Read"; break; case ProbeFlags.DMA_WRITE: s = "DMA Write"; break; case ProbeFlags.INTERRUPT: s = "Interrupt"; break; case ProbeFlags.ILLEGAL: s = "Error"; break; case ProbeFlags.WAIT: s = "Wait"; break; case ProbeFlags.SP_PUSH: s = "Stack Push"; break; case ProbeFlags.SP_POP: s = "Stack Pop"; break; default: return ""; } if (typeof addr == 'number') s += " " + this.addr2str(addr); if ((op & ProbeFlags.HAS_VALUE) && typeof value == 'number') s += " = $" + hex(value,2); return s; } getOpRGB(op:number, addr:number) : number { switch (op) { case ProbeFlags.EXECUTE: return 0x018001; case ProbeFlags.MEM_READ: return 0x800101; case ProbeFlags.MEM_WRITE: return 0x010180; case ProbeFlags.IO_READ: return 0x018080; case ProbeFlags.IO_WRITE: return 0xc00180; case ProbeFlags.DMA_READ: case ProbeFlags.VRAM_READ: return 0x808001; case ProbeFlags.DMA_WRITE: case ProbeFlags.VRAM_WRITE: return 0x4080c0; case ProbeFlags.INTERRUPT: return 0x3fbf3f; case ProbeFlags.ILLEGAL: return 0x3f3fff; case ProbeFlags.WAIT: return 0xff3f3f; default: return 0; } } } abstract class ProbeViewBase extends ProbeViewBaseBase { maindiv : HTMLElement; canvas : HTMLCanvasElement; ctx : CanvasRenderingContext2D; recreateOnResize = true; abstract drawEvent(op, addr, col, row); createCanvas(parent:HTMLElement, width:number, height:number) { var div = document.createElement('div'); var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; canvas.classList.add('pixelated'); canvas.style.width = '100%'; canvas.style.height = '90vh'; // i hate css canvas.style.backgroundColor = 'black'; canvas.style.cursor = 'crosshair'; canvas.onmousemove = (e) => { var pos = getMousePos(canvas, e); this.showTooltip(this.getTooltipText(pos.x, pos.y)); $(this.tooldiv).css('left',e.pageX+10).css('top',e.pageY-30); } canvas.onmouseout = (e) => { $(this.tooldiv).hide(); } parent.appendChild(div); div.appendChild(canvas); this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.initCanvas(); return this.maindiv = div; } initCanvas() { } getTooltipText(x:number, y:number) : string { return null; } getOpAtPos(x:number, y:number, mask:number) : number { x = x|0; y = y|0; let result = 0; this.redraw( (op,addr,col,row,clk,value) => { if (!result && row == y && col >= x && (op & mask) != 0) { result = op | addr; } }); return result; } clear() { } tick() { this.clear(); this.redraw(this.drawEvent.bind(this)); } } abstract class ProbeBitmapViewBase extends ProbeViewBase { imageData : ImageData; datau32 : Uint32Array; recreateOnResize = false; createDiv(parent : HTMLElement) { return this.createCanvas(parent, this.cyclesPerLine, this.totalScanlines); } initCanvas() { this.imageData = this.ctx.createImageData(this.canvas.width, this.canvas.height); this.datau32 = new Uint32Array(this.imageData.data.buffer); } getTooltipText(x:number, y:number) : string { x = x|0; y = y|0; var s = ""; var lastroutine = null; var symstack = []; var lastcol = -1; this.redraw( (op,addr,col,row,clk,value) => { switch (op) { case ProbeFlags.EXECUTE: lastroutine = this.addr2symbol(addr) || lastroutine; break; case ProbeFlags.SP_PUSH: symstack.push(lastroutine); break; case ProbeFlags.SP_POP: lastroutine = symstack.pop(); break; } if (row == y && col <= x) { if (col != lastcol) { s = ""; lastcol = col; } if (s == "" && lastroutine) { s += "\n" + lastroutine; } s += "\n" + this.opToString(op, addr, value); } } ); return 'X: ' + x + ' Y: ' + y + ' ' + s; } refresh() { this.tick(); this.datau32.fill(OPAQUE_BLACK); } tick() { super.tick(); this.drawImage(); } drawImage() { this.ctx.putImageData(this.imageData, 0, 0); } clear() { this.datau32.fill(OPAQUE_BLACK); } } export class AddressHeatMapView extends ProbeBitmapViewBase implements ProjectView { createDiv(parent : HTMLElement) { return this.createCanvas(parent, 256, 256); } initCanvas() { super.initCanvas(); this.canvas.onclick = (e) => { var pos = getMousePos(this.canvas, e); var opaddr = Math.floor(pos.x) + Math.floor(pos.y) * 256; var lastpc = -1; var runpc = -1; this.redraw( (op,addr) => { if (runpc < 0 && lastpc >= 0 && addr == opaddr) { runpc = lastpc; } if (op == ProbeFlags.EXECUTE) lastpc = addr; }); if (runpc >= 0) runToPC(runpc); } } clear() { for (var i=0; i<=0xffff; i++) { var v = platform.readAddress(i); var rgb = (v >> 2) | (v & 0x1f); rgb |= (rgb<<8) | (rgb<<16); this.datau32[i] = rgb | OPAQUE_BLACK; } } // TODO: show current PC drawEvent(op, addr, col, row) { var rgb = this.getOpRGB(op, addr); if (!rgb) return; var x = addr & 0xff; var y = (addr >> 8) & 0xff; var data = this.datau32[addr & 0xffff]; data = data | rgb | OPAQUE_BLACK; this.datau32[addr & 0xffff] = data; } getTooltipText(x:number, y:number) : string { var a = (x & 0xff) + (y << 8); var s = ""; var pc = -1; var already = {}; var lastroutine = null; var symstack = []; this.redraw( (op,addr,col,row,clk,value) => { switch (op) { case ProbeFlags.EXECUTE: pc = addr; lastroutine = this.addr2symbol(addr) || lastroutine; break; case ProbeFlags.SP_PUSH: symstack.push(lastroutine); break; case ProbeFlags.SP_POP: lastroutine = symstack.pop(); break; } var key = op|pc; if (addr == a && !already[key]) { if (s == "" && lastroutine) { s += "\n" + lastroutine; } s += "\nPC " + this.addr2str(pc) + " " + this.opToString(op, null, value); already[key] = 1; } } ); return this.addr2str(a) + s; } } export class RasterPCHeatMapView extends ProbeBitmapViewBase implements ProjectView { initCanvas() { super.initCanvas(); // TODO: run to exact x/y position this.canvas.onclick = (e) => { var pos = getMousePos(this.canvas, e); var x = Math.floor(pos.x); var y = Math.floor(pos.y); var opaddr = this.getOpAtPos(pos.x, pos.y, ProbeFlags.EXECUTE); if (opaddr) { //runToPC(opaddr & 0xffff); setupBreakpoint("toline"); platform.runEval(() => { let onrow = platform.getRasterScanline && platform.getRasterScanline() >= y; if (onrow && platform.getRasterLineClock) { return onrow && platform.getRasterLineClock() > x; } else return onrow; }); } } } drawEvent(op, addr, col, row) { var rgb = this.getOpRGB(op, addr); if (!rgb) return; var iofs = col + row * this.canvas.width; var data = rgb | OPAQUE_BLACK; this.datau32[iofs] |= data; } drawImage() { // fill in the gaps let last = OPAQUE_BLACK; for (let i=0; i 1 && this.sp > this.interrupt) this.interrupt = 0; // track writes if (op == ProbeFlags.MEM_WRITE) { this.rgb |= 0x00002f; } if (op == ProbeFlags.VRAM_WRITE) { this.rgb |= 0x003f80; } if (op == ProbeFlags.IO_WRITE) { this.rgb |= 0x1f3f80; } if (op == ProbeFlags.IO_READ) { this.rgb |= 0x001f00; } if (op == ProbeFlags.WAIT) { this.rgb = 0x008000; } // draw pixels? if (op == ProbeFlags.ILLEGAL || op == ProbeFlags.DMA_READ) { this.datau32[iofs] = 0xff0f0f0f; } else { let data = this.rgb; if (op == ProbeFlags.EXECUTE) { let sp = this.sp & 15; if (sp >= 8) sp = 16-sp; if (Math.abs(this.lastpc) - addr > 16) { sp += 1; } if (Math.abs(this.lastpc) - addr > 256) { sp += 1; } data = this.rgb = (0x080808 * sp) + 0x202020; this.lastpc = addr; } if (this.interrupt) { data |= 0x800040; } if (this.datau32[iofs] == OPAQUE_BLACK) { this.datau32[iofs] = data | OPAQUE_BLACK; } } } } export class ProbeLogView extends ProbeViewBaseBase { vlist : VirtualTextScroller; maindiv : HTMLElement; recreateOnResize = true; dumplines; createDiv(parent : HTMLElement) { this.vlist = new VirtualTextScroller(parent); this.vlist.create(parent, this.cyclesPerLine*this.totalScanlines, this.getMemoryLineAt.bind(this)); return this.vlist.maindiv; } getMemoryLineAt(row : number) : VirtualTextLine { var s : string = ""; var c : string = "seg_data"; var line = this.dumplines && this.dumplines[row]; if (line != null) { var xtra : string = line.info.join(", "); s = "(" + lpad(line.row,4) + ", " + lpad(line.col,4) + ") " + rpad(line.asm||"",20) + xtra; if (xtra.indexOf("Write ") >= 0) c = "seg_io"; // if (xtra.indexOf("Stack ") >= 0) c = "seg_code"; } return {text:s, clas:c}; } refresh() { this.tick(); } tick() { const isz80 = platform instanceof BaseZ80MachinePlatform || platform instanceof BaseZ80Platform; // TODO? // cache each line in frame this.dumplines = {}; this.redraw((op,addr,col,row,clk,value) => { if (isz80) clk >>= 2; var line = this.dumplines[clk]; if (line == null) { line = {op:op, addr:addr, row:row, col:col, asm:null, info:[]}; this.dumplines[clk] = line; } switch (op) { case ProbeFlags.EXECUTE: if (platform.disassemble) { var disasm = platform.disassemble(addr, platform.readAddress.bind(platform)); line.asm = disasm && disasm.line; } break; default: var xtra = this.opToString(op, addr, value); if (xtra != "") line.info.push(xtra); break; } }); this.vlist.refresh(); } } export class ScanlineIOView extends ProbeViewBaseBase { vlist : VirtualTextScroller; maindiv : HTMLElement; recreateOnResize = true; dumplines; createDiv(parent : HTMLElement) { this.vlist = new VirtualTextScroller(parent); this.vlist.create(parent, this.totalScanlines, this.getMemoryLineAt.bind(this)); return this.vlist.maindiv; } getMemoryLineAt(row : number) : VirtualTextLine { var s = lpad(row+"",3) + ' '; var c = 'seg_code'; var line = (this.dumplines && this.dumplines[row]) || []; var hblankCycle = Math.round(this.cyclesPerLine/3.3); for (var i=0; i { var line = this.dumplines[row]; if (line == null) { this.dumplines[row] = line = []; } switch (op) { case ProbeFlags.EXECUTE: var sym = platform.debugSymbols.addr2symbol[addr]; if (sym) line[-1] = sym; break; //case ProbeFlags.MEM_WRITE: case ProbeFlags.IO_READ: case ProbeFlags.IO_WRITE: case ProbeFlags.VRAM_READ: case ProbeFlags.VRAM_WRITE: line[col] = op | addr; break; } }); this.vlist.refresh(); } } /// export class ProbeSymbolView extends ProbeViewBaseBase { vlist : VirtualTextScroller; keys : string[]; recreateOnResize = true; dumplines; cumulativeData = true; // TODO: auto resize createDiv(parent : HTMLElement) { // TODO: what if symbol list changes? if (platform.debugSymbols && platform.debugSymbols.symbolmap) { this.keys = Array.from(Object.keys(platform.debugSymbols.symbolmap).filter(sym => !ignoreSymbol(sym))); } else { this.keys = ['no symbols defined']; } this.vlist = new VirtualTextScroller(parent); this.vlist.create(parent, this.keys.length + 1, this.getMemoryLineAt.bind(this)); return this.vlist.maindiv; } getMemoryLineAt(row : number) : VirtualTextLine { // header line if (row == 0) { return {text: lpad("Symbol",35)+lpad("Reads",8)+lpad("Writes",8)}; } var sym = this.keys[row-1]; var line = this.dumplines && this.dumplines[sym]; function getop(op) { var n = line[op] | 0; return lpad(n ? n.toString() : "", 8); } var s : string; var c : string; if (line != null) { s = lpad(sym, 35) + getop(ProbeFlags.MEM_READ) + getop(ProbeFlags.MEM_WRITE); if (line[ProbeFlags.EXECUTE]) c = 'seg_code'; else if (line[ProbeFlags.IO_READ] || line[ProbeFlags.IO_WRITE]) c = 'seg_io'; else c = 'seg_data'; } else { s = lpad(sym, 35); c = 'seg_unknown'; } return {text:s, clas:c}; } refresh() { this.tick(); } tick() { // cache each line in frame this.dumplines = {}; this.redraw((op,addr,col,row,clk,value) => { var sym = platform.debugSymbols.addr2symbol[addr]; if (sym != null) { var line = this.dumplines[sym]; if (line == null) { line = {}; this.dumplines[sym] = line; } line[op] = (line[op] | 0) + 1; } }); this.vlist.refresh(); if (this.probe) this.probe.clear(); // clear cumulative data (TODO: doesnt work with seeking or debugging) } } ///