Improve emulator color math

Video game emulators seem to convert colors for modern displays poorly.
Check out https://youtu.be/Eds63YbGhDQ?t=481 where we notice in the CRT
displays Super Mario Bros with a blue sky, whereas the PC shows purple.

It's likely b/c NTSC used Illuminant C whereas sRGB uses Illuminant D65.
See the improvement: https://justine.storage.googleapis.com/nesemu3.png
Now you can play video games in the terminal as they looked in the 80's.

This change also reduces CPU usage to a third.
main
Justine Tunney 2 years ago
parent 72b654cb6c
commit 467504308a
  1. 167
      examples/nesemu1.cc

@ -3,6 +3,8 @@
/* PORTED TO TELETYPEWRITERS IN YEAR 2020 BY JUSTINE ALEXANDRA ROBERTS TUNNEY */
/* TRADEMARKS ARE OWNED BY THEIR RESPECTIVE OWNERS LAWYERCATS LUV TAUTOLOGIES */
/* https://bisqwit.iki.fi/jutut/kuvat/programming_examples/nesemu1/nesemu1.cc */
#include "dsp/core/core.h"
#include "dsp/core/illumination.h"
#include "dsp/scale/scale.h"
#include "dsp/tty/itoa8.h"
#include "dsp/tty/quant.h"
@ -35,12 +37,14 @@
#include "libc/sysv/consts/sig.h"
#include "libc/time/time.h"
#include "libc/x/x.h"
#include "tool/viz/lib/knobs.h"
#define DYN 240
#define DXN 256
#define FPS 60.0988
#define HZ 1789773
#define KEYHZ 20
#define GAMMA 2.2
#define CTRL(C) ((C) ^ 0100)
#define ALT(C) ((033 << 010) | (C))
@ -52,8 +56,10 @@ typedef uint16_t u16;
typedef uint8_t u8;
typedef int8_t s8;
static const struct itimerval kNesFps = {{0, 1. / FPS * 1e6},
{0, 1. / FPS * 1e6}};
static const struct itimerval kNesFps = {
{0, 1. / FPS * 1e6},
{0, 1. / FPS * 1e6},
};
struct Frame {
char *p, *w, *mem;
@ -78,11 +84,12 @@ static bool exited_;
static bool timeout_;
static bool resized_;
static size_t vtsize_;
static bool artifacts_;
static long tyn_, txn_;
static const char* ffplay_;
static struct Audio audio_;
static struct TtyRgb* ttyrgb_;
static struct Frame frames_[2];
static struct Frame vf_[2];
static unsigned char *R, *G, *B;
static struct Action arrow_, button_;
static struct SamplingSolution* asx_;
@ -94,8 +101,61 @@ static int joy_current_[2] = {0, 0};
static int joy_next_[2] = {0, 0};
static int joypos_[2] = {0, 0};
static int Clamp(int v) { return v > 255 ? 255 : v; }
static float FixGamma(float f) { return f > 0 ? powf(f, 2.2f / 1.8f) : 0; }
static int Clamp(int v) { return MAX(0, MIN(255, v)); }
static double FixGamma(double x) { return tv2pcgamma(x, GAMMA); }
void InitPalette(void) {
// The input value is a NES color index (with de-emphasis bits).
// See http://wiki.nesdev.com/w/index.php/NTSC_video for magic numbers
// We need RGB values. To produce a RGB value, we emulate the NTSC circuitry.
double A[3] = {-1.109, -.275, .947};
double B[3] = {1.709, -.636, .624};
double rgbc[3], lightbulb[3][3], rgbd65[3];
int o, u, r, c, b, p, y, i, l, q, e, p0, p1, pixel;
signed char volt[] = "\372\273\32\305\35\311I\330D\357\175\13D!}N";
GetChromaticAdaptationMatrix(lightbulb, kIlluminantC, kIlluminantD65);
for (o = 0; o < 3; ++o) {
for (p0 = 0; p0 < 512; ++p0) {
for (p1 = 0; p1 < 64; ++p1) {
for (u = 0; u < 3; ++u) {
// Calculate the luma and chroma by emulating the relevant circuits:
y = 0;
i = 0;
q = 0;
// 12 samples of NTSC signal constitute a color.
for (p = 0; p < 12; ++p) {
// Sample either the previous or the current pixel.
r = (p + o * 4) % 12;
// Decode the color index.
if (artifacts_) {
pixel = r < 8 - u * 2 ? p0 : p1;
} else {
pixel = p0;
}
c = pixel % 16;
l = c < 0xE ? pixel / 4 & 12 : 4;
e = p0 / 64;
// NES NTSC modulator
// square wave between up to four voltage levels
b = 40 + volt[(c > 12 * ((c + 8 + p) % 12 < 6)) +
2 * !(0451326 >> p / 2 * 3 & e) + l];
// Ideal TV NTSC demodulator?
y += b;
i += b * round(cos(M_PI * p / 6) * 5909);
q += b * round(sin(M_PI * p / 6) * 5909);
}
// Converts YIQ to RGB
// Store color at subpixel precision
rgbc[u] = FixGamma(y / 1980. + i * A[u] / 9e6 + q * B[u] / 9e6);
}
matvmul3(rgbd65, lightbulb, rgbc);
for (u = 0; u < 3; ++u) {
palette_[o][p1][p0][u] = Clamp(rgbd65[u] * 255);
}
}
}
}
}
static void WriteStringNow(const char* s) {
ttywrite(STDOUT_FILENO, s, strlen(s));
@ -133,8 +193,8 @@ void GetTermSize(void) {
vtsize_ = ((tyn_ * txn_ * strlen("\e[48;2;255;48;2;255m▄")) +
(tyn_ * strlen("\e[0m\r\n")) + 128);
frame_ = 0;
InitFrame(&frames_[0]);
InitFrame(&frames_[1]);
InitFrame(&vf_[0]);
InitFrame(&vf_[1]);
WriteStringNow("\e[0m\e[H\e[J");
}
@ -203,6 +263,10 @@ void ReadKeyboard(void) {
}
}
switch (ch) {
case '1':
artifacts_ = !artifacts_;
InitPalette();
break;
case ' ':
button_.code = 0b00100000; // A
button_.wait = KEYHZ;
@ -261,22 +325,24 @@ void ReadKeyboard(void) {
}
}
bool HasVideo(struct Frame* f) { return f->w < f->p; }
bool HasPendingVideo(void) { return HasVideo(&vf_[0]) || HasVideo(&vf_[1]); }
bool HasPendingAudio(void) { return playpid_ && audio_.i; }
struct Frame* FlipFrameBuffer(void) {
frame_ = !frame_;
return &frames_[frame_];
return &vf_[frame_];
}
void TransmitVideo(void) {
ssize_t rc;
struct Frame* f;
f = &frames_[frame_];
if (f->w >= f->p) f = FlipFrameBuffer();
if (f->w < f->p) {
if ((rc = write(STDOUT_FILENO, f->w, f->p - f->w)) != -1) {
f->w += rc;
} else {
SystemFailure();
}
f = &vf_[frame_];
if (!HasVideo(f)) f = FlipFrameBuffer();
if ((rc = write(STDOUT_FILENO, f->w, f->p - f->w)) != -1) {
f->w += rc;
} else {
SystemFailure();
}
}
@ -316,15 +382,21 @@ void KeyCountdown(struct Action* a) {
}
}
void DrainAndExit(void) {
while (HasPendingVideo()) TransmitVideo();
WriteStringNow("\r\n\e[0m\e[J");
exit(0);
}
void PollAndSynchronize(void) {
struct pollfd fds[3];
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
fds[1].fd = STDOUT_FILENO;
fds[1].events = POLLOUT;
fds[2].fd = playpid_ ? playfd_ : -1;
fds[2].events = POLLOUT;
do {
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
fds[1].fd = HasPendingVideo() ? STDOUT_FILENO : -1;
fds[1].events = POLLOUT;
fds[2].fd = HasPendingAudio() ? playfd_ : -1;
fds[2].events = POLLOUT;
if (poll(fds, ARRAYLEN(fds), 1. / FPS * 1e3) != -1) {
if (fds[0].revents & (POLLIN | POLLERR)) ReadKeyboard();
if (fds[1].revents & (POLLOUT | POLLERR)) TransmitVideo();
@ -333,8 +405,7 @@ void PollAndSynchronize(void) {
SystemFailure();
}
if (exited_) {
WriteStringNow("\r\n\e[0m\e[J");
exit(0);
DrainAndExit();
}
if (resized_) {
resized_ = false;
@ -354,7 +425,7 @@ void Raster(void) {
struct TtyRgb bg = {0x12, 0x34, 0x56, 0};
struct TtyRgb fg = {0x12, 0x34, 0x56, 0};
ScaleVideoFrameToTeletypewriter();
f = &frames_[!frame_];
f = &vf_[!frame_];
f->p = f->w = f->mem;
f->p = stpcpy(f->p, "\e[0m\e[H");
f->p = ttyraster(f->p, ttyrgb_, tyn_, txn_, bg, fg);
@ -371,52 +442,6 @@ void FlushScanline(unsigned py) {
}
}
void InitPalette(void) {
// The input value is a NES color index (with de-emphasis bits).
// We need RGB values. To produce a RGB value, we emulate the NTSC circuitry.
// For most part, this process is described at:
// http://wiki.nesdev.com/w/index.php/NTSC_video
// Incidentally, this code is shorter than a table of 64*8 RGB values.
signed char sa[] = "\372\273\32\305\35\311I\330D\357\175\13D!}N";
int o, u, r, c, b, p, y, i, l, q, e, p0, p1, pixel;
for (o = 0; o < 3; ++o) {
for (u = 0; u < 3; ++u) {
for (p0 = 0; p0 < 512; ++p0) {
for (p1 = 0; p1 < 64; ++p1) {
// Calculate the luma and chroma by emulating the relevant circuits:
y = 0;
i = 0;
q = 0;
// 12 samples of NTSC signal constitute a color.
for (p = 0; p < 12; ++p) {
// Sample either the previous or the current pixel.
r = (p + o * 4) % 12;
// Use pixel=p0 to disable artifacts.
// Decode the color index.
pixel = r < 8 - u * 2 ? p0 : p1;
c = pixel % 16;
l = c < 0xE ? pixel / 4 & 12 : 4;
e = p0 / 64;
// NES NTSC modulator
// square wave between up to four voltage levels
b = 40 + sa[(c > 12 * ((c + 8 + p) % 12 < 6)) +
2 * !(0451326 >> p / 2 * 3 & e) + l];
// Ideal TV NTSC demodulator?
y += b;
i += b * round(cos(M_PI * p / 6) * 5909);
q += b * round(sin(M_PI * p / 6) * 5909);
}
// Converts YIQ to RGB
// Store color at subpixel precision
float A[3] = {-1.109, -.275, .947}, B[3] = {1.709, -.636, .624};
palette_[o][p1][p0][u] = Clamp(
255 * FixGamma(y / 1980.f + i * A[u] / 9e6f + q * B[u] / 9e6f));
}
}
}
}
}
static void PutPixel(unsigned px, unsigned py, unsigned pixel, int offset) {
static bool once;
static unsigned prev;

Loading…
Cancel
Save