Videomode refactor 2 (#80)
Remove globals from video implementations to allow further refactoring. Experiment with testing video modes.
2
.gitignore
vendored
@ -2,6 +2,8 @@
|
||||
.checked-*.js
|
||||
.DS_Store
|
||||
.vscode
|
||||
/coverage
|
||||
/dist
|
||||
/node_modules
|
||||
/tmp
|
||||
__diff_output__
|
||||
|
@ -1,6 +1,7 @@
|
||||
module.exports = {
|
||||
'moduleNameMapper': {
|
||||
'^js/(.*)': '<rootDir>/js/$1',
|
||||
'^test/(.*)': '<rootDir>/test/$1',
|
||||
},
|
||||
'roots': [
|
||||
'js/',
|
||||
@ -9,8 +10,17 @@ module.exports = {
|
||||
'testMatch': [
|
||||
'**/?(*.)+(spec|test).+(ts|js)'
|
||||
],
|
||||
|
||||
'transform': {
|
||||
'^.+\\.js$': 'babel-jest',
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
'setupFilesAfterEnv': [
|
||||
'<rootDir>/test/jest-setup.js'
|
||||
],
|
||||
'coveragePathIgnorePatterns': [
|
||||
'/node_modules/',
|
||||
'/js/roms/',
|
||||
'/test/',
|
||||
]
|
||||
};
|
||||
|
10
js/apple2.ts
@ -82,11 +82,11 @@ export class Apple2 implements Restorable<State>, DebuggerContainer {
|
||||
const VideoModes = options.gl ? VideoModesGL : VideoModes2D;
|
||||
|
||||
this.cpu = new CPU6502({ '65C02': options.enhanced });
|
||||
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 = new VideoModes(options.canvas, options.e);
|
||||
this.gr = new LoresPage(this.vm, 1, options.characterRom, options.e);
|
||||
this.gr2 = new LoresPage(this.vm, 2, options.characterRom, options.e);
|
||||
this.hgr = new HiresPage(this.vm, 1);
|
||||
this.hgr2 = new HiresPage(this.vm, 2);
|
||||
this.io = new Apple2IO(this.cpu, this.vm);
|
||||
this.tick = options.tick;
|
||||
|
||||
|
383
js/canvas.ts
@ -22,46 +22,6 @@ import {
|
||||
pageNo
|
||||
} from './videomodes';
|
||||
|
||||
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;
|
||||
let monoDHRMode = false;
|
||||
const colorDHRMode = false;
|
||||
let mixedDHRMode = false;
|
||||
let highColorHGRMode = false;
|
||||
let highColorTextMode = false;
|
||||
let oneSixtyMode = false;
|
||||
|
||||
const tmpCanvas = document.createElement('canvas');
|
||||
const tmpContext = tmpCanvas.getContext('2d');
|
||||
|
||||
const buildScreen = (mainData: ImageData, mixData?: ImageData | null) => {
|
||||
if (!tmpContext) {
|
||||
throw new Error('No 2d context');
|
||||
}
|
||||
|
||||
const { width, height } = { width: 560, height: 192 };
|
||||
const { x, y } = _80colMode ? { x: 0, y: 0 } : { x: 0, y: 0 };
|
||||
|
||||
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 tmpCanvas;
|
||||
};
|
||||
|
||||
const dim = (c: Color): Color => {
|
||||
return [
|
||||
c[0] * 0.75 & 0xff,
|
||||
@ -119,7 +79,7 @@ const r4 = [
|
||||
11, // Light Blue
|
||||
13, // Yellow
|
||||
15 // White
|
||||
];
|
||||
] as const;
|
||||
|
||||
const dcolors: Color[] = [
|
||||
[0, 0, 0], // 0x0 black
|
||||
@ -145,7 +105,7 @@ const notDirty: Region = {
|
||||
bottom: -1,
|
||||
left: 561,
|
||||
right: -1
|
||||
};
|
||||
} as const;
|
||||
|
||||
/****************************************************************************
|
||||
*
|
||||
@ -160,21 +120,25 @@ export class LoresPage2D implements LoresPage {
|
||||
|
||||
private _buffer: memory[] = [];
|
||||
private _refreshing = false;
|
||||
private _monoMode = false;
|
||||
private _blink = false;
|
||||
|
||||
private highColorTextMode = false
|
||||
|
||||
dirty: Region = {...notDirty}
|
||||
imageData: ImageData;
|
||||
|
||||
constructor(private page: number,
|
||||
constructor(
|
||||
private vm: VideoModes,
|
||||
private page: pageNo,
|
||||
private readonly charset: rom,
|
||||
private readonly e: boolean) {
|
||||
this.imageData = new ImageData(560, 192);
|
||||
for (let idx = 0; idx < 560 * 192 * 4; idx++) {
|
||||
this.imageData.data[idx] = 0xff;
|
||||
}
|
||||
private readonly e: boolean
|
||||
) {
|
||||
this.imageData = this.vm.context.createImageData(560, 192);
|
||||
this.imageData.data.fill(0xff);
|
||||
this._buffer[0] = allocMemPages(0x4);
|
||||
this._buffer[1] = allocMemPages(0x4);
|
||||
|
||||
this.vm.setLoresPage(page, this);
|
||||
}
|
||||
|
||||
private _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
@ -194,7 +158,7 @@ export class LoresPage2D implements LoresPage {
|
||||
private _checkInverse(val: byte) {
|
||||
let inverse = false;
|
||||
if (this.e) {
|
||||
if (!_80colMode && !altCharMode) {
|
||||
if (!this.vm._80colMode && !this.vm.altCharMode) {
|
||||
inverse = ((val & 0xc0) == 0x40) && this._blink;
|
||||
}
|
||||
} else {
|
||||
@ -264,14 +228,14 @@ export class LoresPage2D implements LoresPage {
|
||||
x += 14;
|
||||
if (x > this.dirty.right) { this.dirty.right = x; }
|
||||
|
||||
if (textMode || hiresMode || (mixedMode && row > 19)) {
|
||||
if (_80colMode) {
|
||||
if (this.vm.textMode || this.vm.hiresMode || (this.vm.mixedMode && row > 19)) {
|
||||
if (this.vm._80colMode) {
|
||||
const inverse = this._checkInverse(val);
|
||||
|
||||
fore = inverse ? blackCol : whiteCol;
|
||||
back = inverse ? whiteCol : blackCol;
|
||||
|
||||
if (!altCharMode) {
|
||||
if (!this.vm.altCharMode) {
|
||||
val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
|
||||
}
|
||||
|
||||
@ -295,13 +259,13 @@ export class LoresPage2D implements LoresPage {
|
||||
fore = inverse ? blackCol : whiteCol;
|
||||
back = inverse ? whiteCol : blackCol;
|
||||
|
||||
if (!altCharMode) {
|
||||
if (!this.vm.altCharMode) {
|
||||
val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
|
||||
}
|
||||
|
||||
let offset = (col * 14 + row * 560 * 8) * 4;
|
||||
|
||||
if (highColorTextMode) {
|
||||
if (this.highColorTextMode) {
|
||||
fore = _colors[this._buffer[1][base] >> 4];
|
||||
back = _colors[this._buffer[1][base] & 0x0f];
|
||||
}
|
||||
@ -318,7 +282,7 @@ export class LoresPage2D implements LoresPage {
|
||||
offset += 546 * 4;
|
||||
}
|
||||
} else {
|
||||
const colorMode = mixedMode && !textMode && !this._monoMode;
|
||||
const colorMode = this.vm.mixedMode && !this.vm.textMode && !this.vm.monoMode;
|
||||
// var val0 = col > 0 ? _buffer[0][base - 1] : 0;
|
||||
// var val2 = col < 39 ? _buffer[0][base + 1] : 0;
|
||||
|
||||
@ -356,24 +320,20 @@ export class LoresPage2D implements LoresPage {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!_80colMode && bank == 1) {
|
||||
return;
|
||||
}
|
||||
if (_80colMode && !an3) {
|
||||
if (this.vm._80colMode && !this.vm.an3State) {
|
||||
let offset = (col * 14 + (bank ? 0 : 1) * 7 + row * 560 * 8) * 4;
|
||||
if (this._monoMode) {
|
||||
fore = whiteCol;
|
||||
back = blackCol;
|
||||
if (this.vm.monoMode) {
|
||||
for (let jdx = 0; jdx < 8; jdx++) {
|
||||
let b = (jdx < 8) ? (val & 0x0f) : (val >> 4);
|
||||
let b = (jdx < 4) ? (val & 0x0f) : (val >> 4);
|
||||
b |= (b << 4);
|
||||
if (bank & 0x1) {
|
||||
b <<= 1;
|
||||
b |= (b << 8);
|
||||
if (col & 0x1) {
|
||||
b >>= 2;
|
||||
}
|
||||
for (let idx = 0; idx < 7; idx++) {
|
||||
const color = (b & 0x80) ? fore : back;
|
||||
const color = (b & 0x01) ? whiteCol : blackCol;
|
||||
this._drawHalfPixel(data, offset, color);
|
||||
b <<= 1;
|
||||
b >>= 1;
|
||||
offset += 4;
|
||||
}
|
||||
offset += 553 * 4;
|
||||
@ -387,28 +347,26 @@ export class LoresPage2D implements LoresPage {
|
||||
(val & 0x0f) : (val >> 4)];
|
||||
for (let idx = 0; idx < 7; idx++) {
|
||||
this._drawHalfPixel(data, offset, color);
|
||||
off += 4;
|
||||
offset += 4;
|
||||
}
|
||||
offset += 553 * 4;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if (bank === 0) {
|
||||
let offset = (col * 14 + row * 560 * 8) * 4;
|
||||
|
||||
if (this._monoMode) {
|
||||
fore = whiteCol;
|
||||
back = blackCol;
|
||||
if (this.vm.monoMode) {
|
||||
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;
|
||||
b >>= 2;
|
||||
}
|
||||
for (let idx = 0; idx < 14; idx++) {
|
||||
const color = (b & 0x8000) ? fore : back;
|
||||
const color = (b & 0x0001) ? whiteCol : blackCol;
|
||||
this._drawHalfPixel(data, offset, color);
|
||||
b <<= 1;
|
||||
b >>= 1;
|
||||
offset += 4;
|
||||
}
|
||||
offset += 546 * 4;
|
||||
@ -429,21 +387,19 @@ export class LoresPage2D implements LoresPage {
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.highColorTextMode = !this.vm.an3State && this.vm.textMode && !this.vm._80colMode;
|
||||
|
||||
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) {
|
||||
if (this.vm._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;
|
||||
@ -476,8 +432,6 @@ export class LoresPage2D implements LoresPage {
|
||||
|
||||
getState(): GraphicsState {
|
||||
return {
|
||||
page: this.page,
|
||||
mono: this._monoMode,
|
||||
buffer: [
|
||||
new Uint8Array(this._buffer[0]),
|
||||
new Uint8Array(this._buffer[1]),
|
||||
@ -486,8 +440,6 @@ export class LoresPage2D implements LoresPage {
|
||||
}
|
||||
|
||||
setState(state: GraphicsState) {
|
||||
this.page = state.page;
|
||||
this._monoMode = state.mono;
|
||||
this._buffer[0] = new Uint8Array(state.buffer[0]);
|
||||
this._buffer[1] = new Uint8Array(state.buffer[1]);
|
||||
|
||||
@ -518,7 +470,7 @@ export class LoresPage2D implements LoresPage {
|
||||
for (row = 0; row < 24; row++) {
|
||||
base = this.rowToBase(row);
|
||||
line = '';
|
||||
if (this.e && _80colMode) {
|
||||
if (this.e && this.vm._80colMode) {
|
||||
for (col = 0; col < 80; col++) {
|
||||
charCode = this.mapCharCode(this._buffer[1 - col % 2][base + Math.floor(col / 2)]);
|
||||
line += String.fromCharCode(charCode);
|
||||
@ -548,16 +500,23 @@ export class HiresPage2D implements HiresPage {
|
||||
|
||||
private _buffer: memory[] = [];
|
||||
private _refreshing = false;
|
||||
private _monoMode = false;
|
||||
|
||||
highColorHGRMode: boolean;
|
||||
oneSixtyMode: boolean;
|
||||
mixedDHRMode: boolean;
|
||||
monoDHRMode: boolean;
|
||||
colorDHRMode: boolean = true;
|
||||
|
||||
constructor(
|
||||
private page: number) {
|
||||
this.imageData = new ImageData(560, 192);
|
||||
for (let idx = 0; idx < 560 * 192 * 4; idx++) {
|
||||
this.imageData.data[idx] = 0xff;
|
||||
}
|
||||
private vm: VideoModes,
|
||||
private page: pageNo,
|
||||
) {
|
||||
this.imageData = this.vm.context.createImageData(560, 192);
|
||||
this.imageData.data.fill(0xff);
|
||||
this._buffer[0] = allocMemPages(0x20);
|
||||
this._buffer[1] = allocMemPages(0x20);
|
||||
|
||||
this.vm.setHiresPage(page, this);
|
||||
}
|
||||
|
||||
private _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
@ -645,7 +604,7 @@ export class HiresPage2D implements HiresPage {
|
||||
|
||||
const data = this.imageData.data;
|
||||
let dx, dy;
|
||||
if ((rowa < 24) && (col < 40) && hiresMode) {
|
||||
if ((rowa < 24) && (col < 40) && this.vm.hiresMode) {
|
||||
let y = rowa << 4 | rowb << 1;
|
||||
if (y < this.dirty.top) { this.dirty.top = y; }
|
||||
y += 1;
|
||||
@ -657,7 +616,7 @@ export class HiresPage2D implements HiresPage {
|
||||
|
||||
dy = rowa << 4 | rowb << 1;
|
||||
let bz, b0, b1, b2, b3, b4, c, hb;
|
||||
if (oneSixtyMode && !this._monoMode) {
|
||||
if (this.oneSixtyMode && !this.vm.monoMode) {
|
||||
// 1 byte = two pixels, but 3:4 ratio
|
||||
const c3 = val & 0xf;
|
||||
const c4 = val >> 4;
|
||||
@ -667,7 +626,7 @@ export class HiresPage2D implements HiresPage {
|
||||
|
||||
this._draw3Pixel(data, offset, dcolors[c3]);
|
||||
this._draw4Pixel(data, offset + 12, dcolors[c4]);
|
||||
} else if (doubleHiresMode) {
|
||||
} else if (this.vm.doubleHiresMode) {
|
||||
val &= 0x7f;
|
||||
|
||||
// Every 4 bytes is 7 pixels
|
||||
@ -719,7 +678,7 @@ export class HiresPage2D implements HiresPage {
|
||||
let offset = dx * 4 + dy * 280 * 4;
|
||||
|
||||
let monoColor = null;
|
||||
if (this._monoMode || monoDHRMode) {
|
||||
if (this.vm.monoMode || this.monoDHRMode) {
|
||||
monoColor = whiteCol;
|
||||
}
|
||||
|
||||
@ -734,7 +693,7 @@ export class HiresPage2D implements HiresPage {
|
||||
} else {
|
||||
this._drawHalfPixel(data, offset, blackCol);
|
||||
}
|
||||
} else if (mixedDHRMode) {
|
||||
} else if (this.mixedDHRMode) {
|
||||
if (hbs) {
|
||||
this._drawHalfPixel(data, offset, dcolor);
|
||||
} else {
|
||||
@ -744,7 +703,7 @@ export class HiresPage2D implements HiresPage {
|
||||
this._drawHalfPixel(data, offset, blackCol);
|
||||
}
|
||||
}
|
||||
} else if (colorDHRMode) {
|
||||
} else if (this.colorDHRMode) {
|
||||
this._drawHalfPixel(data, offset, dcolor);
|
||||
} else if (
|
||||
((c[idx] != c[idx - 1]) && (c[idx] != c[idx + 1])) &&
|
||||
@ -793,7 +752,7 @@ export class HiresPage2D implements HiresPage {
|
||||
|
||||
let offset = dx * 4 + dy * 280 * 4;
|
||||
|
||||
const monoColor = this._monoMode ? whiteCol : null;
|
||||
const monoColor = this.vm.monoMode ? whiteCol : null;
|
||||
|
||||
for (let idx = 0; idx < 9; idx++, offset += 8) {
|
||||
val >>= 1;
|
||||
@ -801,7 +760,7 @@ export class HiresPage2D implements HiresPage {
|
||||
if (v1) {
|
||||
if (monoColor) {
|
||||
color = monoColor;
|
||||
} else if (highColorHGRMode) {
|
||||
} else if (this.highColorHGRMode) {
|
||||
color = dcolors[this._buffer[1][base] >> 4];
|
||||
} else if (v0 || v2) {
|
||||
color = whiteCol;
|
||||
@ -811,7 +770,7 @@ export class HiresPage2D implements HiresPage {
|
||||
} else {
|
||||
if (monoColor) {
|
||||
color = blackCol;
|
||||
} else if (highColorHGRMode) {
|
||||
} else if (this.highColorHGRMode) {
|
||||
color = dcolors[this._buffer[1][base] & 0x0f];
|
||||
} else if (odd && v2 && v0) {
|
||||
color = v0 ? dim(evenCol) : evenCol;
|
||||
@ -837,23 +796,24 @@ export class HiresPage2D implements HiresPage {
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.highColorHGRMode = !this.vm.an3State && this.vm.hiresMode && !this.vm._80colMode;
|
||||
this.oneSixtyMode = this.vm.flag == 1 && this.vm.doubleHiresMode;
|
||||
this.mixedDHRMode = this.vm.flag == 2 && this.vm.doubleHiresMode;
|
||||
this.monoDHRMode = this.vm.flag == 3 && this.vm.doubleHiresMode;
|
||||
|
||||
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) {
|
||||
if (this.vm._80colMode) {
|
||||
this._write(page, off, this._buffer[1][idx], 1);
|
||||
}
|
||||
}
|
||||
this._refreshing = false;
|
||||
}
|
||||
|
||||
mono(on: boolean) {
|
||||
this._monoMode = on;
|
||||
}
|
||||
|
||||
start() {
|
||||
return this._start();
|
||||
}
|
||||
@ -872,8 +832,6 @@ export class HiresPage2D implements HiresPage {
|
||||
|
||||
getState(): GraphicsState {
|
||||
return {
|
||||
page: this.page,
|
||||
mono: this._monoMode,
|
||||
buffer: [
|
||||
new Uint8Array(this._buffer[0]),
|
||||
new Uint8Array(this._buffer[1]),
|
||||
@ -882,8 +840,6 @@ export class HiresPage2D implements HiresPage {
|
||||
}
|
||||
|
||||
setState(state: GraphicsState) {
|
||||
this.page = state.page;
|
||||
this._monoMode = state.mono;
|
||||
this._buffer[0] = new Uint8Array(state.buffer[0]);
|
||||
this._buffer[1] = new Uint8Array(state.buffer[1]);
|
||||
|
||||
@ -892,37 +848,53 @@ export class HiresPage2D implements HiresPage {
|
||||
}
|
||||
|
||||
export class VideoModes2D implements VideoModes {
|
||||
private _grs: LoresPage[];
|
||||
private _hgrs: HiresPage[];
|
||||
private _flag = 0;
|
||||
private _context: CanvasRenderingContext2D | null;
|
||||
private _grs: LoresPage[] = [];
|
||||
private _hgrs: HiresPage[] = [];
|
||||
private _screenContext: CanvasRenderingContext2D;
|
||||
private _canvas: HTMLCanvasElement;
|
||||
private _left: number;
|
||||
private _top: number;
|
||||
private _refreshFlag: boolean = true;
|
||||
|
||||
public ready = Promise.resolve();
|
||||
|
||||
textMode: boolean;
|
||||
mixedMode: boolean;
|
||||
hiresMode: boolean;
|
||||
pageMode: pageNo;
|
||||
_80colMode: boolean;
|
||||
altCharMode: boolean;
|
||||
an3State: boolean;
|
||||
doubleHiresMode: boolean;
|
||||
|
||||
flag = 0;
|
||||
monoMode = false;
|
||||
|
||||
context: CanvasRenderingContext2D;
|
||||
|
||||
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 screen: HTMLCanvasElement,
|
||||
private e: boolean
|
||||
) {
|
||||
this._canvas = document.createElement('canvas');
|
||||
const context = this._canvas.getContext('2d');
|
||||
const screenContext = this.screen.getContext('2d');
|
||||
if (!context || !screenContext) {
|
||||
throw new Error('No 2d context');
|
||||
}
|
||||
this.context = context;
|
||||
|
||||
const { width, height } = { width: 560, height: 192 };
|
||||
this._canvas.width = width;
|
||||
this._canvas.height = height;
|
||||
|
||||
this._screenContext = screenContext;
|
||||
this._left = (this.screen.width - 560) / 2;
|
||||
this._top = (this.screen.height - 384) / 2;
|
||||
}
|
||||
|
||||
private _refresh() {
|
||||
highColorTextMode = !an3 && textMode && !_80colMode;
|
||||
highColorHGRMode = !an3 && hiresMode && !_80colMode;
|
||||
doubleHiresMode = !an3 && hiresMode && _80colMode;
|
||||
oneSixtyMode = this._flag == 1 && doubleHiresMode;
|
||||
mixedDHRMode = this._flag == 2 && doubleHiresMode;
|
||||
monoDHRMode = this._flag == 3 && doubleHiresMode;
|
||||
_refresh() {
|
||||
this.doubleHiresMode = !this.an3State && this.hiresMode && this._80colMode;
|
||||
|
||||
this._refreshFlag = true;
|
||||
}
|
||||
@ -932,25 +904,33 @@ export class VideoModes2D implements VideoModes {
|
||||
}
|
||||
|
||||
reset() {
|
||||
textMode = true;
|
||||
mixedMode = false;
|
||||
hiresMode = true;
|
||||
pageMode = 1;
|
||||
this.textMode = true;
|
||||
this.mixedMode = false;
|
||||
this.hiresMode = true;
|
||||
this.pageMode = 1;
|
||||
|
||||
_80colMode = false;
|
||||
altCharMode = false;
|
||||
this._80colMode = false;
|
||||
this.altCharMode = false;
|
||||
|
||||
this._flag = 0;
|
||||
an3 = true;
|
||||
this.flag = 0;
|
||||
this.an3State = true;
|
||||
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
setLoresPage(page: pageNo, lores: LoresPage) {
|
||||
this._grs[page - 1] = lores;
|
||||
}
|
||||
|
||||
setHiresPage(page: pageNo, hires: HiresPage) {
|
||||
this._hgrs[page - 1] = hires;
|
||||
}
|
||||
|
||||
text(on: boolean) {
|
||||
const old = textMode;
|
||||
textMode = on;
|
||||
const old = this.textMode;
|
||||
this.textMode = on;
|
||||
if (on) {
|
||||
this._flag = 0;
|
||||
this.flag = 0;
|
||||
}
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
@ -960,29 +940,29 @@ export class VideoModes2D implements VideoModes {
|
||||
_80col(on: boolean) {
|
||||
if (!this.e) { return; }
|
||||
|
||||
const old = _80colMode;
|
||||
_80colMode = on;
|
||||
const old = this._80colMode;
|
||||
this._80colMode = on;
|
||||
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
altchar(on: boolean) {
|
||||
altChar(on: boolean) {
|
||||
if (!this.e) { return; }
|
||||
|
||||
const old = altCharMode;
|
||||
altCharMode = on;
|
||||
const old = this.altCharMode;
|
||||
this.altCharMode = on;
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
hires(on: boolean) {
|
||||
const old = hiresMode;
|
||||
hiresMode = on;
|
||||
const old = this.hiresMode;
|
||||
this.hiresMode = on;
|
||||
if (!on) {
|
||||
this._flag = 0;
|
||||
this.flag = 0;
|
||||
}
|
||||
|
||||
if (old != on) {
|
||||
@ -993,11 +973,11 @@ export class VideoModes2D implements VideoModes {
|
||||
an3(on: boolean) {
|
||||
if (!this.e) { return; }
|
||||
|
||||
const old = an3;
|
||||
an3 = on;
|
||||
const old = this.an3State;
|
||||
this.an3State = on;
|
||||
|
||||
if (on) {
|
||||
this._flag = ((this._flag << 1) | (_80colMode ? 0x0 : 0x1)) & 0x3;
|
||||
this.flag = ((this.flag << 1) | (this._80colMode ? 0x0 : 0x1)) & 0x3;
|
||||
}
|
||||
|
||||
if (old != on) {
|
||||
@ -1010,47 +990,60 @@ export class VideoModes2D implements VideoModes {
|
||||
}
|
||||
|
||||
mixed(on: boolean) {
|
||||
const old = mixedMode;
|
||||
mixedMode = on;
|
||||
const old = this.mixedMode;
|
||||
this.mixedMode = on;
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
page(pageNo: pageNo) {
|
||||
const old = pageMode;
|
||||
pageMode = pageNo;
|
||||
const old = this.pageMode;
|
||||
this.pageMode = pageNo;
|
||||
if (old != pageNo) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
isText() {
|
||||
return textMode;
|
||||
return this.textMode;
|
||||
}
|
||||
|
||||
isMixed() {
|
||||
return mixedMode;
|
||||
return this.mixedMode;
|
||||
}
|
||||
|
||||
isPage2() {
|
||||
return pageMode == 2;
|
||||
return this.pageMode == 2;
|
||||
}
|
||||
|
||||
isHires() {
|
||||
return hiresMode;
|
||||
return this.hiresMode;
|
||||
}
|
||||
|
||||
isDoubleHires() {
|
||||
return doubleHiresMode;
|
||||
return this.doubleHiresMode;
|
||||
}
|
||||
|
||||
is80Col() {
|
||||
return _80colMode;
|
||||
return this._80colMode;
|
||||
}
|
||||
|
||||
isAltChar() {
|
||||
return altCharMode;
|
||||
return this.altCharMode;
|
||||
}
|
||||
|
||||
buildScreen(mainData: ImageData, mixData?: ImageData | null) {
|
||||
// TODO(whscullin): - figure out 80 column offset
|
||||
const { x, y } = this._80colMode ? { x: 0, y: 0 } : { x: 0, y: 0 };
|
||||
|
||||
if (mixData) {
|
||||
this.context.putImageData(mainData, x, y, 0, 0, 560, 160);
|
||||
this.context.putImageData(mixData, x, y, 0, 160, 560, 32);
|
||||
} else {
|
||||
this.context.putImageData(mainData, x, y);
|
||||
}
|
||||
return this._canvas;
|
||||
}
|
||||
|
||||
updateImage(
|
||||
@ -1059,14 +1052,11 @@ export class VideoModes2D implements VideoModes {
|
||||
mixData?: ImageData | null,
|
||||
mixDirty?: Region | null
|
||||
) {
|
||||
if (!this._context) {
|
||||
throw new Error('No 2D context');
|
||||
}
|
||||
let blitted = false;
|
||||
|
||||
if (mainDirty.bottom !== -1 || (mixDirty && mixDirty.bottom !== -1)) {
|
||||
const imageData = buildScreen(mainData, mixData);
|
||||
this._context.drawImage(
|
||||
const imageData = this.buildScreen(mainData, mixData);
|
||||
this._screenContext.drawImage(
|
||||
imageData,
|
||||
0, 0, 560, 192,
|
||||
this._left, this._top, 560, 384
|
||||
@ -1078,8 +1068,8 @@ export class VideoModes2D implements VideoModes {
|
||||
|
||||
blit(altData?: ImageData) {
|
||||
let blitted = false;
|
||||
const hgr = this._hgrs[pageMode - 1];
|
||||
const gr = this._grs[pageMode - 1];
|
||||
const hgr = this._hgrs[this.pageMode - 1];
|
||||
const gr = this._grs[this.pageMode - 1];
|
||||
|
||||
if (this._refreshFlag) {
|
||||
hgr.refresh();
|
||||
@ -1092,12 +1082,12 @@ export class VideoModes2D implements VideoModes {
|
||||
altData,
|
||||
{ top: 0, left: 0, right: 560, bottom: 192 }
|
||||
);
|
||||
} else if (hiresMode && !textMode) {
|
||||
} else if (this.hiresMode && !this.textMode) {
|
||||
blitted = this.updateImage(
|
||||
hgr.imageData,
|
||||
hgr.dirty,
|
||||
mixedMode ? gr.imageData : null,
|
||||
mixedMode ? gr.dirty : null
|
||||
this.mixedMode ? gr.imageData : null,
|
||||
this.mixedMode ? gr.dirty : null
|
||||
);
|
||||
} else {
|
||||
blitted = this.updateImage(
|
||||
@ -1114,24 +1104,26 @@ export class VideoModes2D implements VideoModes {
|
||||
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
|
||||
textMode: this.textMode,
|
||||
mixedMode: this.mixedMode,
|
||||
hiresMode: this.hiresMode,
|
||||
pageMode: this.pageMode,
|
||||
_80colMode: this._80colMode,
|
||||
altCharMode: this.altCharMode,
|
||||
an3State: this.an3State,
|
||||
flag: this.flag
|
||||
};
|
||||
}
|
||||
|
||||
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.textMode = state.textMode;
|
||||
this.mixedMode = state.mixedMode;
|
||||
this.hiresMode = state.hiresMode;
|
||||
this.pageMode = state.pageMode;
|
||||
this._80colMode = state._80colMode;
|
||||
this.altCharMode = state.altCharMode;
|
||||
this.an3State = state.an3State;
|
||||
this.flag = state.flag;
|
||||
|
||||
this._grs[0].setState(state.grs[0]);
|
||||
this._grs[1].setState(state.grs[1]);
|
||||
@ -1142,20 +1134,17 @@ export class VideoModes2D implements VideoModes {
|
||||
|
||||
mono(on: boolean) {
|
||||
if (on) {
|
||||
this.canvas.classList.add('mono');
|
||||
this.screen.classList.add('mono');
|
||||
} else {
|
||||
this.canvas.classList.remove('mono');
|
||||
this.screen.classList.remove('mono');
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
scanlines(on: boolean) {
|
||||
// Can't apply scanline filter to canvas
|
||||
const parent = this.canvas.parentElement;
|
||||
const parent = this.screen.parentElement;
|
||||
if (parent) {
|
||||
if (on) {
|
||||
parent.classList.add('scanlines');
|
||||
@ -1166,6 +1155,6 @@ export class VideoModes2D implements VideoModes {
|
||||
}
|
||||
|
||||
getText() {
|
||||
return this._grs[pageMode - 1].getText();
|
||||
return this._grs[this.pageMode - 1].getText();
|
||||
}
|
||||
}
|
||||
|
292
js/gl.ts
@ -25,41 +25,6 @@ import {
|
||||
pageNo
|
||||
} from './videomodes';
|
||||
|
||||
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 2d 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];
|
||||
@ -84,21 +49,23 @@ export class LoresPageGL implements LoresPage {
|
||||
|
||||
private _buffer: memory[] = [];
|
||||
private _refreshing = false;
|
||||
private _monoMode = false;
|
||||
private _blink = false;
|
||||
|
||||
dirty: Region = {...notDirty}
|
||||
imageData: ImageData;
|
||||
|
||||
constructor(private page: number,
|
||||
constructor(
|
||||
private vm: VideoModes,
|
||||
private page: pageNo,
|
||||
private readonly charset: rom,
|
||||
private readonly e: boolean) {
|
||||
this.imageData = new ImageData(560, 192);
|
||||
for (let idx = 0; idx < 560 * 192 * 4; idx++) {
|
||||
this.imageData.data[idx] = 0xff;
|
||||
}
|
||||
private readonly e: boolean
|
||||
) {
|
||||
this.imageData = this.vm.context.createImageData(560, 192);
|
||||
this.imageData.data.fill(0xff);
|
||||
this._buffer[0] = allocMemPages(0x4);
|
||||
this._buffer[1] = allocMemPages(0x4);
|
||||
|
||||
this.vm.setLoresPage(page, this);
|
||||
}
|
||||
|
||||
private _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
@ -118,7 +85,7 @@ export class LoresPageGL implements LoresPage {
|
||||
private _checkInverse(val: byte) {
|
||||
let inverse = false;
|
||||
if (this.e) {
|
||||
if (!_80colMode && !altCharMode) {
|
||||
if (!this.vm._80colMode && !this.vm.altCharMode) {
|
||||
inverse = ((val & 0xc0) == 0x40) && this._blink;
|
||||
}
|
||||
} else {
|
||||
@ -188,14 +155,14 @@ export class LoresPageGL implements LoresPage {
|
||||
x += 14;
|
||||
if (x > this.dirty.right) { this.dirty.right = x; }
|
||||
|
||||
if (textMode || hiresMode || (mixedMode && row > 19)) {
|
||||
if (_80colMode) {
|
||||
if (this.vm.textMode || this.vm.hiresMode || (this.vm.mixedMode && row > 19)) {
|
||||
if (this.vm._80colMode) {
|
||||
const inverse = this._checkInverse(val);
|
||||
|
||||
fore = inverse ? blackCol : whiteCol;
|
||||
back = inverse ? whiteCol : blackCol;
|
||||
|
||||
if (!altCharMode) {
|
||||
if (!this.vm.altCharMode) {
|
||||
val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
|
||||
}
|
||||
|
||||
@ -219,7 +186,7 @@ export class LoresPageGL implements LoresPage {
|
||||
fore = inverse ? blackCol : whiteCol;
|
||||
back = inverse ? whiteCol : blackCol;
|
||||
|
||||
if (!altCharMode) {
|
||||
if (!this.vm.altCharMode) {
|
||||
val = (val >= 0x40 && val < 0x80) ? val - 0x40 : val;
|
||||
}
|
||||
|
||||
@ -251,7 +218,7 @@ export class LoresPageGL implements LoresPage {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (_80colMode && !an3) {
|
||||
if (this.vm._80colMode && !this.vm.an3State) {
|
||||
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);
|
||||
@ -295,17 +262,13 @@ export class LoresPageGL implements LoresPage {
|
||||
this._refreshing = true;
|
||||
for (let idx = 0; idx < 0x400; idx++, addr++) {
|
||||
this._write(addr >> 8, addr & 0xff, this._buffer[0][idx], 0);
|
||||
if (_80colMode) {
|
||||
if (this.vm._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;
|
||||
@ -338,8 +301,6 @@ export class LoresPageGL implements LoresPage {
|
||||
|
||||
getState(): GraphicsState {
|
||||
return {
|
||||
page: this.page,
|
||||
mono: this._monoMode,
|
||||
buffer: [
|
||||
new Uint8Array(this._buffer[0]),
|
||||
new Uint8Array(this._buffer[1]),
|
||||
@ -348,8 +309,6 @@ export class LoresPageGL implements LoresPage {
|
||||
}
|
||||
|
||||
setState(state: GraphicsState) {
|
||||
this.page = state.page;
|
||||
this._monoMode = state.mono;
|
||||
this._buffer[0] = new Uint8Array(state.buffer[0]);
|
||||
this._buffer[1] = new Uint8Array(state.buffer[1]);
|
||||
|
||||
@ -380,7 +339,7 @@ export class LoresPageGL implements LoresPage {
|
||||
for (row = 0; row < 24; row++) {
|
||||
base = this.rowToBase(row);
|
||||
line = '';
|
||||
if (this.e && _80colMode) {
|
||||
if (this.e && this.vm._80colMode) {
|
||||
for (col = 0; col < 80; col++) {
|
||||
charCode = this.mapCharCode(this._buffer[1 - col % 2][base + Math.floor(col / 2)]);
|
||||
line += String.fromCharCode(charCode);
|
||||
@ -410,16 +369,17 @@ export class HiresPageGL implements HiresPage {
|
||||
|
||||
private _buffer: memory[] = [];
|
||||
private _refreshing = false;
|
||||
private _monoMode = false;
|
||||
|
||||
constructor(
|
||||
private page: number) {
|
||||
this.imageData = new ImageData(560, 192);
|
||||
for (let idx = 0; idx < 560 * 192 * 4; idx++) {
|
||||
this.imageData.data[idx] = 0xff;
|
||||
}
|
||||
private vm: VideoModes,
|
||||
private page: pageNo,
|
||||
) {
|
||||
this.imageData = this.vm.context.createImageData(560, 192);
|
||||
this.imageData.data.fill(0xff);
|
||||
this._buffer[0] = allocMemPages(0x20);
|
||||
this._buffer[1] = allocMemPages(0x20);
|
||||
|
||||
this.vm.setHiresPage(page, this);
|
||||
}
|
||||
|
||||
private _drawPixel(data: Uint8ClampedArray, off: number, color: Color) {
|
||||
@ -486,7 +446,7 @@ export class HiresPageGL implements HiresPage {
|
||||
rowb = base >> 10;
|
||||
|
||||
const data = this.imageData.data;
|
||||
if ((rowa < 24) && (col < 40) && hiresMode) {
|
||||
if ((rowa < 24) && (col < 40) && this.vm.hiresMode) {
|
||||
let y = rowa << 3 | rowb;
|
||||
if (y < this.dirty.top) { this.dirty.top = y; }
|
||||
y += 1;
|
||||
@ -497,7 +457,7 @@ export class HiresPageGL implements HiresPage {
|
||||
if (x > this.dirty.right) { this.dirty.right = x; }
|
||||
|
||||
const dy = rowa << 3 | rowb;
|
||||
if (doubleHiresMode) {
|
||||
if (this.vm.doubleHiresMode) {
|
||||
const dx = col * 14 + (bank ? 0 : 7);
|
||||
let offset = dx * 4 + dy * 280 * 4 * 2;
|
||||
|
||||
@ -554,17 +514,13 @@ export class HiresPageGL implements HiresPage {
|
||||
const page = addr >> 8;
|
||||
const off = addr & 0xff;
|
||||
this._write(page, off, this._buffer[0][idx], 0);
|
||||
if (_80colMode) {
|
||||
if (this.vm._80colMode) {
|
||||
this._write(page, off, this._buffer[1][idx], 1);
|
||||
}
|
||||
}
|
||||
this._refreshing = false;
|
||||
}
|
||||
|
||||
mono(on: boolean) {
|
||||
this._monoMode = on;
|
||||
}
|
||||
|
||||
start() {
|
||||
return this._start();
|
||||
}
|
||||
@ -583,8 +539,6 @@ export class HiresPageGL implements HiresPage {
|
||||
|
||||
getState(): GraphicsState {
|
||||
return {
|
||||
page: this.page,
|
||||
mono: this._monoMode,
|
||||
buffer: [
|
||||
new Uint8Array(this._buffer[0]),
|
||||
new Uint8Array(this._buffer[1]),
|
||||
@ -593,8 +547,6 @@ export class HiresPageGL implements HiresPage {
|
||||
}
|
||||
|
||||
setState(state: GraphicsState) {
|
||||
this.page = state.page;
|
||||
this._monoMode = state.mono;
|
||||
this._buffer[0] = new Uint8Array(state.buffer[0]);
|
||||
this._buffer[1] = new Uint8Array(state.buffer[1]);
|
||||
|
||||
@ -603,26 +555,44 @@ export class HiresPageGL implements HiresPage {
|
||||
}
|
||||
|
||||
export class VideoModesGL implements VideoModes {
|
||||
private _grs: LoresPage[];
|
||||
private _hgrs: HiresPage[];
|
||||
private _sv: any;
|
||||
private _grs: LoresPage[] = [];
|
||||
private _hgrs: HiresPage[] = [];
|
||||
private _sv: screenEmu.ScreenView;
|
||||
private _displayConfig: screenEmu.DisplayConfiguration;
|
||||
private _monoMode: boolean = false;
|
||||
private _scanlines: boolean = false;
|
||||
private _refreshFlag: boolean = true;
|
||||
private _canvas: HTMLCanvasElement;
|
||||
|
||||
public ready: Promise<void>
|
||||
|
||||
public textMode: boolean;
|
||||
public mixedMode: boolean;
|
||||
public hiresMode: boolean;
|
||||
public pageMode: pageNo;
|
||||
public _80colMode: boolean;
|
||||
public altCharMode: boolean;
|
||||
public an3State: boolean;
|
||||
public doubleHiresMode: boolean;
|
||||
|
||||
public flag = 0;
|
||||
public monoMode: boolean = false;
|
||||
|
||||
public context: CanvasRenderingContext2D;
|
||||
|
||||
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);
|
||||
private screen: HTMLCanvasElement,
|
||||
private e: boolean
|
||||
) {
|
||||
this._canvas = document.createElement('canvas');
|
||||
const context = this._canvas.getContext('2d');
|
||||
if (!context) {
|
||||
throw new Error('no 2d context');
|
||||
}
|
||||
const { width, height } = screenEmu.C.NTSC_DETAILS.imageSize;
|
||||
this._canvas.width = width;
|
||||
this._canvas.height = height;
|
||||
this.context = context;
|
||||
this._sv = new screenEmu.ScreenView(this.screen);
|
||||
|
||||
this.ready = this.init();
|
||||
}
|
||||
@ -630,15 +600,13 @@ export class VideoModesGL implements VideoModes {
|
||||
async init() {
|
||||
await this._sv.initOpenGL();
|
||||
|
||||
(window as any)._sv = this._sv;
|
||||
|
||||
this._displayConfig = this.defaultMonitor();
|
||||
this._sv.displayConfiguration = this._displayConfig;
|
||||
}
|
||||
|
||||
private defaultMonitor(): screenEmu.DisplayConfiguration {
|
||||
const config = new screenEmu.DisplayConfiguration();
|
||||
config.displayResolution = new screenEmu.Size(this.canvas.width, this.canvas.height);
|
||||
config.displayResolution = new screenEmu.Size(this.screen.width, this.screen.height);
|
||||
config.displayScanlineLevel = 0.5;
|
||||
config.videoWhiteOnly = true;
|
||||
config.videoSaturation = 0.8;
|
||||
@ -651,15 +619,15 @@ export class VideoModesGL implements VideoModes {
|
||||
private monitorII(): screenEmu.DisplayConfiguration {
|
||||
// Values taken from openemulator/libemulation/res/library/Monitors/Apple Monitor II.xml
|
||||
const config = new screenEmu.DisplayConfiguration();
|
||||
config.displayResolution = new screenEmu.Size(this.canvas.width, this.canvas.height);
|
||||
config.displayResolution = new screenEmu.Size(this.screen.width, this.screen.height);
|
||||
config.videoDecoder = 'CANVAS_MONOCHROME';
|
||||
config.videoBrightness = 0.15;
|
||||
config.videoContrast = 0.8;
|
||||
config.videoSaturation = 1.45;
|
||||
config.videoHue = 0.27;
|
||||
config.videoCenter = new screenEmu.Point(0, 0);
|
||||
config.videoSize = new screenEmu.Size(1.05, 1.05);
|
||||
config.videoBandwidth = 6000000;
|
||||
config.videoCenter = new screenEmu.Point(0.01, 0.02);
|
||||
config.videoSize = new screenEmu.Size(1.25, 1.15);
|
||||
config.videoBandwidth = 9000000;
|
||||
config.displayBarrel = 0.1;
|
||||
config.displayScanlineLevel = 0.5;
|
||||
config.displayCenterLighting = 0.5;
|
||||
@ -668,12 +636,12 @@ export class VideoModesGL implements VideoModes {
|
||||
}
|
||||
|
||||
private _refresh() {
|
||||
doubleHiresMode = !an3 && hiresMode && _80colMode;
|
||||
this.doubleHiresMode = !this.an3State && this.hiresMode && this._80colMode;
|
||||
|
||||
this._refreshFlag = true;
|
||||
|
||||
if (this._displayConfig) {
|
||||
this._displayConfig.videoWhiteOnly = textMode || this._monoMode;
|
||||
this._displayConfig.videoWhiteOnly = this.textMode || this.monoMode;
|
||||
this._displayConfig.displayScanlineLevel = this._scanlines ? 0.5 : 0;
|
||||
this._sv.displayConfiguration = this._displayConfig;
|
||||
}
|
||||
@ -684,22 +652,30 @@ export class VideoModesGL implements VideoModes {
|
||||
}
|
||||
|
||||
reset() {
|
||||
textMode = true;
|
||||
mixedMode = false;
|
||||
hiresMode = true;
|
||||
pageMode = 1;
|
||||
this.textMode = true;
|
||||
this.mixedMode = false;
|
||||
this.hiresMode = true;
|
||||
this.pageMode = 1;
|
||||
|
||||
_80colMode = false;
|
||||
altCharMode = false;
|
||||
this._80colMode = false;
|
||||
this.altCharMode = false;
|
||||
|
||||
an3 = true;
|
||||
this.an3State = true;
|
||||
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
setLoresPage(page: pageNo, lores: LoresPage) {
|
||||
this._grs[page - 1] = lores;
|
||||
}
|
||||
|
||||
setHiresPage(page: pageNo, hires: HiresPage) {
|
||||
this._hgrs[page - 1] = hires;
|
||||
}
|
||||
|
||||
text(on: boolean) {
|
||||
const old = textMode;
|
||||
textMode = on;
|
||||
const old = this.textMode;
|
||||
this.textMode = on;
|
||||
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
@ -709,27 +685,27 @@ export class VideoModesGL implements VideoModes {
|
||||
_80col(on: boolean) {
|
||||
if (!this.e) { return; }
|
||||
|
||||
const old = _80colMode;
|
||||
_80colMode = on;
|
||||
const old = this._80colMode;
|
||||
this._80colMode = on;
|
||||
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
altchar(on: boolean) {
|
||||
altChar(on: boolean) {
|
||||
if (!this.e) { return; }
|
||||
|
||||
const old = altCharMode;
|
||||
altCharMode = on;
|
||||
const old = this.altCharMode;
|
||||
this.altCharMode = on;
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
hires(on: boolean) {
|
||||
const old = hiresMode;
|
||||
hiresMode = on;
|
||||
const old = this.hiresMode;
|
||||
this.hiresMode = on;
|
||||
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
@ -739,8 +715,8 @@ export class VideoModesGL implements VideoModes {
|
||||
an3(on: boolean) {
|
||||
if (!this.e) { return; }
|
||||
|
||||
const old = an3;
|
||||
an3 = on;
|
||||
const old = this.an3State;
|
||||
this.an3State = on;
|
||||
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
@ -752,47 +728,47 @@ export class VideoModesGL implements VideoModes {
|
||||
}
|
||||
|
||||
mixed(on: boolean) {
|
||||
const old = mixedMode;
|
||||
mixedMode = on;
|
||||
const old = this.mixedMode;
|
||||
this.mixedMode = on;
|
||||
if (old != on) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
page(pageNo: pageNo) {
|
||||
const old = pageMode;
|
||||
pageMode = pageNo;
|
||||
const old = this.pageMode;
|
||||
this.pageMode = pageNo;
|
||||
if (old != pageNo) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
isText() {
|
||||
return textMode;
|
||||
return this.textMode;
|
||||
}
|
||||
|
||||
isMixed() {
|
||||
return mixedMode;
|
||||
return this.mixedMode;
|
||||
}
|
||||
|
||||
isPage2() {
|
||||
return pageMode == 2;
|
||||
return this.pageMode == 2;
|
||||
}
|
||||
|
||||
isHires() {
|
||||
return hiresMode;
|
||||
return this.hiresMode;
|
||||
}
|
||||
|
||||
isDoubleHires() {
|
||||
return doubleHiresMode;
|
||||
return this.doubleHiresMode;
|
||||
}
|
||||
|
||||
is80Col() {
|
||||
return _80colMode;
|
||||
return this._80colMode;
|
||||
}
|
||||
|
||||
isAltChar() {
|
||||
return altCharMode;
|
||||
return this.altCharMode;
|
||||
}
|
||||
|
||||
updateImage(
|
||||
@ -803,7 +779,7 @@ export class VideoModesGL implements VideoModes {
|
||||
) {
|
||||
let blitted = false;
|
||||
if (mainDirty.bottom !== -1 || (mixDirty && mixDirty.bottom !== -1)) {
|
||||
const imageData = buildScreen(mainData, mixData);
|
||||
const imageData = this.buildScreen(mainData, mixData);
|
||||
const imageInfo = new screenEmu.ImageInfo(imageData);
|
||||
this._sv.image = imageInfo;
|
||||
blitted = true;
|
||||
@ -812,10 +788,24 @@ export class VideoModesGL implements VideoModes {
|
||||
return blitted;
|
||||
}
|
||||
|
||||
buildScreen(mainData: ImageData, mixData?: ImageData | null) {
|
||||
const details = screenEmu.C.NTSC_DETAILS;
|
||||
const { width, height } = details.imageSize;
|
||||
const { x, y } = this._80colMode ? details.topLeft80Col : details.topLeft;
|
||||
|
||||
if (mixData) {
|
||||
this.context.putImageData(mainData, x, y, 0, 0, 560, 160);
|
||||
this.context.putImageData(mixData, x, y, 0, 160, 560, 32);
|
||||
} else {
|
||||
this.context.putImageData(mainData, x, y);
|
||||
}
|
||||
return this.context.getImageData(0, 0, width, height);
|
||||
}
|
||||
|
||||
blit(altData?: ImageData) {
|
||||
let blitted = false;
|
||||
const hgr = this._hgrs[pageMode - 1];
|
||||
const gr = this._grs[pageMode - 1];
|
||||
const hgr = this._hgrs[this.pageMode - 1];
|
||||
const gr = this._grs[this.pageMode - 1];
|
||||
|
||||
if (this._refreshFlag) {
|
||||
hgr.refresh();
|
||||
@ -828,12 +818,12 @@ export class VideoModesGL implements VideoModes {
|
||||
altData,
|
||||
{ top: 0, left: 0, right: 560, bottom: 192 }
|
||||
);
|
||||
} else if (hiresMode && !textMode) {
|
||||
} else if (this.hiresMode && !this.textMode) {
|
||||
blitted = this.updateImage(
|
||||
hgr.imageData,
|
||||
hgr.dirty,
|
||||
mixedMode ? gr.imageData : null,
|
||||
mixedMode ? gr.dirty : null
|
||||
this.mixedMode ? gr.imageData : null,
|
||||
this.mixedMode ? gr.dirty : null
|
||||
);
|
||||
} else {
|
||||
blitted = this.updateImage(
|
||||
@ -850,24 +840,25 @@ export class VideoModesGL implements VideoModes {
|
||||
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
|
||||
textMode: this.textMode,
|
||||
mixedMode: this.mixedMode,
|
||||
hiresMode: this.hiresMode,
|
||||
pageMode: this.pageMode,
|
||||
_80colMode: this._80colMode,
|
||||
altCharMode: this.altCharMode,
|
||||
an3State: this.an3State,
|
||||
flag: 0
|
||||
};
|
||||
}
|
||||
|
||||
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.textMode = state.textMode;
|
||||
this.mixedMode = state.mixedMode;
|
||||
this.hiresMode = state.hiresMode;
|
||||
this.pageMode = state.pageMode;
|
||||
this._80colMode = state._80colMode;
|
||||
this.altCharMode = state.altCharMode;
|
||||
this.an3State = state.an3State;
|
||||
|
||||
this._grs[0].setState(state.grs[0]);
|
||||
this._grs[1].setState(state.grs[1]);
|
||||
@ -877,12 +868,7 @@ export class VideoModesGL implements VideoModes {
|
||||
}
|
||||
|
||||
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.monoMode = on;
|
||||
this._displayConfig = on ? this.monitorII() : this.defaultMonitor();
|
||||
this._refresh();
|
||||
}
|
||||
@ -893,6 +879,6 @@ export class VideoModesGL implements VideoModes {
|
||||
}
|
||||
|
||||
getText() {
|
||||
return this._grs[pageMode - 1].getText();
|
||||
return this._grs[this.pageMode - 1].getText();
|
||||
}
|
||||
}
|
||||
|
@ -519,11 +519,11 @@ export default class MMU implements Memory, Restorable<MMUState> {
|
||||
break;
|
||||
case LOC.CLRALTCH:
|
||||
this._debug('Alt Char off');
|
||||
this.vm.altchar(false);
|
||||
this.vm.altChar(false);
|
||||
break;
|
||||
case LOC.SETALTCH:
|
||||
this._debug('Alt Char on');
|
||||
this.vm.altchar(true);
|
||||
this.vm.altChar(true);
|
||||
break;
|
||||
}
|
||||
this._updateBanks();
|
||||
|
@ -38,10 +38,6 @@ export class OptionsModal {
|
||||
private handlers: Record<string, OptionHandler> = {}
|
||||
private sections: OptionSection[] = []
|
||||
|
||||
construct() {
|
||||
this.prefs = new Prefs();
|
||||
}
|
||||
|
||||
addOptions(handler: OptionHandler) {
|
||||
const sections = handler.getOptions();
|
||||
for (const section of sections) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { MemoryPages, Restorable, byte, memory } from './types';
|
||||
import { MemoryPages, Restorable, memory } from './types';
|
||||
|
||||
export type bank = 0 | 1;
|
||||
export type pageNo = 1 | 2;
|
||||
@ -11,8 +11,6 @@ export interface Region {
|
||||
}
|
||||
|
||||
export interface GraphicsState {
|
||||
page: byte;
|
||||
mono: boolean;
|
||||
buffer: memory[];
|
||||
}
|
||||
|
||||
@ -25,7 +23,8 @@ export interface VideoModesState {
|
||||
pageMode: pageNo,
|
||||
_80colMode: boolean,
|
||||
altCharMode: boolean,
|
||||
an3: boolean,
|
||||
an3State: boolean,
|
||||
flag: number,
|
||||
}
|
||||
|
||||
export interface VideoPage extends MemoryPages, Restorable<GraphicsState> {
|
||||
@ -35,7 +34,6 @@ export interface VideoPage extends MemoryPages, Restorable<GraphicsState> {
|
||||
bank0(): MemoryPages
|
||||
bank1(): MemoryPages
|
||||
|
||||
mono: (on: boolean) => void
|
||||
refresh: () => void
|
||||
}
|
||||
|
||||
@ -48,14 +46,32 @@ export interface HiresPage extends VideoPage {
|
||||
}
|
||||
|
||||
export interface VideoModes extends Restorable<VideoModesState> {
|
||||
textMode: boolean
|
||||
mixedMode: boolean
|
||||
hiresMode: boolean
|
||||
pageMode: pageNo
|
||||
_80colMode: boolean
|
||||
altCharMode: boolean
|
||||
an3State: boolean
|
||||
doubleHiresMode: boolean
|
||||
|
||||
flag: number
|
||||
monoMode: boolean
|
||||
|
||||
context: CanvasRenderingContext2D;
|
||||
|
||||
page(pageNo: number): void
|
||||
|
||||
blit(altData?: ImageData): boolean
|
||||
|
||||
reset(): void
|
||||
|
||||
setLoresPage(page: pageNo, lores: LoresPage): void
|
||||
setHiresPage(page: pageNo, lores: HiresPage): void
|
||||
|
||||
_80col(on: boolean): void
|
||||
altchar(on: boolean): void
|
||||
altChar(on: boolean): void
|
||||
an3(on: boolean): void
|
||||
doubleHires(on: boolean): void
|
||||
hires(on: boolean): void
|
||||
mixed(on: boolean): void
|
||||
|
1340
package-lock.json
generated
@ -29,14 +29,17 @@
|
||||
"@testing-library/dom": "^7.30.3",
|
||||
"@testing-library/user-event": "^13.1.3",
|
||||
"@types/jest": "^26.0.14",
|
||||
"@types/jest-image-snapshot": "^4.3.0",
|
||||
"@types/micromodal": "^0.3.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.6.1",
|
||||
"@typescript-eslint/parser": "^4.6.1",
|
||||
"ajv": "^6.12.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
"canvas": "^2.7.0",
|
||||
"eslint": "^7.22.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"jest-image-snapshot": "^4.5.0",
|
||||
"node-forge": "^0.10.0",
|
||||
"raw-loader": "^4.0.0",
|
||||
"ts-jest": "^26.5.0",
|
||||
@ -48,7 +51,7 @@
|
||||
"y18n": "^4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"apple2shader": "0.0.1",
|
||||
"apple2shader": "0.0.3",
|
||||
"micromodal": "^0.4.2"
|
||||
}
|
||||
}
|
||||
|
3
test/jest-setup.js
Normal file
@ -0,0 +1,3 @@
|
||||
import { toMatchImageSnapshot } from 'jest-image-snapshot';
|
||||
|
||||
expect.extend({ toMatchImageSnapshot });
|
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 21 KiB |
18
test/js/__mocks__/apple2shader.js
Normal file
@ -0,0 +1,18 @@
|
||||
export const screenEmu = (function () {
|
||||
return {
|
||||
C: {
|
||||
NTSC_DETAILS: {
|
||||
imageSize: {
|
||||
width: 560,
|
||||
height: 192,
|
||||
},
|
||||
},
|
||||
},
|
||||
DisplayConfiguration: class {},
|
||||
Point: class {},
|
||||
ScreenView: class {
|
||||
initOpenGL() {}
|
||||
},
|
||||
Size: class{},
|
||||
};
|
||||
})();
|
258
test/js/canvas.test.ts
Normal file
@ -0,0 +1,258 @@
|
||||
/** @fileoverview Test for canvas.ts. */
|
||||
|
||||
import { VideoPage } from 'js/videomodes';
|
||||
import { LoresPage2D, HiresPage2D, VideoModes2D } from 'js/canvas';
|
||||
import apple2enh_char from 'js/roms/apple2enh_char';
|
||||
import { createImageFromImageData } from 'test/util/image';
|
||||
|
||||
function checkImageData(page: VideoPage) {
|
||||
page.refresh();
|
||||
const img = createImageFromImageData(page.imageData);
|
||||
expect(img).toMatchImageSnapshot();
|
||||
}
|
||||
|
||||
describe('canvas', () => {
|
||||
|
||||
describe('LoresPage', () => {
|
||||
let canvas: HTMLCanvasElement;
|
||||
let lores1: LoresPage2D;
|
||||
let vm: VideoModes2D;
|
||||
|
||||
beforeEach(() => {
|
||||
canvas = document.createElement('canvas');
|
||||
vm = new VideoModes2D(canvas, true);
|
||||
lores1 = new LoresPage2D(vm, 1, apple2enh_char, true);
|
||||
vm.reset();
|
||||
vm.hires(false);
|
||||
});
|
||||
|
||||
describe('text mode', () => {
|
||||
describe('40 column', () => {
|
||||
it('renders', () => {
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
lores1.write(page, off, off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
|
||||
it('renders alt chars', () => {
|
||||
vm.altChar(true);
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
lores1.write(page, off, off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('80 column', () => {
|
||||
it('renders', () => {
|
||||
vm._80col(true);
|
||||
const bank0 = lores1.bank0();
|
||||
const bank1 = lores1.bank1();
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
bank0.write(page, off, off);
|
||||
bank1.write(page, off, 255 - off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
|
||||
it('renders alt chars', () => {
|
||||
vm.altChar(true);
|
||||
vm._80col(true);
|
||||
const bank0 = lores1.bank0();
|
||||
const bank1 = lores1.bank1();
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
bank0.write(page, off, off);
|
||||
bank1.write(page, off, 255 - off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('graphics mode', () => {
|
||||
describe('lores', () => {
|
||||
it('renders', () => {
|
||||
vm.text(false);
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
lores1.write(page, off, off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
|
||||
it('renders mixed', () => {
|
||||
vm.text(false);
|
||||
vm.mixed(true);
|
||||
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
lores1.write(page, off, off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
|
||||
it('renders mono', () => {
|
||||
vm.text(false);
|
||||
vm.mono(true);
|
||||
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
lores1.write(page, off, off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('double lores', () => {
|
||||
it('renders', () => {
|
||||
vm.text(false);
|
||||
vm._80col(true);
|
||||
vm.an3(false);
|
||||
|
||||
const bank0 = lores1.bank0();
|
||||
const bank1 = lores1.bank1();
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
bank0.write(page, off, off);
|
||||
bank1.write(page, off, 255 - off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
|
||||
it('renders mixed', () => {
|
||||
vm.text(false);
|
||||
vm.mixed(true);
|
||||
vm._80col(true);
|
||||
vm.an3(false);
|
||||
|
||||
const bank0 = lores1.bank0();
|
||||
const bank1 = lores1.bank1();
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
bank0.write(page, off, off);
|
||||
bank1.write(page, off, 255 - off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
|
||||
it('renders mono', () => {
|
||||
vm.text(false);
|
||||
vm._80col(true);
|
||||
vm.an3(false);
|
||||
vm.mono(true);
|
||||
|
||||
const bank0 = lores1.bank0();
|
||||
const bank1 = lores1.bank1();
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
bank0.write(page, off, off);
|
||||
bank1.write(page, off, 255 - off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('HiresPage', () => {
|
||||
let canvas: HTMLCanvasElement;
|
||||
let hires1: HiresPage2D;
|
||||
let vm: VideoModes2D;
|
||||
|
||||
beforeEach(() => {
|
||||
canvas = document.createElement('canvas');
|
||||
vm = new VideoModes2D(canvas, true);
|
||||
hires1 = new HiresPage2D(vm, 1);
|
||||
vm.reset();
|
||||
vm.hires(true);
|
||||
});
|
||||
|
||||
describe('hires', () => {
|
||||
it('renders', () => {
|
||||
vm.text(false);
|
||||
for (let page = 0x20; page < 0x40; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
hires1.write(page, off, off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(hires1);
|
||||
});
|
||||
|
||||
it('renders mono', () => {
|
||||
vm.text(false);
|
||||
vm.mono(true);
|
||||
|
||||
for (let page = 0x20; page < 0x40; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
hires1.write(page, off, off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(hires1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('double lores', () => {
|
||||
it('renders', () => {
|
||||
vm.text(false);
|
||||
vm._80col(true);
|
||||
vm.an3(false);
|
||||
|
||||
const bank0 = hires1.bank0();
|
||||
const bank1 = hires1.bank1();
|
||||
for (let page = 0x20; page < 0x40; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
bank0.write(page, off, off);
|
||||
bank1.write(page, off, 255 - off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(hires1);
|
||||
});
|
||||
|
||||
it('renders mono', () => {
|
||||
vm.text(false);
|
||||
vm._80col(true);
|
||||
vm.an3(false);
|
||||
vm.mono(true);
|
||||
|
||||
const bank0 = hires1.bank0();
|
||||
const bank1 = hires1.bank1();
|
||||
for (let page = 0x20; page < 0x40; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
bank0.write(page, off, off);
|
||||
bank1.write(page, off, 255 - off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(hires1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
197
test/js/gl.test.ts
Normal file
@ -0,0 +1,197 @@
|
||||
/** @fileoverview Test for canvas.ts. */
|
||||
|
||||
import { VideoPage } from 'js/videomodes';
|
||||
import { LoresPageGL, HiresPageGL, VideoModesGL } from 'js/gl';
|
||||
import apple2enh_char from 'js/roms/apple2enh_char';
|
||||
import { createImageFromImageData } from 'test/util/image';
|
||||
|
||||
function checkImageData(page: VideoPage) {
|
||||
page.refresh();
|
||||
const img = createImageFromImageData(page.imageData);
|
||||
expect(img).toMatchImageSnapshot();
|
||||
}
|
||||
|
||||
describe('gl', () => {
|
||||
|
||||
describe('LoresPage', () => {
|
||||
let canvas: HTMLCanvasElement;
|
||||
let lores1: LoresPageGL;
|
||||
let vm: VideoModesGL;
|
||||
|
||||
beforeEach(async () => {
|
||||
canvas = document.createElement('canvas');
|
||||
vm = new VideoModesGL(canvas, true);
|
||||
await vm.ready;
|
||||
lores1 = new LoresPageGL(vm, 1, apple2enh_char, true);
|
||||
vm.reset();
|
||||
vm.hires(false);
|
||||
});
|
||||
|
||||
describe('text mode', () => {
|
||||
describe('40 column', () => {
|
||||
it('renders', () => {
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
lores1.write(page, off, off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
|
||||
it('renders alt chars', () => {
|
||||
vm.altChar(true);
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
lores1.write(page, off, off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('80 column', () => {
|
||||
it('renders', () => {
|
||||
vm._80col(true);
|
||||
const bank0 = lores1.bank0();
|
||||
const bank1 = lores1.bank1();
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
bank0.write(page, off, off);
|
||||
bank1.write(page, off, 255 - off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
|
||||
it('renders alt chars', () => {
|
||||
vm.altChar(true);
|
||||
vm._80col(true);
|
||||
const bank0 = lores1.bank0();
|
||||
const bank1 = lores1.bank1();
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
bank0.write(page, off, off);
|
||||
bank1.write(page, off, 255 - off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('graphics mode', () => {
|
||||
describe('lores', () => {
|
||||
it('renders', () => {
|
||||
vm.text(false);
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
lores1.write(page, off, off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
|
||||
it('renders mixed', () => {
|
||||
vm.text(false);
|
||||
vm.mixed(true);
|
||||
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
lores1.write(page, off, off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('double lores', () => {
|
||||
it('renders', () => {
|
||||
vm.text(false);
|
||||
vm._80col(true);
|
||||
vm.an3(false);
|
||||
|
||||
const bank0 = lores1.bank0();
|
||||
const bank1 = lores1.bank1();
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
bank0.write(page, off, off);
|
||||
bank1.write(page, off, 255 - off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
|
||||
it('renders mixed', () => {
|
||||
vm.text(false);
|
||||
vm.mixed(true);
|
||||
vm._80col(true);
|
||||
vm.an3(false);
|
||||
|
||||
const bank0 = lores1.bank0();
|
||||
const bank1 = lores1.bank1();
|
||||
for (let page = 0x4; page < 0x8; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
bank0.write(page, off, off);
|
||||
bank1.write(page, off, 255 - off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(lores1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('HiresPage', () => {
|
||||
let canvas: HTMLCanvasElement;
|
||||
let hires1: HiresPageGL;
|
||||
let vm: VideoModesGL;
|
||||
|
||||
beforeEach(() => {
|
||||
canvas = document.createElement('canvas');
|
||||
vm = new VideoModesGL(canvas, true);
|
||||
hires1 = new HiresPageGL(vm, 1);
|
||||
vm.reset();
|
||||
vm.hires(true);
|
||||
});
|
||||
|
||||
describe('hires', () => {
|
||||
it('renders', () => {
|
||||
vm.text(false);
|
||||
for (let page = 0x20; page < 0x40; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
hires1.write(page, off, off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(hires1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('double lores', () => {
|
||||
it('renders', () => {
|
||||
vm.text(false);
|
||||
vm._80col(true);
|
||||
vm.an3(false);
|
||||
|
||||
const bank0 = hires1.bank0();
|
||||
const bank1 = hires1.bank1();
|
||||
for (let page = 0x20; page < 0x40; page++) {
|
||||
for (let off = 0; off < 0x100; off++) {
|
||||
bank0.write(page, off, off);
|
||||
bank1.write(page, off, 255 - off);
|
||||
}
|
||||
}
|
||||
|
||||
checkImageData(hires1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
9
test/util/image.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const createImageFromImageData = (data: ImageData) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = data.width;
|
||||
canvas.height = data.height;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.putImageData(data, 0, 0);
|
||||
const url = canvas.toDataURL('image/png');
|
||||
return Buffer.from(url.split(',')[1], 'base64');
|
||||
};
|
@ -21,8 +21,11 @@
|
||||
],
|
||||
"js/*": [
|
||||
"js/*"
|
||||
],
|
||||
"test/*": [
|
||||
"test/*"
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"js/**/*",
|
||||
|