From 662f8a057d1617a8b3a5dea390bbe249bd8782c0 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Fri, 17 Aug 2018 15:13:58 -0400 Subject: [PATCH] analysis.ts for 6502 cycle counting (vcs, nes) --- index.html | 5 +- presets/nes/scrollrt.asm | 33 +++++-- src/analysis.ts | 196 +++++++++++++++++++++++++++++++++++++++ src/baseplatform.ts | 2 + src/platform/nes.ts | 8 +- src/platform/vcs.ts | 173 +--------------------------------- src/store.ts | 6 +- src/ui.ts | 33 +++---- src/views.ts | 22 +++++ 9 files changed, 279 insertions(+), 199 deletions(-) create mode 100644 src/analysis.ts diff --git a/index.html b/index.html index ee21718f..4042f52c 100644 --- a/index.html +++ b/index.html @@ -116,7 +116,7 @@ if (window.location.host.endsWith('8bitworkshop.com')) { - + @@ -245,12 +245,13 @@ function require(modname) { + + - diff --git a/presets/nes/scrollrt.asm b/presets/nes/scrollrt.asm index 441bb05b..d62aa93f 100644 --- a/presets/nes/scrollrt.asm +++ b/presets/nes/scrollrt.asm @@ -1,11 +1,12 @@  include "nesdefs.asm" -;;;;; VARIABLES +;;;;; ZERO-PAGE VARIABLES seg.u ZEROPAGE org $0 +;;;;; OTHER VARIABLES seg.u RAM org $300 @@ -34,7 +35,7 @@ Start: sta PPU_SCROLL ; scroll = $0000 lda #CTRL_NMI sta PPU_CTRL ; enable NMI - lda #MASK_BG + lda #MASK_BG|MASK_SPR sta PPU_MASK ; enable rendering .endless jmp .endless ; endless loop @@ -70,6 +71,16 @@ SetPalette: subroutine bne .loop rts +; set sprite 0 +SetSprite0: subroutine + sta $200 ;y + lda #1 ;code + sta $201 + lda #0 ;flags + sta $202 + lda #8 ;xpos + sta $203 + rts ;;;;; COMMON SUBROUTINES @@ -79,6 +90,17 @@ SetPalette: subroutine NMIHandler: subroutine SAVE_REGS + lda #112 + jsr SetSprite0 +; load sprites + lda #$02 + sta PPU_OAM_DMA +; wait for sprite 0 +.wait0 bit PPU_STATUS + bvs .wait0 +.wait1 bit PPU_STATUS + bvc .wait1 +; alter horiz. scroll position for each scanline ldy #0 .loop tya @@ -91,10 +113,9 @@ NMIHandler: subroutine sta PPU_SCROLL ; horiz byte lda #0 sta PPU_SCROLL ; vert byte - ldx #15 -.delay - dex - bne .delay + REPEAT 25 + bit $0000 + REPEND iny cpy #224 bne .loop diff --git a/src/analysis.ts b/src/analysis.ts new file mode 100644 index 00000000..73dd5c17 --- /dev/null +++ b/src/analysis.ts @@ -0,0 +1,196 @@ + +import { hex, byte2signed } from "./util"; +import { Platform } from "./baseplatform"; + +export interface CodeAnalyzer { + showLoopTimingForPC(pc:number); + pc2minclocks : {[key:number]:number}; + pc2maxclocks : {[key:number]:number}; + 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; +} + +abstract class CodeAnalyzer6502 implements CodeAnalyzer { + pc2minclocks = {}; + pc2maxclocks = {}; + START_CLOCKS : number; + MAX_CLOCKS : number; + WRAP_CLOCKS : boolean; + jsrresult = {}; + platform : Platform; + + 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) { + //console.log("trace", hex(pc), minclocks, maxclocks); + if (!minclocks) minclocks = 0; + if (!maxclocks) maxclocks = 0; + if (!constraints) constraints = {}; + var modified = true; + var abort = false; + for (var i=0; i<1000 && modified && !abort; i++) { + modified = false; + var meta = this.getClockCountsAtPC(pc); + var lob = this.platform.readAddress(pc+1); + var hib = this.platform.readAddress(pc+2); + var addr = lob + (hib << 8); + var pc0 = pc; + if (!this.pc2minclocks[pc0] || minclocks < this.pc2minclocks[pc0]) { + this.pc2minclocks[pc0] = minclocks; + modified = true; + } + if (!this.pc2maxclocks[pc0] || maxclocks > this.pc2maxclocks[pc0]) { + this.pc2maxclocks[pc0] = maxclocks; + modified = true; + } + //console.log(hex(pc),minclocks,maxclocks,meta); + if (!meta.insnlength) { + console.log("Illegal instruction!", hex(pc), hex(meta.opcode), meta); + break; + } + pc += meta.insnlength; + var oldconstraints = constraints; + constraints = null; + // TODO: if jump to zero-page, maybe assume RTS? + switch (meta.opcode) { + /* + case 0xb9: // TODO: hack for zero page,y + if (addr < 0x100) + meta.maxCycles -= 1; + break; + */ + // TODO: don't do in NES + case 0x85: + if (lob == 0x2) { // STA WSYNC + minclocks = maxclocks = 0; + meta.minCycles = meta.maxCycles = 0; + } + break; + case 0x20: // JSR + 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)); + return; + } + break; + case 0x4c: // JMP + pc = addr; // TODO: make sure in ROM space + 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]); + if (cons0 !== false) { + this.traceInstructions(newpc, minclocks+meta.maxCycles, maxclocks+meta.maxCycles, subaddr, cons[0]); + } + if (cons1 === false) { + console.log("abort", 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; + } + minclocks = Math.min(this.MAX_CLOCKS, minclocks + meta.minCycles); + maxclocks = Math.min(this.MAX_CLOCKS, maxclocks + meta.maxCycles); + if (this.WRAP_CLOCKS && maxclocks >= this.MAX_CLOCKS) { + minclocks = minclocks % this.MAX_CLOCKS; + maxclocks = maxclocks % this.MAX_CLOCKS; + } + } + } + + showLoopTimingForPC(pc:number) { + this.pc2minclocks = {}; + this.pc2maxclocks = {}; + this.jsrresult = {}; + // recurse through all traces + this.traceInstructions(pc | this.platform.getOriginPC(), this.START_CLOCKS, this.MAX_CLOCKS, 0, {}); + } +} + +// 76 cycles * 2 (support two scanline kernels) +export class CodeAnalyzer_vcs extends CodeAnalyzer6502 { + constructor(platform : Platform) { + super(platform); + this.MAX_CLOCKS = this.START_CLOCKS = 76*2; + this.WRAP_CLOCKS = false; + } +} + +// 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/3 + this.START_CLOCKS = 0; + this.WRAP_CLOCKS = true; + } +} + diff --git a/src/baseplatform.ts b/src/baseplatform.ts index 82b4b383..d06d572f 100644 --- a/src/baseplatform.ts +++ b/src/baseplatform.ts @@ -1,6 +1,7 @@ import { RAM, RasterVideo, dumpRAM, dumpStackToString } from "./emu"; import { hex } from "./util"; +import { CodeAnalyzer } from "./analysis"; declare var Z80_fast, jt, CPU6809; @@ -52,6 +53,7 @@ export interface Platform { getDebugCallback?() : any; // TODO getSP?() : number; getOriginPC?() : number; + newCodeAnalyzer() : CodeAnalyzer; getDebugCategories() : string[]; getDebugInfo(category:string, state:EmuState) : string; diff --git a/src/platform/nes.ts b/src/platform/nes.ts index 530a2495..b9c866ff 100644 --- a/src/platform/nes.ts +++ b/src/platform/nes.ts @@ -3,6 +3,7 @@ import { Platform, Base6502Platform, BaseMAMEPlatform, getOpcodeMetadata_6502, cpuStateToLongString_6502, getToolForFilename_6502 } from "../baseplatform"; import { PLATFORMS, RAM, newAddressDecoder, padBytes, noise, setKeyboardFromMap, AnimationTimer, RasterVideo, Keys, makeKeycodeMap, dumpRAM, dumpStackToString } from "../emu"; import { hex, lpad, lzgmini } from "../util"; +import { CodeAnalyzer_nes } from "../analysis"; declare var jsnes : any; @@ -131,7 +132,12 @@ var JSNESPlatform = function(mainElement) { nes.loadROM(romstr); frameindex = 0; } - + this.newCodeAnalyzer = function() { + return new CodeAnalyzer_nes(this); + } + this.getOriginPC = function() { // TODO: is actually NMI + return (this.readAddress(0xfffa) | (this.readAddress(0xfffb) << 8)) & 0xffff; + } this.getOpcodeMetadata = getOpcodeMetadata_6502; this.getDefaultExtension = function() { return ".c"; }; diff --git a/src/platform/vcs.ts b/src/platform/vcs.ts index ff59ce7b..6723caba 100644 --- a/src/platform/vcs.ts +++ b/src/platform/vcs.ts @@ -3,6 +3,7 @@ import { Platform, cpuStateToLongString_6502, BaseMAMEPlatform } from "../baseplatform"; import { PLATFORMS, RAM, newAddressDecoder, dumpRAM } from "../emu"; import { hex, lpad, tobin, byte2signed } from "../util"; +import { CodeAnalyzer_vcs } from "../analysis"; declare var platform : Platform; // global platform object declare var Javatari : any; @@ -99,6 +100,9 @@ var VCSPlatform = function() { this.getOriginPC = function() { return (this.readAddress(0xfffc) | (this.readAddress(0xfffd) << 8)) & 0xffff; } + this.newCodeAnalyzer = function() { + return new CodeAnalyzer_vcs(this); + } /* this.saveState = function() { return Javatari.room.console.saveState(); // TODO @@ -184,175 +188,6 @@ function nonegstr(n) { return n < 0 ? "-" : n.toString(); } -/// VCS TIMING ANALYSIS - -var pc2minclocks = {}; -var pc2maxclocks = {}; -var jsrresult = {}; -var MAX_CLOCKS = 76*2; - -// [taken, not taken] -var 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; -} - -function getClockCountsAtPC(pc) { - var opcode = platform.readAddress(pc); - var meta = platform.getOpcodeMetadata(opcode, pc); - return meta; // minCycles, maxCycles -} - -function _traceInstructions(pc:number, minclocks:number, maxclocks:number, subaddr:number, constraints) { - //console.log("trace", hex(pc), minclocks, maxclocks); - if (!minclocks) minclocks = 0; - if (!maxclocks) maxclocks = 0; - if (!constraints) constraints = {}; - var modified = true; - var abort = false; - for (var i=0; i<1000 && modified && !abort; i++) { - modified = false; - var meta = getClockCountsAtPC(pc); - var lob = platform.readAddress(pc+1); - var hib = platform.readAddress(pc+2); - var addr = lob + (hib << 8); - var pc0 = pc; - if (!pc2minclocks[pc0] || minclocks < pc2minclocks[pc0]) { - pc2minclocks[pc0] = minclocks; - modified = true; - } - if (!pc2maxclocks[pc0] || maxclocks > pc2maxclocks[pc0]) { - pc2maxclocks[pc0] = maxclocks; - modified = true; - } - //console.log(hex(pc),minclocks,maxclocks,meta); - if (!meta.insnlength) { - console.log("Illegal instruction!", hex(pc), hex(meta.opcode), meta); - break; - } - pc += meta.insnlength; - var oldconstraints = constraints; - constraints = null; - // TODO: if jump to zero-page, maybe assume RTS? - switch (meta.opcode) { - /* - case 0xb9: // TODO: hack for zero page,y - if (addr < 0x100) - meta.maxCycles -= 1; - break; - */ - case 0x85: - if (lob == 0x2) { // STA WSYNC - minclocks = maxclocks = 0; - meta.minCycles = meta.maxCycles = 0; - } - break; - case 0x20: // JSR - _traceInstructions(addr, minclocks, maxclocks, addr, constraints); - var result = jsrresult[addr]; - if (result) { - minclocks = result.minclocks; - maxclocks = result.maxclocks; - } else { - console.log("No JSR result!", hex(pc), hex(addr)); - return; - } - break; - case 0x4c: // JMP - pc = addr; // TODO: make sure in ROM space - break; - case 0x60: // RTS - if (subaddr) { // TODO: 0 doesn't work - // TODO: combine with previous result - var result = jsrresult[subaddr]; - if (!result) { - result = {minclocks:minclocks, maxclocks:maxclocks}; - } else { - result = { - minclocks:Math.min(minclocks,result.minclocks), - maxclocks:Math.max(maxclocks,result.maxclocks) - } - } - jsrresult[subaddr] = result; - console.log("RTS", hex(pc), hex(subaddr), 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]); - if (cons0 !== false) { - _traceInstructions(newpc, minclocks+meta.maxCycles, maxclocks+meta.maxCycles, subaddr, cons[0]); - } - if (cons1 === false) { - console.log("abort", 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; - } - // TODO: wraparound? - minclocks = Math.min(MAX_CLOCKS, minclocks + meta.minCycles); - maxclocks = Math.min(MAX_CLOCKS, maxclocks + meta.maxCycles); - } -} - -function showLoopTimingForPC(pc, sourcefile, ed) { - pc2minclocks = {}; - pc2maxclocks = {}; - jsrresult = {}; - // recurse through all traces - _traceInstructions(pc | platform.getOriginPC(), MAX_CLOCKS, MAX_CLOCKS, 0, {}); - ed.editor.clearGutter("gutter-bytes"); - // show the lines - for (var line in sourcefile.line2offset) { - var pc = sourcefile.line2offset[line]; - var minclocks = pc2minclocks[pc]; - var maxclocks = pc2maxclocks[pc]; - if (minclocks>=0 && maxclocks>=0) { - var s; - if (maxclocks == minclocks) - s = minclocks + ""; - else - s = minclocks + "-" + maxclocks; - if (maxclocks == MAX_CLOCKS) - s += "+"; - ed.setGutterBytes(line, s); - } - } -} - /////////////// var VCSMAMEPlatform = function(mainElement) { diff --git a/src/store.ts b/src/store.ts index 703858bf..b6562551 100644 --- a/src/store.ts +++ b/src/store.ts @@ -68,7 +68,7 @@ localforage.defineDriver(OldFileStoreDriver); */ // copy localStorage to new driver -function copyFromOldStorageFormat(platformid:string, newstore, callback:()=>void) { +function copyFromOldStorageFormat(platformid:string, newstore, conversioncallback:()=>void) { var alreadyMigratedKey = "__migrated_" + platformid; //localStorage.removeItem(alreadyMigratedKey); if (localStorage.getItem(alreadyMigratedKey)) @@ -97,8 +97,8 @@ function copyFromOldStorageFormat(platformid:string, newstore, callback:()=>void console.log("Migrated " + len + " local files to new data store"); if (len) { localStorage.setItem(alreadyMigratedKey, 'true'); - if (callback) - callback(); + if (conversioncallback) + conversioncallback(); else window.location.reload(); } diff --git a/src/ui.ts b/src/ui.ts index 07e026c4..6cdbbe95 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1,6 +1,5 @@ "use strict"; -import { getFilenameForPath, getFilenamePrefix, highlightDifferences, invertMap } from "./util"; // 8bitworkshop IDE user interface @@ -12,11 +11,11 @@ import { ProjectWindows } from "./windows"; import { Platform, Preset } from "./baseplatform"; import { PLATFORMS } from "./emu"; import * as Views from "./views"; +import { createNewPersistentStore } from "./store"; +import { getFilenameForPath, getFilenamePrefix, highlightDifferences, invertMap } from "./util"; // external libs (TODO) declare var Octokat, ga, Tour, GIF, saveAs; -declare function createNewPersistentStore(platform_id : string); -declare function showLoopTimingForPC(pc:number, sourcefile:SourceFile, wnd:Views.ProjectView); // make sure VCS doesn't start if (window['Javatari']) window['Javatari'].AUTO_START = false; @@ -669,8 +668,10 @@ function _openBitmapEditor() { function traceTiming() { projectWindows.refresh(false); var wnd = projectWindows.getActive(); - if (wnd.getSourceFile && wnd.setGutterBytes) { // is editor active? - showLoopTimingForPC(0, wnd.getSourceFile(), wnd); + if (wnd.getSourceFile && wnd.setTimingResult) { // is editor active? + var analyzer = platform.newCodeAnalyzer(); + analyzer.showLoopTimingForPC(0); + wnd.setTimingResult(analyzer); } } @@ -689,7 +690,7 @@ function setupDebugControls(){ $("#dbg_tovsync").hide(); if ((platform.runEval || platform.runToPC) && platform_id != 'verilog') $("#dbg_toline").click(runToCursor).show(); - else + else $("#dbg_toline").hide(); if (platform.runUntilReturn) $("#dbg_stepout").click(runUntilReturn).show(); @@ -700,7 +701,7 @@ function setupDebugControls(){ else $("#dbg_stepback").hide(); - if (window['showLoopTimingForPC']) { // VCS-only (TODO: put in platform) + if (platform.newCodeAnalyzer) { $("#dbg_timing").click(traceTiming).show(); } $("#disassembly").hide(); @@ -823,10 +824,6 @@ function gotoNewLocation() { window.location.href = "?" + $.param(qs); } -function initPlatform() { - store = createNewPersistentStore(platform_id); -} - function showBookLink() { if (platform_id == 'vcs') $("#booklink_vcs").show(); @@ -860,7 +857,6 @@ function addPageFocusHandlers() { } function startPlatform() { - initPlatform(); if (!PLATFORMS[platform_id]) throw Error("Invalid platform '" + platform_id + "'."); platform = new PLATFORMS[platform_id]($("#emulator")[0]); PRESETS = platform.getPresets(); @@ -893,11 +889,12 @@ function loadSharedFile(sharekey : string) { var json = JSON.parse(val.description.slice(val.description.indexOf(' ')+1)); console.log("Fetched " + newid, json); platform_id = json['platform']; - initPlatform(); - current_project.updateFile(newid, val.files[filename].content); - reloadPresetNamed(newid); - delete qs['sharekey']; - gotoNewLocation(); + store = createNewPersistentStore(platform_id, () => { + current_project.updateFile(newid, val.files[filename].content); + reloadPresetNamed(newid); + delete qs['sharekey']; + gotoNewLocation(); + }); }).fail(function(err) { alert("Error loading share file: " + err.message); }); @@ -918,9 +915,9 @@ function startUI(loadplatform : boolean) { if (qs['sharekey']) { loadSharedFile(qs['sharekey']); } else { + store = createNewPersistentStore(platform_id, null); // reset file? if (qs['file'] && qs['reset']) { - initPlatform(); store.removeItem(qs['fileview'] || qs['file']); qs['reset'] = ''; gotoNewLocation(); diff --git a/src/views.ts b/src/views.ts index 7a364acf..2b2276dd 100644 --- a/src/views.ts +++ b/src/views.ts @@ -5,6 +5,7 @@ import { CodeProject } from "./project"; import { SourceFile, WorkerError } from "./workertypes"; import { Platform } from "./baseplatform"; import { hex } from "./util"; +import { CodeAnalyzer } from "./analysis"; export interface ProjectView { createDiv(parent:HTMLElement, text:string) : HTMLElement; @@ -17,6 +18,7 @@ export interface ProjectView { openBitmapEditorAtCursor?() : void; markErrors?(errors:WorkerError[]) : void; clearErrors?() : void; + setTimingResult?(result:CodeAnalyzer) : void; }; // TODO: move to different namespace @@ -211,6 +213,26 @@ export class SourceEditor implements ProjectView { this.setGutter("gutter-bytes", line-1, s); } + setTimingResult(result:CodeAnalyzer) : void { + this.editor.clearGutter("gutter-bytes"); + // show the lines + for (const line in Object.keys(this.sourcefile.line2offset)) { + var pc = this.sourcefile.line2offset[line]; + var minclocks = result.pc2minclocks[pc]; + var maxclocks = result.pc2maxclocks[pc]; + if (minclocks>=0 && maxclocks>=0) { + var s; + if (maxclocks == minclocks) + s = minclocks + ""; + else + s = minclocks + "-" + maxclocks; + if (maxclocks == result.MAX_CLOCKS) + s += "+"; + this.setGutterBytes(parseInt(line), s); + } + } + } + setCurrentLine(line:number, moveCursor:boolean) { var addCurrentMarker = (line:number) => {