import { hex, byte2signed } from "./util"; import { OpcodeMetadata, Platform } from "./baseplatform"; const debug = false; export interface CodeAnalyzer { showLoopTimingForPC(pc: number); pc2clockrange: { [key: number]: ClockRange }; MAX_CLOCKS: number; } /// VCS TIMING ANALYSIS // [taken, not taken] const BRANCH_CONSTRAINTS = [ [{ N: 0 }, { N: 1 }], [{ N: 1 }, { N: 0 }], [{ V: 0 }, { V: 1 }], [{ V: 1 }, { V: 0 }], [{ C: 0 }, { C: 1 }], [{ C: 1 }, { C: 0 }], [{ Z: 0 }, { Z: 1 }], [{ Z: 1 }, { Z: 0 }] ]; function constraintEquals(a, b) { if (a == null || b == null) return null; for (var n in a) { if (b[n] !== 'undefined') return a[n] == b[n]; } for (var n in b) { if (a[n] !== 'undefined') return a[n] == b[n]; } return null; } interface ClockRange { minclocks: number; maxclocks: number; } abstract class CodeAnalyzer6502 implements CodeAnalyzer { pc2clockrange: { [key: number]: ClockRange } = {}; jsrresult: { [key: number]: ClockRange } = {}; START_CLOCKS: number; MAX_CLOCKS: number; WRAP_CLOCKS: boolean; platform: Platform; MAX_CYCLES: number = 2000; constructor(platform: Platform) { this.platform = platform; } getClockCountsAtPC(pc) { var opcode = this.platform.readAddress(pc); var meta = this.platform.getOpcodeMetadata(opcode, pc); return meta; // minCycles, maxCycles } traceInstructions(pc: number, minclocks: number, maxclocks: number, subaddr: number, constraints) { if (debug) console.log("trace", hex(pc), minclocks, maxclocks); if (!constraints) constraints = {}; var modified = true; var abort = false; for (let i = 0; modified && !abort; i++) { if (i >= this.MAX_CYCLES) { console.log("too many cycles @", hex(pc), "routine", hex(subaddr)); break; } modified = false; if (this.WRAP_CLOCKS) { // wrap clocks minclocks = minclocks % this.MAX_CLOCKS; maxclocks = maxclocks % this.MAX_CLOCKS; if (maxclocks == minclocks - 1) { if (debug) console.log("0-75", hex(pc), minclocks, maxclocks); minclocks = 0; maxclocks = this.MAX_CLOCKS - 1; } } else { // truncate clocks minclocks = Math.min(this.MAX_CLOCKS, minclocks); maxclocks = Math.min(this.MAX_CLOCKS, maxclocks); } let meta = this.getClockCountsAtPC(pc); let lob = this.platform.readAddress(pc + 1); let hib = this.platform.readAddress(pc + 2); let addr = lob + (hib << 8); let pc0 = pc; let pcrange = this.pc2clockrange[pc0]; if (pcrange == null) { this.pc2clockrange[pc0] = pcrange = { minclocks: minclocks, maxclocks: maxclocks }; if (debug) console.log("new", hex(pc), hex(pc0), hex(subaddr), minclocks, maxclocks); modified = true; } //console.log(hex(pc),minclocks,maxclocks, pcrange); if (pcrange.minclocks != minclocks || pcrange.maxclocks != maxclocks) { if (this.WRAP_CLOCKS && (minclocks <= maxclocks) != (pcrange.minclocks <= pcrange.maxclocks)) { if (debug) console.log("wrap", hex(pc), hex(pc0), hex(subaddr), minclocks, maxclocks, pcrange); pcrange.minclocks = minclocks = 0; pcrange.maxclocks = maxclocks = this.MAX_CLOCKS - 1; modified = true; } if (minclocks < pcrange.minclocks) { if (debug) console.log("min", hex(pc), hex(pc0), hex(subaddr), minclocks, maxclocks, pcrange); pcrange.minclocks = minclocks; modified = true; } if (maxclocks > pcrange.maxclocks) { if (debug) console.log("max", hex(pc), hex(pc0), hex(subaddr), minclocks, maxclocks, pcrange); pcrange.maxclocks = maxclocks; modified = true; } } if (!meta.insnlength) { console.log("Illegal instruction!", hex(pc), hex(meta.opcode), meta); break; } pc += meta.insnlength; var oldconstraints = constraints; constraints = null; let syncMaxCycles = this.getMaxCyclesForSync(meta, lob, hib); if (typeof syncMaxCycles === 'number') { minclocks = 0; maxclocks = syncMaxCycles; meta.minCycles = meta.maxCycles = 0; } else { // TODO: if jump to zero-page, maybe assume RTS? switch (meta.opcode) { case 0x19: case 0x1d: case 0x39: case 0x3d: case 0x59: case 0x5d: case 0x79: case 0x7d: case 0xb9: case 0xbb: case 0xbc: case 0xbd: case 0xbe: case 0xbf: case 0xd9: case 0xdd: case 0xf9: case 0xfd: if (lob == 0) meta.maxCycles -= 1; // no page boundary crossed break; case 0x20: // JSR // TODO: handle bare RTS case minclocks += meta.minCycles; maxclocks += meta.maxCycles; this.traceInstructions(addr, minclocks, maxclocks, addr, constraints); var result = this.jsrresult[addr]; if (result) { minclocks = result.minclocks; maxclocks = result.maxclocks; } else { console.log("No JSR result!", hex(pc), hex(addr)); minclocks = maxclocks; //return; } break; case 0x4c: // JMP pc = addr; // TODO: make sure in ROM space break; case 0x40: // RTI abort = true; break; case 0x60: // RTS if (subaddr) { // TODO: 0 doesn't work // TODO: combine with previous result var result = this.jsrresult[subaddr]; if (!result) { result = { minclocks: minclocks, maxclocks: maxclocks }; } else { result = { minclocks: Math.min(minclocks, result.minclocks), maxclocks: Math.max(maxclocks, result.maxclocks) } } this.jsrresult[subaddr] = result; console.log("RTS", hex(pc), hex(subaddr), this.jsrresult[subaddr]); } return; case 0x10: case 0x30: // branch case 0x50: case 0x70: case 0x90: case 0xB0: case 0xD0: case 0xF0: var newpc = pc + byte2signed(lob); var crosspage = (pc >> 8) != (newpc >> 8); if (!crosspage) meta.maxCycles--; // TODO: other instructions might modify flags too var cons = BRANCH_CONSTRAINTS[Math.floor((meta.opcode - 0x10) / 0x20)]; var cons0 = constraintEquals(oldconstraints, cons[0]); var cons1 = constraintEquals(oldconstraints, cons[1]); // recursively trace the taken branch if (true || cons0 !== false) { // TODO? this.traceInstructions(newpc, minclocks + meta.maxCycles, maxclocks + meta.maxCycles, subaddr, cons[0]); } // abort if we will always take the branch if (cons1 === false) { console.log("branch always taken", hex(pc), oldconstraints, cons[1]); abort = true; } constraints = cons[1]; // not taken meta.maxCycles = meta.minCycles; // branch not taken, no extra clock(s) break; case 0x6c: console.log("Instruction not supported!", hex(pc), hex(meta.opcode), meta); // TODO return; } } // add min/max instruction time to min/max clocks bound if (debug) console.log("add", hex(pc), meta.minCycles, meta.maxCycles); minclocks += meta.minCycles; maxclocks += meta.maxCycles; } } showLoopTimingForPC(pc: number) { this.pc2clockrange = {}; this.jsrresult = {}; // recurse through all traces this.traceInstructions(pc | this.platform.getOriginPC(), this.START_CLOCKS, this.MAX_CLOCKS, 0, {}); } getMaxCyclesForSync(meta: OpcodeMetadata, lob: number, hib: number) { } } // 76 cycles export class CodeAnalyzer_vcs extends CodeAnalyzer6502 { constructor(platform: Platform) { super(platform); this.MAX_CLOCKS = 76; // 1 scanline this.START_CLOCKS = 0; // TODO? this.WRAP_CLOCKS = true; } getMaxCyclesForSync(meta: OpcodeMetadata, lob: number, hib: number) { if (meta.opcode == 0x85) { if (lob == 0x2) { // STA WSYNC return 0; } } } } // https://wiki.nesdev.com/w/index.php/PPU_rendering#Line-by-line_timing // TODO: sprite 0 hit, CPU stalls export class CodeAnalyzer_nes extends CodeAnalyzer6502 { constructor(platform: Platform) { super(platform); this.MAX_CLOCKS = 114; // 341 clocks for 3 scanlines this.START_CLOCKS = 0; this.WRAP_CLOCKS = true; } getMaxCyclesForSync(meta: OpcodeMetadata, lob: number, hib: number) { if (meta.opcode == 0x2c) { if (lob == 0x02 && hib == 0x20) { // BIT $2002 return 4; // uncertainty b/c of assumed branch poll } } } } export class CodeAnalyzer_apple2 extends CodeAnalyzer6502 { constructor(platform: Platform) { super(platform); this.MAX_CLOCKS = 65; this.START_CLOCKS = 0; this.WRAP_CLOCKS = true; } getMaxCyclesForSync(meta: OpcodeMetadata, lob: number, hib: number) { if (meta.opcode == 0xad) { if (lob == 0x61 && hib == 0xc0) { // LDA $C061 return 4; // uncertainty b/c of assumed branch poll } } } }