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