1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2024-05-28 08:41:30 +00:00
8bitworkshop/src/emu.ts

551 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
import { hex, clamp } from "./util";
// external modules
declare var jt, Javatari, Z80_fast, CPU6809;
declare var Mousetrap;
// Emulator classes
export var PLATFORMS = {};
var _random_state = 1;
export function noise() {
let x = _random_state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
return (_random_state = x) & 0xff;
}
export function getNoiseSeed() {
return _random_state;
}
export function setNoiseSeed(x : number) {
_random_state = x;
}
type KeyboardCallback = (which:number, charCode:number, flags:KeyFlags) => void;
function __createCanvas(mainElement:HTMLElement, width:number, height:number) : HTMLCanvasElement {
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.classList.add("emuvideo");
canvas.tabIndex = -1; // Make it focusable
mainElement.appendChild(canvas);
return canvas;
}
export enum KeyFlags {
KeyDown = 1,
Shift = 2,
Ctrl = 4,
Alt = 8,
Meta = 16,
KeyUp = 64,
KeyPress = 128,
}
function _setKeyboardEvents(canvas:HTMLElement, callback:KeyboardCallback) {
canvas.onkeydown = function(e) {
callback(e.which, 0, KeyFlags.KeyDown|_metakeyflags(e));
};
canvas.onkeyup = function(e) {
callback(e.which, 0, KeyFlags.KeyUp|_metakeyflags(e));
};
canvas.onkeypress = function(e) {
callback(e.which, e.charCode, KeyFlags.KeyPress|_metakeyflags(e));
};
};
type VideoCanvasOptions = {rotate?:number, overscan?:boolean};
export class RasterVideo {
mainElement : HTMLElement;
width : number;
height : number;
options : VideoCanvasOptions;
constructor(mainElement:HTMLElement, width:number, height:number, options?:VideoCanvasOptions) {
this.mainElement = mainElement;
this.width = width;
this.height = height;
this.options = options;
}
canvas : HTMLCanvasElement;
ctx;
imageData;
arraybuf;
buf8;
datau32;
vcanvas;
paddle_x = 255;
paddle_y = 255;
setRotate(rotate:number) {
var canvas = this.canvas;
if (rotate) {
// TODO: aspect ratio?
canvas.style.transform = "rotate("+rotate+"deg)";
if (canvas.width < canvas.height)
canvas.style.paddingLeft = canvas.style.paddingRight = "10%";
} else {
canvas.style.transform = null;
canvas.style.paddingLeft = canvas.style.paddingRight = null;
}
}
create() {
var canvas;
this.canvas = canvas = __createCanvas(this.mainElement, this.width, this.height);
this.vcanvas = $(canvas);
if (this.options && this.options.rotate) {
this.setRotate(this.options.rotate);
}
if (this.options && this.options.overscan) {
this.vcanvas.css('padding','0px');
}
this.ctx = canvas.getContext('2d');
this.imageData = this.ctx.createImageData(this.width, this.height);
this.datau32 = new Uint32Array(this.imageData.data.buffer);
}
setKeyboardEvents(callback) {
_setKeyboardEvents(this.canvas, callback);
}
getFrameData() { return this.datau32; }
getContext() { return this.ctx; }
updateFrame(sx:number, sy:number, dx:number, dy:number, w?:number, h?:number) {
if (w && h)
this.ctx.putImageData(this.imageData, sx, sy, dx, dy, w, h);
else
this.ctx.putImageData(this.imageData, 0, 0);
}
setupMouseEvents(el? : HTMLElement) {
if (!el) el = this.canvas;
$(el).mousemove( (e) => {
// TODO: get coords right
var x = e.pageX - this.vcanvas.offset().left;
var y = e.pageY - this.vcanvas.offset().top;
var new_x = Math.floor(x * 255 / this.vcanvas.width() - 20);
var new_y = Math.floor(y * 255 / this.vcanvas.height() - 20);
this.paddle_x = clamp(0, 255, new_x);
this.paddle_y = clamp(0, 255, new_y);
});
};
}
export class VectorVideo extends RasterVideo {
persistenceAlpha = 0.5;
jitter = 1.0;
gamma = 0.8;
sx : number;
sy : number;
create() {
super.create();
this.sx = this.width/1024.0;
this.sy = this.height/1024.0;
}
clear() {
var ctx = this.ctx;
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = this.persistenceAlpha;
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, this.width, this.height);
ctx.globalAlpha = 1.0;
ctx.globalCompositeOperation = 'lighter';
}
COLORS = [
'#111111',
'#1111ff',
'#11ff11',
'#11ffff',
'#ff1111',
'#ff11ff',
'#ffff11',
'#ffffff'
];
drawLine(x1:number, y1:number, x2:number, y2:number, intensity:number, color:number) {
var ctx = this.ctx;
var sx = this.sx;
var sy = this.sy;
//console.log(x1,y1,x2,y2,intensity,color);
if (intensity > 0) {
// TODO: landscape vs portrait
var alpha = Math.pow(intensity / 255.0, this.gamma);
ctx.globalAlpha = alpha;
ctx.beginPath();
// TODO: bright dots
var jx = this.jitter * (Math.random() - 0.5);
var jy = this.jitter * (Math.random() - 0.5);
x1 += jx;
x2 += jx;
y1 += jy;
y2 += jy;
ctx.moveTo(x1*sx, this.height-y1*sy);
if (x1 == x2 && y1 == y2)
ctx.lineTo(x2*sx+1, this.height-y2*sy);
else
ctx.lineTo(x2*sx, this.height-y2*sy);
ctx.strokeStyle = this.COLORS[color & 7];
ctx.stroke();
}
}
}
export class RAM {
mem : Uint8Array;
constructor(size:number) {
this.mem = new Uint8Array(new ArrayBuffer(size));
}
}
export class EmuHalt extends Error {
}
export class AnimationTimer {
callback;
running : boolean = false;
pulsing : boolean = false;
lastts = 0;
useReqAnimFrame = false; //TODO window.requestAnimationFrame ? (frequencyHz>40) : false;
nframes;
startts; // for FPS calc
frameRate;
intervalMsec;
constructor(frequencyHz:number, callback:() => void) {
this.frameRate = frequencyHz;
this.intervalMsec = 1000.0 / frequencyHz;
this.callback = callback;
}
scheduleFrame(msec:number) {
var fn = () => {
try {
this.nextFrame();
} catch (e) {
this.running = false;
this.pulsing = false;
throw e;
}
}
if (this.useReqAnimFrame)
window.requestAnimationFrame(fn);
else
setTimeout(fn, msec);
}
nextFrame(ts?:number) {
if (!ts) ts = Date.now();
if (ts - this.lastts < this.intervalMsec*10) {
this.lastts += this.intervalMsec;
} else {
this.lastts = ts + this.intervalMsec; // frames skipped, catch up
}
if (!this.useReqAnimFrame || this.lastts - ts > this.intervalMsec/2) {
if (this.running) {
this.callback();
}
if (this.nframes == 0)
this.startts = ts;
if (this.nframes++ == 300) {
console.log("Avg framerate: " + this.nframes*1000/(ts-this.startts) + " fps");
}
}
if (this.running) {
this.scheduleFrame(this.lastts - ts);
} else {
this.pulsing = false;
}
}
isRunning() {
return this.running;
}
start() {
if (!this.running) {
this.running = true;
this.lastts = 0;
this.nframes = 0;
if (!this.pulsing) {
this.scheduleFrame(0);
this.pulsing = true;
}
}
}
stop() {
this.running = false;
}
}
// TODO: move to util?
export function dumpRAM(ram:Uint8Array|number[], ramofs:number, ramlen:number) : string {
var s = "";
// TODO: show scrollable RAM for other platforms
for (var ofs=0; ofs<ramlen; ofs+=0x10) {
s += '$' + hex(ofs+ramofs) + ':';
for (var i=0; i<0x10; i++) {
if (ofs+i < ram.length) {
if (i == 8) s += " ";
s += " " + hex(ram[ofs+i]);
}
}
s += "\n";
}
return s;
}
export const Keys = {
VK_ESCAPE: {c: 27, n: "Esc"},
VK_F1: {c: 112, n: "F1"},
VK_F2: {c: 113, n: "F2"},
VK_F3: {c: 114, n: "F3"},
VK_F4: {c: 115, n: "F4"},
VK_F5: {c: 116, n: "F5"},
VK_F6: {c: 117, n: "F6"},
VK_F7: {c: 118, n: "F7"},
VK_F8: {c: 119, n: "F8"},
VK_F9: {c: 120, n: "F9"},
VK_F10: {c: 121, n: "F10"},
VK_F11: {c: 122, n: "F11"},
VK_F12: {c: 123, n: "F12"},
VK_SCROLL_LOCK: {c: 145, n: "ScrLck"},
VK_PAUSE: {c: 19, n: "Pause"},
VK_QUOTE: {c: 192, n: "'"},
VK_1: {c: 49, n: "1"},
VK_2: {c: 50, n: "2"},
VK_3: {c: 51, n: "3"},
VK_4: {c: 52, n: "4"},
VK_5: {c: 53, n: "5"},
VK_6: {c: 54, n: "6"},
VK_7: {c: 55, n: "7"},
VK_8: {c: 56, n: "8"},
VK_9: {c: 57, n: "9"},
VK_0: {c: 48, n: "0"},
VK_MINUS: {c: 189, n: "-"},
VK_MINUS2: {c: 173, n: "-"},
VK_EQUALS: {c: 187, n: "="},
VK_EQUALS2: {c: 61, n: "="},
VK_BACK_SPACE: {c: 8, n: "Bkspc"},
VK_TAB: {c: 9, n: "Tab"},
VK_Q: {c: 81, n: "Q"},
VK_W: {c: 87, n: "W"},
VK_E: {c: 69, n: "E"},
VK_R: {c: 82, n: "R"},
VK_T: {c: 84, n: "T"},
VK_Y: {c: 89, n: "Y"},
VK_U: {c: 85, n: "U"},
VK_I: {c: 73, n: "I"},
VK_O: {c: 79, n: "O"},
VK_P: {c: 80, n: "P"},
VK_ACUTE: {c: 219, n: "´"},
VK_OPEN_BRACKET: {c: 221, n: "["},
VK_CLOSE_BRACKET: {c: 220, n: "]"},
VK_CAPS_LOCK: {c: 20, n: "CpsLck"},
VK_A: {c: 65, n: "A"},
VK_S: {c: 83, n: "S"},
VK_D: {c: 68, n: "D"},
VK_F: {c: 70, n: "F"},
VK_G: {c: 71, n: "G"},
VK_H: {c: 72, n: "H"},
VK_J: {c: 74, n: "J"},
VK_K: {c: 75, n: "K"},
VK_L: {c: 76, n: "L"},
VK_CEDILLA: {c: 186, n: "Ç"},
VK_TILDE: {c: 222, n: "~"},
VK_ENTER: {c: 13, n: "Enter"},
VK_SHIFT: {c: 16, n: "Shift"},
VK_BACK_SLASH: {c: 226, n: "\\"},
VK_Z: {c: 90, n: "Z"},
VK_X: {c: 88, n: "X"},
VK_C: {c: 67, n: "C"},
VK_V: {c: 86, n: "V"},
VK_B: {c: 66, n: "B"},
VK_N: {c: 78, n: "N"},
VK_M: {c: 77, n: "M"},
VK_COMMA: {c: 188, n: "] ="},
VK_PERIOD: {c: 190, n: "."},
VK_SEMICOLON: {c: 191, n: ";"},
VK_SLASH: {c: 193, n: "/"},
VK_CONTROL: {c: 17, n: "Ctrl"},
VK_ALT: {c: 18, n: "Alt"},
VK_SPACE: {c: 32, n: "Space"},
VK_INSERT: {c: 45, n: "Ins"},
VK_DELETE: {c: 46, n: "Del"},
VK_HOME: {c: 36, n: "Home"},
VK_END: {c: 35, n: "End"},
VK_PAGE_UP: {c: 33, n: "PgUp"},
VK_PAGE_DOWN: {c: 34, n: "PgDown"},
VK_UP: {c: 38, n: "Up"},
VK_DOWN: {c: 40, n: "Down"},
VK_LEFT: {c: 37, n: "Left"},
VK_RIGHT: {c: 39, n: "Right"},
VK_NUM_LOCK: {c: 144, n: "Num"},
VK_DIVIDE: {c: 111, n: "Num /"},
VK_MULTIPLY: {c: 106, n: "Num *"},
VK_SUBTRACT: {c: 109, n: "Num -"},
VK_ADD: {c: 107, n: "Num +"},
VK_DECIMAL: {c: 194, n: "Num ."},
VK_NUMPAD0: {c: 96, n: "Num 0"},
VK_NUMPAD1: {c: 97, n: "Num 1"},
VK_NUMPAD2: {c: 98, n: "Num 2"},
VK_NUMPAD3: {c: 99, n: "Num 3"},
VK_NUMPAD4: {c: 100, n: "Num 4"},
VK_NUMPAD5: {c: 101, n: "Num 5"},
VK_NUMPAD6: {c: 102, n: "Num 6"},
VK_NUMPAD7: {c: 103, n: "Num 7"},
VK_NUMPAD8: {c: 104, n: "Num 8"},
VK_NUMPAD9: {c: 105, n: "Num 9"},
VK_NUMPAD_CENTER: {c: 12, n: "Num Cntr"}
};
function _metakeyflags(e) {
return (e.shiftKey?KeyFlags.Shift:0) |
(e.ctrlKey?KeyFlags.Ctrl:0) |
(e.altKey?KeyFlags.Alt:0) |
(e.metaKey?KeyFlags.Meta:0);
}
export function setKeyboardFromMap(video, switches, map, func?) {
video.setKeyboardEvents(function(key,code,flags) {
var o = map[key];
if (o && func) {
func(o, key, code, flags);
}
if (o) {
//console.log(key,code,flags,o);
var mask = o.mask;
if (mask < 0) { // negative mask == active low
mask = -mask;
if (flags & (KeyFlags.KeyDown | KeyFlags.KeyUp))
flags ^= KeyFlags.KeyDown | KeyFlags.KeyUp;
}
if (flags & KeyFlags.KeyDown) {
switches[o.index] |= mask;
} else if (flags & KeyFlags.KeyUp) {
switches[o.index] &= ~mask;
}
}
});
}
export function makeKeycodeMap(table) {
var map = {};
for (var i=0; i<table.length; i++) {
var entry = table[i];
map[entry[0].c] = {index:entry[1], mask:entry[2]};
}
return map;
}
export function padBytes(data, len) {
if (data.length > len) {
throw Error("Data too long, " + data.length + " > " + len);
}
var r = new RAM(len);
r.mem.set(data);
return r.mem;
}
type AddressReadWriteFn = ((a:number) => number) | ((a:number,v:number) => void);
type AddressDecoderEntry = [number, number, number, AddressReadWriteFn];
type AddressDecoderOptions = {gmask?:number};
// TODO: better performance, check values
export function AddressDecoder(table : AddressDecoderEntry[], options?:AddressDecoderOptions) {
var self = this;
function makeFunction(lo, hi) {
var s = "";
if (options && options.gmask) {
s += "a&=" + options.gmask + ";";
}
for (var i=0; i<table.length; i++) {
var entry = table[i];
var start = entry[0];
var end = entry[1];
var mask = entry[2];
var func = entry[3];
self['__fn'+i] = func;
s += "if (a>=" + start + " && a<="+end + "){";
if (mask) s += "a&="+mask+";";
s += "return this.__fn"+i+"(a,v)&0xff;}\n";
}
s += "return 0;"; // TODO: noise()?
return new Function('a', 'v', s);
}
return makeFunction(0x0, 0xffff).bind(self);
}
export function newAddressDecoder(table : AddressDecoderEntry[], options?:AddressDecoderOptions) : (a:number,v?:number) => number {
return new (AddressDecoder as any)(table, options);
}
/// TOOLBAR
export class Toolbar {
span : JQuery;
grp : JQuery;
mousetrap;
boundkeys = [];
constructor(parentDiv:HTMLElement, focusDiv:HTMLElement) {
this.mousetrap = focusDiv ? new Mousetrap(focusDiv) : Mousetrap;
this.span = $(document.createElement("span")).addClass("btn_toolbar");
parentDiv.appendChild(this.span[0]);
this.newGroup();
}
destroy() {
if (this.span) {
this.span.remove();
this.span = null;
}
if (this.mousetrap) {
for (var key of this.boundkeys) {
this.mousetrap.unbind(key);
}
this.mousetrap = null;
}
}
newGroup() {
return this.grp = $(document.createElement("span")).addClass("btn_group").appendTo(this.span);
}
add(key:string, alttext:string, icon:string, fn:(e,combo) => void) {
var btn = null;
if (icon) {
btn = $(document.createElement("button")).addClass("btn");
if (icon.startsWith('glyphicon')) {
icon = '<span class="glyphicon ' + icon + '" aria-hidden="true"></span>';
}
btn.html(icon);
btn.prop("title", alttext + " (" + key + ")");
btn.click(fn);
this.grp.append(btn);
}
if (key) {
this.mousetrap.bind(key, fn);
this.boundkeys.push(key);
}
return btn;
}
}