/* spectrum.js: Spectrum architecture implementation for JSSpeccy, a ZX Spectrum emulator in Javascript 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 3 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, see . Contact details: Matthew Westcott, 14 Daisy Hill Drive, Adlington, Chorley, Lancs PR6 9NE UNITED KINGDOM */ var memory = []; var canvas; var ctx; var imageData; var imageDataData; var keyStates = []; var imageDataShadow = []; /* clone of imageDataData; - used to skip writing pixels to canvas if there's no change */ var hasImageData; var needDrawImage = (navigator.userAgent.indexOf('Firefox/2') != -1); var logo = [ 0x00,0x00,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x68,0x68,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x70,0x00,0x00,0x60,0x60,0x00,0x68,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x68,0x68,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x70,0x00,0x60,0x00,0x00,0x00,0x00,0x68,0x00,0x00,0x58,0x58,0x00,0x00,0x70,0x00,0x70,0x00,0x00,0x60,0x60,0x00,0x68,0x00,0x00,0x00,0x58,0x00,0x58,0x00, 0x00,0x00,0x70,0x00,0x00,0x60,0x00,0x00,0x00,0x00,0x68,0x00,0x58,0x00,0x58,0x00,0x70,0x70,0x70,0x00,0x60,0x00,0x00,0x00,0x68,0x00,0x00,0x00,0x58,0x00,0x58,0x00, 0x70,0x00,0x70,0x00,0x00,0x00,0x60,0x00,0x68,0x00,0x68,0x00,0x58,0x00,0x58,0x00,0x70,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x68,0x00,0x00,0x00,0x58,0x00,0x58,0x00, 0x00,0x70,0x00,0x00,0x60,0x00,0x60,0x00,0x00,0x68,0x00,0x00,0x58,0x58,0x00,0x00,0x00,0x70,0x70,0x00,0x60,0x00,0x00,0x00,0x00,0x68,0x68,0x00,0x00,0x58,0x58,0x00, 0x00,0x00,0x00,0x00,0x00,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x58,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x60,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x58,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x58,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x58,0x58,0x00,0x00 ]; function spectrum_init() { for (var i = 0x0000; i < 0x4000; i++) { memory[i] = roms['48.rom'].charCodeAt(i); } for (var i = 0x4000; i < 0x10000; i++) { memory[i] = 0; } canvas = document.getElementById('screen'); ctx = canvas.getContext('2d'); ctx.fillStyle = 'black'; ctx.fillRect(0,0,256,192); /* set alpha to opaque */ if (ctx.getImageData) { hasImageData = true; imageData = ctx.getImageData(0,0,256,192); imageDataData = imageData.data; } else { /* this browser does not support getImageData / putImageData; use horribly slow fillRect method to plot pixels instead */ hasImageData = false; drawScreenByte = drawScreenByteWithoutImageData; drawAttrByte = drawAttrByteWithoutImageData; } for (i = 0; i < 256; i++) { memory[0x5900 + i] = logo[i]; } paintFullScreen(); paintScreen(); document.onkeydown = keyDown; document.onkeyup = keyUp; document.onkeypress = keyPress; for (var row = 0; row < 8; row++) { keyStates[row] = 0xff; } canvas.onmousedown = joystickKeyDown; canvas.onmouseup = joystickKeyUp; document.ontouchstart = joystickTouchStart; document.ontouchend = joystickTouchEnd; } var keyCodes = { 49: {row: 3, mask: 0x01}, /* 1 */ 50: {row: 3, mask: 0x02}, /* 2 */ 51: {row: 3, mask: 0x04}, /* 3 */ 52: {row: 3, mask: 0x08}, /* 4 */ 53: {row: 3, mask: 0x10}, /* 5 */ 54: {row: 4, mask: 0x10}, /* 6 */ 55: {row: 4, mask: 0x08}, /* 7 */ 56: {row: 4, mask: 0x04}, /* 8 */ 57: {row: 4, mask: 0x02}, /* 9 */ 48: {row: 4, mask: 0x01}, /* 0 */ 81: {row: 2, mask: 0x01}, /* Q */ 87: {row: 2, mask: 0x02}, /* W */ 69: {row: 2, mask: 0x04}, /* E */ 82: {row: 2, mask: 0x08}, /* R */ 84: {row: 2, mask: 0x10}, /* T */ 89: {row: 5, mask: 0x10}, /* Y */ 85: {row: 5, mask: 0x08}, /* U */ 73: {row: 5, mask: 0x04}, /* I */ 79: {row: 5, mask: 0x02}, /* O */ 80: {row: 5, mask: 0x01}, /* P */ 65: {row: 1, mask: 0x01}, /* A */ 83: {row: 1, mask: 0x02}, /* S */ 68: {row: 1, mask: 0x04}, /* D */ 70: {row: 1, mask: 0x08}, /* F */ 71: {row: 1, mask: 0x10}, /* G */ 72: {row: 6, mask: 0x10}, /* H */ 74: {row: 6, mask: 0x08}, /* J */ 75: {row: 6, mask: 0x04}, /* K */ 76: {row: 6, mask: 0x02}, /* L */ 13: {row: 6, mask: 0x01}, /* enter */ 16: {row: 0, mask: 0x01}, /* caps */ 192: {row: 0, mask: 0x01}, /* backtick as caps - because firefox screws up a load of key codes when pressing shift */ 90: {row: 0, mask: 0x02}, /* Z */ 88: {row: 0, mask: 0x04}, /* X */ 67: {row: 0, mask: 0x08}, /* C */ 86: {row: 0, mask: 0x10}, /* V */ 66: {row: 7, mask: 0x10}, /* B */ 78: {row: 7, mask: 0x08}, /* N */ 77: {row: 7, mask: 0x04}, /* M */ 17: {row: 7, mask: 0x02}, /* sym - gah, firefox screws up ctrl+key too */ 32: {row: 7, mask: 0x01}, /* space */ }; var palette = [ [0,0,0], [0,0,192], [192,0,0], [192,0,192], [0,192,0], [0,192,192], [192,192,0], [192,192,192], [0,0,0], [0,0,255], [255,0,0], [255,0,255], [0,255,0], [0,255,255], [255,255,0], [255,255,255] ]; function keyDown(evt) { registerKeyDown(evt.keyCode) if (!evt.metaKey) return false; } function registerKeyDown(keyNum) { var keyCode = keyCodes[keyNum]; if (keyCode == null) return; keyStates[keyCode.row] &= ~(keyCode.mask); } function keyUp(evt) { registerKeyUp(evt.keyCode); if (!evt.metaKey) return false; } function registerKeyUp(keyNum) { var keyCode = keyCodes[keyNum]; if (keyCode == null) return; keyStates[keyCode.row] |= keyCode.mask; } function keyPress(evt) { if (!evt.metaKey) return false; } function locateJoystickKeyFromCanvas(evt) { var selectId; if (evt.layerX < 160) { selectId = 'select_key_left'; } else if (evt.layerX > 480) { selectId = 'select_key_right'; } else if (evt.layerY < 256) { selectId = 'select_key_up'; } else { selectId = 'select_key_down'; } var select = document.getElementById(selectId); var opt = select.options[select.selectedIndex]; return opt.value; } function locateJoystickKeyFromScreen(evt) { var selectId; if (evt.screenX < screen.width / 4) { selectId = 'select_key_left'; } else if (evt.screenX > screen.width * 3/4) { selectId = 'select_key_right'; } else if (evt.screenY < screen.height / 2) { selectId = 'select_key_up'; } else { selectId = 'select_key_down'; } var select = document.getElementById(selectId); var opt = select.options[select.selectedIndex]; return opt.value; } function joystickKeyDown(evt) { registerKeyDown(locateJoystickKeyFromCanvas(evt)); } function joystickKeyUp(evt) { registerKeyUp(locateJoystickKeyFromCanvas(evt)); } function joystickTouchStart(evt) { for (var i = 0; i < evt.changedTouches.length; i++) { registerKeyDown(locateJoystickKeyFromScreen(evt.changedTouches[i])); } } function joystickTouchEnd(evt) { for (var i = 0; i < evt.changedTouches.length; i++) { registerKeyUp(locateJoystickKeyFromScreen(evt.changedTouches[i])); } } function contend_memory(addr) { return 0; /* TODO: implement */ } function contend_port(addr) { return 0; /* TODO: implement */ } function readbyte(addr) { return readbyte_internal(addr); } function readbyte_internal(addr) { return memory[addr]; } function readport(addr) { if ((addr & 0x0001) == 0x0000) { /* read keyboard */ var result = 0xff; for (var row = 0; row < 8; row++) { if (!(addr & (1 << (row+8)))) { /* bit held low, so scan this row */ result &= keyStates[row]; } } return result; } else if ((addr & 0x00e0) == 0x0000) { /* kempston joystick: treat this as attached but unused (for the benefit of Manic Miner) */ return 0x00; } else { return 0xff; /* unassigned port */ } } function writeport(addr, val) { if ((addr & 0x0001) == 0) { var borderColour = palette[val & 0x07]; var borderColourCss = 'rgb('+borderColour[0]+','+borderColour[1]+','+borderColour[2]+')'; canvas.style.borderColor = borderColourCss; } } function writebyte(addr, val) { return writebyte_internal(addr, val) } function writebyte_internal(addr, val) { if (addr < 0x4000) return; if (addr < 0x5800) { var oldByte = memory[addr]; memory[addr] = val; if (val != oldByte) drawScreenByte(addr, val); } else if (addr < 0x5b00) { var oldByte = memory[addr]; memory[addr] = val; if (val != oldByte) drawAttrByte(addr, val); } else { memory[addr] = val; } } function drawScreenByteWithoutImageData(addr, val) { /* 0 1 0 y7 y6 y2 y1 y0 / y5 y4 y3 x4 x3 x2 x1 x0 */ var x = (addr & 0x001f); /* counted in characters */ var y = ((addr & 0x0700) >> 8) | ((addr & 0x00e0) >> 2) | ((addr & 0x1800) >> 5); /* counted in pixels */ var attributeByte = memory[0x5800 | ((y & 0xf8) << 2) | x]; if ((attributeByte & 0x80) && (flashFrame & 0x10)) { /* invert flashing attributes */ var ink = palette[(attributeByte & 0x78) >> 3]; var paper = palette[((attributeByte & 0x40) >> 3) | (attributeByte & 0x07)]; } else { var ink = palette[((attributeByte & 0x40) >> 3) | (attributeByte & 0x07)]; var paper = palette[(attributeByte & 0x78) >> 3]; } var inkRgb = 'rgb(' + ink[0] + ',' + ink[1] + ',' + ink[2] + ')'; var paperRgb = 'rgb(' + paper[0] + ',' + paper[1] + ',' + paper[2] + ')'; var xp = x << 3; var pixelAddress = (y << 10) | (x << 5); for (var bit = 0x80; bit != 0; bit >>= 1) { if (val & bit) { ctx.fillStyle = inkRgb; ctx.fillRect(xp, y, 1, 1); xp++; } else { ctx.fillStyle = paperRgb; ctx.fillRect(xp, y, 1, 1); xp++; } } } function drawScreenByte(addr, val) { /* 0 1 0 y7 y6 y2 y1 y0 / y5 y4 y3 x4 x3 x2 x1 x0 */ var x = (addr & 0x001f); /* counted in characters */ var y = ((addr & 0x0700) >> 8) | ((addr & 0x00e0) >> 2) | ((addr & 0x1800) >> 5); /* counted in pixels */ var attributeByte = memory[0x5800 | ((y & 0xf8) << 2) | x]; if ((attributeByte & 0x80) && (flashFrame & 0x10)) { /* invert flashing attributes */ var ink = palette[(attributeByte & 0x78) >> 3]; var paper = palette[((attributeByte & 0x40) >> 3) | (attributeByte & 0x07)]; } else { var ink = palette[((attributeByte & 0x40) >> 3) | (attributeByte & 0x07)]; var paper = palette[(attributeByte & 0x78) >> 3]; } var pixelAddress = (y << 10) | (x << 5); for (var bit = 0x80; bit != 0; bit >>= 1) { if (val & bit) { if (imageDataShadow[pixelAddress] != ink[0]) { imageDataData[pixelAddress] = ink[0]; imageDataShadow[pixelAddress] = ink[0]; } pixelAddress++; if (imageDataShadow[pixelAddress] != ink[1]) { imageDataData[pixelAddress] = ink[1]; imageDataShadow[pixelAddress] = ink[1]; } pixelAddress++; if (imageDataShadow[pixelAddress] != ink[2]) { imageDataData[pixelAddress] = ink[2]; imageDataShadow[pixelAddress] = ink[2]; } pixelAddress += 2; } else { if (imageDataShadow[pixelAddress] != paper[0]) { imageDataData[pixelAddress] = paper[0]; imageDataShadow[pixelAddress] = paper[0]; } pixelAddress++; if (imageDataShadow[pixelAddress] != paper[1]) { imageDataData[pixelAddress] = paper[1]; imageDataShadow[pixelAddress] = paper[1]; } pixelAddress++; if (imageDataShadow[pixelAddress] != paper[2]) { imageDataData[pixelAddress] = paper[2]; imageDataShadow[pixelAddress] = paper[2]; } pixelAddress += 2; } } } function drawAttrByteWithoutImageData(addr, val) { /* 0 1 0 1 1 0 y4 y3 / y2 y1 y0 x4 x3 x2 x1 x0 */ var x0 = (addr & 0x001f); /* counted in characters */ var y0 = (addr & 0x03e0) >> 2; /* counted in pixels */ if ((val & 0x80) && (flashFrame & 0x10)) { /* invert flashing attributes */ var ink = palette[(val & 0x78) >> 3]; var paper = palette[((val & 0x40) >> 3) | (val & 0x07)]; } else { var ink = palette[((val & 0x40) >> 3) | (val & 0x07)]; var paper = palette[(val & 0x78) >> 3]; } var inkRgb = 'rgb(' + ink[0] + ',' + ink[1] + ',' + ink[2] + ')'; var paperRgb = 'rgb(' + paper[0] + ',' + paper[1] + ',' + paper[2] + ')'; for (var y = 0; y < 8; y++) { var screenByte = memory[0x4000 | ((y0 & 0xc0) << 5) | (y << 8) | ((y0 & 0x38) << 2) | x0]; var xp = x0 << 3; for (var bit = 0x80; bit != 0; bit >>= 1) { if (screenByte & bit) { ctx.fillStyle = inkRgb; ctx.fillRect(xp, y | y0, 1, 1); xp++; } else { ctx.fillStyle = paperRgb; ctx.fillRect(xp, y | y0, 1, 1); xp++; } } } } function drawAttrByte(addr, val) { /* 0 1 0 1 1 0 y4 y3 / y2 y1 y0 x4 x3 x2 x1 x0 */ var x0 = (addr & 0x001f); /* counted in characters */ var y0 = (addr & 0x03e0) >> 2; /* counted in pixels */ if ((val & 0x80) && (flashFrame & 0x10)) { /* invert flashing attributes */ var ink = palette[(val & 0x78) >> 3]; var paper = palette[((val & 0x40) >> 3) | (val & 0x07)]; } else { var ink = palette[((val & 0x40) >> 3) | (val & 0x07)]; var paper = palette[(val & 0x78) >> 3]; } for (var y = 0; y < 8; y++) { var pixelAddress = ((y0 | y) << 10) | (x0 << 5); var screenByte = memory[0x4000 | ((y0 & 0xc0) << 5) | (y << 8) | ((y0 & 0x38) << 2) | x0]; for (var bit = 0x80; bit != 0; bit >>= 1) { if (screenByte & bit) { if (imageDataShadow[pixelAddress] != ink[0]) { imageDataData[pixelAddress] = ink[0]; imageDataShadow[pixelAddress] = ink[0]; } pixelAddress++; if (imageDataShadow[pixelAddress] != ink[1]) { imageDataData[pixelAddress] = ink[1]; imageDataShadow[pixelAddress] = ink[1]; } pixelAddress++; if (imageDataShadow[pixelAddress] != ink[2]) { imageDataData[pixelAddress] = ink[2]; imageDataShadow[pixelAddress] = ink[2]; } pixelAddress += 2; } else { if (imageDataShadow[pixelAddress] != paper[0]) { imageDataData[pixelAddress] = paper[0]; imageDataShadow[pixelAddress] = paper[0]; } pixelAddress++; if (imageDataShadow[pixelAddress] != paper[1]) { imageDataData[pixelAddress] = paper[1]; imageDataShadow[pixelAddress] = paper[1]; } pixelAddress++; if (imageDataShadow[pixelAddress] != paper[2]) { imageDataData[pixelAddress] = paper[2]; imageDataShadow[pixelAddress] = paper[2]; } pixelAddress += 2; } } } } function paintScreen() { if ((flashFrame & 0x0f) == 0) { /* need to redraw flashing attributes on this frame */ for (var addr = 0x5800; addr < 0x5b00; addr++) { if (memory[addr] & 0x80) { drawAttrByte(addr, memory[addr]); } } } if (hasImageData) { ctx.putImageData(imageData, 0, 0); if (needDrawImage) ctx.drawImage(canvas, 0, 0); /* FF2 appears to need this */ } } function paintFullScreen() { /* force repaint of whole screen */ var pixelAddress = 0; /* offset into imageData */ var rowAttributeAddress = 0x5800; for (var third = 0x4000; third < 0x5800; third += 0x0800) { var charRowMax = third + 0x0100; for (var charRow = third; charRow < charRowMax; charRow += 0x20) { var pixRowMax = charRow + 0x0800; for (var pixRow = charRow; pixRow < pixRowMax; pixRow += 0x0100) { var charColMax = pixRow + 0x20; var attributeAddress = rowAttributeAddress; for (var charCol = pixRow; charCol < charColMax; charCol++) { var attributeByte = memory[attributeAddress]; attributeAddress++; if ((attributeByte & 0x80) && (flashFrame & 0x10)) { /* invert flashing attributes */ var ink = palette[(attributeByte & 0x78) >> 3]; var paper = palette[((attributeByte & 0x40) >> 3) | (attributeByte & 0x07)]; } else { var ink = palette[((attributeByte & 0x40) >> 3) | (attributeByte & 0x07)]; var paper = palette[(attributeByte & 0x78) >> 3]; } screenByte = memory[charCol]; for (var bit = 0x80; bit != 0; bit >>= 1) { if (screenByte & bit) { imageDataShadow[pixelAddress] = ink[0]; imageDataData[pixelAddress++] = ink[0]; imageDataShadow[pixelAddress] = ink[1]; imageDataData[pixelAddress++] = ink[1]; imageDataShadow[pixelAddress] = ink[2]; imageDataData[pixelAddress++] = ink[2]; pixelAddress++; } else { imageDataShadow[pixelAddress] = paper[0]; imageDataData[pixelAddress++] = paper[0]; imageDataShadow[pixelAddress] = paper[1]; imageDataData[pixelAddress++] = paper[1]; imageDataShadow[pixelAddress] = paper[2]; imageDataData[pixelAddress++] = paper[2]; pixelAddress++; } } } } rowAttributeAddress += 0x20; } } // for (var addr = 0x5800; addr < 0x5b00; addr++) { // drawAttrByte(addr, memory[addr]); // } }