I am interested to see how a TV works, before had no clue. What they did to put color in (before) monochrome signal was really genious. About the shader, the amount of loops there would run at 0.5 fps lol. Need to convert code, then read what it does, do it in another way, more GPU friendly. In other words do a loose translation. Or else push the code as it is and leave it there…
About the artifacts, they are generated by bandwidth limitations to fit the color in there, for example a transition of very bright saturated colors, the bandwidth can’t hold both Y and C at full, it kind of overflows and returns funky pixels. I am very interested to emulate that
Here is crt_core_c translated to Javascript which should be the easiest language for a human being
to understand (translated to Javascript
first that is closer to C then started converting to GLSL)
cpp
#include <stdlib.h>
#include <string.h>
#define POSMOD(x, n) (((x) % (n) + (n)) % (n))
static int sigpsin15[18] = {
0x0000,
0x0c88,0x18f8,0x2528,0x30f8,0x3c50,0x4718,0x5130,0x5a80,
0x62f0,0x6a68,0x70e0,0x7640,0x7a78,0x7d88,0x7f60,0x8000,
0x7f60
};
static int
sintabil8(int n)
{
int f, i, a, b;
f = n >> 0 & 0xff;
i = n >> 8 & 0xff;
a = sigpsin15[i];
b = sigpsin15[i + 1];
return (a + ((b - a) * f >> 8));
}
function crt_sincos14(s, c, n) {
var h;
n &= T14_MASK;
h = n & ((T14_2PI >> 1) - 1);
if (h > ((T14_2PI >> 2) - 1)) {
c = -sintabil8(h - (T14_2PI >> 2));
s = sintabil8((T14_2PI >> 1) - h);
} else {
c = sintabil8((T14_2PI >> 2) - h);
s = sintabil8(h);
}
if (n > ((T14_2PI >> 1) - 1)) {
c = -c;
s = -s;
}
}
function crt_bpp4fmt(format) {
switch (format) {
case CRT_PIX_FORMAT_RGB:
case CRT_PIX_FORMAT_BGR:
return 3;
case CRT_PIX_FORMAT_ARGB:
case CRT_PIX_FORMAT_RGBA:
case CRT_PIX_FORMAT_ABGR:
case CRT_PIX_FORMAT_BGRA:
return 4;
default:
return 0;
}
}
var USE_CONVOLUTION = 0;
var USE_7_SAMPLE_KERNEL = 1;
var USE_6_SAMPLE_KERNEL = 0;
var USE_5_SAMPLE_KERNEL = 0;
if (CRT_CC_SAMPLES != 4) {
USE_CONVOLUTION = 0;
}
if (USE_CONVOLUTION) {
var eqY = {
h: [0, 0, 0, 0, 0, 0, 0]
};
var eqI = {
h: [0, 0, 0, 0, 0, 0, 0]
};
var eqQ = {
h: [0, 0, 0, 0, 0, 0, 0]
};
function init_eq(f, f_lo, f_hi, rate, g_lo, g_mid, g_hi) {
f.h = [0, 0, 0, 0, 0, 0, 0];
}
function reset_eq(f) {
f.h = [0, 0, 0, 0, 0, 0, 0];
}
function eqf(f, s) {
var i;
var h = f.h;
for (i = 6; i > 0; i--) {
h[i] = h[i - 1];
}
h[0] = s;
if (USE_7_SAMPLE_KERNEL) {
return (s + h[6] + ((h[1] + h[5]) * 4) + ((h[2] + h[4]) * 7) + (h[3] * 8)) >> 5;
} else if (USE_6_SAMPLE_KERNEL) {
return (s + h[5] + 3 * (h[1] + h[4]) + 4 * (h[2] + h[3])) >> 4;
} else if (USE_5_SAMPLE_KERNEL) {
return (s + h[4] + ((h[1] + h[2] + h[3]) << 1)) >> 3;
} else {
return (s + h[3] + h[1] + h[2]) >> 2;
}
}
} else {
var HISTLEN = 3;
var HISTOLD = (HISTLEN - 1);
var HISTNEW = 0;
var EQ_P = 16;
var EQ_R = (1 << (EQ_P - 1));
var eqY = {
lf: 0,
hf: 0,
g: [0, 0, 0],
fL: [0, 0, 0, 0],
fH: [0, 0, 0, 0],
h: [0, 0, 0]
};
var eqI = {
lf: 0,
hf: 0,
g: [0, 0, 0],
fL: [0, 0, 0, 0],
fH: [0, 0, 0, 0],
h: [0, 0, 0]
};
var eqQ = {
lf: 0,
hf: 0,
g: [0, 0, 0],
fL: [0, 0, 0, 0],
fH: [0, 0, 0, 0],
h: [0, 0, 0]
};
function init_eq(f, f_lo, f_hi, rate, g_lo, g_mid, g_hi) {
var sn, cs;
f.g = [g_lo, g_mid, g_hi];
crt_sincos14(sn, cs, T14_PI * f_lo / rate);
if (EQ_P >= 15) {
f.lf = 2 * (sn << (EQ_P - 15));
} else {
f.lf = 2 * (sn >> (15 - EQ_P));
}
crt_sincos14(sn, cs, T14_PI * f_hi / rate);
if (EQ_P >= 15) {
f.hf = 2 * (sn << (EQ_P - 15));
} else {
f.hf = 2 * (sn >> (15 - EQ_P));
}
}
function reset_eq(f) {
f.fL = [0, 0, 0, 0];
f.fH = [0, 0, 0, 0];
f.h = [0, 0, 0];
}
function eqf(f, s) {
var i, r = [0, 0, 0];
f.fL[0] += (f.lf * (s - f.fL[0]) + EQ_R) >> EQ_P;
f.fH[0] += (f.hf * (s - f.fH[0]) + EQ_R) >> EQ_P;
for (i = 1; i < 4; i++) {
f.fL[i] += (f.lf * (f.fL[i - 1] - f.fL[i]) + EQ_R) >> EQ_P;
f.fH[i] += (f.hf * (f.fH[i - 1] - f.fH[i]) + EQ_R) >> EQ_P;
}
r[0] = f.fL[3];
r[1] = f.fH[3] - f.fL[3];
r[2] = f.h[HISTOLD] - f.fH[3];
for (i = 0; i < 3; i++) {
r[i] = (r[i] * f.g[i]) >> EQ_P;
}
for (i = HISTOLD; i > 0; i--) {
f.h[i] = f.h[i - 1];
}
f.h[HISTNEW] = s;
return (r[0] + r[1] + r[2]);
}
}
function crt_resize(v, w, h, f, out) {
v.outw = w;
v.outh = h;
v.out_format = f;
v.out = out;
}
function crt_reset(v) {
v.hue = 0;
v.saturation = 10;
v.brightness = 0;
v.contrast = 180;
v.black_point = 0;
v.white_point = 100;
v.hsync = 0;
v.vsync = 0;
}
function crt_init(v, w, h, f, out) {
v = {
outw: 0,
outh: 0,
out_format: 0,
out: [],
hue: 0,
saturation: 0,
brightness: 0,
contrast: 0,
black_point: 0,
white_point: 0,
hsync: 0,
vsync: 0,
rn: 194,
analog: [],
inp: [],
ccf: [],
scanlines: 0,
blend: false
};
crt_resize(v, w, h, f, out);
crt_reset(v);
v.rn = 194;
function kHz2L(kHz) {
return CRT_HRES * (kHz * 100) / L_FREQ;
}
if (CRT_CC_SAMPLES == 4) {
init_eq(eqY, kHz2L(1500), kHz2L(3000), CRT_HRES, 65536, 8192, 9175);
init_eq(eqI, kHz2L(80), kHz2L(1150), CRT_HRES, 65536, 65536, 1311);
init_eq(eqQ, kHz2L(80), kHz2L(1000), CRT_HRES, 65536, 65536, 0);
} else if (CRT_CC_SAMPLES == 5) {
init_eq(eqY, kHz2L(1500), kHz2L(3000), CRT_HRES, 65536, 12192, 7775);
init_eq(eqI, kHz2L(80), kHz2L(1150), CRT_HRES, 65536, 65536, 1311);
init_eq(eqQ, kHz2L(80), kHz2L(1000), CRT_HRES, 65536, 65536, 0);
} else {
throw new Error("NTSC-CRT currently only supports 4 or 5 samples per chroma period.");
}
}
function crt_demodulate(v, noise) {
var out = [];
var i, j, line, rn;
var sig;
var s = 0;
var field, ratio;
var ccr;
var huesn, huecs;
var xnudge = -3, ynudge = 3;
var bright = v.brightness - (BLACK_LEVEL + v.black_point);
var bpp, pitch;
var prev_e, max_e;
bpp = crt_bpp4fmt(v.out_format);
if (bpp == 0) {
return;
}
pitch = v.outw * bpp;
crt_sincos14(huesn, huecs, ((v.hue % 360) + 33) * 8192 / 180);
huesn >>= 11;
huecs >>= 11;
rn = v.rn;
for (i = 0; i < CRT_INPUT_SIZE; i++) {
var nn = noise;
rn = (214019 * rn + 140327895);
s = v.analog[i] + (((((rn >> 16) & 0xff) - 0x7f) * nn) >> 8);
if (s > 127) { s = 127; }
if (s < -127) { s = -127; }
v.inp[i] = s;
}
v.rn = rn;
for (i = -CRT_VSYNC_WINDOW; i < CRT_VSYNC_WINDOW; i++) {
line = POSMOD(v.vsync + i, CRT_VRES);
sig = v.inp + line * CRT_HRES;
s = 0;
for (j = 0; j < CRT_HRES; j++) {
s += sig[j];
if (s <= (CRT_VSYNC_THRESH * SYNC_LEVEL)) {
break;
}
}
}
field = (j > (CRT_HRES / 2));
v.vsync = -3;
field = (field * (ratio / 2));
for (line = CRT_TOP; line < CRT_BOT; line++) {
var pos, ln;
var scanL, scanR, dx;
var L, R;
var cL, cR;
var wave = [];
var dci, dcq;
var xpos, ypos;
var beg, end;
var phasealign;
beg = (line - CRT_TOP + 0) * (v.outh + v.v_fac) / CRT_LINES + field;
end = (line - CRT_TOP + 1) * (v.outh + v.v_fac) / CRT_LINES + field;
if (beg >= v.outh) { continue; }
if (end > v.outh) { end = v.outh; }
ln = (POSMOD(line + v.vsync, CRT_VRES)) * CRT_HRES;
sig = v.inp + ln + v.hsync;
s = 0;
for (i = -CRT_HSYNC_WINDOW; i < CRT_HSYNC_WINDOW; i++) {
s += sig[SYNC_BEG + i];
if (s <= (CRT_HSYNC_THRESH * SYNC_LEVEL)) {
break;
}
}
v.hsync = 0;
xpos = POSMOD(AV_BEG + v.hsync + xnudge, CRT_HRES);
ypos = POSMOD(line + v.vsync + ynudge, CRT_VRES);
pos = xpos + ypos * CRT_HRES;
ccr = v.ccf[ypos % CRT_CC_VPER];
sig = v.inp + ln + (v.hsync - (v.hsync % CRT_CC_SAMPLES));
for (i = CB_BEG; i < CB_BEG + (CB_CYCLES * CRT_CB_FREQ); i++) {
var p, n;
p = ccr[i % CRT_CC_SAMPLES] * 127 / 128;
n = sig[i];
ccr[i % CRT_CC_SAMPLES] = p + n;
}
phasealign = POSMOD(v.hsync, CRT_CC_SAMPLES);
if (CRT_CC_SAMPLES == 4) {
dci = ccr[(phasealign + 1) & 3] - ccr[(phasealign + 3) & 3];
dcq = ccr[(phasealign + 2) & 3] - ccr[(phasealign + 0) & 3];
wave[0] = ((dci * huecs - dcq * huesn) >> 4) * v.saturation;
wave[1] = ((dcq * huecs + dci * huesn) >> 4) * v.saturation;
wave[2] = -wave[0];
wave[3] = -wave[1];
} else if (CRT_CC_SAMPLES == 5) {
var dciA, dciB;
var dcqA, dcqB;
var ang = (v.hue % 360);
var off180 = CRT_CC_SAMPLES / 2;
var off90 = CRT_CC_SAMPLES / 4;
var peakA = phasealign + off90;
var peakB = phasealign + 0;
dciA = dciB = dcqA = dcqB = 0;
dciA = ccr[(peakA) % CRT_CC_SAMPLES];
dciB = (ccr[(peakA + off180) % CRT_CC_SAMPLES]
+ ccr[(peakA + off180 + 1) % CRT_CC_SAMPLES]) / 2;
dcqA = ccr[(peakB + off180) % CRT_CC_SAMPLES];
dcqB = ccr[(peakB) % CRT_CC_SAMPLES];
dci = dciA - dciB;
dcq = dcqA - dcqB;
for (i = 0; i < CRT_CC_SAMPLES; i++) {
var sn, cs;
crt_sincos14(sn, cs, ang * 8192 / 180);
waveI[i] = ((dci * cs + dcq * sn) >> 15) * v.saturation;
crt_sincos14(sn, cs, (ang + 90) * 8192 / 180);
waveQ[i] = ((dci * cs + dcq * sn) >> 15) * v.saturation;
ang += (360 / CRT_CC_SAMPLES);
}
}
sig = v.inp + pos;
dx = ((AV_LEN - 1) << 12) / v.outw;
scanL = 0;
scanR = (AV_LEN - 1) << 12;
L = 0;
R = AV_LEN;
reset_eq(eqY);
reset_eq(eqI);
reset_eq(eqQ);
for (i = L; i < R; i++) {
out[i].y = eqf(eqY, sig[i] + bright) << 4;
out[i].i = eqf(eqI, sig[i] * waveI[i % CRT_CC_SAMPLES] >> 9) >> 3;
out[i].q = eqf(eqQ, sig[i] * waveQ[i % CRT_CC_SAMPLES] >> 9) >> 3;
}
cL = v.out + (beg * pitch);
cR = cL + pitch;
for (pos = scanL; pos < scanR && cL < cR; pos += dx) {
var y, i, q;
var r, g, b;
var aa, bb;
R = pos & 0xfff;
L = 0xfff - R;
s = pos >> 12;
yiqA = out + s;
yiqB = out + s + 1;
y = ((yiqA.y * L) >> 2) + ((yiqB.y * R) >> 2);
i = ((yiqA.i * L) >> 14) + ((yiqB.i * R) >> 14);
q = ((yiqA.q * L) >> 14) + ((yiqB.q * R) >> 14);
r = (((y + 3879 * i + 2556 * q) >> 12) * v.contrast) >> 8;
g = (((y - 1126 * i - 2605 * q) >> 12) * v.contrast) >> 8;
b = (((y - 4530 * i + 7021 * q) >> 12) * v.contrast) >> 8;
if (r < 0) r = 0;
if (g < 0) g = 0;
if (b < 0) b = 0;
if (r > 255) r = 255;
if (g > 255) g = 255;
if (b > 255) b = 255;
if (v.blend) {
aa = (r << 16 | g << 8 | b);
switch (v.out_format) {
case CRT_PIX_FORMAT_RGB:
case CRT_PIX_FORMAT_RGBA:
bb = cL[0] << 16 | cL[1] << 8 | cL[2];
break;
case CRT_PIX_FORMAT_BGR:
case CRT_PIX_FORMAT_BGRA:
bb = cL[2] << 16 | cL[1] << 8 | cL[0];
break;
case CRT_PIX_FORMAT_ARGB:
bb = cL[1] << 16 | cL[2] << 8 | cL[3];
break;
case CRT_PIX_FORMAT_ABGR:
bb = cL[3] << 16 | cL[2] << 8 | cL[1];
break;
default:
bb = 0;
break;
}
bb = (((aa & 0xfefeff) >> 1) + ((bb & 0xfefeff) >> 1));
} else {
bb = (r << 16 | g << 8 | b);
}
switch (v.out_format) {
case CRT_PIX_FORMAT_RGB:
case CRT_PIX_FORMAT_RGBA:
cL[0] = bb >> 16 & 0xff;
cL[1] = bb >> 8 & 0xff;
cL[2] = bb >> 0 & 0xff;
break;
case CRT_PIX_FORMAT_BGR:
case CRT_PIX_FORMAT_BGRA:
cL[0] = bb >> 0 & 0xff;
cL[1] = bb >> 8 & 0xff;
cL[2] = bb >> 16 & 0xff;
break;
case CRT_PIX_FORMAT_ARGB:
cL[1] = bb >> 16 & 0xff;
cL[2] = bb >> 8 & 0xff;
cL[3] = bb >> 0 & 0xff;
break;
case CRT_PIX_FORMAT_ABGR:
cL[1] = bb >> 0 & 0xff;
cL[2] = bb >> 8 & 0xff;
cL[3] = bb >> 16 & 0xff;
break;
default:
break;
}
cL += bpp;
}
for (s = beg + 1; s < (end - v.scanlines); s++) {
v.out[s * pitch] = v.out[(s - 1) * pitch];
}
}
}