webgl: renderImage now correct except for final copy; untabify all

This commit is contained in:
Zellyn Hunter 2018-05-09 22:35:14 -04:00
parent dba6c94236
commit 1881b62f3a
2 changed files with 262 additions and 228 deletions

View File

@ -52,108 +52,108 @@ void main() {
<div class="controls"> <div class="controls">
<table> <table>
<tr> <tr>
<td>Decoder</td> <td>Decoder</td>
<td> <td>
<select id="decoder"> <select id="decoder">
<option value="COMPOSITE_YUV">Composite Y'UV</option> <option value="COMPOSITE_YUV">Composite Y'UV</option>
<option value="COMPOSITE_YIQ">Composite Y'IQ</option> <option value="COMPOSITE_YIQ">Composite Y'IQ</option>
<option value="COMPOSITE_CXA2025AS">Composite CXA2025AS</option> <option value="COMPOSITE_CXA2025AS">Composite CXA2025AS</option>
</select> </select>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Brightness</td> <td>Brightness</td>
<td><input type="range" min="-1" max="1" step="0.01" value="0" class="slider" id="videoBrightness"></td> <td><input type="range" min="-1" max="1" step="0.01" value="0" class="slider" id="videoBrightness"></td>
</tr> </tr>
<tr> <tr>
<td>Contrast</td> <td>Contrast</td>
<td><input type="range" min="0" max="2" step="0.01" value="1" class="slider" id="videoContrast"></td> <td><input type="range" min="0" max="2" step="0.01" value="1" class="slider" id="videoContrast"></td>
</tr> </tr>
<tr> <tr>
<td>Saturation</td> <td>Saturation</td>
<td><input type="range" min="0" max="2" step="0.01" value="1" class="slider" id="videoSaturation"></td> <td><input type="range" min="0" max="2" step="0.01" value="1" class="slider" id="videoSaturation"></td>
</tr> </tr>
<tr> <tr>
<td>Hue</td> <td>Hue</td>
<td><input type="range" min="-0.5" max="0.5" step="0.01" value="0" class="slider" id="videoHue"></td> <td><input type="range" min="-0.5" max="0.5" step="0.01" value="0" class="slider" id="videoHue"></td>
</tr> </tr>
<tr> <tr>
<td>White Only</td> <td>White Only</td>
<td><input type="checkbox" id="white-only"></td> <td><input type="checkbox" id="white-only"></td>
</tr> </tr>
<tr> <tr>
<td>Horizontal Center</td> <td>Horizontal Center</td>
<td><input type="range" min="-0.1" max="0.1" step="0.01" value="0" class="slider" id="videoHorizontalCenter"></td> <td><input type="range" min="-0.1" max="0.1" step="0.01" value="0" class="slider" id="videoHorizontalCenter"></td>
</tr> </tr>
<tr> <tr>
<td>Horizontal Size</td> <td>Horizontal Size</td>
<td><input type="range" min="0.85" max="1.25" step="0.01" value="1.05" class="slider" id="videoHorizontalSize"></td> <td><input type="range" min="0.85" max="1.25" step="0.01" value="1.05" class="slider" id="videoHorizontalSize"></td>
</tr> </tr>
<tr> <tr>
<td>Vertical Center</td> <td>Vertical Center</td>
<td><input type="range" min="-0.1" max="0.1" step="0.01" value="0" class="slider" id="videoVerticalCenter"></td> <td><input type="range" min="-0.1" max="0.1" step="0.01" value="0" class="slider" id="videoVerticalCenter"></td>
</tr> </tr>
<tr> <tr>
<td>Vertical Size</td> <td>Vertical Size</td>
<td><input type="range" min="0.85" max="1.25" step="0.01" value="1.05" class="slider" id="videoVerticalSize"></td> <td><input type="range" min="0.85" max="1.25" step="0.01" value="1.05" class="slider" id="videoVerticalSize"></td>
</tr> </tr>
<tr> <tr>
<td>Luma Bandwidth</td> <td>Luma Bandwidth</td>
<td><input type="range" min="0" max="7159090" step="1" value="2000000" class="slider" id="videoLumaBandwidth"></td> <td><input type="range" min="0" max="7159090" step="1" value="2000000" class="slider" id="videoLumaBandwidth"></td>
</tr> </tr>
<tr> <tr>
<td>Chroma Bandwidth</td> <td>Chroma Bandwidth</td>
<td><input type="range" min="0" max="7159090" step="1" value="600000" class="slider" id="videoChromaBandwidth"></td> <td><input type="range" min="0" max="7159090" step="1" value="600000" class="slider" id="videoChromaBandwidth"></td>
</tr> </tr>
<tr> <tr>
<td>B/W Bandwidth</td> <td>B/W Bandwidth</td>
<td><input type="range" min="0" max="7159090" step="1" value="6000000" class="slider" id="videoBandwidth"></td> <td><input type="range" min="0" max="7159090" step="1" value="6000000" class="slider" id="videoBandwidth"></td>
</tr> </tr>
<tr> <tr>
<td>Barrel</td> <td>Barrel</td>
<td><input type="range" min="0" max="1" step="0.01" value="0.05" class="slider" id="displayBarrel"></td> <td><input type="range" min="0" max="1" step="0.01" value="0.05" class="slider" id="displayBarrel"></td>
</tr> </tr>
<tr> <tr>
<td>Scanline Level</td> <td>Scanline Level</td>
<td><input type="range" min="0" max="1" step="0.01" value="0.05" class="slider" id="displayScanlineLevel"></td> <td><input type="range" min="0" max="1" step="0.01" value="0.05" class="slider" id="displayScanlineLevel"></td>
</tr> </tr>
<tr> <tr>
<td>Shadow Mask Level</td> <td>Shadow Mask Level</td>
<td><input type="range" min="0" max="1" step="0.01" value="0.05" class="slider" id="displayShadowMaskLevel"></td> <td><input type="range" min="0" max="1" step="0.01" value="0.05" class="slider" id="displayShadowMaskLevel"></td>
</tr> </tr>
<tr> <tr>
<td>Shadow Mask Dot Pitch</td> <td>Shadow Mask Dot Pitch</td>
<td><input type="range" min="0" max="2" step="0.01" value="0.5" class="slider" id="displayShadowMaskDotPitch"></td> <td><input type="range" min="0" max="2" step="0.01" value="0.5" class="slider" id="displayShadowMaskDotPitch"></td>
</tr> </tr>
<tr> <tr>
<td>Shadow Mask</td> <td>Shadow Mask</td>
<td> <td>
<select id="shadow-mask"> <select id="shadow-mask">
<option value="triad">Triad</option> <option value="triad">Triad</option>
<option value="inline">Inline</option> <option value="inline">Inline</option>
<option value="aperture">Aperture</option> <option value="aperture">Aperture</option>
<option value="lcd">LCD</option> <option value="lcd">LCD</option>
<option value="bayer">Bayer</option> <option value="bayer">Bayer</option>
</select> </select>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Persistence</td> <td>Persistence</td>
<td><input type="range" min="0" max="1" step="0.01" value="0" class="slider" id="displayPersistence"></td> <td><input type="range" min="0" max="1" step="0.01" value="0" class="slider" id="displayPersistence"></td>
</tr> </tr>
<tr> <tr>
<td>Center Lighting</td> <td>Center Lighting</td>
<td><input type="range" min="0" max="1" step="0.01" value="1" class="slider" id="displayCenterLighting"></td> <td><input type="range" min="0" max="1" step="0.01" value="1" class="slider" id="displayCenterLighting"></td>
</tr> </tr>
<tr> <tr>
<td>Luminance Gain</td> <td>Luminance Gain</td>
<td><input type="range" min="1" max="2" step="0.01" value="1" class="slider" id="displayLuminanceGain"></td> <td><input type="range" min="1" max="2" step="0.01" value="1" class="slider" id="displayLuminanceGain"></td>
</tr> </tr>
</table> </table>
</div> </div>
<div class="screen"> <div class="screen">
<canvas id="c"></canvas> <canvas id="c"></canvas>
<canvas id="d"></canvas> <canvas id="d" width="755" height="240"></canvas>
</div> </div>
</div><!-- class="wrapper" --> </div><!-- class="wrapper" -->
<script> <script>
@ -218,16 +218,32 @@ void main() {
gl.drawArrays(primitiveType, offset, count); gl.drawArrays(primitiveType, offset, count);
// screenEmu.loadImage("images/airheart-560x192.png").then(image => { // screenEmu.loadImage("images/airheart-560x192.png").then(image => {
// let c = screenEmu.screenData(image, screenEmu.C.NTSC_DETAILS); // let [c, data] = screenEmu.screenData(image, screenEmu.C.NTSC_DETAILS);
// document.body.appendChild(c); // document.body.appendChild(c);
// }); // });
async function tryScreenView() { async function tryScreenView() {
const image = await screenEmu.loadImage("images/airheart-560x192.png");
const [imageCanvas, imageData] = screenEmu.screenData(image, screenEmu.C.NTSC_DETAILS);
let canvas = document.getElementById("d"); let canvas = document.getElementById("d");
let sv = new screenEmu.ScreenView(canvas); let sv = new screenEmu.ScreenView(canvas);
await sv.initOpenGL(); await sv.initOpenGL();
let imageInfo = new screenEmu.ImageInfo(0, 0, 0, 0, 0, 0, new ImageData(10, 10)); const sampleRate = 4 * screenEmu.C.NTSC_DETAILS.fsc;
const blackLevel = 0;
const whiteLevel = 1;
const subCarrier = screenEmu.C.NTSC_DETAILS.fsc;
const colorBurst = screenEmu.C.NTSC_DETAILS.colorBurst;
const phaseAlternation = [false];
let imageInfo = new screenEmu.ImageInfo(
sampleRate,
blackLevel,
whiteLevel,
subCarrier,
colorBurst,
phaseAlternation,
imageData);
let displayConfig = new screenEmu.DisplayConfiguration(); let displayConfig = new screenEmu.DisplayConfiguration();
sv.image = imageInfo; sv.image = imageInfo;
sv.displayConfiguration = displayConfig; sv.displayConfiguration = displayConfig;

View File

@ -52,16 +52,16 @@ const screenEmu = (function () {
// From AppleIIVideo::updateTiming // From AppleIIVideo::updateTiming
const ntscClockFrequency = NTSC_4FSC * HORIZ_TOTAL / 912; const ntscClockFrequency = NTSC_4FSC * HORIZ_TOTAL / 912;
const ntscVisibleRect = [[ntscClockFrequency * NTSC_HSTART, NTSC_VSTART], const ntscVisibleRect = [[ntscClockFrequency * NTSC_HSTART, NTSC_VSTART],
[ntscClockFrequency * NTSC_HLENGTH, NTSC_VLENGTH]]; [ntscClockFrequency * NTSC_HLENGTH, NTSC_VLENGTH]];
const ntscDisplayRect = [[HORIZ_START, VERT_NTSC_START], const ntscDisplayRect = [[HORIZ_START, VERT_NTSC_START],
[HORIZ_DISPLAY, VERT_DISPLAY]]; [HORIZ_DISPLAY, VERT_DISPLAY]];
const ntscVertTotal = NTSC_VTOTAL; const ntscVertTotal = NTSC_VTOTAL;
const palClockFrequency = 14250450.0 * HORIZ_TOTAL / 912; const palClockFrequency = 14250450.0 * HORIZ_TOTAL / 912;
const palVisibleRect = [[palClockFrequency * PAL_HSTART, PAL_VSTART], const palVisibleRect = [[palClockFrequency * PAL_HSTART, PAL_VSTART],
[palClockFrequency * PAL_HLENGTH, PAL_VLENGTH]]; [palClockFrequency * PAL_HLENGTH, PAL_VLENGTH]];
const palDisplayRect = [[HORIZ_START, VERT_PAL_START], const palDisplayRect = [[HORIZ_START, VERT_PAL_START],
[HORIZ_DISPLAY, VERT_DISPLAY]]; [HORIZ_DISPLAY, VERT_DISPLAY]];
const palVertTotal = PAL_VTOTAL; const palVertTotal = PAL_VTOTAL;
const VERTEX_SHADER =` const VERTEX_SHADER =`
@ -208,7 +208,7 @@ void main(void)
} }
`; `;
function buildTiming(clockFrequency, displayRect, visibleRect, vertTotal) { function buildTiming(clockFrequency, displayRect, visibleRect, vertTotal, fsc) {
const vertStart = displayRect[0][1]; const vertStart = displayRect[0][1];
// Total number of CPU cycles per frame: 17030 for NTSC. // Total number of CPU cycles per frame: 17030 for NTSC.
const frameCycleNum = HORIZ_TOTAL * vertTotal; const frameCycleNum = HORIZ_TOTAL * vertTotal;
@ -216,7 +216,7 @@ void main(void)
const horizStart = Math.floor(displayRect[0][0]); const horizStart = Math.floor(displayRect[0][0]);
// imageSize is [14 * visible rect width in cells, visible lines] // imageSize is [14 * visible rect width in cells, visible lines]
const imageSize = [Math.floor(CELL_WIDTH * visibleRect[1][0]), const imageSize = [Math.floor(CELL_WIDTH * visibleRect[1][0]),
Math.floor(visibleRect[1][1])]; Math.floor(visibleRect[1][1])];
// imageLeft is # of pixels from first visible point to first displayed point. // imageLeft is # of pixels from first visible point to first displayed point.
const imageLeft = Math.floor((horizStart-visibleRect[0][0]) * CELL_WIDTH); const imageLeft = Math.floor((horizStart-visibleRect[0][0]) * CELL_WIDTH);
const colorBurst = [2 * Math.PI * (-33/360 + (imageLeft % 4) / 4)]; const colorBurst = [2 * Math.PI * (-33/360 + (imageLeft % 4) / 4)];
@ -228,6 +228,7 @@ void main(void)
const topLeft80Col = [imageLeft - CELL_WIDTH/2, vertStart - visibleRect[0][1]]; const topLeft80Col = [imageLeft - CELL_WIDTH/2, vertStart - visibleRect[0][1]];
return { return {
fsc: fsc,
clockFrequency: clockFrequency, clockFrequency: clockFrequency,
displayRect: displayRect, displayRect: displayRect,
visibleRect: visibleRect, visibleRect: visibleRect,
@ -311,11 +312,11 @@ void main(void)
const vec = this.data; const vec = this.data;
let sum = 0; let sum = 0;
for (const item of vec) { for (const item of vec) {
sum += item; sum += item;
} }
const gain = 1/sum; const gain = 1/sum;
for (const i in vec) { for (const i in vec) {
vec[i] *= gain; vec[i] *= gain;
} }
return this; return this;
} }
@ -324,17 +325,17 @@ void main(void)
mul(other) { mul(other) {
const w = new Vector(0); const w = new Vector(0);
if ((typeof other != "number") && (this.data.length != other.data.length)) { if ((typeof other != "number") && (this.data.length != other.data.length)) {
return w; return w;
} }
w.data = new Float32Array(this.data); w.data = new Float32Array(this.data);
for (let i = 0; i < w.data.length; i++) { for (let i = 0; i < w.data.length; i++) {
if (typeof other == "number") { if (typeof other == "number") {
w.data[i] *= other; w.data[i] *= other;
} else { } else {
w.data[i] *= other.data[i]; w.data[i] *= other.data[i];
} }
} }
return w; return w;
@ -344,15 +345,15 @@ void main(void)
const size = this.data.length; const size = this.data.length;
const w = new Vector(size); const w = new Vector(size);
for (let i = 0; i < size; i++) { for (let i = 0; i < size; i++) {
const omega = 2 * Math.PI * i / size; const omega = 2 * Math.PI * i / size;
for (let j = 0; j < size; j++) { for (let j = 0; j < size; j++) {
w.data[i] += this.data[j] * Math.cos(j * omega); w.data[i] += this.data[j] * Math.cos(j * omega);
} }
} }
for (let i = 0; i < size; i++) { for (let i = 0; i < size; i++) {
w.data[i] /= size; w.data[i] /= size;
} }
return w; return w;
@ -361,7 +362,7 @@ void main(void)
resize(n) { resize(n) {
const newData = new Float32Array(n); const newData = new Float32Array(n);
for (let i=0; i < Math.min(newData.length, this.data.length); i++) { for (let i=0; i < Math.min(newData.length, this.data.length); i++) {
newData[i] = this.data[i]; newData[i] = this.data[i];
} }
this.data = newData; this.data = newData;
return this; return this;
@ -378,11 +379,11 @@ void main(void)
const alpha = Math.cosh(Math.acosh(Math.pow(10, sidelobeDb / 20)) / m); const alpha = Math.cosh(Math.acosh(Math.pow(10, sidelobeDb / 20)) / m);
for (let i = 0; i < m; i++) { for (let i = 0; i < m; i++) {
const a = Math.abs(alpha * Math.cos(Math.PI * i / m)); const a = Math.abs(alpha * Math.cos(Math.PI * i / m));
if (a > 1) if (a > 1)
w.data[i] = Math.pow(-1, i) * Math.cosh(m * Math.acosh(a)); w.data[i] = Math.pow(-1, i) * Math.cosh(m * Math.acosh(a));
else else
w.data[i] = Math.pow(-1, i) * Math.cos(m * Math.acos(a)); w.data[i] = Math.pow(-1, i) * Math.cos(m * Math.acos(a));
} }
w = w.realIDFT(); w = w.realIDFT();
@ -393,7 +394,7 @@ void main(void)
const max = w.data.reduce((prev, cur) => Math.max(prev, Math.abs(cur))); const max = w.data.reduce((prev, cur) => Math.max(prev, Math.abs(cur)));
for (const i in w.data) { for (const i in w.data) {
w.data[i] /= max; w.data[i] /= max;
} }
return w; return w;
@ -406,9 +407,9 @@ void main(void)
const halfN = Math.floor(n / 2); const halfN = Math.floor(n / 2);
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
const x = 2 * Math.PI * fc * (i - halfN); const x = 2 * Math.PI * fc * (i - halfN);
v.data[i] = (x == 0.0) ? 1.0 : Math.sin(x) / x; v.data[i] = (x == 0.0) ? 1.0 : Math.sin(x) / x;
} }
return v; return v;
@ -418,8 +419,8 @@ void main(void)
const Matrix3 = class { const Matrix3 = class {
constructor(c00, c01, c02, constructor(c00, c01, c02,
c10, c11, c12, c10, c11, c12,
c20, c21, c22) { c20, c21, c22) {
this.data = new Float32Array([c00, c01, c02, c10, c11, c12, c20, c21, c22]); this.data = new Float32Array([c00, c01, c02, c10, c11, c12, c20, c21, c22]);
} }
@ -430,15 +431,15 @@ void main(void)
mul(val) { mul(val) {
const m = new Matrix3(0,0,0,0,0,0,0,0,0); const m = new Matrix3(0,0,0,0,0,0,0,0,0);
if (typeof val == "number") { if (typeof val == "number") {
m.data = m.data.map(x => x * val); m.data = this.data.map(x => x * val);
} else { } else {
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) { for (let j = 0; j < 3; j++) {
for (let k = 0; k < 3; k++) { for (let k = 0; k < 3; k++) {
m.data[i * 3 + j] += val.data[i * 3 + k] * this.data[k * 3 + j]; m.data[i * 3 + j] += val.data[i * 3 + k] * this.data[k * 3 + j];
} }
} }
} }
} }
return m; return m;
} }
@ -446,12 +447,12 @@ void main(void)
// https://codereview.stackexchange.com/a/128619 // https://codereview.stackexchange.com/a/128619
const loadImage = path => const loadImage = path =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const img = new Image(); const img = new Image();
img.onload = () => resolve(img); img.onload = () => resolve(img);
img.onerror = () => reject(`error loading '${path}'`); img.onerror = () => reject(`error loading '${path}'`);
img.src = path; img.src = path;
}); });
// Given an image that's 560x192, render it into the larger space // Given an image that's 560x192, render it into the larger space
// required for NTSC or PAL. // required for NTSC or PAL.
@ -461,7 +462,7 @@ void main(void)
const screenData = (image, details) => { const screenData = (image, details) => {
if ((image.naturalWidth != 560) || (image.naturalHeight != 192)) { if ((image.naturalWidth != 560) || (image.naturalHeight != 192)) {
throw new Error('screenData expects an image 560x192;' + throw new Error('screenData expects an image 560x192;' +
` got ${image.naturalWidth}x${image.naturalHeight}`); ` got ${image.naturalWidth}x${image.naturalHeight}`);
} }
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
@ -472,8 +473,22 @@ void main(void)
context.fillStyle = 'rgba(0,0,0,1)'; context.fillStyle = 'rgba(0,0,0,1)';
context.fillRect(0, 0, width, height); context.fillRect(0, 0, width, height);
context.drawImage(image, details.topLeft80Col[0], details.topLeft80Col[1]); context.drawImage(image, details.topLeft80Col[0], details.topLeft80Col[1]);
// const myData = context.getImageData(0, 0, image.naturalWidth, image.naturalHeight); const imageData = context.getImageData(0, 0, width, height);
return canvas; return [canvas, imageData];
};
// Given an ImageData (RGBA), convert to luminance by taking the max
// of (R,G,B) for each pixel. Return a Uint8Array.
const luminanceData = (imageData) => {
const width = imageData.width;
const height = imageData.height;
const data = imageData.data;
const size = width * height;
const ary = new Uint8Array(size);
for (let i = 0; i < size; i++) {
ary[i] = Math.max(data[i*4], data[i*4+1], data[i*4+2]);
}
return ary;
}; };
const TEXTURE_NAMES = [ const TEXTURE_NAMES = [
@ -566,17 +581,17 @@ void main(void)
this.videoHue = 0; this.videoHue = 0;
this.videoCenter = [0,0]; this.videoCenter = [0,0];
this.videoSize = [1,1]; this.videoSize = [1,1];
this.videoBandwidth = 14318180; this.videoBandwidth = 6000000; // 14318180;
this.videoLumaBandwidth = 600000; this.videoLumaBandwidth = 2000000; // 600000;
this.videoChromaBandwidth = 2000000; this.videoChromaBandwidth = 600000; // 2000000;
this.videoWhiteOnly = false; this.videoWhiteOnly = false;
this.displayResolution = [640, 480]; this.displayResolution = [640, 480];
this.displayPixelDensity = 72; this.displayPixelDensity = 72;
this.displayBarrel = 0; this.displayBarrel = 0.05; // 0;
this.displayScanlineLevel = 0; this.displayScanlineLevel = 0.05; // 0;
this.displayShadowMaskLevel = 0; this.displayShadowMaskLevel = 0.05; // 0;
this.displayShadowMaskDotPitch = 1; this.displayShadowMaskDotPitch = 0.5; // 1;
this.displayShadowMask = "SHADOWMASK_TRIAD"; this.displayShadowMask = "SHADOWMASK_TRIAD";
this.displayPersistence = 0; this.displayPersistence = 0;
this.displayCenterLighting = 1; this.displayCenterLighting = 1;
@ -589,12 +604,12 @@ void main(void)
// image data. // image data.
const ImageInfo = class { const ImageInfo = class {
constructor(sampleRate, blackLevel, whiteLevel, subCarrier, colorBurst, constructor(sampleRate, blackLevel, whiteLevel, subCarrier, colorBurst,
phaseAlternation, data) { phaseAlternation, data) {
if (typeof data != "object") { if (typeof data != "object") {
throw new Error(`want typeof data == 'object'; got '${typeof data}'`); throw new Error(`want typeof data == 'object'; got '${typeof data}'`);
} }
if (!(data instanceof ImageData)) { if (!(data instanceof ImageData)) {
throw new Error(`want data instanceof ImageData; got '${data.constructor.name}'`); throw new Error(`want data instanceof ImageData; got '${data.constructor.name}'`);
} }
this.sampleRate = sampleRate; this.sampleRate = sampleRate;
this.blackLevel = blackLevel; this.blackLevel = blackLevel;
@ -623,7 +638,7 @@ void main(void)
const gl = canvas.getContext("webgl"); const gl = canvas.getContext("webgl");
const float_texture_ext = gl.getExtension('OES_texture_float'); const float_texture_ext = gl.getExtension('OES_texture_float');
if (float_texture_ext == null) { if (float_texture_ext == null) {
throw new Error("WebGL extension 'OES_texture_float' unavailable"); throw new Error("WebGL extension 'OES_texture_float' unavailable");
} }
this.canvas = canvas; this.canvas = canvas;
@ -667,11 +682,11 @@ void main(void)
this.textures = {}; this.textures = {};
for (let name of TEXTURE_NAMES) { for (let name of TEXTURE_NAMES) {
this.textures[name] = new TextureInfo(0, 0, gl.createTexture()); this.textures[name] = new TextureInfo(0, 0, gl.createTexture());
} }
for (let name of BUFFER_NAMES) { for (let name of BUFFER_NAMES) {
this.buffers[name] = gl.createBuffer(); this.buffers[name] = gl.createBuffer();
} }
await this.loadTextures(); await this.loadTextures();
@ -686,11 +701,11 @@ void main(void)
const gl = this.gl; const gl = this.gl;
for (let name of TEXTURE_NAMES) { for (let name of TEXTURE_NAMES) {
gl.deleteTexture(this.textures[name].glTexture); gl.deleteTexture(this.textures[name].glTexture);
} }
for (let name of BUFFER_NAMES) { for (let name of BUFFER_NAMES) {
gl.deleteBuffer(this.buffers[name]); gl.deleteBuffer(this.buffers[name]);
} }
this.deleteShaders(); this.deleteShaders();
@ -698,11 +713,11 @@ void main(void)
loadTextures() { loadTextures() {
return Promise.all([ return Promise.all([
this.loadTexture("textures/Shadow Mask Triad.png", true, "SHADOWMASK_TRIAD"), this.loadTexture("textures/Shadow Mask Triad.png", true, "SHADOWMASK_TRIAD"),
this.loadTexture("textures/Shadow Mask Inline.png", true, "SHADOWMASK_INLINE"), this.loadTexture("textures/Shadow Mask Inline.png", true, "SHADOWMASK_INLINE"),
this.loadTexture("textures/Shadow Mask Aperture.png", true, "SHADOWMASK_APERTURE"), this.loadTexture("textures/Shadow Mask Aperture.png", true, "SHADOWMASK_APERTURE"),
this.loadTexture("textures/Shadow Mask LCD.png", true, "SHADOWMASK_LCD"), this.loadTexture("textures/Shadow Mask LCD.png", true, "SHADOWMASK_LCD"),
this.loadTexture("textures/Shadow Mask Bayer.png", true, "SHADOWMASK_BAYER"), this.loadTexture("textures/Shadow Mask Bayer.png", true, "SHADOWMASK_BAYER"),
]); ]);
} }
@ -712,9 +727,9 @@ void main(void)
const image = await loadImage(path); const image = await loadImage(path);
gl.bindTexture(gl.TEXTURE_2D, texInfo.glTexture); gl.bindTexture(gl.TEXTURE_2D, texInfo.glTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA,
gl.RGBA, gl.UNSIGNED_BYTE, image); gl.RGBA, gl.UNSIGNED_BYTE, image);
if (isMipMap) { if (isMipMap) {
gl.generateMipmap(gl.TEXTURE_2D); gl.generateMipmap(gl.TEXTURE_2D);
} }
texInfo.width = image.naturalWidth; texInfo.width = image.naturalWidth;
@ -738,10 +753,10 @@ void main(void)
deleteShaders() { deleteShaders() {
for (let name of SHADER_NAMES) { for (let name of SHADER_NAMES) {
if (this.shaders[name]) { if (this.shaders[name]) {
this.gl.deleteProgram(this.shaders[name]); this.gl.deleteProgram(this.shaders[name]);
this.shaders[name] = false; this.shaders[name] = false;
} }
} }
} }
@ -753,27 +768,27 @@ void main(void)
// if viewport size has changed: // if viewport size has changed:
if ((this.viewportSize.width != canvasWidth) if ((this.viewportSize.width != canvasWidth)
|| (this.viewportSize.height != this.canvasHeight)) { || (this.viewportSize.height != this.canvasHeight)) {
this.viewportSize = new Size(canvasWidth, canvasHeight); this.viewportSize = new Size(canvasWidth, canvasHeight);
gl.viewport(0, 0, canvasWidth, canvasHeight); gl.viewport(0, 0, canvasWidth, canvasHeight);
this.configurationChanged = true; this.configurationChanged = true;
} }
if (this.imageChanged) { if (this.imageChanged) {
this.uploadImage(); this.uploadImage();
} }
if (this.configurationChanged) { if (this.configurationChanged) {
this.configureShaders(); this.configureShaders();
} }
if (this.imageChanged || this.configurationChanged) { if (this.imageChanged || this.configurationChanged) {
this.renderImage(); this.renderImage();
} }
if (this.imageChanged || this.configurationChanged || if (this.imageChanged || this.configurationChanged ||
this.image.displayPersistence != 0) { this.image.displayPersistence != 0) {
this.drawDisplayCanvas(); this.drawDisplayCanvas();
} }
} }
@ -786,20 +801,23 @@ void main(void)
gl.bindTexture(gl.TEXTURE_2D, texInfoImage.glTexture); gl.bindTexture(gl.TEXTURE_2D, texInfoImage.glTexture);
const format = gl.LUMINANCE; const format = gl.LUMINANCE;
const type = gl.UNSIGNED_BYTE; const type = gl.UNSIGNED_BYTE;
const luminance = luminanceData(image.data);
gl.texSubImage2D(gl.TEXTURE_2D, 0, gl.texSubImage2D(gl.TEXTURE_2D, 0,
0, 0, // xoffset, yoffset 0, 0, // xoffset, yoffset
format, type, image.data); image.data.width,
image.data.height,
format, type, luminance);
// Update configuration // Update configuration
if ((image.sampleRate != this.imageSampleRate) || if ((image.sampleRate != this.imageSampleRate) ||
(image.blackLevel != this.imageBlackLevel) || (image.blackLevel != this.imageBlackLevel) ||
(image.whiteLevel != this.imageWhiteLevel) || (image.whiteLevel != this.imageWhiteLevel) ||
(image.subcarrier != this.imageSubcarrier)) (image.subCarrier != this.imageSubcarrier))
{ {
this.imageSampleRate = image.sampleRate; this.imageSampleRate = image.sampleRate;
this.imageBlackLevel = image.blackLevel; this.imageBlackLevel = image.blackLevel;
this.imageWhiteLevel = image.whiteLevel; this.imageWhiteLevel = image.whiteLevel;
this.imageSubcarrier = image.subcarrier; this.imageSubcarrier = image.subCarrier;
this.configurationChanged = true; this.configurationChanged = true;
} }
@ -812,26 +830,26 @@ void main(void)
const phaseInfo = new Float32Array(3 * texHeight); const phaseInfo = new Float32Array(3 * texHeight);
for (let x = 0; x < image.height; x++) { for (let x = 0; x < image.height; x++) {
const 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 + 0] = c - Math.floor(c);
phaseInfo[3 * x + 1] = phaseAlternation[x % phaseAlternation.length]; phaseInfo[3 * x + 1] = phaseAlternation[x % phaseAlternation.length];
} }
const texInfoPhase = this.textures["IMAGE_PHASEINFO"]; const texInfoPhase = this.textures["IMAGE_PHASEINFO"];
gl.bindTexture(gl.TEXTURE_2D, texInfoPhase.glTexture); gl.bindTexture(gl.TEXTURE_2D, texInfoPhase.glTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 1, texHeight, 0, gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 1, texHeight, 0,
gl.RGB, gl.FLOAT, phaseInfo); gl.RGB, gl.FLOAT, phaseInfo);
} }
getRenderShader() { getRenderShader() {
switch (this.display.videoDecoder) { switch (this.display.videoDecoder) {
case "CANVAS_RGB": case "CANVAS_RGB":
case "CANVAS_MONOCHROME": case "CANVAS_MONOCHROME":
return [this.shaders["RGB"], "RGB"]; return [this.shaders["RGB"], "RGB"];
case "CANVAS_YUV": case "CANVAS_YUV":
case "CANVAS_YIQ": case "CANVAS_YIQ":
case "CANVAS_CXA2025AS": case "CANVAS_CXA2025AS":
return [this.shaders["COMPOSITE"], "COMPOSITE"]; return [this.shaders["COMPOSITE"], "COMPOSITE"];
} }
return [null, null]; return [null, null];
} }
@ -843,7 +861,7 @@ void main(void)
const displayShader = this.shaders["DISPLAY"]; const displayShader = this.shaders["DISPLAY"];
if (!renderShader || !displayShader) if (!renderShader || !displayShader)
return; return;
const isCompositeDecoder = (renderShaderName == "COMPOSITE"); const isCompositeDecoder = (renderShaderName == "COMPOSITE");
@ -852,8 +870,8 @@ void main(void)
// Subcarrier // Subcarrier
if (isCompositeDecoder) { if (isCompositeDecoder) {
gl.uniform1f(gl.getUniformLocation(renderShader, "subcarrier"), gl.uniform1f(gl.getUniformLocation(renderShader, "subcarrier"),
this.imageSubcarrier / this.imageSampleRate); this.imageSubcarrier / this.imageSampleRate);
} }
// Filters // Filters
@ -864,12 +882,12 @@ void main(void)
const bandwidth = this.display.videoBandwidth / this.imageSampleRate; const bandwidth = this.display.videoBandwidth / this.imageSampleRate;
if (isCompositeDecoder) { if (isCompositeDecoder) {
let yBandwidth = this.display.videoLumaBandwidth / this.imageSampleRate; let yBandwidth = this.display.videoLumaBandwidth / this.imageSampleRate;
let uBandwidth = this.display.videoChromaBandwidth / this.imageSampleRate; let uBandwidth = this.display.videoChromaBandwidth / this.imageSampleRate;
let vBandwidth = uBandwidth; let vBandwidth = uBandwidth;
if (this.display.videoDecoder == "CANVAS_YIQ") if (this.display.videoDecoder == "CANVAS_YIQ")
uBandwidth = uBandwidth + NTSC_IQ_DELTA / this.imageSampleRate; uBandwidth = uBandwidth + NTSC_IQ_DELTA / this.imageSampleRate;
// Switch to video bandwidth when no subcarrier // Switch to video bandwidth when no subcarrier
if ((this.imageSubcarrier == 0.0) || this.display.videoWhiteOnly) if ((this.imageSubcarrier == 0.0) || this.display.videoWhiteOnly)
@ -893,28 +911,28 @@ void main(void)
} }
gl.uniform3f(gl.getUniformLocation(renderShader, "c0"), gl.uniform3f(gl.getUniformLocation(renderShader, "c0"),
wy.data[8], wu.data[8], wv.data[8]); wy.data[8], wu.data[8], wv.data[8]);
gl.uniform3f(gl.getUniformLocation(renderShader, "c1"), gl.uniform3f(gl.getUniformLocation(renderShader, "c1"),
wy.data[7], wu.data[7], wv.data[7]); wy.data[7], wu.data[7], wv.data[7]);
gl.uniform3f(gl.getUniformLocation(renderShader, "c2"), gl.uniform3f(gl.getUniformLocation(renderShader, "c2"),
wy.data[6], wu.data[6], wv.data[6]); wy.data[6], wu.data[6], wv.data[6]);
gl.uniform3f(gl.getUniformLocation(renderShader, "c3"), gl.uniform3f(gl.getUniformLocation(renderShader, "c3"),
wy.data[5], wu.data[5], wv.data[5]); wy.data[5], wu.data[5], wv.data[5]);
gl.uniform3f(gl.getUniformLocation(renderShader, "c4"), gl.uniform3f(gl.getUniformLocation(renderShader, "c4"),
wy.data[4], wu.data[4], wv.data[4]); wy.data[4], wu.data[4], wv.data[4]);
gl.uniform3f(gl.getUniformLocation(renderShader, "c5"), gl.uniform3f(gl.getUniformLocation(renderShader, "c5"),
wy.data[3], wu.data[3], wv.data[3]); wy.data[3], wu.data[3], wv.data[3]);
gl.uniform3f(gl.getUniformLocation(renderShader, "c6"), gl.uniform3f(gl.getUniformLocation(renderShader, "c6"),
wy.data[2], wu.data[2], wv.data[2]); wy.data[2], wu.data[2], wv.data[2]);
gl.uniform3f(gl.getUniformLocation(renderShader, "c7"), gl.uniform3f(gl.getUniformLocation(renderShader, "c7"),
wy.data[1], wu.data[1], wv.data[1]); wy.data[1], wu.data[1], wv.data[1]);
gl.uniform3f(gl.getUniformLocation(renderShader, "c8"), gl.uniform3f(gl.getUniformLocation(renderShader, "c8"),
wy.data[0], wu.data[0], wv.data[0]); wy.data[0], wu.data[0], wv.data[0]);
// Decoder matrix // Decoder matrix
let decoderMatrix = new Matrix3(1, 0, 0, let decoderMatrix = new Matrix3(1, 0, 0,
0, 1, 0, 0, 1, 0,
0, 0, 1); 0, 0, 1);
// Encode // Encode
if (!isCompositeDecoder) { if (!isCompositeDecoder) {
@ -942,15 +960,15 @@ void main(void)
// Saturation // Saturation
decoderMatrix = new Matrix3(1, 0, 0, decoderMatrix = new Matrix3(1, 0, 0,
0, this.display.videoSaturation, 0, 0, this.display.videoSaturation, 0,
0, 0, this.display.videoSaturation).mul(decoderMatrix); 0, 0, this.display.videoSaturation).mul(decoderMatrix);
// Hue // Hue
const hue = 2 * Math.PI * this.display.videoHue; const hue = 2 * Math.PI * this.display.videoHue;
decoderMatrix = new Matrix3(1, 0, 0, decoderMatrix = new Matrix3(1, 0, 0,
0, Math.cos(hue), -Math.sin(hue), 0, Math.cos(hue), -Math.sin(hue),
0, Math.sin(hue), Math.cos(hue)).mul(decoderMatrix); 0, Math.sin(hue), Math.cos(hue)).mul(decoderMatrix);
// Decode // Decode
switch (this.display.videoDecoder) { switch (this.display.videoDecoder) {
@ -988,7 +1006,7 @@ void main(void)
0.317, -0.466, 1.677).mul(decoderMatrix); 0.317, -0.466, 1.677).mul(decoderMatrix);
break; break;
default: default:
throw new Error(`unknown videoDecoder: ${this.display.videoDecoder}`); throw new Error(`unknown videoDecoder: ${this.display.videoDecoder}`);
} }
// Brightness // Brightness
@ -1024,7 +1042,7 @@ void main(void)
decoderMatrix = decoderMatrix.mul(contrast); decoderMatrix = decoderMatrix.mul(contrast);
gl.uniformMatrix3fv(gl.getUniformLocation(renderShader, "decoderMatrix"), gl.uniformMatrix3fv(gl.getUniformLocation(renderShader, "decoderMatrix"),
false, decoderMatrix.data); false, decoderMatrix.data);
// Display shader // Display shader
gl.useProgram(displayShader); gl.useProgram(displayShader);
@ -1064,7 +1082,7 @@ void main(void)
const gl = this.gl; const gl = this.gl;
const [renderShader, renderShaderName] = this.getRenderShader(); const [renderShader, renderShaderName] = this.getRenderShader();
if (!renderShader || !this.shaderEnabled) if (!renderShader || !this.shaderEnabled)
return; return;
const isCompositeDecoder = (renderShaderName == "COMPOSITE"); const isCompositeDecoder = (renderShaderName == "COMPOSITE");
@ -1105,7 +1123,7 @@ void main(void)
const imageSize = this.image.size; const imageSize = this.image.size;
for (let y = 0; y < this.image.height; y += this.viewportSize.height) { for (let y = 0; y < this.image.height; y += this.viewportSize.height) {
for (let x = 0; x < this.image.width; x += this.viewportSize.width) { for (let x = 0; x < this.image.width; x += this.viewportSize.width) {
// Calculate rects // Calculate rects
const clipSize = this.viewportSize.copy(); const clipSize = this.viewportSize.copy();
@ -1128,54 +1146,54 @@ void main(void)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// What is this, some ancient fixed pipeline nonsense? No way. // What is this, some ancient fixed pipeline nonsense? No way.
// glLoadIdentity(); // glLoadIdentity();
const positionLocation = gl.getAttribLocation(renderShader, "a_position"); const positionLocation = gl.getAttribLocation(renderShader, "a_position");
const texcoordLocation = gl.getAttribLocation(renderShader, "a_texCoord"); const texcoordLocation = gl.getAttribLocation(renderShader, "a_texCoord");
const positionBuffer = this.buffers["POSITION"]; const positionBuffer = this.buffers["POSITION"];
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const p_x1 = canvasRect.l; const p_x1 = canvasRect.l;
const p_x2 = canvasRect.r; const p_x2 = canvasRect.r;
const p_y1 = canvasRect.t; const p_y1 = canvasRect.t;
const p_y2 = canvasRect.b; const p_y2 = canvasRect.b;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
p_x1, p_y1, p_x1, p_y1,
p_x2, p_y1, p_x2, p_y1,
p_x1, p_y2, p_x1, p_y2,
p_x1, p_y2, p_x1, p_y2,
p_x2, p_y1, p_x2, p_y1,
p_x2, p_y2, p_x2, p_y2,
]), gl.STATIC_DRAW); ]), gl.STATIC_DRAW);
const texcoordBuffer = this.buffers["TEXCOORD"]; const texcoordBuffer = this.buffers["TEXCOORD"];
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
const t_x1 = textureRect.l; const t_x1 = textureRect.l;
const t_x2 = textureRect.r; const t_x2 = textureRect.r;
const t_y1 = textureRect.t; const t_y1 = textureRect.t;
const t_y2 = textureRect.b; const t_y2 = textureRect.b;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
t_x1, t_y1, t_x1, t_y1,
t_x2, t_y1, t_x2, t_y1,
t_x1, t_y2, t_x1, t_y2,
t_x1, t_y2, t_x1, t_y2,
t_x2, t_y1, t_x2, t_y1,
t_x2, t_y2, t_x2, t_y2,
]), gl.STATIC_DRAW); ]), gl.STATIC_DRAW);
gl.clearColor(0, 0, 0, 0); gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT); gl.clear(gl.COLOR_BUFFER_BIT);
gl.enableVertexAttribArray(positionLocation); gl.enableVertexAttribArray(positionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(texcoordLocation); gl.enableVertexAttribArray(texcoordLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
gl.vertexAttribPointer(texcoordLocation, 2, gl.FLOAT, false, 0, 0); gl.vertexAttribPointer(texcoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6); gl.drawArrays(gl.TRIANGLES, 0, 6);
} }
} }
@ -1193,19 +1211,19 @@ void main(void)
const gl = this.gl; const gl = this.gl;
const texInfo = this.textures[name]; const texInfo = this.textures[name];
if (!texInfo) { if (!texInfo) {
throw new Error(`Cannot find texture named ${name}`); throw new Error(`Cannot find texture named ${name}`);
} }
if (width < 4) width = 4; if (width < 4) width = 4;
if (height < 4) height = 4; if (height < 4) height = 4;
width = 2**Math.ceil(Math.log2(width)); width = 2**Math.ceil(Math.log2(width));
height = 2**Math.ceil(Math.log2(height)); height = 2**Math.ceil(Math.log2(height));
if (texInfo.width != width || texInfo.height != height) { if (texInfo.width != width || texInfo.height != height) {
texInfo.width = width; texInfo.width = width;
texInfo.height = height; texInfo.height = height;
gl.bindTexture(gl.TEXTURE_2D, texInfo.glTexture); gl.bindTexture(gl.TEXTURE_2D, texInfo.glTexture);
const dummy = new Uint8Array(width * height); const dummy = new Uint8Array(width * height);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, width, height, 0, gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width, height, 0,
gl.LUMINANCE, gl.UNSIGNED_BYTE, dummy); gl.LUMINANCE, gl.UNSIGNED_BYTE, dummy);
} }
} }
} }
@ -1227,9 +1245,9 @@ void main(void)
NTSC_Q_CUTOFF: NTSC_Q_CUTOFF, NTSC_Q_CUTOFF: NTSC_Q_CUTOFF,
NTSC_IQ_DELTA: NTSC_IQ_DELTA, NTSC_IQ_DELTA: NTSC_IQ_DELTA,
NTSC_DETAILS: buildTiming(ntscClockFrequency, ntscDisplayRect, NTSC_DETAILS: buildTiming(ntscClockFrequency, ntscDisplayRect,
ntscVisibleRect, ntscVertTotal), ntscVisibleRect, ntscVertTotal, NTSC_FSC),
PAL_DETAILS: buildTiming(palClockFrequency, palDisplayRect, PAL_DETAILS: buildTiming(palClockFrequency, palDisplayRect,
palVisibleRect, palVertTotal), palVisibleRect, palVertTotal, PAL_FSC),
}, },
loadImage: loadImage, loadImage: loadImage,
screenData: screenData, screenData: screenData,