From 467504308a103865c058f9a0ac858cc22e72240e Mon Sep 17 00:00:00 2001 From: Justine Tunney Date: Thu, 2 Jul 2020 20:18:08 -0700 Subject: [PATCH] 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. --- examples/nesemu1.cc | 167 +++++++++++++++++++++++++------------------- 1 file changed, 96 insertions(+), 71 deletions(-) diff --git a/examples/nesemu1.cc b/examples/nesemu1.cc index c92cb33b..04d53af2 100644 --- a/examples/nesemu1.cc +++ b/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;