8bitworkshop/src/ui.js

1047 lines
29 KiB
JavaScript
Raw Normal View History

"use strict";
2016-12-16 01:21:51 +00:00
// catch errors
function installErrorHandler() {
if (typeof window.onerror == "object") {
window.onerror = function (msgevent, url, line, col, error) {
console.log(msgevent, url, line, col);
console.log(error);
//$("#editor").hide();
if (window.location.host.endsWith('8bitworkshop.com')) {
ga('send', 'exception', {
'exDescription': msgevent + " " + url + " " + " " + line + ":" + col + ", " + error,
'exFatal': true
});
}
alert(msgevent+"");
};
}
}
function uninstallErrorHandler() {
window.onerror = null;
}
function gotoNewLocation() {
uninstallErrorHandler();
window.location = "?" + $.param(qs);
}
// make sure VCS doesn't start
Javatari.AUTO_START = false;
2017-01-13 02:21:35 +00:00
// 8bitworkshop IDE user interface
var PRESETS; // presets array
var platform_id;
var platform; // platform object
var toolbar = $("#controls_top");
var FileStore = function(storage, prefix) {
var self = this;
this.saveFile = function(name, text) {
storage.setItem(prefix + name, text);
}
this.loadFile = function(name) {
return storage.getItem(prefix + name) || storage.getItem(name);
}
this.getFiles = function(prefix2) {
2017-01-13 02:21:35 +00:00
// rename items for compatibility
for (var i = 0; i < storage.length; i++) {
var key = storage.key(i);
if (key.startsWith(prefix2) && platform_id == 'vcs') {
this.saveFile(key, storage.getItem(key));
storage.removeItem(key);
console.log("Renamed",key,'to',prefix+key);
i=-1; // reset loop
}
}
// iterate over files with <platform>/<dir> prefix
var files = [];
for (var i = 0; i < storage.length; i++) {
var key = storage.key(i);
if (key.startsWith(prefix + prefix2)) {
var name = key.substring(prefix.length + prefix2.length);
files.push(name);
}
}
return files;
}
this.deleteFile = function(name) {
2017-02-18 22:50:51 +00:00
storage.removeItem(prefix + name);
storage.removeItem(prefix + 'local/' + name);
}
}
var SourceFile = function(lines, text) {
lines = lines || [];
this.text = text;
this.offset2line = {};
this.line2offset = {};
for (var info of lines) {
if (info.offset >= 0) {
this.offset2line[info.offset] = info.line;
this.line2offset[info.line] = info.offset;
}
}
this.findLineForOffset = function(PC) {
if (this.offset2line) {
for (var i=0; i<16; i++) {
var line = this.offset2line[PC];
if (line >= 0) {
return line;
}
PC--;
}
}
return 0;
}
}
var TOOL_TO_SOURCE_STYLE = {
'dasm': '6502',
'acme': '6502',
'cc65': 'text/x-csrc',
'ca65': '6502',
'z80asm': 'z80',
'sdasz80': 'z80',
'sdcc': 'text/x-csrc',
}
2016-12-16 01:21:51 +00:00
var worker = new Worker("./src/worker/workermain.js");
var current_output = null;
var current_preset_index = -1; // TODO: use URL
2016-12-16 01:21:51 +00:00
var current_preset_id = null;
var assemblyfile = null;
var sourcefile = null;
2017-01-06 14:49:07 +00:00
var pcvisits;
2016-12-16 01:21:51 +00:00
var trace_pending_at_pc;
var store;
var pendingWorkerMessages = 0;
var editor;
2016-12-16 01:21:51 +00:00
2017-01-08 15:51:19 +00:00
var disasmview = CodeMirror(document.getElementById('disassembly'), {
mode: 'z80',
2017-01-08 15:51:19 +00:00
theme: 'cobalt',
tabSize: 8,
readOnly: true,
styleActiveLine: true
2017-01-08 15:51:19 +00:00
});
function newEditor(mode) {
var isAsm = (mode != 'text/x-csrc');
editor = CodeMirror(document.getElementById('editor'), {
theme: 'mbo',
lineNumbers: true,
matchBrackets: true,
tabSize: 8,
2017-02-20 23:50:29 +00:00
indentAuto: true,
gutters: isAsm ? ["CodeMirror-linenumbers", "gutter-offset", "gutter-bytes", "gutter-clock", "gutter-info"]
: ["CodeMirror-linenumbers", "gutter-offset", "gutter-info"],
});
2017-02-20 23:50:29 +00:00
var timer;
editor.on('changes', function(ed, changeobj) {
2017-02-20 23:50:29 +00:00
clearTimeout(timer);
timer = setTimeout(function() {
setCode(editor.getValue());
2017-02-20 23:50:29 +00:00
}, 200);
});
editor.setOption("mode", mode);
}
2016-12-16 01:21:51 +00:00
function getCurrentPresetTitle() {
if (current_preset_index < 0)
2016-12-16 01:21:51 +00:00
return "ROM";
else
return PRESETS[current_preset_index].title || PRESETS[current_preset_index].name || "ROM";
2016-12-16 01:21:51 +00:00
}
function setLastPreset(id) {
localStorage.setItem("__lastplatform", platform_id);
localStorage.setItem("__lastid_"+platform_id, id);
2016-12-16 01:21:51 +00:00
}
function updatePreset(current_preset_id, text) {
if (text.trim().length) {
store.saveFile(current_preset_id, text);
2016-12-16 01:21:51 +00:00
}
}
function loadCode(text, fileid) {
var tool = platform.getToolForFilename(fileid);
newEditor(tool && TOOL_TO_SOURCE_STYLE[tool]);
editor.setValue(text); // calls setCode()
editor.clearHistory();
2016-12-16 01:21:51 +00:00
current_output = null;
setLastPreset(fileid);
2016-12-16 01:21:51 +00:00
}
function loadFile(fileid, filename, index) {
current_preset_id = fileid;
current_preset_index = index;
var text = store.loadFile(fileid) || "";
2016-12-16 01:21:51 +00:00
if (text) {
loadCode(text, fileid);
2016-12-16 01:21:51 +00:00
} else if (!text && index >= 0) {
2017-02-14 23:15:06 +00:00
if (filename.indexOf('.') <= 0)
filename += ".a";
2016-12-16 01:21:51 +00:00
console.log("Loading preset", fileid, filename, index, PRESETS[index]);
if (text.length == 0) {
console.log("Fetching", filename);
$.get( filename, function( text ) {
console.log("GET",text.length,'bytes');
loadCode(text, fileid);
}, 'text')
.fail(function() {
alert("Could not load preset " + fileid);
loadCode("", fileid);
});
2016-12-16 01:21:51 +00:00
}
} else {
var ext = platform.getToolForFilename(fileid);
$.get( "presets/"+platform_id+"/skeleton."+ext, function( text ) {
loadCode(text, fileid);
2016-12-16 01:21:51 +00:00
updatePreset(fileid, text);
}, 'text')
.fail(function() {
alert("Could not load skeleton for " + platform_id + "/" + ext);
loadCode("", fileid);
});
2016-12-16 01:21:51 +00:00
}
}
function loadPreset(preset_id) {
// TODO
var index = parseInt(preset_id+"");
for (var i=0; i<PRESETS.length; i++)
if (PRESETS[i].id == preset_id)
index = i;
index = (index + PRESETS.length) % PRESETS.length;
if (index >= 0) {
// load the preset
loadFile(preset_id, "presets/" + platform_id + "/" + PRESETS[index].id, index);
2016-12-16 01:21:51 +00:00
} else {
// no preset found? load local
loadFile(preset_id, "local/" + platform_id + "/" + preset_id, -1);
2016-12-16 01:21:51 +00:00
}
}
function gotoPresetAt(index) {
var index = (index + PRESETS.length) % PRESETS.length;
qs['file'] = PRESETS[index].id;
gotoNewLocation();
2016-12-16 01:21:51 +00:00
}
function gotoPresetNamed(id) {
qs['platform'] = platform_id;
qs['file'] = id;
gotoNewLocation();
2016-12-16 01:21:51 +00:00
}
function _createNewFile(e) {
var filename = prompt("Create New File", "newfile.a");
if (filename && filename.length) {
if (filename.indexOf(".") < 0) {
filename += ".a";
}
qs['file'] = "local/" + filename;
gotoNewLocation();
2016-12-16 01:21:51 +00:00
}
2016-12-30 23:51:15 +00:00
return true;
2016-12-16 01:21:51 +00:00
}
2017-02-02 19:11:52 +00:00
function getCurrentFilename() {
var toks = current_preset_id.split("/");
return toks[toks.length-1];
}
2016-12-16 01:21:51 +00:00
function _shareFile(e) {
if (current_output == null) {
2016-12-30 23:51:15 +00:00
alert("Please fix errors before sharing.");
return true;
}
2017-01-25 17:30:05 +00:00
var github = new Octokat();
var files = {};
2016-12-16 01:21:51 +00:00
var text = editor.getValue();
2017-02-02 19:11:52 +00:00
files[getCurrentFilename()] = {"content": text};
2017-01-25 17:30:05 +00:00
var gistdata = {
"description": '8bitworkshop.com {"platform":"' + platform_id + '"}',
"public": true,
"files": files
};
var gist = github.gists.create(gistdata).done(function(val) {
var url = "http://8bitworkshop.com/?sharekey=" + val.id;
window.prompt("Copy link to clipboard (Ctrl+C, Enter)", url);
}).fail(function(err) {
alert("Error sharing file: " + err.message);
2016-12-16 01:21:51 +00:00
});
2016-12-30 23:51:15 +00:00
return true;
2016-12-16 01:21:51 +00:00
}
function _resetPreset(e) {
if (current_preset_index < 0) {
2017-01-03 01:42:15 +00:00
alert("Can only reset built-in file examples.")
} else if (confirm("Reset '" + PRESETS[current_preset_index].name + "' to default?")) {
2016-12-16 01:21:51 +00:00
qs['reset'] = '1';
gotoNewLocation();
2016-12-16 01:21:51 +00:00
}
2016-12-30 23:51:15 +00:00
return true;
2016-12-16 01:21:51 +00:00
}
2017-02-02 19:11:52 +00:00
function _downloadROMImage(e) {
if (current_output == null) {
alert("Please fix errors before downloading ROM.");
return true;
}
var blob = new Blob([current_output], {type: "application/octet-stream"});
saveAs(blob, getCurrentFilename()+".rom");
}
2016-12-16 01:21:51 +00:00
function populateExamples(sel) {
sel.append($("<option />").text("--------- Chapters ---------").attr('disabled',true));
2016-12-16 01:21:51 +00:00
for (var i=0; i<PRESETS.length; i++) {
var preset = PRESETS[i];
2017-02-14 23:15:06 +00:00
var name = preset.chapter ? (preset.chapter + ". " + preset.name) : preset.name;
2016-12-16 01:21:51 +00:00
sel.append($("<option />").val(preset.id).text(name).attr('selected',preset.id==current_preset_id));
}
}
function populateFiles(sel, name, prefix) {
sel.append($("<option />").text("------- " + name + " -------").attr('disabled',true));
var filenames = store.getFiles(prefix);
var foundSelected = false;
for (var i = 0; i < filenames.length; i++) {
var name = filenames[i];
var key = prefix + name;
sel.append($("<option />").val(key).text(name).attr('selected',key==current_preset_id));
if (key == current_preset_id) foundSelected = true;
2016-12-16 01:21:51 +00:00
}
if (!foundSelected && current_preset_id && current_preset_id.startsWith(prefix)) {
var name = current_preset_id.slice(prefix.length);
var key = prefix + name;
sel.append($("<option />").val(key).text(name).attr('selected',true));
2016-12-16 01:21:51 +00:00
}
}
function updateSelector() {
var sel = $("#preset_select").empty();
populateFiles(sel, "Local Files", "local/");
populateFiles(sel, "Shared", "shared/");
2016-12-16 01:21:51 +00:00
populateExamples(sel);
// set click handlers
sel.off('change').change(function(e) {
gotoPresetNamed($(this).val());
});
$("#preset_prev").off('click').click(function() {
gotoPresetAt(current_preset_index - 1);
2016-12-16 01:21:51 +00:00
});
$("#preset_next").off('click').click(function() {
gotoPresetAt(current_preset_index + 1);
2016-12-16 01:21:51 +00:00
});
}
function setCode(text) {
if (pendingWorkerMessages++ > 0)
return;
2017-01-15 03:46:12 +00:00
worker.postMessage({code:text, platform:platform_id,
tool:platform.getToolForFilename(current_preset_id)});
toolbar.addClass("is-busy");
$('#compile_spinner').css('visibility', 'visible');
2016-12-16 01:21:51 +00:00
}
function arrayCompare(a,b) {
if (a == null && b == null) return true;
if (a == null) return false;
if (b == null) return false;
if (a.length != b.length) return false;
for (var i=0; i<a.length; i++)
if (a[i] != b[i])
return false;
return true;
}
function setCompileOutput(data) {
sourcefile = new SourceFile(data.lines);
if (data.asmlines) {
assemblyfile = new SourceFile(data.asmlines, data.intermediate.listing);
}
2016-12-16 01:21:51 +00:00
// errors?
function addErrorMarker(line, msg) {
var div = document.createElement("div");
div.setAttribute("class", "tooltipbox tooltiperror");
div.style.color = '#ff3333'; // TODO
div.appendChild(document.createTextNode("\u24cd"));
var tooltip = document.createElement("span");
tooltip.setAttribute("class", "tooltiptext");
tooltip.appendChild(document.createTextNode(msg));
div.appendChild(tooltip);
editor.setGutterMarker(line, "gutter-info", div);
}
if (data.errors.length > 0) {
// TODO: move cursor to error line if offscreen?
2017-01-06 14:49:07 +00:00
toolbar.addClass("has-errors");
2016-12-16 01:21:51 +00:00
editor.clearGutter("gutter-info");
2017-02-20 23:50:29 +00:00
var numLines = editor.lineCount();
for (info of data.errors) {
2017-02-20 23:50:29 +00:00
var line = info.line-1;
if (line < 0 || line >= numLines) line = numLines-1;
addErrorMarker(line, info.msg);
2016-12-16 01:21:51 +00:00
}
current_output = null;
} else {
updatePreset(current_preset_id, editor.getValue()); // update persisted entry
2016-12-16 01:21:51 +00:00
// load ROM
var rom = data.output;
2016-12-16 01:21:51 +00:00
var rom_changed = rom && !arrayCompare(rom, current_output);
if (rom_changed) {
try {
//console.log("Loading ROM length", rom.length);
2016-12-31 16:05:22 +00:00
platform.loadROM(getCurrentPresetTitle(), rom);
resume();
2016-12-16 01:21:51 +00:00
current_output = rom;
2017-01-06 14:49:07 +00:00
pcvisits = {};
toolbar.removeClass("has-errors");
2016-12-16 01:21:51 +00:00
} catch (e) {
console.log(e); // TODO: show error
toolbar.addClass("has-errors");
addErrorMarker(0, e+"");
2016-12-16 01:21:51 +00:00
current_output = null;
}
}
if (rom_changed || trace_pending_at_pc) {
// update editor annotations
2017-01-17 21:50:57 +00:00
// TODO: do incrementally for performance
2016-12-16 01:21:51 +00:00
editor.clearGutter("gutter-info");
editor.clearGutter("gutter-bytes");
editor.clearGutter("gutter-offset");
editor.clearGutter("gutter-clock");
for (var info of data.lines) {
if (info.offset >= 0) {
var textel = document.createTextNode(hex(info.offset,4));
2016-12-16 01:21:51 +00:00
editor.setGutterMarker(info.line-1, "gutter-offset", textel);
}
if (info.insns) {
var insnstr = info.insns.length > 8 ? ("...") : info.insns;
var textel = document.createTextNode(insnstr);
editor.setGutterMarker(info.line-1, "gutter-bytes", textel);
if (info.iscode) {
var opcode = parseInt(info.insns.split()[0], 16);
if (platform.getOpcodeMetadata) {
var meta = platform.getOpcodeMetadata(opcode, info.offset);
var clockstr = meta.minCycles+"";
var textel = document.createTextNode(clockstr);
editor.setGutterMarker(info.line-1, "gutter-clock", textel);
}
2016-12-16 01:21:51 +00:00
}
}
}
}
if (trace_pending_at_pc) {
showLoopTimingForPC(trace_pending_at_pc);
}
}
trace_pending_at_pc = null;
}
worker.onmessage = function(e) {
toolbar.removeClass("is-busy");
$('#compile_spinner').css('visibility', 'hidden');
2017-04-01 17:34:24 +00:00
// TODO: this doesn't completely work yet
if (pendingWorkerMessages > 1) {
pendingWorkerMessages = 0;
setCode(editor.getValue());
2017-04-13 19:48:37 +00:00
} else {
pendingWorkerMessages = 0;
}
setCompileOutput(e.data);
}
2016-12-16 01:21:51 +00:00
function setCurrentLine(line) {
editor.setSelection({line:line,ch:0}, {line:line-1,ch:0}, {scroll:true});
}
var lastDebugInfo;
2017-01-06 14:49:07 +00:00
var lastDebugState;
2016-12-16 01:21:51 +00:00
function highlightDifferences(s1, s2) {
var split1 = s1.split(/(\S+\s+)/).filter(function(n) {return n});
var split2 = s2.split(/(\S+\s+)/).filter(function(n) {return n});
var i = 0;
var j = 0;
var result = "";
while (i < split1.length && j < split2.length) {
var w1 = split1[i];
var w2 = split2[j];
if (w2 && w2.indexOf("\n") >= 0) {
while (i < s1.length && split1[i].indexOf("\n") < 0)
i++;
}
if (w1 != w2) {
w2 = '<span class="hilite">' + w2 + '</span>';
}
result += w2;
i++;
j++;
}
while (j < split2.length) {
result += split2[j++];
}
return result;
}
function showMemory(state) {
var s = "";
if (state) {
s = platform.cpuStateToLongString(state.c);
2017-01-20 03:42:58 +00:00
if (platform.getRasterPosition) {
var pos = platform.getRasterPosition();
s += "H:" + pos.x + " V:" + pos.y + "\n"; // TODO: padding
}
if (platform.ramStateToLongString) {
s += platform.ramStateToLongString(state);
2016-12-16 01:21:51 +00:00
}
var hs = lastDebugInfo ? highlightDifferences(lastDebugInfo, s) : s;
$("#mem_info").show().html(hs);
lastDebugInfo = s;
} else {
$("#mem_info").hide();
lastDebugInfo = null;
}
}
function setupBreakpoint() {
// TODO
2016-12-31 16:05:22 +00:00
platform.setupDebug(function(state) {
2017-01-06 14:49:07 +00:00
lastDebugState = state;
2016-12-16 01:21:51 +00:00
var PC = state.c.PC;
var line = sourcefile.findLineForOffset(PC);
if (line >= 0) {
2016-12-16 01:21:51 +00:00
console.log("BREAKPOINT", hex(PC), line);
setCurrentLine(line);
} else {
console.log("BREAKPOINT", hex(PC));
// TODO: switch to disasm
2016-12-16 01:21:51 +00:00
}
2017-01-06 14:49:07 +00:00
pcvisits[PC] = pcvisits[PC] ? pcvisits[PC]+1 : 1;
2016-12-16 01:21:51 +00:00
showMemory(state);
2017-01-06 14:49:07 +00:00
updateDisassembly();
2016-12-31 16:05:22 +00:00
});
2016-12-16 01:21:51 +00:00
}
function pause() {
clearBreakpoint();
2016-12-31 16:05:22 +00:00
if (platform.isRunning()) {
platform.pause();
2016-12-16 01:21:51 +00:00
}
}
function resume() {
clearBreakpoint();
2016-12-31 16:05:22 +00:00
if (! platform.isRunning()) {
platform.resume();
2016-12-16 01:21:51 +00:00
editor.setSelection(editor.getCursor());
}
}
function singleStep() {
setupBreakpoint();
2016-12-31 16:05:22 +00:00
platform.step();
2016-12-16 01:21:51 +00:00
}
function getCurrentLine() {
return editor.getCursor().line+1;
}
2017-02-18 22:50:51 +00:00
function getDisasmViewPC() {
var line = disasmview.getCursor().line;
2017-02-25 15:57:39 +00:00
if (line >= 0) {
2017-02-18 22:50:51 +00:00
var toks = disasmview.getLine(line).split(/\s+/);
2017-02-25 15:57:39 +00:00
if (toks && toks[0].length == 4) {
2017-02-18 22:50:51 +00:00
return parseInt(toks[0], 16);
}
}
}
2017-02-05 04:19:54 +00:00
function getCurrentPC() {
var line = getCurrentLine();
2017-02-25 15:57:39 +00:00
while (line >= 0) {
var pc = sourcefile.line2offset[line];
if (pc >= 0) return pc;
line--;
2017-02-05 04:19:54 +00:00
}
2017-02-25 15:57:39 +00:00
return getDisasmViewPC();
2017-02-05 04:19:54 +00:00
}
function runToCursor() {
setupBreakpoint();
var pc = getCurrentPC();
if (pc >= 0) {
2017-02-05 04:19:54 +00:00
console.log("Run to", pc.toString(16));
2017-01-06 16:57:28 +00:00
platform.runEval(function(c) {
return c.PC == pc;
});
2016-12-16 01:21:51 +00:00
}
}
function runUntilReturn() {
setupBreakpoint();
platform.runUntilReturn();
}
function runStepBackwards() {
setupBreakpoint();
platform.stepBack();
}
2016-12-16 01:21:51 +00:00
function clearBreakpoint() {
2017-01-06 14:49:07 +00:00
lastDebugState = null;
2016-12-31 16:05:22 +00:00
platform.clearDebug();
2016-12-16 01:21:51 +00:00
showMemory();
}
function getClockCountsAtPC(pc) {
2016-12-31 16:05:22 +00:00
var opcode = platform.readAddress(pc);
var meta = platform.getOpcodeMetadata(opcode, pc);
2016-12-16 01:21:51 +00:00
return meta; // minCycles, maxCycles
}
var pc2minclocks = {};
var pc2maxclocks = {};
var jsrresult = {};
var MAX_CLOCKS = 76*2;
function byte2signed(b) {
b &= 0xff;
return (b < 0x80) ? b : -(256-b);
}
// [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;
}
// TODO: move to file
2016-12-16 01:21:51 +00:00
function _traceInstructions(pc, minclocks, maxclocks, subaddr, 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);
2016-12-31 16:05:22 +00:00
var lob = platform.readAddress(pc+1);
var hib = platform.readAddress(pc+2);
2016-12-16 01:21:51 +00:00
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: 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) {
pc2minclocks = {};
pc2maxclocks = {};
jsrresult = {};
// recurse through all traces
2016-12-31 16:05:22 +00:00
_traceInstructions(pc | platform.getOriginPC(), MAX_CLOCKS, MAX_CLOCKS);
2016-12-16 01:21:51 +00:00
// show the lines
for (var line in sourcefile.line2offset) {
var pc = sourcefile.line2offset[line];
2016-12-16 01:21:51 +00:00
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 += "+";
var textel = document.createTextNode(s);
editor.setGutterMarker(line-1, "gutter-bytes", textel);
}
}
}
function traceTiming() {
2016-12-31 16:05:22 +00:00
trace_pending_at_pc = platform.getOriginPC();
2016-12-16 01:21:51 +00:00
setCode(editor.getValue());
}
/*
function showLoopTimingForCurrentLine() {
var line = getCurrentLine();
var pc = line2offset[line];
if (pc) {
showLoopTimingForPC(pc);
}
}
*/
function jumpToLine(ed, i) {
var t = ed.charCoords({line: i, ch: 0}, "local").top;
var middleHeight = ed.getScrollerElement().offsetHeight / 2;
ed.scrollTo(null, t - middleHeight - 5);
}
2017-01-06 14:49:07 +00:00
function updateDisassembly() {
var div = $("#disassembly");
if (div.is(':visible')) {
var state = lastDebugState || platform.saveState();
var pc = state.c.PC;
if (assemblyfile && assemblyfile.text) {
disasmview.setValue(assemblyfile.text);
2017-02-18 22:50:51 +00:00
var findPC = platform.getDebugCallback() ? pc : getCurrentPC();
if (findPC) {
var lineno = assemblyfile.findLineForOffset(findPC);
if (lineno) {
2017-02-18 22:50:51 +00:00
if (platform.getDebugCallback()) disasmview.setCursor(lineno-1, 0);
jumpToLine(disasmview, lineno-1);
}
}
return;
}
2017-01-20 03:42:58 +00:00
var curline = 0;
2017-01-08 15:51:19 +00:00
var selline = 0;
// TODO: not perfect disassembler
function disassemble(start, end) {
if (start < 0) start = 0;
2017-01-20 03:42:58 +00:00
if (end > 0xffff) end = 0xffff;
// TODO: use pc2visits
var a = start;
2017-01-08 15:51:19 +00:00
var s = "";
2017-01-20 03:42:58 +00:00
while (a < end) {
var disasm = platform.disassemble(a, platform.readAddress);
var srclinenum = sourcefile.offset2line[a];
2017-01-08 15:51:19 +00:00
if (srclinenum) {
var srcline = editor.getLine(srclinenum-1);
if (srcline && srcline.trim().length) {
s += "; " + srclinenum + ":\t" + srcline + "\n";
}
}
2017-01-20 22:54:02 +00:00
var bytes = "";
for (var i=0; i<disasm.nbytes; i++)
bytes += hex(platform.readAddress(a+i));
while (bytes.length < 14)
bytes += ' ';
var dline = hex(parseInt(a)) + "\t" + bytes + "\t" + disasm.line + "\n";
2017-01-08 15:51:19 +00:00
s += dline;
2017-01-20 03:42:58 +00:00
if (a == pc) selline = curline;
curline++;
2017-02-05 04:19:54 +00:00
a += disasm.nbytes || 1;
2017-01-08 15:51:19 +00:00
}
return s;
}
var text = disassemble(pc-96, pc) + disassemble(pc, pc+96);
disasmview.setValue(text);
disasmview.setCursor(selline, 0);
jumpToLine(disasmview, selline);
2017-01-06 14:49:07 +00:00
}
}
function toggleDisassembly() {
$("#disassembly").toggle();
$("#editor").toggle();
updateDisassembly();
}
2016-12-16 01:21:51 +00:00
function resetAndDebug() {
2017-01-06 16:57:28 +00:00
clearBreakpoint();
platform.resume();
2017-01-03 01:42:15 +00:00
platform.reset();
setupBreakpoint();
platform.runEval(function(c) { return true; });
2017-01-03 01:42:15 +00:00
}
2017-01-06 16:57:28 +00:00
function _breakExpression() {
var exprs = window.prompt("Enter break expression", "c.PC == 0x6000"); // TODO
if (exprs) {
var fn = new Function('c', 'return (' + exprs + ');');
setupBreakpoint();
platform.runEval(fn);
}
}
2016-12-16 01:21:51 +00:00
function setupDebugControls(){
$("#dbg_reset").click(resetAndDebug);
$("#dbg_pause").click(pause);
$("#dbg_go").click(resume);
$("#dbg_step").click(singleStep);
$("#dbg_toline").click(runToCursor);
$("#dbg_stepout").click(runUntilReturn);
$("#dbg_stepback").click(runStepBackwards);
if (platform_id == 'vcs') {
$("#dbg_timing").click(traceTiming).show();
}
if (platform.saveState) { // TODO: only show if listing or disasm available
$("#dbg_disasm").click(toggleDisassembly).show();
}
2017-01-06 14:49:07 +00:00
$("#disassembly").hide();
2016-12-30 23:51:15 +00:00
$(".dropdown-menu").collapse({toggle: false});
$("#item_new_file").click(_createNewFile);
$("#item_share_file").click(_shareFile);
$("#item_reset_file").click(_resetPreset);
$("#item_debug_expr").click(_breakExpression);
2017-02-02 19:11:52 +00:00
$("#item_download_rom").click(_downloadROMImage);
2016-12-16 01:21:51 +00:00
}
2016-12-18 20:59:31 +00:00
function showWelcomeMessage() {
if (!localStorage.getItem("8bitworkshop.hello"))
{
2016-12-30 23:51:15 +00:00
// Instance the tour
var tour = new Tour({
autoscroll:false,
2016-12-30 23:51:15 +00:00
//storage:false,
steps: [
{
element: "#editor",
title: "Welcome to 8bitworkshop!",
content: "Type your 6502 code on the left side, and it'll be assembled in real-time. All changes are saved to browser local storage.",
},
{
element: "#emulator",
placement: 'left',
title: "Atari VCS Emulator",
content: "This is an emulator for the Atari VCS/2600. We'll load your assembled code into the emulator whenever you make changes.",
},
{
element: "#preset_select",
title: "File Selector",
content: "Pick a code example from the book, or access your own files and files shared by others."
},
{
element: "#debug_bar",
placement: 'bottom',
title: "Debug Tools",
content: "Use these buttons to set breakpoints, single step through code, pause/resume, and perform timing analysis."
},
{
element: "#dropdownMenuButton",
title: "Main Menu",
content: "Click the menu to create new files and share your work with others."
},
]});
tour.init();
setTimeout(function() { tour.start(); }, 2000);
2016-12-18 20:59:31 +00:00
}
}
2016-12-16 01:21:51 +00:00
///////////////////////////////////////////////////
function setupBitmapEditor() {
}
///////////////////////////////////////////////////
2016-12-16 01:21:51 +00:00
var qs = (function (a) {
if (!a || a == "")
return {};
var b = {};
for (var i = 0; i < a.length; ++i) {
var p = a[i].split('=', 2);
if (p.length == 1)
b[p[0]] = "";
else
b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
}
return b;
})(window.location.search.substr(1).split('&'));
function preloadWorker(fileid) {
var tool = platform.getToolForFilename(fileid);
if (tool) worker.postMessage({preload:tool});
}
2017-01-25 17:30:05 +00:00
function initPlatform() {
store = new FileStore(localStorage, platform_id + '/');
}
2017-01-14 16:14:25 +00:00
function startPlatform() {
2017-01-25 17:30:05 +00:00
initPlatform();
2017-01-29 21:06:05 +00:00
if (!PLATFORMS[platform_id]) throw Error("Invalid platform '" + platform_id + "'.");
2017-01-14 16:14:25 +00:00
platform = new PLATFORMS[platform_id]($("#emulator")[0]);
PRESETS = platform.getPresets();
if (qs['file']) {
// start platform and load file
preloadWorker(qs['file']);
setupDebugControls();
setupBitmapEditor();
platform.start();
2017-01-14 16:14:25 +00:00
loadPreset(qs['file']);
updateSelector();
return true;
2017-01-14 16:14:25 +00:00
} else {
// try to load last file (redirect)
2017-01-14 16:14:25 +00:00
var lastid = localStorage.getItem("__lastid_"+platform_id) || localStorage.getItem("__lastid");
localStorage.removeItem("__lastid");
gotoPresetNamed(lastid || PRESETS[0].id);
return false;
2017-01-14 16:14:25 +00:00
}
}
2017-01-25 17:30:05 +00:00
function loadSharedFile(sharekey) {
var github = new Octokat();
var gist = github.gists(sharekey);
gist.fetch().done(function(val) {
var filename;
for (filename in val.files) { break; }
var newid = 'shared/' + filename;
var json = JSON.parse(val.description.slice(val.description.indexOf(' ')+1));
console.log("Fetched " + newid, json);
platform_id = json['platform'];
initPlatform();
updatePreset(newid, val.files[filename].content);
qs['file'] = newid;
qs['platform'] = platform_id;
delete qs['sharekey'];
gotoNewLocation();
}).fail(function(err) {
alert("Error loading share file: " + err.message);
});
return true;
}
2017-01-03 01:42:15 +00:00
// start
2017-01-14 16:14:25 +00:00
function startUI(loadplatform) {
installErrorHandler();
2017-01-25 17:30:05 +00:00
// add default platform?
platform_id = qs['platform'] || localStorage.getItem("__lastplatform");
if (!platform_id) {
platform_id = qs['platform'] = "vcs";
}
2017-01-14 16:14:25 +00:00
// parse query string
// is this a share URL?
if (qs['sharekey']) {
2017-01-25 17:30:05 +00:00
loadSharedFile(qs['sharekey']);
} else {
2017-01-14 16:14:25 +00:00
// reset file?
if (qs['file'] && qs['reset']) {
2017-01-27 02:59:34 +00:00
initPlatform();
2017-01-14 16:14:25 +00:00
store.deleteFile(qs['file']);
qs['reset'] = '';
gotoNewLocation();
2017-01-14 16:14:25 +00:00
} else {
// load and start platform object
if (loadplatform) {
2017-01-29 21:06:05 +00:00
var scriptfn = 'src/platform/' + platform_id.split('-')[0] + '.js';
$.getScript(scriptfn, function() {
2017-01-14 16:14:25 +00:00
console.log("loaded platform", platform_id);
startPlatform();
});
} else {
2017-01-14 16:14:25 +00:00
startPlatform();
}
2017-01-14 16:14:25 +00:00
}
2016-12-16 01:21:51 +00:00
}
}