emulator/gba3/resources/console.js
2019-06-14 09:33:19 +08:00

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