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