analysis.ts for 6502 cycle counting (vcs, nes)

This commit is contained in:
Steven Hugg 2018-08-17 15:13:58 -04:00
parent 9d3e658a7b
commit 662f8a057d
9 changed files with 279 additions and 199 deletions

View File

@ -116,7 +116,7 @@ if (window.location.host.endsWith('8bitworkshop.com')) {
<button id="dbg_fastest" type="submit" title="Faster"><span class="glyphicon glyphicon-fast-forward" aria-hidden="true"></span></button>
</span>
<span class="btn_group view_group" id="extra_bar">
<button id="dbg_timing" type="submit" title="See Timing" style="display:none"><span class="glyphicon glyphicon-time" aria-hidden="true"></span></button>
<button id="dbg_timing" type="submit" title="Analyze CPU Timing" style="display:none"><span class="glyphicon glyphicon-time" aria-hidden="true"></span></button>
<button id="dbg_disasm" type="submit" title="Show Disassembly" style="display:none"><span class="glyphicon glyphicon-list" aria-hidden="true"></span></button>
<button id="dbg_memory" type="submit" title="Show Memory" style="display:none"><span class="glyphicon glyphicon-sunglasses" aria-hidden="true"></span></button>
<button id="dbg_profile" type="submit" title="Show Profile" style="display:none"><span class="glyphicon glyphicon-stats" aria-hidden="true"></span></button>
@ -245,12 +245,13 @@ function require(modname) {
<script src="tss/js/tss/AudioLooper.js"></script>
<script src="tss/js/Log.js"></script>
<script src="gen/util.js"></script>
<script src="gen/store.js"></script>
<script src="src/vlist.js"></script>
<script src="gen/emu.js"></script>
<script src="gen/baseplatform.js"></script>
<script src="gen/analysis.js"></script>
<script src="gen/audio.js"></script>
<script src="gen/util.js"></script>
<script src="gen/cpu/disasm6502.js"></script>
<script src="gen/workertypes.js"></script>
<script src="gen/project.js"></script>

View File

@ -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

196
src/analysis.ts Normal file
View File

@ -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;
}
}

View File

@ -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;

View File

@ -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"; };

View File

@ -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) {

View File

@ -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();
}

View File

@ -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();

View File

@ -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) => {