diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 53e25ee..070db9f 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -12,11 +12,14 @@ jobs:
node-version: [12.x]
steps:
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: webfactory/ssh-agent@v0.5.0
+ with:
+ ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: npm install, build, and test
run: |
npm ci
diff --git a/apple2js.html b/apple2js.html
index 2a365c2..3f0b9b2 100644
--- a/apple2js.html
+++ b/apple2js.html
@@ -46,7 +46,7 @@
@@ -195,10 +195,10 @@
Monitor
Sound
diff --git a/apple2jse.html b/apple2jse.html
index 76765d2..e398d7c 100644
--- a/apple2jse.html
+++ b/apple2jse.html
@@ -50,7 +50,7 @@
@@ -200,10 +200,10 @@
Monitor
Sound
diff --git a/css/apple2.css b/css/apple2.css
index b8e1192..99c0721 100644
--- a/css/apple2.css
+++ b/css/apple2.css
@@ -55,9 +55,8 @@ body {
margin: auto;
position: relative;
background-color: black;
- padding: 16px;
- width: 560px;
- height: 384px;
+ width: 592px;
+ height: 416px;
border: 6px inset #f0edd0;
border-radius: 10px;
}
@@ -189,7 +188,7 @@ canvas {
float: left;
}
-.green {
+.mono {
filter: url('#green');
}
@@ -209,6 +208,8 @@ canvas {
-moz-image-rendering: -moz-crisp-edges;
-webkit-image-rendering: -webkit-optimize-contrast;
image-rendering: optimizeSpeed;
+ width: 592px;
+ height: 416px;
}
#screen:-webkit-full-screen {
diff --git a/js/apple2.ts b/js/apple2.ts
index a8e16f2..64ec1c3 100644
--- a/js/apple2.ts
+++ b/js/apple2.ts
@@ -1,21 +1,36 @@
import Apple2IO from './apple2io';
-import { HiresPage, LoresPage, VideoModes } from './canvas';
+// import * as gl from './gl';
+import {
+ HiresPage,
+ LoresPage,
+ VideoModes,
+} from './videomodes';
+import {
+ HiresPage2D,
+ LoresPage2D,
+ VideoModes2D,
+} from './canvas';
+import {
+ HiresPageGL,
+ LoresPageGL,
+ VideoModesGL,
+} from './gl';
import CPU6502, { PageHandler, CpuState } from './cpu6502';
import MMU from './mmu';
import RAM from './ram';
import { debug } from './util';
import SYMBOLS from './symbols';
-import { Restorable } from './types';
+import { Restorable, memory } from './types';
import { processGamepad } from './ui/gamepad';
interface Options {
- characterRom: any,
+ characterRom: memory,
enhanced: boolean,
e: boolean,
- multiScreen: boolean,
+ gl: boolean,
rom: PageHandler,
- screen: any[],
+ canvas: HTMLCanvasElement,
tick: () => void,
}
@@ -44,7 +59,6 @@ export class Apple2 implements Restorable {
private io: Apple2IO;
private mmu: MMU;
- private multiScreen: boolean;
private tick: () => void;
private stats = {
@@ -53,16 +67,18 @@ export class Apple2 implements Restorable {
};
constructor(options: Options) {
+ const LoresPage = options.gl ? LoresPageGL : LoresPage2D;
+ const HiresPage = options.gl ? HiresPageGL : HiresPage2D;
+ const VideoModes = options.gl ? VideoModesGL : VideoModes2D;
+
this.cpu = new CPU6502({ '65C02': options.enhanced });
- this.gr = new LoresPage(1, options.characterRom, options.e, options.screen[0]);
- this.gr2 = new LoresPage(2, options.characterRom, options.e, options.screen[1]);
- this.hgr = new HiresPage(1, options.screen[2]);
- this.hgr2 = new HiresPage(2, options.screen[3]);
- this.vm = new VideoModes(this.gr, this.hgr, this.gr2, this.hgr2, options.e);
- this.vm.multiScreen(options.multiScreen);
+ this.gr = new LoresPage(1, options.characterRom, options.e);
+ this.gr2 = new LoresPage(2, options.characterRom, options.e);
+ this.hgr = new HiresPage(1);
+ this.hgr2 = new HiresPage(2);
+ this.vm = new VideoModes(this.gr, this.hgr, this.gr2, this.hgr2, options.canvas, options.e);
this.vm.enhanced(options.enhanced);
this.io = new Apple2IO(this.cpu, this.vm);
- this.multiScreen = options.multiScreen;
this.tick = options.tick;
if (options.e) {
@@ -129,11 +145,9 @@ export class Apple2 implements Restorable {
this.mmu.resetVB();
}
if (this.io.annunciator(0)) {
- if (this.multiScreen) {
- this.vm.blit();
- }
- if (this.io.blit()) {
- this.stats.renderedFrames++;
+ const imageData = this.io.blit();
+ if (imageData) {
+ this.vm.blit(imageData);
}
} else {
if (this.vm.blit()) {
diff --git a/js/apple2io.ts b/js/apple2io.ts
index 278b8bb..c0285c6 100644
--- a/js/apple2io.ts
+++ b/js/apple2io.ts
@@ -10,7 +10,7 @@
*/
import CPU6502, { PageHandler } from './cpu6502';
-import { byte } from './types';
+import { Card, Memory, TapeData, byte } from './types';
import { debug } from './util';
type slot = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
@@ -66,8 +66,8 @@ const LOC = {
};
export default class Apple2IO implements PageHandler {
- private _slot: any[] = []; // TODO(flan): Needs typing.
- private _auxRom: any = null; // TODO(flan): Needs typing.
+ private _slot: Card[] = [];
+ private _auxRom: Memory | null = null;
private _khz = 1023;
private _rate = 44000;
@@ -94,9 +94,9 @@ export default class Apple2IO implements PageHandler {
private _trigger = 0;
private _annunciators: Annunciators = [false, false, false, false];
- private _tape = [];
+ private _tape: TapeData = [];
private _tapeOffset = 0;
- private _tapeNext = 0;
+ private _tapeNext: number = 0;
private _tapeCurrent = false;
constructor(private readonly cpu: CPU6502, private readonly vm: any) {
@@ -107,7 +107,7 @@ export default class Apple2IO implements PageHandler {
this._calcSampleRate();
}
- _debug(..._args: any) {
+ _debug(..._args: any[]) {
// debug.apply(this, arguments);
}
@@ -318,8 +318,8 @@ export default class Apple2IO implements PageHandler {
reset() {
for (let slot = 0; slot < 8; slot++) {
const card = this._slot[slot];
- if (card && card.reset) {
- card.reset();
+ if (card) {
+ card.reset?.();
}
}
this.vm.reset();
@@ -327,20 +327,20 @@ export default class Apple2IO implements PageHandler {
blit() {
const card = this._slot[3];
- if (card && card.blit) {
- return card.blit();
+ if (card) {
+ return card.blit?.();
}
- return false;
+ return undefined;
}
read(page: byte, off: byte) {
- let result = 0;
+ let result: number = 0;
let slot;
let card;
switch (page) {
case 0xc0:
- result = this.ioSwitch(off, undefined);
+ result = this.ioSwitch(off, undefined) || 0;
break;
case 0xc1:
case 0xc2:
@@ -411,7 +411,7 @@ export default class Apple2IO implements PageHandler {
this._annunciators = state.annunciators;
}
- setSlot(slot: slot, card: byte) {
+ setSlot(slot: slot, card: Card) {
this._slot[slot] = card;
}
@@ -453,7 +453,7 @@ export default class Apple2IO implements PageHandler {
}
}
- setTape(tape: any) { // TODO(flan): Needs typing.
+ setTape(tape: TapeData) { // TODO(flan): Needs typing.
debug('Tape length: ' + tape.length);
this._tape = tape;
this._tapeOffset = -1;
@@ -470,8 +470,8 @@ export default class Apple2IO implements PageHandler {
tick() {
this._tick();
for (let idx = 0; idx < 8; idx++) {
- if (this._slot[idx] && this._slot[idx].tick) {
- this._slot[idx].tick();
+ if (this._slot[idx]) {
+ this._slot[idx].tick?.();
}
}
}
diff --git a/js/canvas.ts b/js/canvas.ts
index 5c5763b..3a3994d 100644
--- a/js/canvas.ts
+++ b/js/canvas.ts
@@ -10,11 +10,21 @@
*/
import { base64_decode, base64_encode } from './base64';
-import { byte, memory, Memory, Restorable } from './types';
+import { byte, memory, Memory } from './types';
import { allocMemPages } from './util';
+import {
+ Color,
+ GraphicsState,
+ HiresPage,
+ LoresPage,
+ Region,
+ VideoModes,
+ VideoModesState,
+ bank,
+ pageNo
+} from './videomodes';
let enhanced = false;
-let multiScreen = false;
let textMode = true;
let mixedMode = false;
let hiresMode = false;
@@ -30,41 +40,6 @@ let highColorHGRMode = false;
let highColorTextMode = false;
let oneSixtyMode = false;
-type bank = 0 | 1;
-type pageNo = 1 | 2;
-
-interface Color {
- 0: byte, // red
- 1: byte, // green
- 2: byte, // blue
-}
-
-interface Region {
- top: number,
- bottom: number,
- left: number,
- right: number,
-}
-
-
-interface GraphicsState {
- page: byte;
- mono: boolean;
- buffer: string[];
-}
-
-interface VideoModesState {
- grs: [gr1: GraphicsState, _gr2: GraphicsState],
- hgrs: [hgr1: GraphicsState, hgr2: GraphicsState],
- textMode: boolean,
- mixedMode: boolean,
- hiresMode: boolean,
- pageMode: pageNo,
- _80colMode: boolean,
- altCharMode: boolean,
- an3: boolean,
-}
-
function dim(c: Color): Color {
return [
c[0] * 0.75 & 0xff,
@@ -151,6 +126,12 @@ const dcolors: Color[] = [
[255, 255, 255], // 0xf white
];
+const notDirty: Region = {
+ top: 385,
+ bottom: -1,
+ left: 561,
+ right: -1
+};
/****************************************************************************
*
@@ -158,40 +139,31 @@ const dcolors: Color[] = [
*
***************************************************************************/
-export class LoresPage implements Memory, Restorable {
+export class LoresPage2D implements LoresPage {
// $00-$3F inverse
// $40-$7F flashing
// $80-$FF normal
- private _imageData: { data: number[]; };
private _buffer: memory[] = [];
private _refreshing = false;
private _monoMode = false;
private _blink = false;
- private _dirty: Region = {
- top: 385,
- bottom: -1,
- left: 561,
- right: -1
- };
+
+ dirty: Region = {...notDirty}
+ imageData: ImageData;
constructor(private page: number,
- private readonly charset: any,
- private readonly e: any,
- private readonly context: any) {
- this._init();
- }
-
- _init() {
- this._imageData = this.context.createImageData(560, 384);
+ private readonly charset: memory,
+ private readonly e: boolean) {
+ this.imageData = new ImageData(560, 384);
for (let idx = 0; idx < 560 * 384 * 4; idx++) {
- this._imageData.data[idx] = 0xff;
+ this.imageData.data[idx] = 0xff;
}
this._buffer[0] = allocMemPages(0x4);
this._buffer[1] = allocMemPages(0x4);
}
- _drawPixel(data: number[], off: number, color: Color) {
+ _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
data[off + 0] = data[off + 4] = c0;
data[off + 1] = data[off + 5] = c1;
@@ -202,7 +174,7 @@ export class LoresPage implements Memory, Restorable {
data[nextOff + 2] = data[nextOff + 6] = c2;
}
- _drawHalfPixel(data: number[], off: number, color: Color) {
+ _drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
data[off + 0] = c0;
data[off + 1] = c1;
@@ -263,16 +235,16 @@ export class LoresPage implements Memory, Restorable {
const ee = adj >> 7;
const row = ab | cd | ee;
- const data = this._imageData.data;
+ const data = this.imageData.data;
if ((row < 24) && (col < 40)) {
let y = row << 4;
- if (y < this._dirty.top) { this._dirty.top = y; }
+ if (y < this.dirty.top) { this.dirty.top = y; }
y += 16;
- if (y > this._dirty.bottom) { this._dirty.bottom = y; }
+ if (y > this.dirty.bottom) { this.dirty.bottom = y; }
let x = col * 14;
- if (x < this._dirty.left) { this._dirty.left = x; }
+ if (x < this.dirty.left) { this.dirty.left = x; }
x += 14;
- if (x > this._dirty.right) { this._dirty.right = x; }
+ if (x > this.dirty.right) { this.dirty.right = x; }
let color;
if (textMode || hiresMode || (mixedMode && row > 19)) {
@@ -474,29 +446,6 @@ export class LoresPage implements Memory, Restorable {
this.refresh();
}
- blit(mixed: boolean = false): boolean {
- if (this._dirty.top === 385) { return false; }
- let top = this._dirty.top;
- const bottom = this._dirty.bottom;
- const left = this._dirty.left;
- const right = this._dirty.right;
-
- if (mixed) {
- if (bottom < 320) { return false; }
- if (top < 320) { top = 320; }
- }
- this.context.putImageData(
- this._imageData, 0, 0, left, top, right - left, bottom - top
- );
- this._dirty = {
- top: 385,
- bottom: -1,
- left: 561,
- right: -1
- };
- return true;
- }
-
start() {
setInterval(() => this.blink(), 267);
return this._start();
@@ -574,14 +523,9 @@ export class LoresPage implements Memory, Restorable {
*
***************************************************************************/
-export class HiresPage implements Memory, Restorable {
- private _imageData: { data: number[]; };
- private _dirty: Region = {
- top: 385,
- bottom: -1,
- left: 561,
- right: -1
- };
+export class HiresPage2D implements HiresPage {
+ public imageData: ImageData;
+ dirty: Region = {...notDirty}
private _buffer: memory[] = [];
private _refreshing = false;
@@ -589,21 +533,16 @@ export class HiresPage implements Memory, Restorable {
private _monoMode = false;
constructor(
- private page: number,
- private readonly context: any) {
- this._init();
- }
-
- _init() {
- this._imageData = this.context.createImageData(560, 384);
+ private page: number) {
+ this.imageData = new ImageData(560, 384);
for (let idx = 0; idx < 560 * 384 * 4; idx++) {
- this._imageData.data[idx] = 0xff;
+ this.imageData.data[idx] = 0xff;
}
this._buffer[0] = allocMemPages(0x20);
this._buffer[1] = allocMemPages(0x20);
}
- _drawPixel(data: number[], off: number, color: Color) {
+ _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
data[off + 0] = data[off + 4] = c0;
@@ -615,7 +554,7 @@ export class HiresPage implements Memory, Restorable {
data[nextOff + 2] = data[nextOff + 6] = c2;
}
- _drawHalfPixel(data: number[], off: number, color: Color) {
+ _drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
data[off + 0] = c0;
data[off + 1] = c1;
@@ -630,7 +569,7 @@ export class HiresPage implements Memory, Restorable {
// 160x192 pixels alternate 3 and 4 base pixels wide
//
- _draw3Pixel(data: number[], off: number, color: Color) {
+ _draw3Pixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
data[off + 0] = data[off + 4] = data[off + 8] = c0;
@@ -642,7 +581,7 @@ export class HiresPage implements Memory, Restorable {
data[nextOff + 2] = data[nextOff + 6] = data[nextOff + 10] = c2;
}
- _draw4Pixel(data: number[], off: number, color: Color) {
+ _draw4Pixel(data: Uint8ClampedArray, off: number, color: Color) {
const c0 = color[0], c1 = color[1], c2 = color[2];
data[off + 0] = data[off + 4] = data[off + 8] = data[off + 12] = c0;
@@ -703,21 +642,21 @@ export class HiresPage implements Memory, Restorable {
const rowa = ab | cd | e,
rowb = base >> 10;
- const data = this._imageData.data;
+ const data = this.imageData.data;
let dx, dy;
if ((rowa < 24) && (col < 40)) {
- if (!multiScreen && !hiresMode) {
+ if (!hiresMode) {
return;
}
let y = rowa << 4 | rowb << 1;
- if (y < this._dirty.top) { this._dirty.top = y; }
+ if (y < this.dirty.top) { this.dirty.top = y; }
y += 2;
- if (y > this._dirty.bottom) { this._dirty.bottom = y; }
+ if (y > this.dirty.bottom) { this.dirty.bottom = y; }
let x = col * 14 - 2;
- if (x < this._dirty.left) { this._dirty.left = x; }
+ if (x < this.dirty.left) { this.dirty.left = x; }
x += 18;
- if (x > this._dirty.right) { this._dirty.right = x; }
+ if (x > this.dirty.right) { this.dirty.right = x; }
dy = rowa << 4 | rowb << 1;
let bz, b0, b1, b2, b3, b4, c, hb;
@@ -919,29 +858,6 @@ export class HiresPage implements Memory, Restorable {
this.refresh();
}
- blit(mixed: boolean = false) {
- if (this._dirty.top === 385) { return false; }
- const top = this._dirty.top;
- let bottom = this._dirty.bottom;
- const left = this._dirty.left;
- const right = this._dirty.right;
-
- if (mixed) {
- if (top > 320) { return false; }
- if (bottom > 320) { bottom = 320; }
- }
- this.context.putImageData(
- this._imageData, 0, 0, left, top, right - left, bottom - top
- );
- this._dirty = {
- top: 385,
- bottom: -1,
- left: 561,
- right: -1
- };
- return true;
- }
-
start() {
return this._start();
}
@@ -979,19 +895,28 @@ export class HiresPage implements Memory, Restorable {
}
}
-export class VideoModes implements Restorable {
+export class VideoModes2D implements VideoModes {
private _grs: LoresPage[];
private _hgrs: HiresPage[];
private _flag = 0;
+ private _context: CanvasRenderingContext2D | null;
+ private _left: number;
+ private _top: number;
+
+ public ready = Promise.resolve();
constructor(
gr: LoresPage,
hgr: HiresPage,
gr2: LoresPage,
hgr2: HiresPage,
+ private canvas: HTMLCanvasElement,
private e: boolean) {
this._grs = [gr, gr2];
this._hgrs = [hgr, hgr2];
+ this._context = this.canvas.getContext('2d');
+ this._left = (this.canvas.width - 560) / 2;
+ this._top = (this.canvas.height - 384) / 2;
}
private _refresh() {
@@ -1108,10 +1033,6 @@ export class VideoModes implements Restorable {
enhanced = on;
}
- multiScreen(on: boolean) {
- multiScreen = on;
- }
-
isText() {
return textMode;
}
@@ -1140,25 +1061,56 @@ export class VideoModes implements Restorable {
return altCharMode;
}
- blit() {
- let blitted = false;
- if (multiScreen) {
- blitted = this._grs[0].blit() || blitted;
- blitted = this._grs[1].blit() || blitted;
- blitted = this._hgrs[0].blit() || blitted;
- blitted = this._hgrs[1].blit() || blitted;
- } else {
- if (hiresMode && !textMode) {
- if (mixedMode) {
- blitted = this._grs[pageMode - 1].blit(true) || blitted;
- blitted = this._hgrs[pageMode - 1].blit(true) || blitted;
- } else {
- blitted = this._hgrs[pageMode - 1].blit();
- }
- } else {
- blitted = this._grs[pageMode - 1].blit();
- }
+ updateImage(
+ mainData: ImageData,
+ mainDirty: Region,
+ mixData?: ImageData | null,
+ mixDirty?: Region | null
+ ) {
+ if (!this._context) {
+ throw new Error('No 2D context');
}
+ if (mainDirty.bottom !== -1 || (mixDirty && mixDirty.bottom !== -1)) {
+ if (mixData) {
+ this._context.putImageData(
+ mainData, this._left, this._top, 0, 0, 560, 320
+ );
+ this._context.putImageData(
+ mixData, this._left, this._top, 0, 320, 560, 64
+ );
+ } else {
+ this._context.putImageData(mainData, this._top, this._left);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ blit(altData?: ImageData) {
+ let blitted = false;
+ const hgr = this._hgrs[pageMode - 1];
+ const gr = this._grs[pageMode - 1];
+
+ if (altData) {
+ blitted = this.updateImage(
+ altData,
+ {top: 0, left: 0, right: 560, bottom: 384}
+ );
+ } else if (hiresMode && !textMode) {
+ blitted = this.updateImage(
+ hgr.imageData,
+ hgr.dirty,
+ mixedMode ? gr.imageData : null,
+ mixedMode ? gr.dirty : null
+ );
+ } else {
+ blitted = this.updateImage(
+ gr.imageData, gr.dirty
+ );
+ }
+ hgr.dirty = {...notDirty};
+ gr.dirty = {...notDirty};
return blitted;
}
@@ -1197,6 +1149,7 @@ export class VideoModes implements Restorable {
this._hgrs[0].mono(on);
this._hgrs[1].mono(on);
}
+
getText() {
return this._grs[pageMode - 1].getText();
}
diff --git a/js/cards/videoterm.js b/js/cards/videoterm.js
index 5ecbd03..43460b6 100644
--- a/js/cards/videoterm.js
+++ b/js/cards/videoterm.js
@@ -12,7 +12,7 @@
import { allocMemPages, debug } from '../util';
import { ROM, VIDEO_ROM } from '../roms/cards/videoterm';
-export default function Videoterm(io, context) {
+export default function Videoterm(_io) {
debug('Videx Videoterm');
var LOC = {
@@ -78,7 +78,7 @@ export default function Videoterm(io, context) {
function _init() {
var idx;
- _imageData = context.createImageData(560, 384);
+ _imageData = new ImageData(560, 384);
for (idx = 0; idx < 560 * 384 * 4; idx++) {
_imageData.data[idx] = 0xff;
}
@@ -252,11 +252,10 @@ export default function Videoterm(io, context) {
_shouldRefresh = false;
}
if (_dirty) {
- context.putImageData(_imageData, 0, 0);
_dirty = false;
- return true;
+ return _imageData;
}
- return false;
+ return;
}
};
}
diff --git a/js/gl.ts b/js/gl.ts
new file mode 100644
index 0000000..b1045ba
--- /dev/null
+++ b/js/gl.ts
@@ -0,0 +1,942 @@
+/* Copyright 2010-2021 Will Scullin
+ *
+ * Permission to use, copy, modify, distribute, and sell this software and its
+ * documentation for any purpose is hereby granted without fee, provided that
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation. No representations are made about the suitability of this
+ * software for any purpose. It is provided "as is" without express or
+ * implied warranty.
+ */
+
+import { base64_decode, base64_encode } from './base64';
+import { byte, memory, Memory, Restorable } from './types';
+import { allocMemPages } from './util';
+
+import { screenEmu } from 'apple2shader';
+import {
+ Color,
+ GraphicsState,
+ HiresPage,
+ LoresPage,
+ Region,
+ VideoModes,
+ VideoModesState,
+ bank,
+ pageNo
+} from './videomodes';
+
+let enhanced = false;
+let textMode = true;
+let mixedMode = false;
+let hiresMode = false;
+let pageMode: pageNo = 1;
+let _80colMode = false;
+let altCharMode = false;
+let an3 = false;
+let doubleHiresMode = false;
+
+const tmpCanvas = document.createElement('canvas');
+const tmpContext = tmpCanvas.getContext('2d');
+
+const buildScreen = (mainData: ImageData, mixData?: ImageData | null) => {
+ if (!tmpContext) {
+ throw new Error('No webgl context');
+ }
+
+ const details = screenEmu.C.NTSC_DETAILS;
+ const { width, height } = details.imageSize;
+ const { x, y } = _80colMode ? details.topLeft80Col : details.topLeft;
+
+ tmpCanvas.width = width;
+ tmpCanvas.height = height;
+ tmpContext.fillStyle = 'rgba(0,0,0,1)';
+ tmpContext.fillRect(0, 0, width, height);
+
+ if (mixData) {
+ tmpContext.putImageData(mainData, x, y, 0, 0, 560, 160);
+ tmpContext.putImageData(mixData, x, y, 0, 160, 560, 32);
+ } else {
+ tmpContext.putImageData(mainData, x, y);
+ }
+ return tmpContext.getImageData(0, 0, width, height);
+};
+
+// Color constants
+const whiteCol: Color = [255, 255, 255];
+const blackCol: Color = [0, 0, 0];
+
+const notDirty: Region = {
+ top: 385,
+ bottom: -1,
+ left: 561,
+ right: -1
+};
+
+/****************************************************************************
+ *
+ * Text/Lores Graphics
+ *
+ ***************************************************************************/
+
+export class LoresPageGL implements LoresPage {
+ // $00-$3F inverse
+ // $40-$7F flashing
+ // $80-$FF normal
+
+ private _buffer: memory[] = [];
+ private _monoMode = false;
+ private _refreshing = false;
+ private _blink = false;
+
+ dirty: Region = {...notDirty}
+ imageData: ImageData;
+
+ constructor(private page: number,
+ private readonly charset: memory,
+ private readonly e: boolean) {
+ this.imageData = new ImageData(560, 192);
+ for (let idx = 0; idx < 560 * 192 * 4; idx++) {
+ this.imageData.data[idx] = 0xff;
+ }
+ this._buffer[0] = allocMemPages(0x4);
+ this._buffer[1] = allocMemPages(0x4);
+ }
+
+ _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
+ const c0 = color[0], c1 = color[1], c2 = color[2];
+ data[off + 0] = data[off + 4] = c0;
+ data[off + 1] = data[off + 5] = c1;
+ data[off + 2] = data[off + 6] = c2;
+ }
+
+ _drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
+ const c0 = color[0], c1 = color[1], c2 = color[2];
+ data[off + 0] = c0;
+ data[off + 1] = c1;
+ data[off + 2] = c2;
+ }
+
+ bank0(): Memory {
+ return {
+ start: () => this._start(),
+ end: () => this._end(),
+ read: (page, off) => this._read(page, off, 0),
+ write: (page, off, val) => this._write(page, off, val, 0),
+ };
+ }
+
+ bank1(): Memory {
+ return {
+ start: () => this._start(),
+ end: () => this._end(),
+ read: (page, off) => this._read(page, off, 1),
+ write: (page, off, val) => this._write(page, off, val, 1),
+ };
+ }
+
+ // These are used by both bank 0 and 1
+
+ private _start() {
+ return (0x04 * this.page);
+ }
+
+ private _end() { return (0x04 * this.page) + 0x03; }
+
+ private _read(page: byte, off: byte, bank: bank) {
+ const addr = (page << 8) | off, base = addr & 0x3FF;
+ return this._buffer[bank][base];
+ }
+
+ private _write(page: byte, off: byte, val: byte, bank: bank) {
+ const addr = (page << 8) | off;
+ const base = addr & 0x3FF;
+ let fore, back;
+
+ if (this._buffer[bank][base] == val && !this._refreshing) {
+ return;
+ }
+ this._buffer[bank][base] = val;
+
+ const col = (base % 0x80) % 0x28;
+ const adj = off - col;
+
+ // 000001cd eabab000 -> 000abcde
+ const ab = (adj & 0x18);
+ const cd = (page & 0x03) << 1;
+ const ee = adj >> 7;
+ const row = ab | cd | ee;
+
+ const data = this.imageData.data;
+ if ((row < 24) && (col < 40)) {
+ let y = row << 3;
+ if (y < this.dirty.top) { this.dirty.top = y; }
+ y += 8;
+ if (y > this.dirty.bottom) { this.dirty.bottom = y; }
+ let x = col * 14;
+ if (x < this.dirty.left) { this.dirty.left = x; }
+ x += 14;
+ if (x > this.dirty.right) { this.dirty.right = x; }
+
+ if (textMode || hiresMode || (mixedMode && row > 19)) {
+ let inverse;
+ if (this.e) {
+ if (!_80colMode && !altCharMode) {
+ inverse = ((val & 0xc0) == 0x40) && this._blink;
+ }
+ } else {
+ inverse = !((val & 0x80) || (val & 0x40) && this._blink);
+ }
+
+ fore = inverse ? blackCol : whiteCol;
+ back = inverse ? whiteCol : blackCol;
+
+ if (_80colMode) {
+ if (!enhanced) {
+ val = (val >= 0x40 && val < 0x60) ? val - 0x40 : val;
+ } else if (!altCharMode) {
+ val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
+ }
+
+ let offset = (col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
+
+ for (let jdx = 0; jdx < 8; jdx++) {
+ let b = this.charset[val * 8 + jdx];
+ for (let idx = 0; idx < 7; idx++) {
+ const color = (b & 0x01) ? back : fore;
+ this._drawHalfPixel(data, offset, color);
+ b >>= 1;
+ offset += 4;
+ }
+ offset += 553 * 4;
+ }
+ } else {
+ val = this._buffer[0][base];
+
+ if (!enhanced) {
+ val = (val >= 0x40 && val < 0x60) ? val - 0x40 : val;
+ } else if (!altCharMode) {
+ val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
+ }
+
+ let offset = (col * 14 + row * 560 * 8) * 4;
+
+ if (this.e) {
+ for (let jdx = 0; jdx < 8; jdx++) {
+ let b = this.charset[val * 8 + jdx];
+ for (let idx = 0; idx < 7; idx++) {
+ const color = (b & 0x01) ? back : fore;
+ this._drawPixel(data, offset, color);
+ b >>= 1;
+ offset += 8;
+ }
+ offset += 546 * 4;
+ }
+ } else {
+ for (let jdx = 0; jdx < 8; jdx++) {
+ let b = this.charset[val * 8 + jdx] << 1;
+
+ for (let idx = 0; idx < 7; idx++) {
+ const color = (b & 0x80) ? fore : back;
+ this._drawPixel(data, offset, color);
+ b <<= 1;
+ offset += 8;
+ }
+ offset += 546 * 4;
+ }
+ }
+ }
+ } else {
+ if (!_80colMode && bank == 1) {
+ return;
+ }
+ if (_80colMode && !an3) {
+ let offset = (col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
+ for (let jdx = 0; jdx < 8; jdx++) {
+ let b = (jdx < 4) ? (val & 0x0f) : (val >> 4);
+ b |= (b << 4);
+ b |= (b << 8);
+ if (col & 0x1) {
+ b >>= 2;
+ }
+ for (let idx = 0; idx < 7; idx++) {
+ const color = (b & 0x01) ? whiteCol : blackCol;
+ this._drawHalfPixel(data, offset, color);
+ b >>= 1;
+ offset += 4;
+ }
+ offset += 553 * 4;
+ }
+ } else {
+ let offset = (col * 14 + row * 560 * 8) * 4;
+ for (let jdx = 0; jdx < 8; jdx++) {
+ let b = (jdx < 4) ? (val & 0x0f) : (val >> 4);
+ b |= (b << 4);
+ b |= (b << 8);
+ if (col & 0x1) {
+ b >>= 2;
+ }
+ for (let idx = 0; idx < 14; idx++) {
+ const color = (b & 0x0001) ? whiteCol : blackCol;
+ this._drawHalfPixel(data, offset, color);
+ b >>= 1;
+ offset += 4;
+ }
+ offset += 546 * 4;
+ }
+ }
+ }
+ }
+ }
+
+ refresh() {
+ let addr = 0x400 * this.page;
+ this._refreshing = true;
+ for (let idx = 0; idx < 0x400; idx++, addr++) {
+ this._write(addr >> 8, addr & 0xff, this._buffer[0][idx], 0);
+ if (_80colMode) {
+ this._write(addr >> 8, addr & 0xff, this._buffer[1][idx], 1);
+ }
+ }
+ this._refreshing = false;
+ }
+
+ mono(on: boolean) {
+ this._monoMode = on;
+ }
+
+ blink() {
+ let addr = 0x400 * this.page;
+ this._refreshing = true;
+ this._blink = !this._blink;
+ for (let idx = 0; idx < 0x400; idx++, addr++) {
+ const b = this._buffer[0][idx];
+ if ((b & 0xC0) == 0x40) {
+ this._write(addr >> 8, addr & 0xff, this._buffer[0][idx], 0);
+ }
+ }
+ this._refreshing = false;
+ }
+
+ start() {
+ setInterval(() => this.blink(), 267);
+ return this._start();
+ }
+
+ end() {
+ return this._end();
+ }
+
+ read(page: byte, off: byte) {
+ return this._read(page, off, 0);
+ }
+
+ write(page: byte, off: byte, val: byte) {
+ return this._write(page, off, val, 0);
+ }
+
+ getState(): GraphicsState {
+ return {
+ page: this.page,
+ mono: this._monoMode,
+ buffer: [
+ base64_encode(this._buffer[0]),
+ base64_encode(this._buffer[1])
+ ]
+ };
+ }
+
+ setState(state: GraphicsState) {
+ this.page = state.page;
+ this._buffer[0] = base64_decode(state.buffer[0]);
+ this._buffer[1] = base64_decode(state.buffer[1]);
+
+ this.refresh();
+ }
+
+ private rowToBase(row: number) {
+ const ab = (row >> 3) & 3;
+ const cd = (row >> 1) & 0x3;
+ const e = row & 1;
+ return (cd << 8) | (e << 7) | (ab << 5) | (ab << 3);
+ }
+
+ private mapCharCode(charCode: byte) {
+ charCode &= 0x7F;
+ if (charCode < 0x20) {
+ charCode += 0x40;
+ }
+ if (!this.e && (charCode >= 0x60)) {
+ charCode -= 0x40;
+ }
+ return charCode;
+ }
+
+ getText() {
+ let buffer = '', line, charCode;
+ let row, col, base;
+ for (row = 0; row < 24; row++) {
+ base = this.rowToBase(row);
+ line = '';
+ if (this.e && _80colMode) {
+ for (col = 0; col < 80; col++) {
+ charCode = this.mapCharCode(this._buffer[1 - col % 2][base + Math.floor(col / 2)]);
+ line += String.fromCharCode(charCode);
+ }
+ } else {
+ for (col = 0; col < 40; col++) {
+ charCode = this.mapCharCode(this._buffer[0][base + col]);
+ line += String.fromCharCode(charCode);
+ }
+ }
+ line = line.trimRight();
+ buffer += line + '\n';
+ }
+ return buffer;
+ }
+}
+
+/****************************************************************************
+ *
+ * Hires Graphics
+ *
+ ***************************************************************************/
+
+export class HiresPageGL implements Memory, Restorable {
+
+ private _buffer: memory[] = [];
+ private _refreshing = false;
+ private _monoMode = false;
+
+ dirty: Region = {
+ top: 193,
+ bottom: -1,
+ left: 561,
+ right: -1
+ };
+ imageData: ImageData;
+
+ constructor(
+ private page: number) {
+ this.imageData = new ImageData(560, 192);
+ for (let idx = 0; idx < 560 * 192 * 4; idx++) {
+ this.imageData.data[idx] = 0xff;
+ }
+ this._buffer[0] = allocMemPages(0x20);
+ this._buffer[1] = allocMemPages(0x20);
+ }
+
+ _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
+ const c0 = color[0], c1 = color[1], c2 = color[2];
+
+ data[off + 0] = data[off + 4] = c0;
+ data[off + 1] = data[off + 5] = c1;
+ data[off + 2] = data[off + 6] = c2;
+ }
+
+ _drawHalfPixel(data: Uint8ClampedArray, off: number, color: Color) {
+ const c0 = color[0], c1 = color[1], c2 = color[2];
+
+ data[off + 0] = c0;
+ data[off + 1] = c1;
+ data[off + 2] = c2;
+ }
+
+ bank0(): Memory {
+ return {
+ start: () => this._start(),
+ end: () => this._end(),
+ read: (page, off) => this._read(page, off, 0),
+ write: (page, off, val) => this._write(page, off, val, 0),
+ };
+ }
+
+ bank1(): Memory {
+ return {
+ start: () => this._start(),
+ end: () => this._end(),
+ read: (page, off) => this._read(page, off, 1),
+ write: (page, off, val) => this._write(page, off, val, 1),
+ };
+ }
+
+ _start() { return (0x20 * this.page); }
+
+ _end() { return (0x020 * this.page) + 0x1f; }
+
+ _read(page: byte, off: byte, bank: bank) {
+ const addr = (page << 8) | off, base = addr & 0x1FFF;
+ return this._buffer[bank][base];
+ }
+
+ _write(page: byte, off: byte, val: byte, bank: bank) {
+ const addr = (page << 8) | off;
+ const base = addr & 0x1FFF;
+
+ if (this._buffer[bank][base] == val && !this._refreshing) {
+ return;
+ }
+ this._buffer[bank][base] = val;
+
+ // let hbs = val & 0x80;
+
+ const col = (base % 0x80) % 0x28;
+ const adj = off - col;
+
+ // 000001cd eabab000 -> 000abcde
+ const ab = (adj & 0x18);
+ const cd = (page & 0x03) << 1;
+ const e = adj >> 7;
+
+ const rowa = ab | cd | e,
+ rowb = base >> 10;
+
+ const data = this.imageData.data;
+ let dx, dy;
+ if ((rowa < 24) && (col < 40)) {
+ if (!hiresMode) {
+ return;
+ }
+
+ let y = rowa << 3 | rowb;
+ if (y < this.dirty.top) { this.dirty.top = y; }
+ y += 1;
+ if (y > this.dirty.bottom) { this.dirty.bottom = y; }
+ let x = col * 14 - 2;
+ if (x < this.dirty.left) { this.dirty.left = x; }
+ x += 18;
+ if (x > this.dirty.right) { this.dirty.right = x; }
+
+ dy = rowa << 3 | rowb;
+ let bz, b0, b1, b2, b3, b4, c, hb;
+ if (doubleHiresMode) {
+ val &= 0x7f;
+
+ // Every 4 bytes is 7 pixels
+ // 2 bytes per bank
+
+ // b0 b1 b2 b3
+ // c0 c1 c2 c3 c4 c5 c6
+ // 76543210 76543210 76543210 76543210
+ // 1111222 2333344 4455556 6667777
+
+ const mod = col % 2, mcol = col - mod, baseOff = base - mod;
+ bz = this._buffer[0][baseOff - 1];
+ b0 = this._buffer[1][baseOff];
+ b1 = this._buffer[0][baseOff];
+ b2 = this._buffer[1][baseOff + 1];
+ b3 = this._buffer[0][baseOff + 1];
+ b4 = this._buffer[1][baseOff + 2];
+ c = [
+ 0,
+ ((b0 & 0x0f) >> 0), // 0
+ ((b0 & 0x70) >> 4) | ((b1 & 0x01) << 3), // 1
+ ((b1 & 0x1e) >> 1), // 2
+ ((b1 & 0x60) >> 5) | ((b2 & 0x03) << 2), // 3
+ ((b2 & 0x3c) >> 2), // 4
+ ((b2 & 0x40) >> 6) | ((b3 & 0x07) << 1), // 5
+ ((b3 & 0x78) >> 3), // 6
+ 0
+ ], // 7
+ hb = [
+ 0,
+ b0 & 0x80, // 0
+ b0 & 0x80, // 1
+ b1 & 0x80, // 2
+ b2 & 0x80, // 3
+ b2 & 0x80, // 4
+ b3 & 0x80, // 5
+ b3 & 0x80, // 6
+ 0
+ ]; // 7
+ if (col > 0) {
+ c[0] = (bz & 0x78) >> 3;
+ hb[0] = bz & 0x80;
+ }
+ if (col < 39) {
+ c[8] = b4 & 0x0f;
+ hb[8] = b4 & 0x80;
+ }
+ dx = mcol * 14;
+ let offset = dx * 4 + dy * 280 * 4 * 2;
+
+ for (let idx = 1; idx < 8; idx++) {
+ // hbs = hb[idx];
+ let bits = c[idx - 1] | (c[idx] << 4) | (c[idx + 1] << 8);
+ for (let jdx = 0; jdx < 4; jdx++, offset += 4) {
+ if (bits & 0x10) {
+ this._drawHalfPixel(data, offset, whiteCol);
+ } else {
+ this._drawHalfPixel(data, offset, blackCol);
+ }
+ bits >>= 1;
+ }
+ }
+
+ if (!this._refreshing) {
+ this._refreshing = true;
+ const bb: bank = bank ? 0 : 1;
+ for (let rr = addr - 1; rr <= addr + 1; rr++) {
+ const vv = this._buffer[bb][rr - 0x2000 * this.page];
+ this._write(rr >> 8, rr & 0xff, vv, bb);
+ }
+ this._refreshing = false;
+ }
+ } else {
+ val = this._buffer[0][base];
+ const hbs = val & 0x80;
+ val &= 0x7f;
+ dx = col * 14 - 2;
+ b0 = col > 0 ? this._buffer[0][base - 1] : 0;
+ b2 = col < 39 ? this._buffer[0][base + 1] : 0;
+ val |= (b2 & 0x3) << 7;
+ let v1 = b0 & 0x40,
+ v2 = val & 0x1,
+ color;
+
+ let offset = dx * 4 + dy * 560 * 4 + (hbs ? 4 : 0);
+
+ for (let idx = 0; idx < 9; idx++, offset += 8) {
+ val >>= 1;
+
+ if (v1) {
+ color = whiteCol;
+ } else {
+ color = blackCol;
+ }
+
+ if (dx > -1 && dx < 560) {
+ this._drawPixel(data, offset, color);
+ }
+ dx += 2;
+
+ v1 = v2;
+ v2 = val & 0x01;
+ }
+ }
+ }
+ }
+
+ refresh() {
+ let addr = 0x2000 * this.page;
+ this._refreshing = true;
+ for (let idx = 0; idx < 0x2000; idx++, addr++) {
+ const page = addr >> 8;
+ const off = addr & 0xff;
+ this._write(page, off, this._buffer[0][idx], 0);
+ if (_80colMode) {
+ this._write(page, off, this._buffer[1][idx], 1);
+ }
+ }
+ this._refreshing = false;
+ }
+
+ mono(on: boolean) {
+ this._monoMode = on;
+ }
+
+ start() {
+ return this._start();
+ }
+
+ end() {
+ return this._end();
+ }
+
+ read(page: byte, off: byte) {
+ return this._read(page, off, 0);
+ }
+
+ write(page: byte, off: byte, val: byte) {
+ return this._write(page, off, val, 0);
+ }
+
+ getState(): GraphicsState {
+ return {
+ page: this.page,
+ mono: this._monoMode,
+ buffer: [
+ base64_encode(this._buffer[0]),
+ base64_encode(this._buffer[1])
+ ]
+ };
+ }
+
+ setState(state: GraphicsState) {
+ this.page = state.page;
+ this._buffer[0] = base64_decode(state.buffer[0]);
+ this._buffer[1] = base64_decode(state.buffer[1]);
+
+ this.refresh();
+ }
+}
+
+export class VideoModesGL implements VideoModes {
+ private _grs: LoresPage[];
+ private _hgrs: HiresPage[];
+ private _flag = 0;
+ private _sv: any;
+ private _displayConfig: any;
+ private _monoMode: boolean = false;
+
+ ready: Promise
+
+ constructor(
+ gr: LoresPage,
+ hgr: HiresPage,
+ gr2: LoresPage,
+ hgr2: HiresPage,
+ private canvas: HTMLCanvasElement,
+ private e: boolean) {
+ this._grs = [gr, gr2];
+ this._hgrs = [hgr, hgr2];
+ this._sv = new screenEmu.ScreenView(this.canvas);
+
+ this.ready = this.init();
+ }
+
+ async init() {
+ await this._sv.initOpenGL();
+
+ (window as any)._sv = this._sv;
+
+ this._displayConfig = new screenEmu.DisplayConfiguration();
+ this._displayConfig.displayResolution = new screenEmu.Size(this.canvas.width, this.canvas.height);
+ this._displayConfig.displayResolution = new screenEmu.Size(this.canvas.width, this.canvas.height);
+ this._displayConfig.displayScanlineLevel = 0.5;
+ this._displayConfig.videoWhiteOnly = true;
+ this._displayConfig.videoSaturation = 0.8;
+ this._displayConfig.videoSize = new screenEmu.Size(1.25, 1.15);
+ this._displayConfig.videoCenter = new screenEmu.Point(0.01, 0.02);
+ // this._displayConfig.videoDecoder = 'CANVAS_CXA2025AS';
+ this._sv.displayConfiguration = this._displayConfig;
+ }
+
+ private _refresh() {
+ doubleHiresMode = !an3 && hiresMode && _80colMode;
+
+ this._grs[0].refresh();
+ this._grs[1].refresh();
+ this._hgrs[0].refresh();
+ this._hgrs[1].refresh();
+
+ if (this._displayConfig) {
+ this._displayConfig.videoWhiteOnly = textMode || this._monoMode;
+ this._sv.displayConfiguration = this._displayConfig;
+ }
+ }
+
+ refresh() {
+ this._refresh();
+ }
+
+ reset() {
+ textMode = true;
+ mixedMode = false;
+ hiresMode = true;
+ pageMode = 1;
+
+ _80colMode = false;
+ altCharMode = false;
+
+ this._flag = 0;
+ an3 = true;
+
+ this._refresh();
+ }
+
+ text(on: boolean) {
+ const old = textMode;
+ textMode = on;
+
+ if (old != on) {
+ this._refresh();
+ }
+ }
+
+ _80col(on: boolean) {
+ if (!this.e) { return; }
+
+ const old = _80colMode;
+ _80colMode = on;
+
+ if (old != on) {
+ this._refresh();
+ }
+ }
+
+ altchar(on: boolean) {
+ if (!this.e) { return; }
+
+ const old = altCharMode;
+ altCharMode = on;
+ if (old != on) {
+ this._refresh();
+ }
+ }
+
+ hires(on: boolean) {
+ const old = hiresMode;
+ hiresMode = on;
+ if (!on) {
+ this._flag = 0;
+ }
+
+ if (old != on) {
+ this._refresh();
+ }
+ }
+
+ an3(on: boolean) {
+ if (!this.e) { return; }
+
+ const old = an3;
+ an3 = on;
+
+ if (on) {
+ this._flag = ((this._flag << 1) | (_80colMode ? 0x0 : 0x1)) & 0x3;
+ }
+
+ if (old != on) {
+ this._refresh();
+ }
+ }
+
+ doubleHires(on: boolean) {
+ this.an3(!on);
+ }
+
+ mixed(on: boolean) {
+ const old = mixedMode;
+ mixedMode = on;
+ if (old != on) {
+ this._refresh();
+ }
+ }
+
+ page(pageNo: pageNo) {
+ const old = pageMode;
+ pageMode = pageNo;
+ if (old != pageNo) {
+ this._refresh();
+ }
+ }
+
+ enhanced(on: boolean) {
+ enhanced = on;
+ }
+
+ isText() {
+ return textMode;
+ }
+
+ isMixed() {
+ return mixedMode;
+ }
+
+ isPage2() {
+ return pageMode == 2;
+ }
+
+ isHires() {
+ return hiresMode;
+ }
+
+ isDoubleHires() {
+ return doubleHiresMode;
+ }
+
+ is80Col() {
+ return _80colMode;
+ }
+
+ isAltChar() {
+ return altCharMode;
+ }
+
+ updateImage(
+ mainData: ImageData,
+ mainDirty: Region,
+ mixData?: ImageData | null,
+ mixDirty?: Region | null
+ ) {
+ let blitted = false;
+ if (mainDirty.bottom !== -1 || (mixDirty && mixDirty.bottom !== -1)) {
+ const imageData = buildScreen(mainData, mixData);
+ const imageInfo = new screenEmu.ImageInfo(imageData);
+ this._sv.image = imageInfo;
+ blitted = true;
+ }
+ this._sv.vsync();
+ return blitted;
+ }
+
+ blit(altData?: ImageData) {
+ let blitted = false;
+ const hgr = this._hgrs[pageMode - 1];
+ const gr = this._grs[pageMode - 1];
+
+ if (altData) {
+ blitted = this.updateImage(
+ altData,
+ { top: 0, left: 0, right: 560, bottom: 384 }
+ );
+ } else if (hiresMode && !textMode) {
+ blitted = this.updateImage(
+ hgr.imageData, hgr.dirty,
+ mixedMode ? gr.imageData : null, mixedMode ? gr.dirty : null,
+ );
+ } else {
+ blitted = this.updateImage(
+ gr.imageData, gr.dirty
+ );
+ }
+ hgr.dirty = {...notDirty};
+ gr.dirty = {...notDirty};
+
+ return blitted;
+ }
+
+ getState(): VideoModesState {
+ return {
+ grs: [this._grs[0].getState(), this._grs[1].getState()],
+ hgrs: [this._hgrs[0].getState(), this._hgrs[1].getState()],
+ textMode: textMode,
+ mixedMode: mixedMode,
+ hiresMode: hiresMode,
+ pageMode: pageMode,
+ _80colMode: _80colMode,
+ altCharMode: altCharMode,
+ an3: an3
+ };
+ }
+
+ setState(state: VideoModesState) {
+ textMode = state.textMode;
+ mixedMode = state.mixedMode;
+ hiresMode = state.hiresMode;
+ pageMode = state.pageMode;
+ _80colMode = state._80colMode;
+ altCharMode = state.altCharMode;
+ an3 = state.an3;
+
+ this._grs[0].setState(state.grs[0]);
+ this._grs[1].setState(state.grs[1]);
+ this._hgrs[0].setState(state.hgrs[0]);
+ this._hgrs[1].setState(state.hgrs[1]);
+ }
+
+ mono(on: boolean) {
+ this._grs[0].mono(on);
+ this._grs[1].mono(on);
+ this._hgrs[0].mono(on);
+ this._hgrs[1].mono(on);
+
+ this._monoMode = on;
+ this._refresh();
+ }
+
+ getText() {
+ return this._grs[pageMode - 1].getText();
+ }
+}
diff --git a/js/main2.js b/js/main2.js
index f859cbc..9c35c5d 100644
--- a/js/main2.js
+++ b/js/main2.js
@@ -59,8 +59,9 @@ switch (romVersion) {
}
var options = {
+ canvas: document.getElementById('screen'),
+ gl: prefs.readPref('gl_canvas', 'true') === 'true',
screen: [],
- multiScreen: false,
rom: rom,
characterRom: characterRom,
e: false,
@@ -69,28 +70,6 @@ var options = {
tick: updateUI
};
-var canvas1 = document.getElementById('screen');
-var canvas2 = document.getElementById('screen2');
-var canvas3 = document.getElementById('screen3');
-var canvas4 = document.getElementById('screen4');
-
-options.screen[0] = canvas1.getContext('2d');
-if (canvas4) {
- options.multiScreen = true;
- options.screen[1] = canvas2.getContext('2d');
- options.screen[2] = canvas3.getContext('2d');
- options.screen[3] = canvas4.getContext('2d');
-} else if (canvas2) {
- options.multiScreen = true;
- options.screen[1] = options.screen[0];
- options.screen[2] = canvas2.getContext('2d');
- options.screen[3] = options.screen[2];
-} else {
- options.screen[1] = options.screen[0];
- options.screen[2] = options.screen[0];
- options.screen[3] = options.screen[0];
-}
-
var apple2 = new Apple2(options);
var cpu = apple2.getCPU();
var io = apple2.getIO();
@@ -99,7 +78,7 @@ var printer = new Printer('#printer-modal .paper');
var lc = new LanguageCard(io, rom);
var parallel = new Parallel(io, printer);
-var videoTerm = new VideoTerm(io, options.screen[0]);
+var videoTerm = new VideoTerm(io);
var slinky = new RAMFactor(io, 1024 * 1024);
var disk2 = new DiskII(io, driveLights, sectors);
var clock = new Thunderclock(io);
diff --git a/js/main2e.js b/js/main2e.js
index 1ba64be..7288efa 100644
--- a/js/main2e.js
+++ b/js/main2e.js
@@ -42,8 +42,8 @@ switch (romVersion) {
}
var options = {
- screen: [],
- multiScreen: false,
+ gl: prefs.readPref('gl_canvas') === 'true',
+ canvas: document.getElementById('screen'),
rom: rom,
characterRom: characterRom,
e: true,
@@ -52,28 +52,6 @@ var options = {
tick: updateUI
};
-var canvas1 = document.getElementById('screen');
-var canvas2 = document.getElementById('screen2');
-var canvas3 = document.getElementById('screen3');
-var canvas4 = document.getElementById('screen4');
-
-options.screen[0] = canvas1.getContext('2d');
-if (canvas4) {
- options.multiScreen = true;
- options.screen[1] = canvas2.getContext('2d');
- options.screen[2] = canvas3.getContext('2d');
- options.screen[3] = canvas4.getContext('2d');
-} else if (canvas2) {
- options.multiScreen = true;
- options.screen[1] = options.screen[0];
- options.screen[2] = canvas2.getContext('2d');
- options.screen[3] = options.screen[2];
-} else {
- options.screen[1] = options.screen[0];
- options.screen[2] = options.screen[0];
- options.screen[3] = options.screen[0];
-}
-
var apple2 = new Apple2(options);
var io = apple2.getIO();
var cpu = apple2.getCPU();
diff --git a/js/mmu.ts b/js/mmu.ts
index 35dde07..fd15508 100644
--- a/js/mmu.ts
+++ b/js/mmu.ts
@@ -14,7 +14,7 @@ import RAM from './ram';
import { debug, toHex } from './util';
import { byte, Memory } from './types';
import Apple2IO from './apple2io';
-import { HiresPage, LoresPage, VideoModes } from './canvas';
+import { HiresPage, LoresPage, VideoModes } from './videomodes';
/*
* I/O Switch locations
diff --git a/js/prefs.ts b/js/prefs.ts
index c18ccf4..548ac83 100644
--- a/js/prefs.ts
+++ b/js/prefs.ts
@@ -16,13 +16,17 @@ export default class Prefs {
havePrefs() {
return havePrefs;
}
- readPref(name: string): string | null {
- if (havePrefs)
- return window.localStorage.getItem(name);
- return null;
+
+ readPref(name: string, defaultValue: string | null = null) {
+ if (havePrefs) {
+ return window.localStorage.getItem(name) ?? defaultValue;
+ }
+ return defaultValue;
}
+
writePref(name: string, value: string) {
- if (havePrefs)
+ if (havePrefs) {
window.localStorage.setItem(name, value);
+ }
}
}
diff --git a/js/types.ts b/js/types.ts
index 2388726..3add0d4 100644
--- a/js/types.ts
+++ b/js/types.ts
@@ -1,7 +1,7 @@
/**
* Extracts the members of a constant array as a type. Used as:
- *
+ *
* @example
* const SOME_VALUES = ['a', 'b', 1, 2] as const;
* type SomeValues = MemberOf; // 'a' | 'b' | 1 | 2
@@ -41,6 +41,21 @@ export interface Memory {
write(page: byte, offset: byte, value: byte): void;
}
+/* An interface card */
+export interface Card extends Memory {
+ /* Reset the card */
+ reset(): void;
+
+ /* Draw card to canvas */
+ blit?(): ImageData;
+
+ /* Process period events */
+ tick?(): void;
+
+ /* Read or Write an I/O switch */
+ ioSwitch(off: byte, val?: byte): byte | undefined;
+}
+
export const DISK_FORMATS = [
'2mg',
'd13',
@@ -70,6 +85,8 @@ export interface DiskIIDrive extends Drive {
dirty: boolean,
}
+export type TapeData = Array<[duration: number, high: boolean]>;
+
export interface Restorable {
getState(): T;
setState(state: T): void;
@@ -79,4 +96,4 @@ export interface Restorable {
export type TypedArrayMutableProperties = 'copyWithin' | 'fill' | 'reverse' | 'set' | 'sort';
export interface ReadonlyUint8Array extends Omit {
readonly [n: number]: number
-}
\ No newline at end of file
+}
diff --git a/js/ui/apple2.js b/js/ui/apple2.js
index 77bd84b..4e9d3f3 100644
--- a/js/ui/apple2.js
+++ b/js/ui/apple2.js
@@ -164,7 +164,9 @@ function loadingStop () {
MicroModal.close('loading-modal');
if (!paused) {
- _apple2.run();
+ vm.ready.then(() => {
+ _apple2.run();
+ });
}
}
@@ -704,22 +706,23 @@ function _keyup(evt) {
}
export function updateScreen() {
- var green = document.querySelector('#green_screen').checked;
+ var mono = document.querySelector('#mono_screen').checked;
var scanlines = document.querySelector('#show_scanlines').checked;
+ var gl = document.querySelector('#gl_canvas').checked;
var screen = document.querySelector('#screen');
var overscan = document.querySelector('.overscan');
- if (scanlines) {
+ if (scanlines && !gl) {
overscan.classList.add('scanlines');
} else {
overscan.classList.remove('scanlines');
}
- if (green) {
- screen.classList.add('green');
+ if (mono && !gl) {
+ screen.classList.add('mono');
} else {
- screen.classList.remove('green');
+ screen.classList.remove('mono');
}
- vm.mono(green);
+ vm.mono(mono);
}
export function updateCPU() {
@@ -780,7 +783,9 @@ function _mousemove(evt) {
export function pauseRun() {
var label = document.querySelector('#pause-run i');
if (paused) {
- _apple2.run();
+ vm.ready.then(() => {
+ _apple2.run();
+ });
label.classList.remove('fa-play');
label.classList.add('fa-pause');
} else {
@@ -921,6 +926,8 @@ export function initUI(apple2, disk2, smartPort, printer, e) {
_apple2.stop();
processHash(hash);
} else {
- _apple2.run();
+ vm.ready.then(() => {
+ _apple2.run();
+ });
}
}
diff --git a/js/videomodes.ts b/js/videomodes.ts
new file mode 100644
index 0000000..614fb2a
--- /dev/null
+++ b/js/videomodes.ts
@@ -0,0 +1,75 @@
+import { Memory, Restorable, byte } from './types';
+
+export type bank = 0 | 1;
+export type pageNo = 1 | 2;
+
+export interface Color {
+ 0: byte, // red
+ 1: byte, // green
+ 2: byte, // blue
+}
+
+export interface Region {
+ top: number,
+ bottom: number,
+ left: number,
+ right: number,
+}
+
+export interface GraphicsState {
+ page: byte;
+ mono: boolean;
+ buffer: string[];
+}
+
+export interface VideoModesState {
+ grs: [gr1: GraphicsState, gr2: GraphicsState],
+ hgrs: [hgr1: GraphicsState, hgr2: GraphicsState],
+ textMode: boolean,
+ mixedMode: boolean,
+ hiresMode: boolean,
+ pageMode: pageNo,
+ _80colMode: boolean,
+ altCharMode: boolean,
+ an3: boolean,
+}
+
+export interface VideoPage extends Memory, Restorable {
+ imageData: ImageData
+ dirty: Region;
+
+ bank0(): Memory
+ bank1(): Memory
+
+ mono: (on: boolean) => void
+ refresh: () => void
+}
+
+export interface LoresPage extends VideoPage {
+ getText: () => string
+}
+
+export interface HiresPage extends VideoPage {
+
+}
+
+export interface VideoModes extends Restorable {
+ page(pageNo: number): void
+
+ blit(altData?: ImageData): boolean
+
+ reset(): void
+
+ _80col(on: boolean): void
+ altchar(on: boolean): void
+ doubleHires(on: boolean): void
+ enhanced(on: boolean): void
+
+ is80Col(): boolean
+ isAltChar(): boolean
+ isDoubleHires(): boolean
+ isHires(): boolean
+ isMixed(): boolean
+ isPage2(): boolean
+ isText(): boolean
+}
diff --git a/package-lock.json b/package-lock.json
index f9c05d2..55498f3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,10 +5,10 @@
"requires": true,
"packages": {
"": {
- "name": "apple2js",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
+ "apple2shader": "whscullin/apple2shader",
"micromodal": "^0.4.2"
},
"devDependencies": {
@@ -3534,6 +3534,12 @@
"node": ">= 8"
}
},
+ "node_modules/apple2shader": {
+ "version": "0.0.1",
+ "resolved": "git+ssh://git@github.com/whscullin/apple2shader.git#e87a445148bbcc7f4dbfa2609902071631f96bbc",
+ "integrity": "sha512-nk+2iHnKtWHsUvRFiG77x5A5GmScQKc+MHDJOFB6GYmqTomhakyPdfw1Dkupj+iDQc6EmelUgbnF/weZoTtQ1Q==",
+ "license": "GPL-2.0"
+ },
"node_modules/aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
@@ -20734,6 +20740,11 @@
"picomatch": "^2.0.4"
}
},
+ "apple2shader": {
+ "version": "git+ssh://git@github.com/whscullin/apple2shader.git#e87a445148bbcc7f4dbfa2609902071631f96bbc",
+ "integrity": "sha512-nk+2iHnKtWHsUvRFiG77x5A5GmScQKc+MHDJOFB6GYmqTomhakyPdfw1Dkupj+iDQc6EmelUgbnF/weZoTtQ1Q==",
+ "from": "apple2shader@whscullin/apple2shader"
+ },
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
diff --git a/package.json b/package.json
index 3f61b26..52eff85 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
+ "apple2shader": "whscullin/apple2shader",
"micromodal": "^0.4.2"
}
}
diff --git a/tsconfig.json b/tsconfig.json
index 04d745d..9b2f993 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -17,11 +17,11 @@
"paths": {
"*": [
"node_modules/*",
- "src/types/*"
+ "types/*"
]
}
},
"include": [
"js/**/*"
]
-}
\ No newline at end of file
+}
diff --git a/types/apple2shader.d.ts b/types/apple2shader.d.ts
new file mode 100644
index 0000000..5c0a057
--- /dev/null
+++ b/types/apple2shader.d.ts
@@ -0,0 +1,3 @@
+declare module 'apple2shader';
+
+declare const apple2shader: any;