From 531b9327c659e94f41e191cc579f6ba7c77d84ac Mon Sep 17 00:00:00 2001 From: Jeff Parsons Date: Thu, 29 Jan 2015 18:44:58 -0800 Subject: [PATCH] More panel work --- modules/pcjs/lib/bus.js | 14 +- modules/pcjs/lib/memory.js | 8 +- modules/pcjs/lib/panel.js | 296 ++++++++++++++++++++++++++++++++----- 3 files changed, 271 insertions(+), 47 deletions(-) diff --git a/modules/pcjs/lib/bus.js b/modules/pcjs/lib/bus.js index 892e71e6f..12165c4f1 100644 --- a/modules/pcjs/lib/bus.js +++ b/modules/pcjs/lib/bus.js @@ -130,7 +130,7 @@ function Bus(parmsBus, cpu, dbg) this.blockLimit = this.blockSize - 1; this.blockTotal = ((this.busLimit + this.blockSize) / this.blockSize) | 0; this.blockMask = this.blockTotal - 1; - this.assert(this.blockTotal <= Bus.BLOCK.MASK); + this.assert(this.blockTotal <= Bus.BLOCK.NUM_MASK); /* * Lists of I/O notification functions: aPortInputNotify and aPortOutputNotify are arrays, indexed by @@ -237,12 +237,18 @@ if (BACKTRACK) { } /* - * scanMemory() records block numbers in bits 0-14, a BackTrack "mod" bit in bit 15, and the block type at bit 16. + * scanMemory() records block numbers in bits 0-14, a BackTrack "mod" bit in bit 15, and a block type at bit 28; + * the bits reserved for a count are not used. */ Bus.BLOCK = { - MASK: 0x7fff, + NUM_SHIFT: 0, + NUM_MASK: 0x7fff, BTMOD_SHIFT: 15, - TYPE_SHIFT: 16 + BTMOD_MASK: 0x1, + COUNT_SHIFT: 16, + COUNT_MASK: 0x0fff, + TYPE_SHIFT: 28, + TYPE_MASK: 0x7 }; /** diff --git a/modules/pcjs/lib/memory.js b/modules/pcjs/lib/memory.js index 42b3c584b..ee10afb90 100644 --- a/modules/pcjs/lib/memory.js +++ b/modules/pcjs/lib/memory.js @@ -204,13 +204,11 @@ Memory.TYPE = { NONE: 0, RAM: 1, ROM: 2, - VIDEO: 3 + VIDEO: 3, + NAMES: ["NONE", "RAM", "ROM", "VIDEO"], + COLORS: ["black", "blue", "green", "cyan"] }; -if (DEBUG) { - Memory.TYPE.NAMES = ["NONE", "RAM", "ROM", "VIDEO"]; -} - /** * readNone(off) * diff --git a/modules/pcjs/lib/panel.js b/modules/pcjs/lib/panel.js index b29bc6de0..2b55d98e9 100644 --- a/modules/pcjs/lib/panel.js +++ b/modules/pcjs/lib/panel.js @@ -84,6 +84,126 @@ Panel.LIVEREGISTERS = { HEIGHT: Panel.LIVECANVAS.HEIGHT }; +/* + * findRegions() records block numbers in bits 0-14, a BackTrack "mod" bit in bit 15, and the block type at bit 16. + */ +Panel.REGION = { + MASK: 0x7fff, + BTMOD_SHIFT: 15, + TYPE_SHIFT: 16 +}; + +/** + * Color(r, g, b, a) + * + * @constructor + * @param {number} [r] + * @param {number} [g] + * @param {number} [b] + * @param {number} [a] + */ +function Color(r, g, b, a) +{ + this.rgb = [r, g, b, a]; + this.sValue = null; + if (r === undefined) this.randomize(); +} + +/** + * getRandom(nLimit) + * + * @this {Color} + * @param {number} [nLimit] + */ +Color.prototype.getRandom = function(nLimit) +{ + return (Math.random() * (nLimit || 0x100)) | 0; +}; + +/** + * randomize() + * + * @this {Color} + */ +Color.prototype.randomize = function() +{ + this.rgb[0] = this.getRandom(); this.rgb[1] = this.getRandom(); this.rgb[2] = this.getRandom(); this.rgb[3] = 0xff; + this.sValue = null; +}; + +/** + * toString() + * + * @this {Color} + * @return {string} + */ +Color.prototype.toString = function() +{ + if (!this.sValue) this.sValue = '#' + str.toHexByte(this.rgb[0]) + str.toHexByte(this.rgb[1]) + str.toHexByte(this.rgb[2]); + return this.sValue; +}; + +/** + * Rectangle(x, y, cx, cy) + * + * @constructor + * @param {number} x + * @param {number} y + * @param {number} cx + * @param {number} cy + */ +function Rectangle(x, y, cx, cy) +{ + this.x = x; + this.y = y; + this.cx = cx; + this.cy = cy; +} + +/** + * subDivide(units, unitsTotal, fHorizontal) + * + * Return a new rectangle that is a subset of the current rectangle, based on the ratio of + * units to unitsTotal, and then update the dimensions of the current rectangle. Whether the + * original rectangle is divided horizontally or vertically is entirely arbitrary; currently, + * the criteria is horizontal if the ratio is 1/4 or more, vertical otherwise. + * + * @this {Rectangle} + * @param {number} units + * @param {number} unitsTotal + * @param {boolean} [fHorizontal] + * @return {Rectangle} + */ +Rectangle.prototype.subDivide = function(units, unitsTotal, fHorizontal) +{ + var rect; + if (fHorizontal || units >= (unitsTotal >> 2)) { + rect = new Rectangle(this.x, this.y, this.cx, ((this.cy * units) / unitsTotal) | 0); + this.y += rect.cy; + this.cy -= rect.cy; + Component.assert(this.cy >= 0); + } else { + rect = new Rectangle(this.x, this.y, ((this.cx * units) / unitsTotal) | 0, this.cy); + this.x += rect.cx; + this.cx -= rect.cx; + Component.assert(this.cx >= 0); + } + return rect; +}; + +/** + * drawWith(context, color) + * + * @param {Object} context + * @param {Color|string} [color] + */ +Rectangle.prototype.drawWith = function(context, color) +{ + if (!color) color = new Color(); + context.fillStyle = (typeof color == "string"? color : color.toString()); + context.fillRect(this.x, this.y, this.cx, this.cy); +}; + /** * setBinding(sHTMLType, sBinding, control) * @@ -103,6 +223,7 @@ Panel.prototype.setBinding = function(sHTMLType, sBinding, control) if (this.cpu && this.cpu.setBinding(sHTMLType, sBinding, control)) return true; if (this.kbd && this.kbd.setBinding(sHTMLType, sBinding, control)) return true; if (DEBUGGER && this.dbg && this.dbg.setBinding(sHTMLType, sBinding, control)) return true; + if (!this.canvas && sHTMLType == "canvas") { var fPanel = false; if (BACKTRACK && sBinding == "btpanel") { @@ -124,14 +245,12 @@ Panel.prototype.setBinding = function(sHTMLType, sBinding, control) this.canvasLiveMemory.width = Panel.LIVEMEMORY.WIDTH; this.canvasLiveMemory.height = Panel.LIVEMEMORY.HEIGHT; this.contextLiveMemory = this.canvasLiveMemory.getContext("2d"); - this.contextLiveMemory.font = Panel.LIVECANVAS.FONT.HEIGHT + "px " + Panel.LIVECANVAS.FONT.NAME; this.imageLiveMemory = this.contextLiveMemory.createImageData(this.canvasLiveMemory.width, this.canvasLiveMemory.height); this.canvasLiveRegisters = window.document.createElement("canvas"); this.canvasLiveRegisters.width = Panel.LIVEREGISTERS.WIDTH; this.canvasLiveRegisters.height = Panel.LIVEREGISTERS.HEIGHT; this.contextLiveRegisters = this.canvasLiveRegisters.getContext("2d"); - this.contextLiveRegisters.font = Panel.LIVECANVAS.FONT.HEIGHT + "px " + Panel.LIVECANVAS.FONT.NAME; this.fRedraw = true; return true; @@ -168,9 +287,7 @@ Panel.prototype.initBus = function(cmp, bus, cpu, dbg) */ Panel.prototype.powerUp = function(data, fRepower) { - if (!fRepower) { - Panel.init(); - } + if (!fRepower) Panel.init(); return true; }; @@ -196,14 +313,49 @@ Panel.prototype.powerDown = function(fSave) Panel.prototype.updateAnimation = function() { if (this.fRedraw) { + + this.initPen(10, Panel.LIVECANVAS.FONT.HEIGHT, this.canvasLiveMemory, this.contextLiveMemory, this.canvas.style.color); + if (this.fBackTrack) { - if (MAXDEBUG) this.log("begin scanMemory()"); + if (DEBUG) this.log("begin scanMemory()"); this.stats = this.bus.scanMemory(this.stats); - this.findRegions(); - if (MAXDEBUG) this.log("end scanMemory(): total bytes: " + this.stats.cbTotal + ", total blocks: " + this.stats.cBlocks + ", total regions: " + this.stats.cRegions); + /* + * Update the Stats object with region information (cRegions and aRegions); return true if region + * information has changed since the last call. + */ + if (this.findRegions()) { + /* + * For each region, I choose a slice of the LiveMemory canvas and record the corresponding rectangle + * within an aRects array (parallel to the aRegions array) in the Stats object. + * + * I don't need a sophisticated Treemap algorithm, because at this level, the data is not hierarchical. + * subDivide() makes a simple horizontal or vertical slicing decision based on the ratio of region blocks + * to remaining blocks. + */ + var i; + var rectAvail = new Rectangle(0, 0, this.canvasLiveMemory.width, this.canvasLiveMemory.height); + this.stats.aRects = []; + var cBlocksRemaining = this.stats.cBlocks; + for (i = 0; i < this.stats.cRegions; i++) { + var cBlocksRegion = (this.stats.aRegions[i] >> Bus.BLOCK.COUNT_SHIFT) & Bus.BLOCK.COUNT_MASK; + this.stats.aRects.push(rectAvail.subDivide(cBlocksRegion, cBlocksRemaining, true)); + cBlocksRemaining -= cBlocksRegion; + } + this.assert(!cBlocksRemaining && (!rectAvail.cx || !rectAvail.cy)); + for (i = 0; i < this.stats.aRects.length; i++) { + var nRegion = this.stats.aRegions[i]; + var type = (nRegion >> Bus.BLOCK.TYPE_SHIFT) & Bus.BLOCK.TYPE_MASK; + var cBlocks = (nRegion >> Bus.BLOCK.COUNT_SHIFT) & Bus.BLOCK.COUNT_MASK; + var rect = this.stats.aRects[i]; + rect.drawWith(this.contextLiveMemory, Memory.TYPE.COLORS[type]); + this.centerPen(rect); + this.centerText(Memory.TYPE.NAMES[type] + " (" + (((cBlocks * this.bus.blockSize) / 1024) | 0) + "Kb)"); + } + } + if (DEBUG) this.log("end scanMemory(): total bytes: " + this.stats.cbTotal + ", total blocks: " + this.stats.cBlocks + ", total regions: " + this.stats.cRegions); + } else { + this.drawText("This space intentionally left blank"); } - this.initPen(10, Panel.LIVECANVAS.FONT.HEIGHT, this.canvasLiveMemory, this.contextLiveMemory); - this.drawText("This space intentionally left blank"); this.context.drawImage(this.canvasLiveMemory, 0, 0, this.canvasLiveMemory.width, this.canvasLiveMemory.height, this.xMemory, this.yMemory, this.cxMemory, this.cyMemory); this.fRedraw = false; } @@ -212,7 +364,7 @@ Panel.prototype.updateAnimation = function() /** * updateStatus() * - * Update function for Control Panels containing DOM elements with low-frequency display requirements. + * Update function for Control Panels containing DOM elements with low-frequencyxt display requirements. * * For the time being, the X86CPU component has its own updateStatus() handler, and displays all CPU registers itself. * @@ -228,34 +380,41 @@ Panel.prototype.updateStatus = function() /** * findRegions() * - * This takes the stats object produced by scanMemory() and adds the following: + * This takes the Stats object produced by scanMemory() and adds the following: * * cRegions: number of contiguous memory regions - * aRegions: array of aBlocks indexes (bits 0-15) combined with block counts (bits 16-31) + * aRegions: array of aBlocks indexes (bits 0-14) combined with block counts (bits 16-27) and block types (bits 28-31) * * It calls addRegion() for each discrete region (set of contiguous blocks with the same type) that it finds. * * @this {Panel} + * @return {boolean} true if current region checksum differed from previous checksum (ie, one or more regions changed) */ Panel.prototype.findRegions = function() { + var checksum = 0; this.stats.cRegions = 0; if (!this.stats.aRegions) this.stats.aRegions = []; var typeRegion = -1, iBlockRegion = 0, addrRegion = 0, nBlockPrev = -1; for (var iBlock = 0; iBlock < this.stats.cBlocks; iBlock++) { var nBlock = this.stats.aBlocks[iBlock]; var type = nBlock >>> Bus.BLOCK.TYPE_SHIFT; - var nBlockCurr = (nBlock & Bus.BLOCK.MASK); + var nBlockCurr = (nBlock & Bus.BLOCK.NUM_MASK); if (type != typeRegion || nBlockCurr != nBlockPrev + 1) { var cBlocks = iBlock - iBlockRegion; - if (cBlocks) this.addRegion(addrRegion, iBlockRegion, cBlocks, typeRegion); + if (cBlocks) { + checksum += this.addRegion(addrRegion, iBlockRegion, cBlocks, typeRegion); + } typeRegion = type; iBlockRegion = iBlock; - addrRegion = (nBlock & Bus.BLOCK.MASK) << this.bus.blockShift; + addrRegion = (nBlock & Bus.BLOCK.NUM_MASK) << this.bus.blockShift; } nBlockPrev = nBlockCurr; } - this.addRegion(addrRegion, iBlockRegion, iBlock - iBlockRegion, typeRegion); + checksum += this.addRegion(addrRegion, iBlockRegion, iBlock - iBlockRegion, typeRegion); + var fChanged = (this.stats.checksumRegions != checksum); + this.stats.checksumRegions = checksum; + return fChanged; }; /** @@ -266,11 +425,13 @@ Panel.prototype.findRegions = function() * @param {number} iBlock * @param {number} cBlocks * @param {number} type + * @return {number} region data added */ Panel.prototype.addRegion = function(addr, iBlock, cBlocks, type) { - this.stats.aRegions[this.stats.cRegions++] = (iBlock | cBlocks << 16); - if (MAXDEBUG) this.log("region " + this.stats.cRegions + " (addr " + str.toHex(addr) + ", type " + Memory.TYPE.NAMES[type] + ") contains " + cBlocks + " blocks"); + if (DEBUG) this.log("region " + this.stats.cRegions + " (addr " + str.toHex(addr) + ", type " + Memory.TYPE.NAMES[type] + ") contains " + cBlocks + " blocks"); + this.assert(iBlock <= Bus.BLOCK.NUM_MASK && cBlocks <= Bus.BLOCK.COUNT_MASK && type <= Bus.BLOCK.TYPE_MASK); + return this.stats.aRegions[this.stats.cRegions++] = (iBlock | (cBlocks << Bus.BLOCK.COUNT_SHIFT) | (type << Bus.BLOCK.TYPE_SHIFT)); }; /** @@ -287,7 +448,7 @@ Panel.prototype.updateRegisters = function() this.contextLiveRegisters.fillStyle = "black"; this.contextLiveRegisters.fillRect(0, 0, this.canvasLiveRegisters.width, this.canvasLiveRegisters.height); - this.initPen(10, Panel.LIVECANVAS.FONT.HEIGHT, this.canvasLiveRegisters, this.contextLiveRegisters); + this.initPen(10, Panel.LIVECANVAS.FONT.HEIGHT, this.canvasLiveRegisters, this.contextLiveRegisters, this.canvas.style.color); this.initCols(3); this.drawText("CPU"); this.drawText("Target"); @@ -329,27 +490,59 @@ Panel.prototype.updateRegisters = function() }; /** - * initPen(xLeft, yTop, canvasText, contextText) + * initPen(xLeft, yTop, canvas, context, sColor) * * @this {Panel} * @param {number} xLeft * @param {number} yTop - * @param {Object} [canvasText] - * @param {Object} [contextText] + * @param {HTMLCanvasElement} [canvas] + * @param {Object} [context] + * @param {string} [sColor] */ -Panel.prototype.initPen = function(xLeft, yTop, canvasText, contextText) +Panel.prototype.initPen = function(xLeft, yTop, canvas, context, sColor) { - this.xLeftMargin = this.xLeft = xLeft; - this.yTop = yTop; - if (canvasText) { - this.canvasText = canvasText; + this.heightText = this.heightDefault = yTop; + this.fontText = this.fontDefault = this.heightText + "px " + Panel.LIVECANVAS.FONT.NAME; + this.setPen(this.xLeftMargin = xLeft, yTop); + if (canvas) { + this.canvasText = canvas; } - if (contextText) { - this.contextText = contextText; - this.contextText.fillStyle = this.canvas.style.color + if (context) { + this.contextText = context; + this.colorText = sColor || "white"; } }; +/** + * setPen(x, y) + * + * @this {Panel} + * @param {number} x + * @param {number} y + */ +Panel.prototype.setPen = function(x, y) +{ + this.xText = x; + this.yText = y; +}; + +/** + * centerPen(rect) + * + * @this {Panel} + * @param {Rectangle} rect + */ +Panel.prototype.centerPen = function(rect) +{ + this.fontText = this.fontDefault; + this.heightText = this.heightDefault; + if (rect.cy < this.heightText) { + this.heightText = rect.cy; + this.fontText = this.heightText + "px " + Panel.LIVECANVAS.FONT.NAME; + } + this.setPen(rect.x + (rect.cx >> 1), rect.y + (rect.cy >> 1)); +}; + /** * initCols(nCols) * @@ -369,7 +562,7 @@ Panel.prototype.initCols = function(nCols) */ Panel.prototype.skipCols = function(nCols) { - this.xLeft += this.cxColumn * nCols; + this.xText += this.cxColumn * nCols; }; /** @@ -380,13 +573,14 @@ Panel.prototype.skipCols = function(nCols) */ Panel.prototype.skipLines = function(nLines) { - this.xLeft = this.xLeftMargin; - this.yTop += (Panel.LIVECANVAS.FONT.HEIGHT + 2) * (nLines || 1); + this.xText = this.xLeftMargin; + this.yText += (this.heightText + 2) * (nLines || 1); }; /** * initNumberFormat(nBase, nDigits) * + * @this {Panel} * @param {number} nBase * @param {number} nDigits */ @@ -407,20 +601,46 @@ Panel.prototype.initNumberFormat = function(nBase, nDigits) */ Panel.prototype.drawText = function(sText, nValue, nColsSkip, nLinesSkip) { - this.contextText.fillText(sText, this.xLeft, this.yTop); - this.xLeft += this.cxColumn; + this.contextText.font = this.fontText; + this.contextText.fillStyle = this.colorText; + this.contextText.fillText(sText, this.xText, this.yText); + this.xText += this.cxColumn; if (nValue !== undefined) { var sValue = nValue.toString(); if (this.nDefaultBase == 16) { sValue = "0x" + str.toHex(nValue, this.nDefaultDigits); } - this.contextText.fillText(sValue, this.xLeft, this.yTop); - this.xLeft += this.cxColumn; + this.contextText.fillText(sValue, this.xText, this.yText); + this.xText += this.cxColumn; } if (nColsSkip) this.skipCols(nColsSkip); if (nLinesSkip) this.skipLines(nLinesSkip); }; +/** + * centerText(sText) + * + * To center text within a given Rectangle: + * + * centerPen(rect) + * centerText(sText) + * + * centerPen() sets xLeft and yTop to the center of the specified rectangle, and centerText() calculates + * the width of the text, adjusting the horizontal centering by its width and the vertical centering by the + * default font height. Then it call drawText(). + * + * @this {Panel} + * @param {string} sText + */ +Panel.prototype.centerText = function(sText) +{ + this.contextText.font = this.fontText; + var tm = this.contextText.measureText(sText); + this.xText -= tm.width >> 1; + this.yText += (this.heightText >> 1) - 1; + this.drawText(sText); +}; + /** * Panel.init() *