"use strict"; import { hex, clamp } from "./util"; // external modules declare var jt, Javatari, Z80_fast, CPU6809; // Emulator classes export var PLATFORMS = {}; export var frameUpdateFunction : (Canvas) => void = null; 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:number) => void; function __createCanvas(mainElement:HTMLElement, width:number, height:number) : HTMLElement { 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; } function _setKeyboardEvents(canvas:HTMLElement, callback:KeyboardCallback) { canvas.onkeydown = function(e) { callback(e.which, 0, 1|_metakeyflags(e)); }; canvas.onkeyup = function(e) { callback(e.which, 0, 0|_metakeyflags(e)); }; canvas.onkeypress = function(e) { callback(e.which, e.charCode, 1|_metakeyflags(e)); }; }; type VideoCanvasOptions = {rotate?:number, overscan?:boolean}; export var RasterVideo = function(mainElement:HTMLElement, width:number, height:number, options?:VideoCanvasOptions) { var canvas, ctx; var imageData, arraybuf, buf8, datau32; var vcanvas; this.paddle_x = 255; this.paddle_y = 255; this.setRotate = function(rotate) { 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; } } this.create = function() { this.canvas = canvas = __createCanvas(mainElement, width, height); vcanvas = $(canvas); if (options && options.rotate) { this.setRotate(options.rotate); } if (options && options.overscan) { vcanvas.css('padding','0px'); } ctx = canvas.getContext('2d'); imageData = ctx.createImageData(width, height); datau32 = new Uint32Array(imageData.data.buffer); } this.setKeyboardEvents = function(callback) { _setKeyboardEvents(canvas, callback); } this.getFrameData = function() { return datau32; } this.getContext = function() { return ctx; } this.updateFrame = function(sx:number, sy:number, dx:number, dy:number, width?:number, height?:number) { if (width && height) ctx.putImageData(imageData, sx, sy, dx, dy, width, height); else ctx.putImageData(imageData, 0, 0); if (frameUpdateFunction) frameUpdateFunction(canvas); } this.setupMouseEvents = function(el? : HTMLElement) { if (!el) el = canvas; $(el).mousemove( (e) => { // TODO: get coords right var x = e.pageX - vcanvas.offset().left; var y = e.pageY - vcanvas.offset().top; var new_x = Math.floor(x * 255 / vcanvas.width() - 20); var new_y = Math.floor(y * 255 / vcanvas.height() - 20); this.paddle_x = clamp(0, 255, new_x); this.paddle_y = clamp(0, 255, new_y); }); }; } export var VectorVideo = function(mainElement:HTMLElement, width:number, height:number) { var self = this; var canvas, ctx; var persistenceAlpha = 0.5; var jitter = 1.0; var gamma = 0.8; var sx = width/1024.0; var sy = height/1024.0; this.create = function() { this.canvas = canvas = __createCanvas(mainElement, width, height); ctx = canvas.getContext('2d'); } this.setKeyboardEvents = function(callback) { _setKeyboardEvents(canvas, callback); } this.clear = function() { ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = persistenceAlpha; ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, width, height); ctx.globalAlpha = 1.0; ctx.globalCompositeOperation = 'lighter'; if (frameUpdateFunction) frameUpdateFunction(canvas); } var COLORS = [ '#111111', '#1111ff', '#11ff11', '#11ffff', '#ff1111', '#ff11ff', '#ffff11', '#ffffff' ]; this.drawLine = function(x1:number, y1:number, x2:number, y2:number, intensity:number, color:number) { //console.log(x1,y1,x2,y2,intensity,color); if (intensity > 0) { // TODO: landscape vs portrait var alpha = Math.pow(intensity / 255.0, gamma); ctx.globalAlpha = alpha; ctx.beginPath(); // TODO: bright dots var jx = jitter * (Math.random() - 0.5); var jy = jitter * (Math.random() - 0.5); x1 += jx; x2 += jx; y1 += jy; y2 += jy; ctx.moveTo(x1*sx, height-y1*sy); if (x1 == x2 && y1 == y2) ctx.lineTo(x2*sx+1, height-y2*sy); else ctx.lineTo(x2*sx, height-y2*sy); ctx.strokeStyle = COLORS[color & 7]; ctx.stroke(); } } } export class RAM { mem : Uint8Array; constructor(size:number) { this.mem = new Uint8Array(new ArrayBuffer(size)); } } export var AnimationTimer = function(frequencyHz:number, callback:() => void) { var intervalMsec = 1000.0 / frequencyHz; var running : boolean = false; var pulsing : boolean = false; var lastts = 0; var useReqAnimFrame = false; //TODO window.requestAnimationFrame ? (frequencyHz>40) : false; var nframes, startts; // for FPS calc this.frameRate = frequencyHz; function scheduleFrame(msec:number) { if (useReqAnimFrame) window.requestAnimationFrame(nextFrame); else setTimeout(nextFrame, msec); } function nextFrame(ts:number) { if (!ts) ts = Date.now(); if (ts - lastts < intervalMsec*10) { lastts += intervalMsec; } else { lastts = ts + intervalMsec; // frames skipped, catch up } if (!useReqAnimFrame || lastts - ts > intervalMsec/2) { if (running) { callback(); } if (nframes == 0) startts = ts; if (nframes++ == 300) { console.log("Avg framerate: " + nframes*1000/(ts-startts) + " fps"); } } if (running) { scheduleFrame(lastts - ts); } else { pulsing = false; } } this.isRunning = function() { return running; } this.start = function() { if (!running) { running = true; lastts = 0; nframes = 0; if (!pulsing) { scheduleFrame(0); pulsing = true; } } } this.stop = function() { 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 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 number { return new (AddressDecoder as any)(table, options); } // STACK DUMP var addr2symbol = {}; // address to symbol name map (TODO: import) export function lookupSymbol(addr) { var start = addr; var foundsym; while (addr2symbol && addr >= 0) { var sym = addr2symbol[addr]; if (sym && sym.startsWith('_')) { // return first C symbol we find return addr2symbol[addr] + " + " + (start-addr); } else if (sym && !foundsym) { // cache first non-C symbol found foundsym = sym; } addr--; } return foundsym || ""; }