348 lines
10 KiB
JavaScript
348 lines
10 KiB
JavaScript
/*
|
|
* 6502 Based Virtual Computer
|
|
* Copyright (C) 2015 Scott C. Duensing <scott@jaegertech.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
// Our display adapter object.
|
|
function textDisplayCanvas() {
|
|
|
|
// This display adapter can render text in 16 foreground colors
|
|
// on 16 background colors. A simple "BIOS" is available by
|
|
// writing command data into the byte directly following the
|
|
// display memory and then the command into the byte after that.
|
|
// You may also write directly to display RAM.
|
|
|
|
// Font texture is 144x256 pixels.
|
|
// Characters are 9x16
|
|
|
|
|
|
// --- Private variables.
|
|
|
|
var self = this;
|
|
|
|
// Memory Information.
|
|
var MEMORY = null;
|
|
var MEMORY_START = 0;
|
|
var DATA_BYTE = 0;
|
|
var DATA_LSB = 0;
|
|
var DATA_BYTE_ADDRESS = 0;
|
|
var COMMAND_BYTE_ADDRESS = 0;
|
|
|
|
// Font atlas size.
|
|
var ATLAS_WIDTH = 16;
|
|
|
|
// Character cell size.
|
|
var CELL_WIDTH = 9;
|
|
var CELL_HEIGHT = 16;
|
|
|
|
// Default display size.
|
|
var WIDTH = 80;
|
|
var HEIGHT = 25;
|
|
|
|
// Is this a color display?
|
|
var IS_COLOR = true;
|
|
|
|
// These are good old PC DOS colors.
|
|
// black, dark red, dark green, brown,
|
|
// dark blue, dark magenta, dark cyan, gray
|
|
// dim gray, red, green, yellow,
|
|
// blue, magenta, cyan, white
|
|
var COLOR_HEX = ["#000000", "#8B0000", "#006400", "#8B8B00",
|
|
"#00008B", "#8B008B", "#008B8B", "#808080",
|
|
"#696969", "#FF0000", "#00FF00", "#FFFF00",
|
|
"#0000FF", "#FF00FF", "#00FFFF", "#FFFFFF"];
|
|
var COLOR_RGB = new Array();
|
|
|
|
// Cursor position, blink state, and ASCII symbol.
|
|
var CURSOR_X = 0;
|
|
var CURSOR_Y = 0;
|
|
var CURSOR_B = false;
|
|
var CURSOR_C = 177;
|
|
|
|
// Current color attribute. Boring white on black. (LSN=FG, MSN=BG)
|
|
var COLOR_VALUE = 7;
|
|
|
|
// The actual DOM canvases.
|
|
var CANVAS = null;
|
|
var CANVAS_CONTEXT = null;
|
|
var FONT = null;
|
|
var FONT_CONTEXT = null;
|
|
|
|
|
|
// --- Private methods.
|
|
|
|
var blinkCursor = function() {
|
|
// Change cursor state first so it is correct for the rest of the code.
|
|
CURSOR_B = !CURSOR_B;
|
|
if (CURSOR_B) {
|
|
renderCell(CURSOR_X, CURSOR_Y, CURSOR_C, 15, 0, true);
|
|
} else {
|
|
self.refresh(CURSOR_X, CURSOR_Y);
|
|
}
|
|
};
|
|
|
|
// Clear the screen to the current color attributes.
|
|
var clearDisplay = function() {
|
|
var byte = MEMORY_START;
|
|
for (var y=0; y<HEIGHT; y++) {
|
|
for (var x=0; x<WIDTH; x++) {
|
|
MEMORY.writeByte(byte++, 32); // [SPACE] in ASCII.
|
|
if (IS_COLOR) {
|
|
MEMORY.writeByte(byte++, COLOR_VALUE);
|
|
}
|
|
}
|
|
}
|
|
moveCursor(0, 0);
|
|
var MSN = (COLOR_VALUE & 0xf0) >> 4;
|
|
CANVAS_CONTEXT.fillStyle = COLOR_HEX[MSN];
|
|
CANVAS_CONTEXT.fillRect(0, 0, CANVAS.width, CANVAS.height);
|
|
};
|
|
|
|
// Move the cursor to a new location.
|
|
var moveCursor = function(x, y) {
|
|
// Be sure old character cell has been restored if the cursor is currently visible.
|
|
if (CURSOR_B) self.refresh(CURSOR_X, CURSOR_Y);
|
|
// Move it.
|
|
CURSOR_X = x;
|
|
CURSOR_Y = y;
|
|
};
|
|
|
|
// Draw one character to the display & update cursor position.
|
|
var drawCharacter = function(c) {
|
|
var color = (IS_COLOR ? 2 : 1);
|
|
var byte = MEMORY_START + (CURSOR_Y * WIDTH + CURSOR_X) * color;
|
|
var x = CURSOR_X;
|
|
var y = CURSOR_Y;
|
|
if (c == 8) {
|
|
// Backspace
|
|
if (x > 0)
|
|
x--;
|
|
} else if (c == 9) {
|
|
// TAB
|
|
x = (x + 8) & ~7;
|
|
} else if (c == 10) {
|
|
// Line Feed
|
|
y++;
|
|
} else if (c == 13) {
|
|
// Carriage Return
|
|
x = 0;
|
|
} else {
|
|
// Other characters.
|
|
MEMORY.writeByte(byte++, c);
|
|
if (IS_COLOR) MEMORY.writeByte(byte++, COLOR_VALUE);
|
|
self.refresh(x, y);
|
|
x++;
|
|
}
|
|
// Line wrap?
|
|
if (x >= WIDTH - 1) {
|
|
x = 0;
|
|
y++;
|
|
}
|
|
// Scroll required?
|
|
var didScroll = false;
|
|
while (y >= HEIGHT) {
|
|
didScroll = true;
|
|
byte = MEMORY_START;
|
|
for (var i=0; i<(HEIGHT - 1) * WIDTH; i++) {
|
|
MEMORY.writeByte(byte, MEMORY.readByte(byte + WIDTH * color));
|
|
byte++;
|
|
if (IS_COLOR) {
|
|
MEMORY.writeByte(byte, MEMORY.readByte(byte + WIDTH * color));
|
|
byte++;
|
|
}
|
|
}
|
|
for (var i=0; i<WIDTH; i++) {
|
|
MEMORY.writeByte(byte++, 32);
|
|
if (IS_COLOR) MEMORY.writeByte(byte++, COLOR_VALUE);
|
|
}
|
|
y--;
|
|
}
|
|
// Update display if we scrolled.
|
|
if (didScroll) {
|
|
var display = CANVAS_CONTEXT.getImageData(0, CELL_HEIGHT, CANVAS.width, CANVAS.height - CELL_HEIGHT);
|
|
CANVAS_CONTEXT.putImageData(display, 0, 0);
|
|
var MSN = (COLOR_VALUE & 0xf0) >> 4;
|
|
CANVAS_CONTEXT.fillStyle = COLOR_HEX[MSN];
|
|
CANVAS_CONTEXT.fillRect(0, CANVAS.height - CELL_HEIGHT, CANVAS.width, CELL_HEIGHT);
|
|
}
|
|
// Reposition cursor.
|
|
moveCursor(x, y);
|
|
};
|
|
|
|
// Draw a zero-terminated string to the display & update cursor position.
|
|
var drawString = function(address) {
|
|
var byte = address;
|
|
do {
|
|
var c = MEMORY.readByte(byte++);
|
|
if (c != 0) drawCharacter(c);
|
|
} while (c != 0);
|
|
};
|
|
|
|
// Finish attaching this display to the VM.
|
|
var finishAttaching = function(newMemory, newStartPosition, divId) {
|
|
MEMORY = newMemory;
|
|
MEMORY_START = newStartPosition;
|
|
// Pre-convert our hex colors to RGB values.
|
|
for (c=0; c<COLOR_HEX.length; ++c) {
|
|
COLOR_RGB.push(util.hexToRgb(COLOR_HEX[c]));
|
|
}
|
|
// Create canvas and context for display.
|
|
CANVAS = document.createElement('canvas');
|
|
CANVAS.height = CELL_HEIGHT * HEIGHT;
|
|
CANVAS.width = CELL_WIDTH * WIDTH;
|
|
CANVAS.style = 'border: 1px solid black';
|
|
CANVAS_CONTEXT = CANVAS.getContext('2d');
|
|
document.getElementById(divId).appendChild(CANVAS);
|
|
var byte = MEMORY_START + HEIGHT * WIDTH * (IS_COLOR ? 2 : 1);
|
|
DATA_BYTE_ADDRESS = byte++;
|
|
COMMAND_BYTE_ADDRESS = byte++;
|
|
clearDisplay();
|
|
// Set up memory mapped hardware ports.
|
|
MEMORY.addWriteCallback(MEMORY_START, MEMORY_START + self.getMemoryNeeded() - 1, null, function(address, value, data){
|
|
MEMORY.callbacksEnabled(false);
|
|
var offset = address - MEMORY_START;
|
|
// Is this a write to one of the command ports or direct to video memory?
|
|
if (address >= DATA_BYTE_ADDRESS) {
|
|
// Command byte?
|
|
if (address == COMMAND_BYTE_ADDRESS) {
|
|
if (value == 0) DATA_LSB = DATA_BYTE;
|
|
if (value == 1) clearDisplay();
|
|
if (value == 2) moveCursor(DATA_BYTE, CURSOR_Y);
|
|
if (value == 3) moveCursor(CURSOR_X, DATA_BYTE);
|
|
if (value == 4) MEMORY.writeByte(DATA_BYTE_ADDRESS, CURSOR_X);
|
|
if (value == 5) MEMORY.writeByte(DATA_BYTE_ADDRESS, CURSOR_Y);
|
|
if (value == 6) COLOR_VALUE = DATA_BYTE;
|
|
if (value == 7) drawCharacter(DATA_BYTE);
|
|
if (value == 8) drawString((DATA_BYTE << 8) + DATA_LSB);
|
|
} else {
|
|
// Data byte.
|
|
DATA_BYTE = value;
|
|
}
|
|
} else {
|
|
if (IS_COLOR) {
|
|
// Is this the character byte or the color byte?
|
|
if (offset % 2 == 0)
|
|
offset = offset / 2; // Character.
|
|
else
|
|
offset = ((offset + 1) / 2) - 1; // Color.
|
|
}
|
|
var y = Math.floor(offset / WIDTH);
|
|
var x = offset - y * WIDTH;
|
|
self.refresh(x, y);
|
|
}
|
|
MEMORY.callbacksEnabled(true);
|
|
});
|
|
};
|
|
|
|
var renderCell = function(x, y, c, f, b, t) {
|
|
// Find pixel offset in display canvas.
|
|
var cx = x * CELL_WIDTH;
|
|
var cy = y * CELL_HEIGHT;
|
|
// Locate this character in the font atlas.
|
|
var ax = (c % ATLAS_WIDTH) * CELL_WIDTH;
|
|
var ay = Math.floor(c / ATLAS_WIDTH) * CELL_HEIGHT;
|
|
// Render character.
|
|
var cell = CANVAS_CONTEXT.getImageData(cx, cy, CELL_WIDTH, CELL_HEIGHT);
|
|
var pixelColor = null;
|
|
for (rx=0; rx<CELL_WIDTH; rx++) {
|
|
for (ry=0; ry<CELL_HEIGHT; ry++) {
|
|
var pixel = (FONT_CONTEXT.getImageData(ax + rx, ay + ry, 1, 1).data[0] != 0);
|
|
// If we've requested transparent backgrounds, skip drawing them.
|
|
if (pixel || !t) {
|
|
pixelColor = (pixel ? COLOR_RGB[f] : COLOR_RGB[b]);
|
|
var index = (rx + ry * cell.width) * 4;
|
|
cell.data[index ] = pixelColor.r;
|
|
cell.data[index + 1] = pixelColor.g;
|
|
cell.data[index + 2] = pixelColor.b;
|
|
cell.data[index + 3] = 255;
|
|
}
|
|
}
|
|
}
|
|
CANVAS_CONTEXT.putImageData(cell, cx, cy);
|
|
//if (t) console.log("Rendered " + c + " at " + x + ", " + y);
|
|
}
|
|
|
|
|
|
// --- Public methods.
|
|
|
|
// Begin attaching this display to the VM.
|
|
this.attach = function(newMemory, newStartPosition, divId) {
|
|
// Create hidden canvas and load our font bitmap into it.
|
|
var fontImage = new Image();
|
|
fontImage.onload = function() {
|
|
FONT = document.createElement('canvas');
|
|
FONT.width = fontImage.width;
|
|
FONT.height = fontImage.height;
|
|
FONT_CONTEXT = FONT.getContext('2d');
|
|
FONT_CONTEXT.drawImage(fontImage, 0, 0);
|
|
// After the font loads, finish attaching to the computer.
|
|
finishAttaching(newMemory, newStartPosition, divId);
|
|
window.setInterval(blinkCursor.bind(self), 250);
|
|
//FONT.style = 'visibility: false';
|
|
//document.getElementById(divId).appendChild(FONT);
|
|
}
|
|
fontImage.src = 'VGAFont.png';
|
|
}
|
|
|
|
// Deserialize display state.
|
|
this.deserialize = function(state) {
|
|
DDATA_LSB = state.D;
|
|
IS_COLOR = state.C;
|
|
COLOR_VALUE = state.A;
|
|
moveCursor(state.X, state.Y);
|
|
for (x=0; x<WIDTH; x++)
|
|
for (y=0; y<HEIGHT; y++)
|
|
self.refresh(x, y);
|
|
};
|
|
|
|
// Refresh a single character cell.
|
|
this.refresh = function(x, y) {
|
|
var byte = MEMORY_START + (y * WIDTH + x) * (IS_COLOR ? 2 : 1);
|
|
// Assume B&W display for now.
|
|
var foreground = 7;
|
|
var background = 0;
|
|
// Get the character code we are going to render.
|
|
var c = MEMORY.readByte(byte);
|
|
// If we're using a color display, get the color byte.
|
|
if (IS_COLOR) {
|
|
// LSN=FG, MSN=BG
|
|
var value = MEMORY.readByte(byte + 1);
|
|
var LSN = value & 0x0f;
|
|
var MSN = (value & 0xf0) >> 4;
|
|
foreground = LSN;
|
|
background = MSN;
|
|
}
|
|
renderCell(x, y, c, foreground, background, false);
|
|
};
|
|
|
|
// Fetch how much RAM the current settings will require.
|
|
this.getMemoryNeeded = function() {
|
|
return WIDTH * HEIGHT * (IS_COLOR ? 2 : 1) + 2;
|
|
};
|
|
|
|
// Serialize display state.
|
|
this.serialize = function() {
|
|
var state = {
|
|
X: CURSOR_X,
|
|
Y: CURSOR_Y,
|
|
C: IS_COLOR,
|
|
A: COLOR_VALUE,
|
|
D: DATA_LSB
|
|
};
|
|
return state;
|
|
};
|
|
}
|