diff --git a/index.html b/index.html
index a97ece5..a1cb746 100644
--- a/index.html
+++ b/index.html
@@ -225,7 +225,7 @@ void main() {
async function tryScreenView() {
let canvas = document.getElementById("d");
let gl = canvas.getContext("webgl");
- let sv = screenEmu.getScreenView(gl);
+ let sv = new screenEmu.ScreenView(gl);
await sv.initOpenGL();
sv.freeOpenGL();
}
diff --git a/screenEmu.js b/screenEmu.js
index 31a5dee..0915921 100644
--- a/screenEmu.js
+++ b/screenEmu.js
@@ -1,63 +1,63 @@
"use strict";
-let screenEmu = (function () {
+const screenEmu = (function () {
// From AppleIIVideo.cpp
- let HORIZ_START = 16;
- let HORIZ_BLANK = (9 + HORIZ_START) // 25;
- let HORIZ_DISPLAY = 40;
- let HORIZ_TOTAL = (HORIZ_BLANK + HORIZ_DISPLAY) // 65;
+ const HORIZ_START = 16;
+ const HORIZ_BLANK = (9 + HORIZ_START) // 25;
+ const HORIZ_DISPLAY = 40;
+ const HORIZ_TOTAL = (HORIZ_BLANK + HORIZ_DISPLAY) // 65;
- let CELL_WIDTH = 14;
- let CELL_HEIGHT = 8;
+ const CELL_WIDTH = 14;
+ const CELL_HEIGHT = 8;
- let VERT_NTSC_START = 38;
- let VERT_PAL_START = 48;
- let VERT_DISPLAY = 192;
+ const VERT_NTSC_START = 38;
+ const VERT_PAL_START = 48;
+ const VERT_DISPLAY = 192;
- let BLOCK_WIDTH = HORIZ_DISPLAY; // 40
- let BLOCK_HEIGHT = (VERT_DISPLAY / CELL_HEIGHT); // 24
+ const BLOCK_WIDTH = HORIZ_DISPLAY; // 40
+ const BLOCK_HEIGHT = (VERT_DISPLAY / CELL_HEIGHT); // 24
// From CanvasInterface.h
- let NTSC_FSC = 315/88 * 1e6; // 3579545 = 3.5 Mhz: Color Subcarrier
- let NTSC_4FSC = 4 * NTSC_FSC; // 14318180 = 14.3 Mhz
- let NTSC_HTOTAL = (63+5/9) * 1e-6;
- let NTSC_HLENGTH = (52+8/9) * 1e-6;
- let NTSC_HHALF = (35+2/3) * 1e-6;
- let NTSC_HSTART = NTSC_HHALF - NTSC_HLENGTH/2;
- let NTSC_HEND = NTSC_HHALF + NTSC_HLENGTH/2;
- let NTSC_VTOTAL = 262;
- let NTSC_VLENGTH = 240;
- let NTSC_VSTART = 19;
- let NTSC_VEND = NTSC_VSTART + NTSC_VLENGTH;
+ const NTSC_FSC = 315/88 * 1e6; // 3579545 = 3.5 Mhz: Color Subcarrier
+ const NTSC_4FSC = 4 * NTSC_FSC; // 14318180 = 14.3 Mhz
+ const NTSC_HTOTAL = (63+5/9) * 1e-6;
+ const NTSC_HLENGTH = (52+8/9) * 1e-6;
+ const NTSC_HHALF = (35+2/3) * 1e-6;
+ const NTSC_HSTART = NTSC_HHALF - NTSC_HLENGTH/2;
+ const NTSC_HEND = NTSC_HHALF + NTSC_HLENGTH/2;
+ const NTSC_VTOTAL = 262;
+ const NTSC_VLENGTH = 240;
+ const NTSC_VSTART = 19;
+ const NTSC_VEND = NTSC_VSTART + NTSC_VLENGTH;
- let PAL_FSC = 4433618.75; // Color subcarrier
- let PAL_4FSC = 4 * PAL_FSC;
- let PAL_HTOTAL = 64e-6;
- let PAL_HLENGTH = 52e-6;
- let PAL_HHALF = (37+10/27) * 1e-6;
- let PAL_HSTART = PAL_HHALF - PAL_HLENGTH / 2;
- let PAL_HEND = PAL_HHALF + PAL_HLENGTH / 2;
- let PAL_VTOTAL = 312;
- let PAL_VLENGTH = 288;
- let PAL_VSTART = 21;
- let PAL_VEND = PAL_VSTART + PAL_VLENGTH;
+ const PAL_FSC = 4433618.75; // Color subcarrier
+ const PAL_4FSC = 4 * PAL_FSC;
+ const PAL_HTOTAL = 64e-6;
+ const PAL_HLENGTH = 52e-6;
+ const PAL_HHALF = (37+10/27) * 1e-6;
+ const PAL_HSTART = PAL_HHALF - PAL_HLENGTH / 2;
+ const PAL_HEND = PAL_HHALF + PAL_HLENGTH / 2;
+ const PAL_VTOTAL = 312;
+ const PAL_VLENGTH = 288;
+ const PAL_VSTART = 21;
+ const PAL_VEND = PAL_VSTART + PAL_VLENGTH;
// From AppleIIVideo::updateTiming
- let ntscClockFrequency = NTSC_4FSC * HORIZ_TOTAL / 912;
- let ntscVisibleRect = [[ntscClockFrequency * NTSC_HSTART, NTSC_VSTART],
+ const ntscClockFrequency = NTSC_4FSC * HORIZ_TOTAL / 912;
+ const ntscVisibleRect = [[ntscClockFrequency * NTSC_HSTART, NTSC_VSTART],
[ntscClockFrequency * NTSC_HLENGTH, NTSC_VLENGTH]];
- let ntscDisplayRect = [[HORIZ_START, VERT_NTSC_START],
+ const ntscDisplayRect = [[HORIZ_START, VERT_NTSC_START],
[HORIZ_DISPLAY, VERT_DISPLAY]];
- let ntscVertTotal = NTSC_VTOTAL;
+ const ntscVertTotal = NTSC_VTOTAL;
- let palClockFrequency = 14250450.0 * HORIZ_TOTAL / 912;
- let palVisibleRect = [[palClockFrequency * PAL_HSTART, PAL_VSTART],
+ const palClockFrequency = 14250450.0 * HORIZ_TOTAL / 912;
+ const palVisibleRect = [[palClockFrequency * PAL_HSTART, PAL_VSTART],
[palClockFrequency * PAL_HLENGTH, PAL_VLENGTH]];
- let palDisplayRect = [[HORIZ_START, VERT_PAL_START],
+ const palDisplayRect = [[HORIZ_START, VERT_PAL_START],
[HORIZ_DISPLAY, VERT_DISPLAY]];
- let palVertTotal = PAL_VTOTAL;
+ const palVertTotal = PAL_VTOTAL;
const VERTEX_SHADER =`
// an attribute will receive data from a buffer
@@ -77,6 +77,8 @@ void main() {
const COMPOSITE_SHADER = `
precision mediump float;
+varying vec2 v_texCoord;
+
uniform sampler2D texture;
uniform vec2 textureSize;
uniform float subcarrier;
@@ -84,7 +86,6 @@ uniform sampler2D phaseInfo;
uniform vec3 c0, c1, c2, c3, c4, c5, c6, c7, c8;
uniform mat3 decoderMatrix;
uniform vec3 decoderOffset;
-varying vec2 v_texCoord;
float PI = 3.14159265358979323846264;
@@ -120,6 +121,8 @@ void main(void)
const DISPLAY_SHADER = `
precision mediump float;
+varying vec2 v_texCoord;
+
uniform sampler2D texture;
uniform vec2 textureSize;
uniform float barrel;
@@ -134,7 +137,6 @@ uniform vec2 persistenceSize;
uniform vec2 persistenceOrigin;
uniform float persistenceLevel;
uniform float luminanceGain;
-varying vec2 v_texCoord;
float PI = 3.14159265358979323846264;
@@ -162,26 +164,63 @@ void main(void)
gl_FragColor = vec4(c, 1.0);
}
+`;
+
+ const RGB_SHADER = `
+precision mediump float;
+
+varying vec2 v_texCoord;
+
+uniform sampler2D texture;
+uniform vec2 textureSize;
+uniform vec3 c0, c1, c2, c3, c4, c5, c6, c7, c8;
+uniform mat3 decoderMatrix;
+uniform vec3 decoderOffset;
+
+vec3 pixel(vec2 q)
+{
+ return texture2D(texture, q).rgb;
+}
+
+vec3 pixels(in vec2 q, in float i)
+{
+ return pixel(vec2(q.x + i, q.y)) + pixel(vec2(q.x - i, q.y));
+}
+
+void main(void)
+{
+ vec2 q = v_texCoord;
+ vec3 c = pixel(q) * c0;
+ c += pixels(q, 1.0 / textureSize.x) * c1;
+ c += pixels(q, 2.0 / textureSize.x) * c2;
+ c += pixels(q, 3.0 / textureSize.x) * c3;
+ c += pixels(q, 4.0 / textureSize.x) * c4;
+ c += pixels(q, 5.0 / textureSize.x) * c5;
+ c += pixels(q, 6.0 / textureSize.x) * c6;
+ c += pixels(q, 7.0 / textureSize.x) * c7;
+ c += pixels(q, 8.0 / textureSize.x) * c8;
+ gl_FragColor = vec4(decoderMatrix * c + decoderOffset, 1.0);
+}
`;
function buildTiming(clockFrequency, displayRect, visibleRect, vertTotal) {
- let vertStart = displayRect[0][1];
+ const vertStart = displayRect[0][1];
// Total number of CPU cycles per frame: 17030 for NTSC.
- let frameCycleNum = HORIZ_TOTAL * vertTotal;
+ const frameCycleNum = HORIZ_TOTAL * vertTotal;
// first displayed column.
- let horizStart = Math.floor(displayRect[0][0]);
+ const horizStart = Math.floor(displayRect[0][0]);
// imageSize is [14 * visible rect width in cells, visible lines]
- let imageSize = [Math.floor(CELL_WIDTH * visibleRect[1][0]),
+ const imageSize = [Math.floor(CELL_WIDTH * visibleRect[1][0]),
Math.floor(visibleRect[1][1])];
// imageLeft is # of pixels from first visible point to first displayed point.
- let imageLeft = Math.floor((horizStart-visibleRect[0][0]) * CELL_WIDTH);
- let colorBurst = [2 * Math.PI * (-33/360 + (imageLeft % 4) / 4)];
- let cycleNum = frameCycleNum + 16;
+ const imageLeft = Math.floor((horizStart-visibleRect[0][0]) * CELL_WIDTH);
+ const colorBurst = [2 * Math.PI * (-33/360 + (imageLeft % 4) / 4)];
+ const cycleNum = frameCycleNum + 16;
// First pixel that OpenEmulator draws when painting normally.
- let topLeft = [imageLeft, vertStart - visibleRect[0][1]];
+ const topLeft = [imageLeft, vertStart - visibleRect[0][1]];
// First pixel that OpenEmulator draws when painting 80-column mode.
- let topLeft80Col = [imageLeft - CELL_WIDTH/2, vertStart - visibleRect[0][1]];
+ const topLeft80Col = [imageLeft - CELL_WIDTH/2, vertStart - visibleRect[0][1]];
return {
clockFrequency: clockFrequency,
@@ -218,16 +257,16 @@ void main(void)
if ((image.naturalWidth != 560) || (image.naturalHeight != 192)) {
throw `screenData expects an image 560x192; got ${image.naturalWidth}x${image.naturalHeight}`;
}
- let canvas = document.createElement('canvas');
- let context = canvas.getContext('2d');
- let width = details.imageSize[0];
- let height = details.imageSize[1];
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+ const width = details.imageSize[0];
+ const height = details.imageSize[1];
canvas.width = width;
canvas.height = height;
context.fillStyle = 'rgba(0,0,0,1)';
context.fillRect(0, 0, width, height);
context.drawImage(image, details.topLeft80Col[0], details.topLeft80Col[1]);
- // let myData = context.getImageData(0, 0, image.naturalWidth, image.naturalHeight);
+ // const myData = context.getImageData(0, 0, image.naturalWidth, image.naturalHeight);
return canvas;
};
@@ -246,12 +285,13 @@ void main(void)
const SHADER_NAMES = [
"COMPOSITE",
"DISPLAY",
+ "RGB",
];
const resizeCanvas = (canvas) => {
// Lookup the size the browser is displaying the canvas.
- let displayWidth = canvas.clientWidth;
- let displayHeight = canvas.clientHeight;
+ const displayWidth = canvas.clientWidth;
+ const displayHeight = canvas.clientHeight;
// Check if the canvas is not the same size.
if (canvas.width != displayWidth ||
@@ -264,15 +304,15 @@ void main(void)
// Code from:
// https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html
const createShader = (gl, name, type, source) => {
- let shader = gl.createShader(type);
+ const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
- let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
+ const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
- let log = gl.getShaderInfoLog(shader);
+ const log = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw `unable to compile shader ${name}: \n${log}`;
};
@@ -280,16 +320,16 @@ void main(void)
// Code from:
// https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html
const createProgram = (gl, name, ...shaders) => {
- let program = gl.createProgram();
+ const program = gl.createProgram();
for (let shader of shaders) {
gl.attachShader(program, shader);
}
gl.linkProgram(program);
- let success = gl.getProgramParameter(program, gl.LINK_STATUS);
+ const success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
- let log = gl.getProgramInfoLog(program);
+ const log = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw `unable to compile program ${name}: \n${log}`;
};
@@ -302,6 +342,36 @@ void main(void)
}
};
+ const DisplayConfiguration = class {
+ constructor() {
+ this.videoDecoder = "CANVAS_RGB";
+ this.videoBrightness = 0;
+ this.videoContrast = 1;
+ this.videoSaturation = 1;
+ this.videoHue = 0;
+ this.videoCenter = [0,0];
+ this.videoSize = [1,1];
+ this.videoBandwidth = 14318180;
+ this.videoLumaBandwidth = 600000;
+ this.videoChromaBandwidth = 2000000;
+ this.videoWhiteOnly = false;
+
+ this.displayResolution = [640, 480];
+ this.displayPixelDensity = 72;
+ this.displayBarrel = 0;
+ this.displayScanlineLevel = 0;
+ this.displayShadowMaskLevel = 0;
+ this.displayShadowMaskDotPitch = 1;
+ this.displayShadowMask = "SHADOWMASK_TRIAD";
+ this.displayPersistence = 0;
+ this.displayCenterLighting = 1;
+ this.displayLuminanceGain = 1;
+ }
+ };
+
+ // Corresponds to OEImage. Contains the data on an NTSC/PAL/whatever
+ // image. The `data` field is an ImageData object with the actual
+ // image data.
const ImageInfo = class {
constructor(sampleRate, blackLevel, whiteLevel, subCarrier, colorBurst,
phaseAlternation, data) {
@@ -315,13 +385,11 @@ void main(void)
}
get width() {
- // TODO(zellyn): implement
- return 100;
+ return this.data.width;
}
get height() {
- // TODO(zellyn): implement
- return 100;
+ return this.data.height;
}
};
@@ -338,8 +406,13 @@ void main(void)
this.imageChanged = true;
}
+ set displayConfiguration(displayConfiguration) {
+ this.display = displayConfiguration;
+ this.configurationChanged = true;
+ }
+
async initOpenGL() {
- let gl = this.gl;
+ const gl = this.gl;
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
@@ -359,7 +432,7 @@ void main(void)
}
freeOpenGL() {
- let gl = this.gl;
+ const gl = this.gl;
for (let name of TEXTURE_NAMES) {
gl.deleteTexture(this.textures[name].glTexture);
@@ -379,9 +452,9 @@ void main(void)
}
async loadTexture(path, isMipMap, name) {
- let gl = this.gl;
- let texInfo = this.textures[name];
- let image = await loadImage(path);
+ const gl = this.gl;
+ const texInfo = this.textures[name];
+ const image = await loadImage(path);
gl.bindTexture(gl.TEXTURE_2D, texInfo.glTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA,
gl.RGBA, gl.UNSIGNED_BYTE, image);
@@ -396,14 +469,15 @@ void main(void)
loadShaders() {
this.loadShader("COMPOSITE", COMPOSITE_SHADER);
this.loadShader("DISPLAY", DISPLAY_SHADER);
+ this.loadShader("RGB", RGB_SHADER);
}
loadShader(name, source) {
console.log(`ScreenView.loadShader(${name}): not implemented yet`);
- let glVertexShader = createShader(this.gl, name, this.gl.VERTEX_SHADER, VERTEX_SHADER);
- let glFragmentShader = createShader(this.gl, name, this.gl.FRAGMENT_SHADER, source);
- let glProgram = createProgram(this.gl, name, glVertexShader, glFragmentShader);
+ const glVertexShader = createShader(this.gl, name, this.gl.VERTEX_SHADER, VERTEX_SHADER);
+ const glFragmentShader = createShader(this.gl, name, this.gl.FRAGMENT_SHADER, source);
+ const glProgram = createProgram(this.gl, name, glVertexShader, glFragmentShader);
this.gl.deleteShader(glVertexShader);
this.gl.deleteShader(glFragmentShader);
this.shaders[name] = glProgram;
@@ -418,7 +492,6 @@ void main(void)
}
}
- // TODO(zellyn): implement
vsync() {
// if viewport size has changed:
// glViewPort(0, 0, new_width, new_height);
@@ -427,27 +500,31 @@ void main(void)
this.uploadImage();
}
- // if configuration updated:
- // configureShaders();
+ if (this.configurationChanged) {
+ this.configureShaders();
+ }
- // if image or configuration updated:
- // renderImage();
+ if (this.imageChanged || this.configurationChanged) {
+ this.renderImage();
+ }
- // if anything updated, or displayPersistence != 0.0
- // drawDisplayCanvas();
+ if (this.imageChanged || this.configurationChanged ||
+ this.image.displayPersistence != 0) {
+ this.drawDisplayCanvas();
+ }
}
uploadImage() {
- let image = this.imageInfo;
+ const image = this.imageInfo;
this.resizeTexture("IMAGE_IN", image.width, image.height);
- let texInfo = this.textures["IMAGE_IN"];
+ const texInfo = this.textures["IMAGE_IN"];
gl.bindTexture(gl.TEXTURE_2D, texInfo.glTexture);
- let format = gl.XYZZY;
- gl.texSubImage(gl.TEXTURE_2D, 0,
- 0, 0,
- image.width, image.height,
- format, gl.UNSIGNED_BYTE, image.pixels);
+ const format = gl.LUMINANCE;
+ const type = gl.UNSIGNED_BYTE;
+ gl.texSubImage2D(gl.TEXTURE_2D, 0,
+ 0, 0, // xoffset, yoffset
+ format, type, image.data);
// Update configuration
if ((image.sampleRate != this.imageSampleRate) ||
@@ -460,18 +537,18 @@ void main(void)
this.imageWhiteLevel = image.whiteLevel;
this.imageSubcarrier = image.subcarrier;
- this.isConfigurationUpdated = true;
+ this.configurationChanged = true;
}
// Upload phase info
- let texHeight = 2**Math.ceil(Math.log2(image.height));
- let colorBurst = image.colorBurst
- let phaseAlternation = image.phaseAlternation;
+ const texHeight = 2**Math.ceil(Math.log2(image.height));
+ const colorBurst = image.colorBurst
+ const phaseAlternation = image.phaseAlternation;
- let phaseInfo = new Float32Array(3 * texHeight);
+ const phaseInfo = new Float32Array(3 * texHeight);
for (let x = 0; x < image.height; x++) {
- let c = colorBurst[x % colorBurst.length] / 2 / Math.PI;
+ const c = colorBurst[x % colorBurst.length] / 2 / Math.PI;
phaseInfo[3 * x + 0] = c - Math.floor(c);
phaseInfo[3 * x + 1] = phaseAlternation[x % phaseAlternation.length];
}
@@ -482,8 +559,34 @@ void main(void)
gl.RGB, gl.FLOAT, phaseInfo);
}
- // TODO(zellyn): implement
+ getRenderShader() {
+ switch (this.display.videoDecoder) {
+ case "CANVAS_RGB":
+ case "CANVAS_MONOCHROME":
+ return [this.shaders["RGB"], "RGB"];
+ case "CANVAS_YUV":
+ case "CANVAS_YIQ":
+ case "CANVAS_CXA2025AS":
+ return [this.shaders["COMPOSITE"], "COMPOSITE"];
+ }
+ return [null, null];
+ }
+
configureShaders() {
+ const gl = this.gl;
+
+ const [renderShader, renderShaderName] = this.getRenderShader();
+ const displayShader = this.shaders["DISPLAY"];
+
+ if (!renderShader || !displayShader)
+ return;
+
+ const isCompositeShader = (renderShaderName == "RGB");
+
+ // Render shader
+ gl.useProgram(renderShader);
+
+ // TODO(zellyn): implement the rest
}
// TODO(zellyn): implement
@@ -498,8 +601,8 @@ void main(void)
// highest power of two width and height. Wouldn't be
// necessary with webgl2.
resizeTexture(name, width, height) {
- let gl = this.gl;
- let texInfo = this.textures[name];
+ const gl = this.gl;
+ const texInfo = this.textures[name];
if (!!texInfo) {
throw `Cannot find texture named ${name}`;
}
@@ -538,9 +641,13 @@ void main(void)
},
loadImage: loadImage,
screenData: screenData,
- getScreenView: (gl) => new ScreenView(gl),
resizeCanvas: resizeCanvas,
createShader: createShader,
createProgram: createProgram,
+
+ // Classes.
+ ScreenView: ScreenView,
+ DisplayConfiguration: DisplayConfiguration,
+ ImageInfo: ImageInfo,
};
})();