479 lines
12 KiB
JavaScript
479 lines
12 KiB
JavaScript
function Console(gba) {
|
|
this.cpu = gba.cpu;
|
|
this.gba = gba;
|
|
this.ul = document.getElementById('console');
|
|
this.gprs = document.getElementById('gprs');
|
|
this.memory = new Memory(gba.mmu);
|
|
this.breakpoints = [];
|
|
this.logQueue = [];
|
|
|
|
this.activeView = null;
|
|
this.paletteView = new PaletteViewer(gba.video.renderPath.palette);
|
|
this.tileView = new TileViewer(gba.video.renderPath.vram, gba.video.renderPath.palette);
|
|
this.update();
|
|
|
|
var self = this;
|
|
gba.setLogger(function (level, message) { self.log(level, message) });
|
|
this.gba.doStep = function () { return self.testBreakpoints() };
|
|
}
|
|
|
|
Console.prototype.updateGPRs = function() {
|
|
for (var i = 0; i < 16; ++i) {
|
|
this.gprs.children[i].textContent = hex(this.cpu.gprs[i]);
|
|
}
|
|
}
|
|
|
|
Console.prototype.updateCPSR = function() {
|
|
var cpu = this.cpu;
|
|
var bit = function(psr, member) {
|
|
var element = document.getElementById(psr);
|
|
if (cpu[member]) {
|
|
element.removeAttribute('class');
|
|
} else {
|
|
element.setAttribute('class', 'disabled');
|
|
}
|
|
}
|
|
bit('cpsrN', 'cpsrN');
|
|
bit('cpsrZ', 'cpsrZ');
|
|
bit('cpsrC', 'cpsrC');
|
|
bit('cpsrV', 'cpsrV');
|
|
bit('cpsrI', 'cpsrI');
|
|
bit('cpsrT', 'execMode');
|
|
|
|
var mode = document.getElementById('mode');
|
|
switch (cpu.mode) {
|
|
case cpu.MODE_USER:
|
|
mode.textContent = 'USER';
|
|
break;
|
|
case cpu.MODE_IRQ:
|
|
mode.textContent = 'IRQ';
|
|
break;
|
|
case cpu.MODE_FIQ:
|
|
mode.textContent = 'FIQ';
|
|
break;
|
|
case cpu.MODE_SUPERVISOR:
|
|
mode.textContent = 'SVC';
|
|
break;
|
|
case cpu.MODE_ABORT:
|
|
mode.textContent = 'ABORT';
|
|
break;
|
|
case cpu.MODE_UNDEFINED:
|
|
mode.textContent = 'UNDEFINED';
|
|
break;
|
|
case cpu.MODE_SYSTEM:
|
|
mode.textContent = 'SYSTEM';
|
|
break;
|
|
default:
|
|
mode.textContent = '???';
|
|
break;
|
|
}
|
|
}
|
|
|
|
Console.prototype.log = function(level, message) {
|
|
switch (level) {
|
|
case this.gba.LOG_ERROR:
|
|
message = '[ERROR] ' + message;
|
|
break;
|
|
case this.gba.LOG_WARN:
|
|
message = '[WARN] ' + message;
|
|
break;
|
|
case this.gba.LOG_STUB:
|
|
message = '[STUB] ' + message;
|
|
break;
|
|
case this.gba.LOG_INFO:
|
|
message = '[INFO] ' + message;
|
|
break;
|
|
case this.gba.LOG_DEBUG:
|
|
message = '[DEBUG] ' + message;
|
|
break;
|
|
}
|
|
this.logQueue.push(message);
|
|
if (level == this.gba.LOG_ERROR) {
|
|
this.pause();
|
|
}
|
|
if (!this.stillRunning) {
|
|
this.flushLog();
|
|
}
|
|
}
|
|
|
|
Console.prototype.flushLog = function() {
|
|
var doScroll = this.ul.scrollTop == this.ul.scrollHeight - this.ul.offsetHeight;
|
|
while (this.logQueue.length) {
|
|
var entry = document.createElement('li');
|
|
entry.textContent = this.logQueue.shift();
|
|
this.ul.appendChild(entry);
|
|
}
|
|
if (doScroll) {
|
|
var ul = this.ul;
|
|
var last = ul.scrollTop;
|
|
var scrollUp = function() {
|
|
if (ul.scrollTop == last) {
|
|
ul.scrollTop = (ul.scrollHeight - ul.offsetHeight) * 0.2 + last * 0.8;
|
|
last = ul.scrollTop;
|
|
if (last != ul.scrollHeight - ul.offsetHeight) {
|
|
setTimeout(scrollUp, 25);
|
|
}
|
|
}
|
|
}
|
|
setTimeout(scrollUp, 25);
|
|
}
|
|
|
|
}
|
|
|
|
Console.prototype.update = function() {
|
|
this.updateGPRs();
|
|
this.updateCPSR();
|
|
this.memory.refreshAll();
|
|
if (this.activeView) {
|
|
this.activeView.redraw();
|
|
}
|
|
}
|
|
|
|
Console.prototype.setView = function(view) {
|
|
var container = document.getElementById('debugViewer');
|
|
while (container.hasChildNodes()) {
|
|
container.removeChild(container.lastChild);
|
|
}
|
|
if (view) {
|
|
view.insertChildren(container);
|
|
view.redraw();
|
|
}
|
|
this.activeView = view;
|
|
}
|
|
|
|
Console.prototype.step = function() {
|
|
try {
|
|
this.cpu.step();
|
|
this.update();
|
|
} catch (exception) {
|
|
this.log(this.gba.LOG_DEBUG, exception);
|
|
throw exception;
|
|
}
|
|
}
|
|
|
|
Console.prototype.runVisible = function() {
|
|
if (this.stillRunning) {
|
|
return;
|
|
}
|
|
|
|
this.stillRunning = true;
|
|
var self = this;
|
|
run = function() {
|
|
if (self.stillRunning) {
|
|
try {
|
|
self.step();
|
|
if (self.breakpoints.length && self.breakpoints[self.cpu.gprs[self.cpu.PC]]) {
|
|
self.breakpointHit();
|
|
return;
|
|
}
|
|
self.flushLog();
|
|
setTimeout(run, 0);
|
|
} catch (exception) {
|
|
self.log(this.gba.LOG_DEBUG, exception);
|
|
self.pause();
|
|
throw exception;
|
|
}
|
|
}
|
|
}
|
|
setTimeout(run, 0);
|
|
}
|
|
|
|
Console.prototype.run = function() {
|
|
if (this.stillRunning) {
|
|
return;
|
|
}
|
|
|
|
this.stillRunning = true;
|
|
var regs = document.getElementById('registers');
|
|
var mem = document.getElementById('memory');
|
|
var start = Date.now();
|
|
regs.setAttribute('class', 'disabled');
|
|
mem.setAttribute('class', 'disabled');
|
|
var self = this;
|
|
this.gba.runStable();
|
|
}
|
|
|
|
Console.prototype.runFrame = function() {
|
|
if (this.stillRunning) {
|
|
return;
|
|
}
|
|
|
|
this.stillRunning = true;
|
|
var regs = document.getElementById('registers');
|
|
var mem = document.getElementById('memory');
|
|
var start = Date.now();
|
|
regs.setAttribute('class', 'disabled');
|
|
mem.setAttribute('class', 'disabled');
|
|
var self = this;
|
|
run = function() {
|
|
self.gba.step();
|
|
self.pause();
|
|
}
|
|
setTimeout(run, 0);
|
|
}
|
|
|
|
Console.prototype.pause = function() {
|
|
this.stillRunning = false;
|
|
this.gba.pause();
|
|
var regs = document.getElementById('registers');
|
|
var mem = document.getElementById('memory');
|
|
mem.removeAttribute('class');
|
|
regs.removeAttribute('class');
|
|
this.update();
|
|
this.flushLog();
|
|
}
|
|
|
|
Console.prototype.breakpointHit = function() {
|
|
this.pause();
|
|
this.log(this.gba.LOG_DEBUG, 'Hit breakpoint at ' + hex(this.cpu.gprs[this.cpu.PC]));
|
|
}
|
|
|
|
Console.prototype.addBreakpoint = function(addr) {
|
|
this.breakpoints[addr] = true;
|
|
var bpLi = document.getElementById('bp' + addr);
|
|
if (!bpLi) {
|
|
bpLi = document.createElement('li');
|
|
bpLi.address = addr;
|
|
var cb = document.createElement('input');
|
|
cb.setAttribute('type', 'checkbox');
|
|
cb.setAttribute('checked', 'checked');
|
|
var self = this;
|
|
cb.addEventListener('click', function() {
|
|
self.breakpoints[addr] = cb.checked;
|
|
}, false);
|
|
bpLi.appendChild(cb);
|
|
bpLi.appendChild(document.createTextNode(hex(addr)));
|
|
document.getElementById('breakpointView').appendChild(bpLi);
|
|
}
|
|
}
|
|
|
|
Console.prototype.testBreakpoints = function() {
|
|
if (this.breakpoints.length && this.breakpoints[this.cpu.gprs[this.cpu.PC]]) {
|
|
this.breakpointHit();
|
|
return false;
|
|
}
|
|
return this.gba.waitFrame();
|
|
};
|
|
|
|
Memory = function(mmu) {
|
|
this.mmu = mmu;
|
|
this.ul = document.getElementById('memoryView');
|
|
row = this.createRow(0);
|
|
this.ul.appendChild(row);
|
|
this.rowHeight = row.offsetHeight;
|
|
this.numberRows = this.ul.parentNode.offsetHeight / this.rowHeight + 2;
|
|
this.ul.removeChild(row);
|
|
this.scrollTop = 50 - this.ul.parentElement.firstElementChild.offsetHeight;
|
|
|
|
for (var i = 0; i < this.numberRows; ++i) {
|
|
this.ul.appendChild(this.createRow(i << 4));
|
|
}
|
|
this.ul.parentElement.scrollTop = this.scrollTop;
|
|
|
|
var self = this;
|
|
this.ul.parentElement.addEventListener('scroll', function(e) { self.scroll(e) }, true);
|
|
window.addEventListener('resize', function(e) { self.resize() }, true);
|
|
}
|
|
|
|
Memory.prototype.scroll = function(e) {
|
|
while (this.ul.parentElement.scrollTop - this.scrollTop < this.rowHeight) {
|
|
if (this.ul.firstChild.offset == 0) {
|
|
break;
|
|
}
|
|
var victim = this.ul.lastChild;
|
|
this.ul.removeChild(victim);
|
|
victim.offset = this.ul.firstChild.offset - 16;
|
|
this.refresh(victim);
|
|
this.ul.insertBefore(victim, this.ul.firstChild);
|
|
this.ul.parentElement.scrollTop += this.rowHeight;
|
|
}
|
|
while (this.ul.parentElement.scrollTop - this.scrollTop > this.rowHeight * 2) {
|
|
var victim = this.ul.firstChild;
|
|
this.ul.removeChild(victim);
|
|
victim.offset = this.ul.lastChild.offset + 16;
|
|
this.refresh(victim);
|
|
this.ul.appendChild(victim);
|
|
this.ul.parentElement.scrollTop -= this.rowHeight;
|
|
}
|
|
if (this.ul.parentElement.scrollTop < this.scrollTop) {
|
|
this.ul.parentElement.scrollTop = this.scrollTop;
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
Memory.prototype.resize = function() {
|
|
this.numberRows = this.ul.parentNode.offsetHeight / this.rowHeight + 2;
|
|
if (this.numberRows > this.ul.children.length) {
|
|
var offset = this.ul.lastChild.offset + 16;
|
|
for (var i = 0; i < this.numberRows - this.ul.children.length; ++i) {
|
|
var row = this.createRow(offset);
|
|
this.refresh(row);
|
|
this.ul.appendChild(row);
|
|
offset += 16;
|
|
}
|
|
} else {
|
|
for (var i = 0; i < this.ul.children.length - this.numberRows; ++i) {
|
|
this.ul.removeChild(this.ul.lastChild);
|
|
}
|
|
}
|
|
}
|
|
|
|
Memory.prototype.refresh = function(row) {
|
|
var showChanged;
|
|
var newValue;
|
|
var child;
|
|
row.firstChild.textContent = hex(row.offset);
|
|
if (row.oldOffset == row.offset) {
|
|
showChanged = true;
|
|
} else {
|
|
row.oldOffset = row.offset;
|
|
showChanged = false;
|
|
}
|
|
for (var i = 0; i < 16; ++i) {
|
|
child = row.children[i + 1];
|
|
try {
|
|
newValue = this.mmu.loadU8(row.offset + i);
|
|
if (newValue >= 0) {
|
|
newValue = hex(newValue, 2, false);
|
|
if (child.textContent == newValue) {
|
|
child.setAttribute('class', 'memoryCell');
|
|
} else if (showChanged) {
|
|
child.setAttribute('class', 'memoryCell changed');
|
|
child.textContent = newValue;
|
|
} else {
|
|
child.setAttribute('class', 'memoryCell');
|
|
child.textContent = newValue;
|
|
}
|
|
} else {
|
|
child.setAttribute('class', 'memoryCell');
|
|
child.textContent = '--';
|
|
}
|
|
} catch (exception) {
|
|
child.setAttribute('class', 'memoryCell');
|
|
child.textContent = '--';
|
|
}
|
|
}
|
|
}
|
|
|
|
Memory.prototype.refreshAll = function() {
|
|
for (var i = 0; i < this.ul.children.length; ++i) {
|
|
this.refresh(this.ul.children[i]);
|
|
}
|
|
}
|
|
|
|
Memory.prototype.createRow = function(startOffset) {
|
|
var li = document.createElement('li');
|
|
var offset = document.createElement('span');
|
|
offset.setAttribute('class', 'memoryOffset');
|
|
offset.textContent = hex(startOffset);
|
|
li.appendChild(offset);
|
|
|
|
for (var i = 0; i < 16; ++i) {
|
|
var b = document.createElement('span');
|
|
b.textContent = '00';
|
|
b.setAttribute('class', 'memoryCell');
|
|
li.appendChild(b);
|
|
}
|
|
li.offset = startOffset;
|
|
li.oldOffset = startOffset;
|
|
return li;
|
|
}
|
|
|
|
Memory.prototype.scrollTo = function(offset) {
|
|
offset &= 0xFFFFFFF0;
|
|
if (offset) {
|
|
for (var i = 0; i < this.ul.children.length; ++i) {
|
|
var child = this.ul.children[i];
|
|
child.offset = offset + (i - 1) * 16;
|
|
this.refresh(child);
|
|
}
|
|
this.ul.parentElement.scrollTop = this.scrollTop + this.rowHeight;
|
|
} else {
|
|
for (var i = 0; i < this.ul.children.length; ++i) {
|
|
var child = this.ul.children[i];
|
|
child.offset = offset + i * 16;
|
|
this.refresh(child);
|
|
}
|
|
this.ul.parentElement.scrollTop = this.scrollTop;
|
|
}
|
|
}
|
|
|
|
function PaletteViewer(palette) {
|
|
this.palette = palette;
|
|
this.view = document.createElement('canvas');
|
|
this.view.setAttribute('class', 'paletteView');
|
|
this.view.setAttribute('width', '240');
|
|
this.view.setAttribute('height', '500');
|
|
}
|
|
|
|
PaletteViewer.prototype.insertChildren = function(container) {
|
|
container.appendChild(this.view);
|
|
}
|
|
|
|
PaletteViewer.prototype.redraw = function() {
|
|
var context = this.view.getContext('2d');
|
|
context.clearRect(0, 0, this.view.width, this.view.height);
|
|
for (var p = 0; p < 2; ++p) {
|
|
for (var y = 0; y < 16; ++y) {
|
|
for (var x = 0; x < 16; ++x) {
|
|
var color = this.palette.loadU16((p * 256 + y * 16 + x) * 2);
|
|
var r = (color & 0x001F) << 3;
|
|
var g = (color & 0x03E0) >> 2;
|
|
var b = (color & 0x7C00) >> 7;
|
|
context.fillStyle = '#' + hex(r, 2, false) + hex(g, 2, false) + hex(b, 2, false);
|
|
context.fillRect(x * 15 + 1, y * 15 + p * 255 + 1, 13, 13);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function TileViewer(vram, palette) {
|
|
this.BG_MAP_WIDTH = 256;
|
|
this.vram = vram;
|
|
this.palette = palette;
|
|
|
|
this.view = document.createElement('canvas');
|
|
this.view.setAttribute('class', 'tileView');
|
|
this.view.setAttribute('width', '256');
|
|
this.view.setAttribute('height', '512');
|
|
|
|
this.activePalette = 0;
|
|
}
|
|
|
|
TileViewer.prototype.insertChildren = function(container) {
|
|
container.appendChild(this.view);
|
|
};
|
|
|
|
TileViewer.prototype.redraw = function() {
|
|
var context = this.view.getContext('2d');
|
|
var data = context.createImageData(this.BG_MAP_WIDTH, 512);
|
|
var t = 0;
|
|
for (var y = 0; y < 512; y += 8) {
|
|
for (var x = 0; x < this.BG_MAP_WIDTH; x += 8) {
|
|
this.drawTile(data.data, t, this.activePalette, x + y * this.BG_MAP_WIDTH, this.BG_MAP_WIDTH);
|
|
++t;
|
|
}
|
|
}
|
|
context.putImageData(data, 0, 0);
|
|
};
|
|
|
|
TileViewer.prototype.drawTile = function(data, tile, palette, offset, stride) {
|
|
for (var j = 0; j < 8; ++j) {
|
|
var memOffset = tile << 5;
|
|
memOffset |= j << 2;
|
|
|
|
var row = this.vram.load32(memOffset);
|
|
for (var i = 0; i < 8; ++i) {
|
|
var index = (row >> (i << 2)) & 0xF;
|
|
var color = this.palette.loadU16((index << 1) + (palette << 5));
|
|
var r = (color & 0x001F) << 3;
|
|
var g = (color & 0x03E0) >> 2;
|
|
var b = (color & 0x7C00) >> 7;
|
|
data[(offset + i + stride * j) * 4 + 0] = r;
|
|
data[(offset + i + stride * j) * 4 + 1] = g;
|
|
data[(offset + i + stride * j) * 4 + 2] = b;
|
|
data[(offset + i + stride * j) * 4 + 3] = 255;
|
|
}
|
|
}
|
|
};
|