diff --git a/gfx/video_filters/crop_borders.c b/gfx/video_filters/crop_borders.c new file mode 100644 index 00000000000..7c7e5292c39 --- /dev/null +++ b/gfx/video_filters/crop_borders.c @@ -0,0 +1,193 @@ +#include "softfilter.h" +#include +#include + +#ifdef RARCH_INTERNAL +#define softfilter_get_implementation crop_borders_get_implementation +#define softfilter_thread_data crop_borders_softfilter_thread_data +#define filter_data crop_borders_filter_data +#endif + +struct softfilter_thread_data +{ + void *out_data; + const void *in_data; + size_t out_pitch; + size_t in_pitch; + unsigned width; + unsigned height; +}; + +struct filter_data +{ + struct softfilter_thread_data *workers; + unsigned in_fmt; + float crop_x; + float crop_y; +}; + +static unsigned crop_borders_generic_input_fmts(void) +{ + return SOFTFILTER_FMT_RGB565 | SOFTFILTER_FMT_XRGB8888; +} + +static unsigned crop_borders_generic_output_fmts(unsigned input_fmts) +{ + return input_fmts; +} + +static unsigned crop_borders_generic_threads(void *data) +{ + return 1; +} + +static void crop_borders_initialize(struct filter_data *filt, + const struct softfilter_config *config, + void *userdata) +{ + /* RetroArch wull look at .filt for: crop_borders_crop_x */ + config->get_float(userdata, "crop_x", &filt->crop_x, 0.0f); + config->get_float(userdata, "crop_y", &filt->crop_y, 0.0f); +} + +static void *crop_borders_generic_create(const struct softfilter_config *config, + unsigned in_fmt, unsigned out_fmt, + unsigned max_width, unsigned max_height, + unsigned threads, softfilter_simd_mask_t simd, void *userdata) +{ + struct filter_data *filt = (struct filter_data*)calloc(1, sizeof(*filt)); + if (!filt) return NULL; + + if (!(filt->workers = (struct softfilter_thread_data*)calloc(1, sizeof(struct softfilter_thread_data)))) + { + free(filt); + return NULL; + } + + filt->in_fmt = in_fmt; + + crop_borders_initialize(filt, config, userdata); + + return filt; +} + +static void crop_borders_generic_output(void *data, + unsigned *out_width, unsigned *out_height, + unsigned width, unsigned height) +{ + *out_width = width; + *out_height = height; +} + +static void crop_borders_generic_destroy(void *data) +{ + struct filter_data *filt = (struct filter_data*)data; + if (!filt) return; + free(filt->workers); + free(filt); +} + +/* Rendering Logic with Float Coordinates */ +static void crop_borders_work_cb_xrgb8888(void *data, void *thread_data) +{ + struct filter_data *filt = (struct filter_data*)data; + struct softfilter_thread_data *thr = (struct softfilter_thread_data*)thread_data; + const uint32_t *input = (const uint32_t*)thr->in_data; + uint32_t *output = (uint32_t*)thr->out_data; + unsigned in_stride = (unsigned)(thr->in_pitch >> 2); + unsigned out_stride = (unsigned)(thr->out_pitch >> 2); + + float visible_w = (float)thr->width - (filt->crop_x * 2.0f); + float visible_h = (float)thr->height - (filt->crop_y * 2.0f); + + if (visible_w <= 0.0f || visible_h <= 0.0f) return; + + float step_x = visible_w / (float)thr->width; + float step_y = visible_h / (float)thr->height; + + for (unsigned y = 0; y < thr->height; y++) + { + unsigned i_y = (unsigned)(filt->crop_y + (y * step_y)); + for (unsigned x = 0; x < thr->width; x++) + { + unsigned i_x = (unsigned)(filt->crop_x + (x * step_x)); + output[y * out_stride + x] = input[i_y * in_stride + i_x]; + } + } +} + +static void crop_borders_work_cb_rgb565(void *data, void *thread_data) +{ + struct filter_data *filt = (struct filter_data*)data; + struct softfilter_thread_data *thr = (struct softfilter_thread_data*)thread_data; + const uint16_t *input = (const uint16_t*)thr->in_data; + uint16_t *output = (uint16_t*)thr->out_data; + unsigned in_stride = (unsigned)(thr->in_pitch >> 1); + unsigned out_stride = (unsigned)(thr->out_pitch >> 1); + + float visible_w = (float)thr->width - (filt->crop_x * 2.0f); + float visible_h = (float)thr->height - (filt->crop_y * 2.0f); + + if (visible_w <= 0.0f || visible_h <= 0.0f) return; + + float step_x = visible_w / (float)thr->width; + float step_y = visible_h / (float)thr->height; + + for (unsigned y = 0; y < thr->height; y++) + { + unsigned i_y = (unsigned)(filt->crop_y + (y * step_y)); + for (unsigned x = 0; x < thr->width; x++) + { + unsigned i_x = (unsigned)(filt->crop_x + (x * step_x)); + output[y * out_stride + x] = input[i_y * in_stride + i_x]; + } + } +} + +static void crop_borders_generic_packets(void *data, + struct softfilter_work_packet *packets, + void *output, size_t output_stride, + const void *input, unsigned width, unsigned height, size_t input_stride) +{ + struct filter_data *filt = (struct filter_data*)data; + struct softfilter_thread_data *thr = &filt->workers[0]; + + thr->out_data = output; + thr->in_data = input; + thr->out_pitch = output_stride; + thr->in_pitch = input_stride; + thr->width = width; + thr->height = height; + + if (filt->in_fmt == SOFTFILTER_FMT_RGB565) + packets[0].work = crop_borders_work_cb_rgb565; + else + packets[0].work = crop_borders_work_cb_xrgb8888; + + packets[0].thread_data = thr; +} + +static const struct softfilter_implementation crop_borders_generic = { + crop_borders_generic_input_fmts, + crop_borders_generic_output_fmts, + crop_borders_generic_create, + crop_borders_generic_destroy, + crop_borders_generic_threads, + crop_borders_generic_output, + crop_borders_generic_packets, + SOFTFILTER_API_VERSION, + "Crop Borders", + "crop_borders", +}; + +const struct softfilter_implementation *softfilter_get_implementation(softfilter_simd_mask_t simd) +{ + (void)simd; + return &crop_borders_generic; +} + +#ifdef RARCH_INTERNAL +#undef softfilter_get_implementation +#undef softfilter_thread_data +#undef filter_data +#endif diff --git a/gfx/video_filters/crop_borders.filt b/gfx/video_filters/crop_borders.filt new file mode 100644 index 00000000000..6322c33fa31 --- /dev/null +++ b/gfx/video_filters/crop_borders.filt @@ -0,0 +1,4 @@ +filter = crop_borders + +crop_borders_crop_x = 8.0 +crop_borders_crop_y = 8.0 \ No newline at end of file diff --git a/gfx/video_filters/ntsc.c b/gfx/video_filters/ntsc.c new file mode 100644 index 00000000000..5a90c2d6911 --- /dev/null +++ b/gfx/video_filters/ntsc.c @@ -0,0 +1,224 @@ +#include "softfilter.h" +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +#ifdef RARCH_INTERNAL +#define softfilter_get_implementation ntsc_get_implementation +#define softfilter_thread_data ntsc_softfilter_thread_data +#define filter_data ntsc_filter_data +#endif + +#define NTSC_SCALE_X 2 +#define NTSC_SCALE_Y 1 +#define NTSC_MAX_WIDTH 1024 +#define PHASE_MAX 16 // Max phases for Atari 2600 + +typedef struct { int Y, I, Q; } yiq_t; + +struct softfilter_thread_data { + void *out_data; + const void *in_data; + size_t out_pitch; + size_t in_pitch; + unsigned width; + unsigned height; + int first; +}; + +struct filter_data { + unsigned threads; + struct softfilter_thread_data *workers; + unsigned in_fmt; + int hue_cos, hue_sin; + int saturation, sharpness, artifacts; + int huesat_identity, pal_mode, atari_mode, c64_mode; + int phase_count, nes_frame_count; + int lut_sin[PHASE_MAX], lut_cos[PHASE_MAX]; +}; + +static inline void rgb_to_yiq(int r, int g, int b, int *Y, int *I, int *Q) { + *Y = ( 77*r + 150*g + 29*b) >> 8; + *I = ( 157*r - 132*g - 26*b) >> 8; + *Q = ( -38*r - 74*g + 112*b) >> 8; +} + +static inline void yiq_to_rgb(int Y, int I, int Q, int *r, int *g, int *b) { + int rv = Y + ((292*I) >> 8); + int gv = Y - ((149*I) >> 8) - ((101*Q) >> 8); + int bv = Y + ((520*Q) >> 8); + *r = (rv < 0) ? 0 : (rv > 255 ? 255 : rv); + *g = (gv < 0) ? 0 : (gv > 255 ? 255 : gv); + *b = (bv < 0) ? 0 : (bv > 255 ? 255 : bv); +} + +static void ntsc_process_line(const struct filter_data *filt, + const struct softfilter_thread_data *thr, unsigned y, + int *cbuf, int *lineI, int *lineQ, int *lineY, yiq_t *yiq_cache, + void *dst_ptr) { + unsigned width = thr->width; + unsigned ow = width * 2; + int phases = filt->phase_count; + + // C64 uses 2-phase steps per output pixel if sampled at 8 phases + int phase_step = (filt->c64_mode) ? 2 : 4; + int frame_offset = (filt->nes_frame_count % 2) * (phases / 2); + int line_mult = (filt->pal_mode) ? (phase_step + 1) : phase_step; + int line_phase = (frame_offset + ((thr->first + (int)y) * line_mult)) % phases; + + for (unsigned x = 0; x < width; x++) { + int r, g, b, Y, I, Q; + if (filt->in_fmt == SOFTFILTER_FMT_RGB565) { + uint16_t p = ((uint16_t*)thr->in_data)[y * (thr->in_pitch/2) + x]; + r = ((p >> 11) & 0x1f) << 3; g = ((p >> 5) & 0x3f) << 2; b = (p & 0x1f) << 3; + } else { + uint32_t p = ((uint32_t*)thr->in_data)[y * (thr->in_pitch/4) + x]; + r = (p >> 16) & 0xFF; g = (p >> 8) & 0xFF; b = p & 0xFF; + } + rgb_to_yiq(r, g, b, &Y, &I, &Q); + yiq_cache[x].Y = Y; yiq_cache[x].I = I; yiq_cache[x].Q = Q; + + for (int p_idx = 0; p_idx < 2; p_idx++) { + int ph = (line_phase + (x * phase_step * 2) + (p_idx * phase_step)) % phases; + cbuf[x * 2 + p_idx] = Y + ((I * filt->lut_cos[ph] + Q * filt->lut_sin[ph]) >> 8); + } + } + + for (unsigned x = 0; x < ow; x++) { + int accI = 0, accQ = 0; + int taps = (filt->atari_mode) ? 8 : (filt->c64_mode ? 4 : 6); + + for (int t = -taps; t < taps; t++) { + int idx = (x + t < 0) ? 0 : (x + t >= (int)ow ? (int)ow - 1 : x + t); + int ph = (line_phase + (x * phase_step) + (t * phase_step)) % phases; + accI += cbuf[idx] * filt->lut_cos[ph]; + accQ += cbuf[idx] * filt->lut_sin[ph]; + } + lineI[x] = accI / (taps * 256); lineQ[x] = accQ / (taps * 256); + + int i_m2 = (x > 1) ? (int)x - 2 : 0, i_m1 = (x > 0) ? (int)x - 1 : 0; + int i_p1 = (x < ow - 1) ? (int)x + 1 : (int)ow - 1, i_p2 = (x < ow - 2) ? (int)x + 2 : (int)ow - 1; + + // Notch filter optimized for system-specific bandwidth + int notchedY = (filt->c64_mode) ? + (cbuf[i_m1] + (cbuf[x] << 1) + cbuf[i_p1]) >> 2 : + (cbuf[i_m2] + (cbuf[i_m1] << 2) + (cbuf[x] * 6) + (cbuf[i_p1] << 2) + cbuf[i_p2]) >> 4; + + lineY[x] = ((notchedY * (256 - filt->artifacts)) + (cbuf[x] * filt->artifacts)) >> 8; + } + + for (unsigned x = 0; x < ow; x++) { + int Y = lineY[x], I = lineI[x], Q = lineQ[x]; + if (filt->sharpness > 0) { + int x_m1 = (x > 0) ? (int)x - 1 : 0, x_p1 = (x < ow - 1) ? (int)x + 1 : (int)ow - 1; + int edge = (Y << 1) - (lineY[x_m1] + lineY[x_p1]); + Y += (edge * filt->sharpness) >> 9; + Y = (Y < 0) ? 0 : (Y > 255 ? 255 : Y); + } + if (!filt->huesat_identity) { + int Ir = (I * filt->hue_cos - Q * filt->hue_sin) >> 8; + int Qr = (I * filt->hue_sin + Q * filt->hue_cos) >> 8; + I = (Ir * filt->saturation) >> 8; Q = (Qr * filt->saturation) >> 8; + } + int r, g, b; + yiq_to_rgb(Y, I, Q, &r, &g, &b); + if (filt->in_fmt == SOFTFILTER_FMT_RGB565) + ((uint16_t*)dst_ptr)[x] = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3); + else + ((uint32_t*)dst_ptr)[x] = 0xFF000000u | (r << 16) | (g << 8) | b; + } +} + +static void ntsc_work_cb(void *data, void *thread_data) { + struct filter_data *filt = (struct filter_data*)data; + struct softfilter_thread_data *thr = (struct softfilter_thread_data*)thread_data; + int cbuf[NTSC_MAX_WIDTH * 2], lineI[NTSC_MAX_WIDTH * 2], lineQ[NTSC_MAX_WIDTH * 2], lineY[NTSC_MAX_WIDTH * 2]; + yiq_t yiq_cache[NTSC_MAX_WIDTH]; + for (unsigned y = 0; y < thr->height; y++) { + void *dst = (uint8_t*)thr->out_data + (y * thr->out_pitch); + ntsc_process_line(filt, thr, y, cbuf, lineI, lineQ, lineY, yiq_cache, dst); + } +} + +static void *ntsc_create(const struct softfilter_config *config, + unsigned in_fmt, unsigned out_fmt, unsigned max_width, unsigned max_height, + unsigned threads, softfilter_simd_mask_t simd, void *userdata) { + struct filter_data *filt = (struct filter_data*)calloc(1, sizeof(*filt)); + if (!filt) return NULL; + float h = 0.0f, s = 1.0f, sh = 0.0f, art = 0.5f, pal = 0.0f, atari = 0.0f, c64 = 0.0f; + if (config) { + config->get_float(userdata, "hue", &h, 0.0f); + config->get_float(userdata, "saturation", &s, 1.0f); + config->get_float(userdata, "sharpness", &sh, 0.0f); + config->get_float(userdata, "artifacts", &art, 0.5f); + config->get_float(userdata, "pal_mode", &pal, 0.0f); + config->get_float(userdata, "atari_mode", &atari, 0.0f); + config->get_float(userdata, "c64_mode", &c64, 0.0f); + } + filt->in_fmt = in_fmt; + filt->pal_mode = (pal != 0.0f); + filt->atari_mode = (atari != 0.0f); + filt->c64_mode = (c64 != 0.0f); + + // Determine phase count: Atari (16), NES (12), C64/Generic (8) + if (filt->atari_mode) filt->phase_count = 16; + else if (filt->c64_mode) filt->phase_count = 8; + else filt->phase_count = 12; + + filt->hue_cos = (int)(cos(h * M_PI / 180.0) * 256.0); + filt->hue_sin = (int)(sin(h * M_PI / 180.0) * 256.0); + filt->saturation = (int)(s * 256.0); + filt->sharpness = (int)(sh * 256.0f); + filt->artifacts = (int)(art * 256.0f); + filt->huesat_identity = (h == 0.0f && s == 1.0f); + for (int i = 0; i < filt->phase_count; i++) { + float rad = (float)(2.0 * M_PI * i / filt->phase_count); + filt->lut_sin[i] = (int)(sin(rad) * 256.0f); + filt->lut_cos[i] = (int)(cos(rad) * 256.0f); + } + filt->threads = threads; + filt->workers = (struct softfilter_thread_data*)calloc(threads, sizeof(struct softfilter_thread_data)); + return filt; +} + +static void ntsc_packets(void *data, struct softfilter_work_packet *packets, + void *output, size_t output_stride, const void *input, unsigned width, + unsigned height, size_t input_stride) { + struct filter_data *filt = (struct filter_data*)data; + filt->nes_frame_count++; + for (unsigned i = 0; i < filt->threads; i++) { + struct softfilter_thread_data *thr = &filt->workers[i]; + unsigned y_start = (height * i) / filt->threads, y_end = (height * (i + 1)) / filt->threads; + thr->in_data = (const uint8_t*)input + y_start * input_stride; + thr->out_data = (uint8_t*)output + y_start * output_stride; + thr->in_pitch = input_stride; thr->out_pitch = output_stride; + thr->width = width; thr->height = y_end - y_start; + thr->first = (int)y_start; + packets[i].work = ntsc_work_cb; + packets[i].thread_data = thr; + } +} + +static void ntsc_destroy(void *data) { + struct filter_data *f = (struct filter_data*)data; + if (f) { free(f->workers); free(f); } +} + +static void ntsc_output(void *data, unsigned *ow, unsigned *oh, unsigned w, unsigned h) { *ow = w*2; *oh = h; } +static unsigned ntsc_query_num_threads(void *data) { return ((struct filter_data*)data)->threads; } +static unsigned ntsc_input_fmts(void) { return SOFTFILTER_FMT_XRGB8888 | SOFTFILTER_FMT_RGB565; } +static unsigned ntsc_output_fmts(unsigned fmt) { return fmt; } + +static const struct softfilter_implementation ntsc_impl = { + ntsc_input_fmts, ntsc_output_fmts, ntsc_create, ntsc_destroy, + ntsc_query_num_threads, ntsc_output, ntsc_packets, SOFTFILTER_API_VERSION, "NTSC-Multi-System", "ntsc", +}; + +const struct softfilter_implementation *softfilter_get_implementation(softfilter_simd_mask_t simd) { + (void)simd; return &ntsc_impl; +} \ No newline at end of file diff --git a/gfx/video_filters/ntsc.filt b/gfx/video_filters/ntsc.filt new file mode 100644 index 00000000000..9ef5c4c7a3c --- /dev/null +++ b/gfx/video_filters/ntsc.filt @@ -0,0 +1,20 @@ +filter = ntsc + +# --- System Selection --- +# Set only one to 1.0 +# If both are 0.0, defaults to 12-phase NES logic + +ntsc_atari_mode = 0.0 # 16-phase Atari 2600 logic +ntsc_c64_mode = 0.0 # 8-phase Commodore 64 logic + + +# --- Global Settings --- +# 1.0 for PAL/50Hz stability + +ntsc_pal_mode = 0.0 +ntsc_hue = 0.0 +ntsc_saturation = 1.0 + +# Increase for stronger C64 color bleeding +ntsc_artifacts = 0.0 +ntsc_sharpness = 0.3 \ No newline at end of file diff --git a/gfx/video_filters/ntsc_crt_filter/Makefile b/gfx/video_filters/ntsc_crt_filter/Makefile new file mode 100644 index 00000000000..483351d0dd0 --- /dev/null +++ b/gfx/video_filters/ntsc_crt_filter/Makefile @@ -0,0 +1,37 @@ +# Makefile for ntsc_crt RetroArch softfilter +# +# Usage: +# make -> builds ntsc_crt.so (Linux) +# make WINDOWS=1 -> cross-compiles ntsc_crt.dll (requires mingw-w64) +# make install -> copies .so + .filt to FILTERDIR + +CC = gcc +CFLAGS = -O2 -std=c99 -Wall -fPIC -DCRT_SYSTEM=0 -I.. +LDFLAGS = -shared -lm + +# Adjust this to your RetroArch installation path +FILTERDIR = /usr/share/retroarch/filters/video + +SRC = ntsc_crt.c crt_core.c crt_ntsc.c +TARGET = ntsc_crt.so + +ifdef WINDOWS + CC = x86_64-w64-mingw32-gcc + TARGET = ntsc_crt.dll + CFLAGS += -D_WIN32 +endif + +all: $(TARGET) + +$(TARGET): $(SRC) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ + +install: $(TARGET) + install -d $(FILTERDIR) + install -m 644 $(TARGET) $(FILTERDIR)/ + install -m 644 ntsc_crt.filt $(FILTERDIR)/ + +clean: + rm -f $(TARGET) + +.PHONY: all install clean diff --git a/gfx/video_filters/ntsc_crt_filter/crt_core.c b/gfx/video_filters/ntsc_crt_filter/crt_core.c new file mode 100644 index 00000000000..3dfed0cec2f --- /dev/null +++ b/gfx/video_filters/ntsc_crt_filter/crt_core.c @@ -0,0 +1,666 @@ +/*****************************************************************************/ +/* + * NTSC/CRT - integer-only NTSC video signal encoding / decoding emulation + * + * by EMMIR 2018-2023 + * + * YouTube: https://www.youtube.com/@EMMIR_KC/videos + * Discord: https://discord.com/invite/hdYctSmyQJ + */ +/*****************************************************************************/ +#include "crt_core.h" + +#include +#include + +/* ensure negative values for x get properly modulo'd */ +#define POSMOD(x, n) (((x) % (n) + (n)) % (n)) + +static int sigpsin15[18] = { /* significant points on sine wave (15-bit) */ + 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; + + /* looks scary but if you don't change T14_2PI + * it won't cause out of bounds memory reads + */ + f = n >> 0 & 0xff; + i = n >> 8 & 0xff; + a = sigpsin15[i]; + b = sigpsin15[i + 1]; + return (a + ((b - a) * f >> 8)); +} + +/* 14-bit interpolated sine/cosine */ +extern void +crt_sincos14(int *s, int *c, int n) +{ + int 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; + } +} + +extern int +crt_bpp4fmt(int 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; + } +} + +/*****************************************************************************/ +/********************************* FILTERS ***********************************/ +/*****************************************************************************/ + +/* convolution is much faster but the EQ looks softer, more authentic, and more analog */ +#define USE_CONVOLUTION 0 +#define USE_7_SAMPLE_KERNEL 1 +#define USE_6_SAMPLE_KERNEL 0 +#define USE_5_SAMPLE_KERNEL 0 + +#if (CRT_CC_SAMPLES != 4) +/* the current convolutions do not filter properly at > 4 samples */ +#undef USE_CONVOLUTION +#define USE_CONVOLUTION 0 +#endif + +#if USE_CONVOLUTION + +/* NOT 3 band equalizer, faster convolution instead. + * eq function names preserved to keep code clean + */ +static struct EQF { + int h[7]; +} eqY, eqI, eqQ; + +/* params unused to keep the function the same */ +static void +init_eq(struct EQF *f, + int f_lo, int f_hi, int rate, + int g_lo, int g_mid, int g_hi) +{ + memset(f, 0, sizeof(struct EQF)); +} + +static void +reset_eq(struct EQF *f) +{ + memset(f->h, 0, sizeof(f->h)); +} + +static int +eqf(struct EQF *f, int s) +{ + int i; + int *h = f->h; + + for (i = 6; i > 0; i--) { + h[i] = h[i - 1]; + } + h[0] = s; +#if USE_7_SAMPLE_KERNEL + /* index : 0 1 2 3 4 5 6 */ + /* weight: 1 4 7 8 7 4 1 */ + return (s + h[6] + ((h[1] + h[5]) * 4) + ((h[2] + h[4]) * 7) + (h[3] * 8)) >> 5; +#elif USE_6_SAMPLE_KERNEL + /* index : 0 1 2 3 4 5 */ + /* weight: 1 3 4 4 3 1 */ + return (s + h[5] + 3 * (h[1] + h[4]) + 4 * (h[2] + h[3])) >> 4; +#elif USE_5_SAMPLE_KERNEL + /* index : 0 1 2 3 4 */ + /* weight: 1 2 2 2 1 */ + return (s + h[4] + ((h[1] + h[2] + h[3]) << 1)) >> 3; +#else + /* index : 0 1 2 3 */ + /* weight: 1 1 1 1*/ + return (s + h[3] + h[1] + h[2]) >> 2; +#endif +} + +#else + +#define HISTLEN 3 +#define HISTOLD (HISTLEN - 1) /* oldest entry */ +#define HISTNEW 0 /* newest entry */ + +#define EQ_P 16 /* if changed, the gains will need to be adjusted */ +#define EQ_R (1 << (EQ_P - 1)) /* rounding */ +/* three band equalizer */ +static struct EQF { + int lf, hf; /* fractions */ + int g[3]; /* gains */ + int fL[4]; + int fH[4]; + int h[HISTLEN]; /* history */ +} eqY, eqI, eqQ; + +/* f_lo - low cutoff frequency + * f_hi - high cutoff frequency + * rate - sampling rate + * g_lo, g_mid, g_hi - gains + */ +static void +init_eq(struct EQF *f, + int f_lo, int f_hi, int rate, + int g_lo, int g_mid, int g_hi) +{ + int sn, cs; + + memset(f, 0, sizeof(struct EQF)); + + f->g[0] = g_lo; + f->g[1] = g_mid; + f->g[2] = 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)); +#endif + 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)); +#endif +} + +static void +reset_eq(struct EQF *f) +{ + memset(f->fL, 0, sizeof(f->fL)); + memset(f->fH, 0, sizeof(f->fH)); + memset(f->h, 0, sizeof(f->h)); +} + +static int +eqf(struct EQF *f, int s) +{ + int i, r[3]; + + 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]); +} + +#endif + +/*****************************************************************************/ +/***************************** PUBLIC FUNCTIONS ******************************/ +/*****************************************************************************/ + +extern void +crt_resize(struct CRT *v, int w, int h, int f, unsigned char *out) +{ + v->outw = w; + v->outh = h; + v->out_format = f; + v->out = out; +} + +extern void +crt_reset(struct CRT *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; +} + +extern void +crt_init(struct CRT *v, int w, int h, int f, unsigned char *out) +{ + memset(v, 0, sizeof(struct CRT)); + crt_resize(v, w, h, f, out); + crt_reset(v); + v->rn = 194; + + /* kilohertz to line sample conversion */ +#define kHz2L(kHz) (CRT_HRES * (kHz * 100) / L_FREQ) + + /* band gains are pre-scaled as 16-bit fixed point + * if you change the EQ_P define, you'll need to update these gains too + */ +#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); +#elif (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 +#error "NTSC-CRT currently only supports 4 or 5 samples per chroma period." +#endif + +} + +extern void +crt_demodulate(struct CRT *v, int noise) +{ + /* made static so all this data does not go on the stack */ + static struct { + int y, i, q; + } out[AV_LEN + 1], *yiqA, *yiqB; + int i, j, line, rn; + signed char *sig; + int s = 0; + int field, ratio; + int *ccr; /* color carrier signal */ + int huesn, huecs; + int xnudge = -3, ynudge = 3; + int bright = v->brightness - (BLACK_LEVEL + v->black_point); + int bpp, pitch; +#if CRT_DO_BLOOM + int prev_e; /* filtered beam energy per scan line */ + int max_e; /* approx maximum energy in a scan line */ +#endif + + 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; /* make 4-bit */ + huecs >>= 11; + + rn = v->rn; +#if !CRT_DO_VSYNC + /* determine field before we add noise, + * otherwise it's not reliably recoverable + */ + for (i = -CRT_VSYNC_WINDOW; i < CRT_VSYNC_WINDOW; i++) { + line = POSMOD(v->vsync + i, CRT_VRES); + sig = v->analog + line * CRT_HRES; + s = 0; + for (j = 0; j < CRT_HRES; j++) { + s += sig[j]; + if (s <= (CRT_VSYNC_THRESH * SYNC_LEVEL)) { + goto found_field; + } + } + } +found_field: + /* if vsync signal was in second half of line, odd field */ + field = (j > (CRT_HRES / 2)); + v->vsync = -3; +#endif +#if ((CRT_SYSTEM == CRT_SYSTEM_NTSCVHS) && CRT_VHS_NOISE) + line = ((rand() % 8) - 4) + 14; +#endif + for (i = 0; i < CRT_INPUT_SIZE; i++) { + int nn = noise; +#if ((CRT_SYSTEM == CRT_SYSTEM_NTSCVHS) && CRT_VHS_NOISE) + rn = rand(); + if (i > (CRT_INPUT_SIZE - CRT_HRES * (16 + ((rand() % 20) - 10))) && + i < (CRT_INPUT_SIZE - CRT_HRES * (5 + ((rand() % 8) - 4)))) { + int ln, sn, cs; + + ln = (i * line) / CRT_HRES; + crt_sincos14(&sn, &cs, ln * 8192 / 180); + nn = cs >> 8; + } +#else + rn = (214019 * rn + 140327895); +#endif + /* signal + noise */ + 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; + +#if CRT_DO_VSYNC + /* Look for vertical sync. + * + * This is done by integrating the signal and + * seeing if it exceeds a threshold. The threshold of + * the vertical sync pulse is much higher because the + * vsync pulse is a lot longer than the hsync pulse. + * The signal needs to be integrated to lessen + * the noise in the signal. + */ + 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]; + /* increase the multiplier to make the vsync + * more stable when there is a lot of noise + */ + if (s <= (CRT_VSYNC_THRESH * SYNC_LEVEL)) { + goto vsync_found; + } + } + } +vsync_found: + v->vsync = line; /* vsync found (or gave up) at this line */ + /* if vsync signal was in second half of line, odd field */ + field = (j > (CRT_HRES / 2)); +#endif + +#if CRT_DO_BLOOM + max_e = (128 + (noise / 2)) * AV_LEN; + prev_e = (16384 / 8); +#endif + /* ratio of output height to active video lines in the signal */ + ratio = (v->outh << 16) / CRT_LINES; + ratio = (ratio + 32768) >> 16; + + field = (field * (ratio / 2)); + + for (line = CRT_TOP; line < CRT_BOT; line++) { + unsigned pos, ln, scanR; + int scanL, dx; + int L, R; + unsigned char *cL, *cR; +#if (CRT_CC_SAMPLES == 4) + int wave[CRT_CC_SAMPLES]; +#else + int waveI[CRT_CC_SAMPLES]; + int waveQ[CRT_CC_SAMPLES]; +#endif + int dci, dcq; /* decoded I, Q */ + int xpos, ypos; + int beg, end; + int phasealign; +#if CRT_DO_BLOOM + int line_w; +#endif + + 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; } + + /* Look for horizontal sync. + * See comment above regarding vertical sync. + */ + 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; + } + } +#if CRT_DO_HSYNC + v->hsync = POSMOD(i + v->hsync, CRT_HRES); +#else + v->hsync = 0; +#endif + + 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]; +#if (CRT_CC_SAMPLES == 4) + sig = v->inp + ln + (v->hsync & ~3); /* faster */ +#else + sig = v->inp + ln + (v->hsync - (v->hsync % CRT_CC_SAMPLES)); +#endif + for (i = CB_BEG; i < CB_BEG + (CB_CYCLES * CRT_CB_FREQ); i++) { + int p, n; + p = ccr[i % CRT_CC_SAMPLES] * 127 / 128; /* fraction of the previous */ + n = sig[i]; /* mixed with the new sample */ + ccr[i % CRT_CC_SAMPLES] = p + n; + } + + phasealign = POSMOD(v->hsync, CRT_CC_SAMPLES); + +#if (CRT_CC_SAMPLES == 4) + /* amplitude of carrier = saturation, phase difference = hue */ + 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]; +#elif (CRT_CC_SAMPLES == 5) + { + int dciA, dciB; + int dcqA, dcqB; + int ang = (v->hue % 360); + int off180 = CRT_CC_SAMPLES / 2; + int off90 = CRT_CC_SAMPLES / 4; + int peakA = phasealign + off90; + int peakB = phasealign + 0; + dciA = dciB = dcqA = dcqB = 0; + /* amplitude of carrier = saturation, phase difference = hue */ + dciA = ccr[(peakA) % CRT_CC_SAMPLES]; + /* average */ + 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; + /* create wave tables and rotate them by the hue adjustment angle */ + for (i = 0; i < CRT_CC_SAMPLES; i++) { + int sn, cs; + crt_sincos14(&sn, &cs, ang * 8192 / 180); + waveI[i] = ((dci * cs + dcq * sn) >> 15) * v->saturation; + /* Q is offset by 90 */ + crt_sincos14(&sn, &cs, (ang + 90) * 8192 / 180); + waveQ[i] = ((dci * cs + dcq * sn) >> 15) * v->saturation; + ang += (360 / CRT_CC_SAMPLES); + } + } +#endif + sig = v->inp + pos; +#if CRT_DO_BLOOM + s = 0; + for (i = 0; i < AV_LEN; i++) { + s += sig[i]; /* sum up the scan line */ + } + /* bloom emulation */ + prev_e = (prev_e * 123 / 128) + ((((max_e >> 1) - s) << 10) / max_e); + line_w = (AV_LEN * 112 / 128) + (prev_e >> 9); + + dx = (line_w << 12) / v->outw; + scanL = ((AV_LEN / 2) - (line_w >> 1) + 8) << 12; + scanR = (AV_LEN - 1) << 12; + + L = (scanL >> 12); + R = (scanR >> 12); +#else + dx = ((AV_LEN - 1) << 12) / v->outw; + scanL = 0; + scanR = (AV_LEN - 1) << 12; + L = 0; + R = AV_LEN; +#endif + reset_eq(&eqY); + reset_eq(&eqI); + reset_eq(&eqQ); + +#if (CRT_CC_SAMPLES == 4) + for (i = L; i < R; i++) { + out[i].y = eqf(&eqY, sig[i] + bright) << 4; + out[i].i = eqf(&eqI, sig[i] * wave[(i + 0) & 3] >> 9) >> 3; + out[i].q = eqf(&eqQ, sig[i] * wave[(i + 3) & 3] >> 9) >> 3; + } +#else + 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; + } +#endif + + cL = v->out + (beg * pitch); + cR = cL + pitch; + + for (pos = scanL; pos < scanR && cL < cR; pos += dx) { + int y, i, q; + int r, g, b; + int aa, bb; + + R = pos & 0xfff; + L = 0xfff - R; + s = pos >> 12; + + yiqA = out + s; + yiqB = out + s + 1; + + /* interpolate between samples if needed */ + 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); + + /* YIQ to RGB */ + 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; + } + + /* blend with previous color there */ + bb = (((aa & 0xfefeff) >> 1) + ((bb & 0xfefeff) >> 1)); + } else { + bb = (r << 16 | g << 8 | b); + } + + switch (v->out_format) { + case CRT_PIX_FORMAT_RGB: + cL[0] = bb >> 16 & 0xff; + cL[1] = bb >> 8 & 0xff; + cL[2] = bb >> 0 & 0xff; + break; + + case CRT_PIX_FORMAT_RGBA: + cL[0] = bb >> 16 & 0xff; + cL[1] = bb >> 8 & 0xff; + cL[2] = bb >> 0 & 0xff; + cL[3] = 0xff; + break; + + case CRT_PIX_FORMAT_BGR: + cL[0] = bb >> 0 & 0xff; + cL[1] = bb >> 8 & 0xff; + cL[2] = bb >> 16 & 0xff; + break; + + case CRT_PIX_FORMAT_BGRA: + cL[0] = bb >> 0 & 0xff; + cL[1] = bb >> 8 & 0xff; + cL[2] = bb >> 16 & 0xff; + cL[3] = 0xff; + break; + + case CRT_PIX_FORMAT_ARGB: + cL[0] = 0xff; + cL[1] = bb >> 16 & 0xff; + cL[2] = bb >> 8 & 0xff; + cL[3] = bb >> 0 & 0xff; + break; + + case CRT_PIX_FORMAT_ABGR: + cL[0] = 0xff; + cL[1] = bb >> 0 & 0xff; + cL[2] = bb >> 8 & 0xff; + cL[3] = bb >> 16 & 0xff; + break; + + default: + break; + } + + cL += bpp; + } + + /* duplicate extra lines */ + for (s = beg + 1; s < (end - v->scanlines); s++) { + memcpy(v->out + s * pitch, v->out + (s - 1) * pitch, pitch); + } + } +} diff --git a/gfx/video_filters/ntsc_crt_filter/crt_core.h b/gfx/video_filters/ntsc_crt_filter/crt_core.h new file mode 100644 index 00000000000..456191bb2a6 --- /dev/null +++ b/gfx/video_filters/ntsc_crt_filter/crt_core.h @@ -0,0 +1,145 @@ +/*****************************************************************************/ +/* + * NTSC/CRT - integer-only NTSC video signal encoding / decoding emulation + * + * by EMMIR 2018-2023 + * + * YouTube: https://www.youtube.com/@EMMIR_KC/videos + * Discord: https://discord.com/invite/hdYctSmyQJ + */ +/*****************************************************************************/ +#ifndef _CRT_CORE_H_ +#define _CRT_CORE_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +/* crt_core.h + * + * The demodulator. This is also where you can define which system to emulate. + * + */ + +/* library version */ +#define CRT_MAJOR 2 +#define CRT_MINOR 3 +#define CRT_PATCH 2 + + +#define CRT_SYSTEM_NTSC 0 /* standard NTSC */ +#define CRT_SYSTEM_NES 1 /* decode 6 or 9-bit NES pixels */ +#define CRT_SYSTEM_PV1K 2 /* Casio PV-1000 */ +#define CRT_SYSTEM_SNES 3 /* SNES - uses RGB */ +#define CRT_SYSTEM_TEMP 4 /* template implementation */ +#define CRT_SYSTEM_NTSCVHS 5 /* standard NTSC VHS */ +#define CRT_SYSTEM_NESRGB 6 /* encode RGB image with NES artifacts */ + +/* the system to be compiled */ +#ifndef CRT_SYSTEM +#define CRT_SYSTEM CRT_SYSTEM_NTSC +#endif + +#if (CRT_SYSTEM == CRT_SYSTEM_NES) +#include "crt_nes.h" +#elif (CRT_SYSTEM == CRT_SYSTEM_SNES) +#include "crt_snes.h" +#elif (CRT_SYSTEM == CRT_SYSTEM_NTSC) +#include "crt_ntsc.h" +#elif (CRT_SYSTEM == CRT_SYSTEM_PV1K) +#include "crt_pv1k.h" +#elif (CRT_SYSTEM == CRT_SYSTEM_TEMP) +#include "crt_template.h" +#elif (CRT_SYSTEM == CRT_SYSTEM_NTSCVHS) +#include "crt_ntscvhs.h" +#elif (CRT_SYSTEM == CRT_SYSTEM_NESRGB) +#include "crt_nesrgb.h" +#else +#error No system defined +#endif + +/* NOTE: this library does not use the alpha channel at all */ +#define CRT_PIX_FORMAT_RGB 0 /* 3 bytes per pixel [R,G,B,R,G,B,R,G,B...] */ +#define CRT_PIX_FORMAT_BGR 1 /* 3 bytes per pixel [B,G,R,B,G,R,B,G,R...] */ +#define CRT_PIX_FORMAT_ARGB 2 /* 4 bytes per pixel [A,R,G,B,A,R,G,B...] */ +#define CRT_PIX_FORMAT_RGBA 3 /* 4 bytes per pixel [R,G,B,A,R,G,B,A...] */ +#define CRT_PIX_FORMAT_ABGR 4 /* 4 bytes per pixel [A,B,G,R,A,B,G,R...] */ +#define CRT_PIX_FORMAT_BGRA 5 /* 4 bytes per pixel [B,G,R,A,B,G,R,A...] */ + +/* do bloom emulation (side effect: makes screen have black borders) */ +#define CRT_DO_BLOOM 0 /* does not work for NES */ +#define CRT_DO_VSYNC 1 /* look for VSYNC */ +#define CRT_DO_HSYNC 1 /* look for HSYNC */ + +struct CRT { + signed char analog[CRT_INPUT_SIZE]; + signed char inp[CRT_INPUT_SIZE]; /* CRT input, can be noisy */ + + int outw, outh; /* output width/height */ + int out_format; /* output pixel format (one of the CRT_PIX_FORMATs) */ + unsigned char *out; /* output image */ + + int hue, brightness, contrast, saturation; /* common monitor settings */ + int black_point, white_point; /* user-adjustable */ + int scanlines; /* leave gaps between lines if necessary */ + int blend; /* blend new field onto previous image */ + unsigned v_fac; /* factor to stretch img vertically onto the output img */ + + /* internal data */ + int ccf[CRT_CC_VPER][CRT_CC_SAMPLES]; /* faster color carrier convergence */ + int hsync, vsync; /* keep track of sync over frames */ + int rn; /* seed for the 'random' noise */ +}; + +/* Initializes the library. Sets up filters. + * w - width of the output image + * h - height of the output image + * f - format of the output image + * out - pointer to output image data + */ +extern void crt_init(struct CRT *v, int w, int h, int f, unsigned char *out); + +/* Updates the output image parameters + * w - width of the output image + * h - height of the output image + * f - format of the output image + * out - pointer to output image data + */ +extern void crt_resize(struct CRT *v, int w, int h, int f, unsigned char *out); + +/* Resets the CRT settings back to their defaults */ +extern void crt_reset(struct CRT *v); + +/* Modulates RGB image into an analog NTSC signal + * s - struct containing settings to apply to this field + */ +extern void crt_modulate(struct CRT *v, struct NTSC_SETTINGS *s); + +/* Demodulates the NTSC signal generated by crt_modulate() + * noise - the amount of noise added to the signal (0 - inf) + */ +extern void crt_demodulate(struct CRT *v, int noise); + +/* Get the bytes per pixel for a certain CRT_PIX_FORMAT_ + * + * format - the format to get the bytes per pixel for + * + * returns 0 if the specified format does not exist + */ +extern int crt_bpp4fmt(int format); + +/*****************************************************************************/ +/*************************** FIXED POINT SIN/COS *****************************/ +/*****************************************************************************/ + +#define T14_2PI 16384 +#define T14_MASK (T14_2PI - 1) +#define T14_PI (T14_2PI / 2) + +extern void crt_sincos14(int *s, int *c, int n); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/gfx/video_filters/ntsc_crt_filter/crt_ntsc.c b/gfx/video_filters/ntsc_crt_filter/crt_ntsc.c new file mode 100644 index 00000000000..107a2eace93 --- /dev/null +++ b/gfx/video_filters/ntsc_crt_filter/crt_ntsc.c @@ -0,0 +1,331 @@ +/*****************************************************************************/ +/* + * NTSC/CRT - integer-only NTSC video signal encoding / decoding emulation + * + * by EMMIR 2018-2023 + * + * YouTube: https://www.youtube.com/@EMMIR_KC/videos + * Discord: https://discord.com/invite/hdYctSmyQJ + */ +/*****************************************************************************/ + +#include "crt_core.h" + +#if (CRT_SYSTEM == CRT_SYSTEM_NTSC) +#include +#include + +#if (CRT_CHROMA_PATTERN == 1) +/* 227.5 subcarrier cycles per line means every other line has reversed phase */ +#define CC_PHASE(ln) (((ln) & 1) ? -1 : 1) +#else +#define CC_PHASE(ln) (1) +#endif + +#define EXP_P 11 +#define EXP_ONE (1 << EXP_P) +#define EXP_MASK (EXP_ONE - 1) +#define EXP_PI 6434 +#define EXP_MUL(x, y) (((x) * (y)) >> EXP_P) +#define EXP_DIV(x, y) (((x) << EXP_P) / (y)) + +static int e11[] = { + EXP_ONE, + 5567, /* e */ + 15133, /* e^2 */ + 41135, /* e^3 */ + 111817 /* e^4 */ +}; + +/* fixed point e^x */ +static int +expx(int n) +{ + int neg, idx, res; + int nxt, acc, del; + int i; + + if (n == 0) { + return EXP_ONE; + } + neg = n < 0; + if (neg) { + n = -n; + } + idx = n >> EXP_P; + res = EXP_ONE; + for (i = 0; i < idx / 4; i++) { + res = EXP_MUL(res, e11[4]); + } + idx &= 3; + if (idx > 0) { + res = EXP_MUL(res, e11[idx]); + } + + n &= EXP_MASK; + nxt = EXP_ONE; + acc = 0; + del = 1; + for (i = 1; i < 17; i++) { + acc += nxt / del; + nxt = EXP_MUL(nxt, n); + del *= i; + if (del > nxt || nxt <= 0 || del <= 0) { + break; + } + } + res = EXP_MUL(res, acc); + + if (neg) { + res = EXP_DIV(EXP_ONE, res); + } + return res; +} + +/*****************************************************************************/ +/********************************* FILTERS ***********************************/ +/*****************************************************************************/ + +/* infinite impulse response low pass filter for bandlimiting YIQ */ +static struct IIRLP { + int c; + int h; /* history */ +} iirY, iirI, iirQ; + +/* freq - total bandwidth + * limit - max frequency + */ +static void +init_iir(struct IIRLP *f, int freq, int limit) +{ + int rate; /* cycles/pixel rate */ + + memset(f, 0, sizeof(struct IIRLP)); + rate = (freq << 9) / limit; + f->c = EXP_ONE - expx(-((EXP_PI << 9) / rate)); +} + +static void +reset_iir(struct IIRLP *f) +{ + f->h = 0; +} + +/* hi-pass for debugging */ +#define HIPASS 0 + +static int +iirf(struct IIRLP *f, int s) +{ + f->h += EXP_MUL(s - f->h, f->c); +#if HIPASS + return s - f->h; +#else + return f->h; +#endif +} + +extern void +crt_modulate(struct CRT *v, struct NTSC_SETTINGS *s) +{ + int x, y, xo, yo; + int destw = AV_LEN; + int desth = ((CRT_LINES * 64500) >> 16); + int iccf[CRT_CC_SAMPLES]; + int ccmodI[CRT_CC_SAMPLES]; /* color phase for mod */ + int ccmodQ[CRT_CC_SAMPLES]; /* color phase for mod */ + int ccburst[CRT_CC_SAMPLES]; /* color phase for burst */ + int sn, cs, n, ph; + int inv_phase = 0; + int bpp; + + if (!s->iirs_initialized) { + init_iir(&iirY, L_FREQ, Y_FREQ); + init_iir(&iirI, L_FREQ, I_FREQ); + init_iir(&iirQ, L_FREQ, Q_FREQ); + s->iirs_initialized = 1; + } +#if CRT_DO_BLOOM + if (s->raw) { + destw = s->w; + desth = s->h; + if (destw > ((AV_LEN * 55500) >> 16)) { + destw = ((AV_LEN * 55500) >> 16); + } + if (desth > ((CRT_LINES * 63500) >> 16)) { + desth = ((CRT_LINES * 63500) >> 16); + } + } else { + destw = (AV_LEN * 55500) >> 16; + desth = (CRT_LINES * 63500) >> 16; + } +#else + if (s->raw) { + destw = s->w; + desth = s->h; + if (destw > AV_LEN) { + destw = AV_LEN; + } + if (desth > ((CRT_LINES * 64500) >> 16)) { + desth = ((CRT_LINES * 64500) >> 16); + } + } +#endif + if (s->as_color) { + for (x = 0; x < CRT_CC_SAMPLES; x++) { + n = s->hue + x * (360 / CRT_CC_SAMPLES); + crt_sincos14(&sn, &cs, (n + 33) * 8192 / 180); + ccburst[x] = sn >> 10; + crt_sincos14(&sn, &cs, n * 8192 / 180); + ccmodI[x] = sn >> 10; + crt_sincos14(&sn, &cs, (n - 90) * 8192 / 180); + ccmodQ[x] = sn >> 10; + } + } else { + memset(ccburst, 0, sizeof(ccburst)); + memset(ccmodI, 0, sizeof(ccmodI)); + memset(ccmodQ, 0, sizeof(ccmodQ)); + } + + bpp = crt_bpp4fmt(s->format); + if (bpp == 0) { + return; /* just to be safe */ + } + xo = AV_BEG + s->xoffset + (AV_LEN - destw) / 2; + yo = CRT_TOP + s->yoffset + (CRT_LINES - desth) / 2; + + s->field &= 1; + s->frame &= 1; + inv_phase = (s->field == s->frame); + ph = CC_PHASE(inv_phase); + + /* align signal */ + xo = (xo & ~3); + + for (n = 0; n < CRT_VRES; n++) { + int t; /* time */ + signed char *line = &v->analog[n * CRT_HRES]; + + t = LINE_BEG; + + if (n <= 3 || (n >= 7 && n <= 9)) { + /* equalizing pulses - small blips of sync, mostly blank */ + while (t < (4 * CRT_HRES / 100)) line[t++] = SYNC_LEVEL; + while (t < (50 * CRT_HRES / 100)) line[t++] = BLANK_LEVEL; + while (t < (54 * CRT_HRES / 100)) line[t++] = SYNC_LEVEL; + while (t < (100 * CRT_HRES / 100)) line[t++] = BLANK_LEVEL; + } else if (n >= 4 && n <= 6) { + int even[4] = { 46, 50, 96, 100 }; + int odd[4] = { 4, 50, 96, 100 }; + int *offs = even; + if (s->field == 1) { + offs = odd; + } + /* vertical sync pulse - small blips of blank, mostly sync */ + while (t < (offs[0] * CRT_HRES / 100)) line[t++] = SYNC_LEVEL; + while (t < (offs[1] * CRT_HRES / 100)) line[t++] = BLANK_LEVEL; + while (t < (offs[2] * CRT_HRES / 100)) line[t++] = SYNC_LEVEL; + while (t < (offs[3] * CRT_HRES / 100)) line[t++] = BLANK_LEVEL; + } else { + int cb; + + /* video line */ + while (t < SYNC_BEG) line[t++] = BLANK_LEVEL; /* FP */ + while (t < BW_BEG) line[t++] = SYNC_LEVEL; /* SYNC */ + while (t < AV_BEG) line[t++] = BLANK_LEVEL; /* BW + CB + BP */ + if (n < CRT_TOP) { + while (t < CRT_HRES) line[t++] = BLANK_LEVEL; + } + + /* CB_CYCLES of color burst at 3.579545 Mhz */ + for (t = CB_BEG; t < CB_BEG + (CB_CYCLES * CRT_CB_FREQ); t++) { +#if (CRT_CHROMA_PATTERN == 1) + int off180 = CRT_CC_SAMPLES / 2; + cb = ccburst[(t + inv_phase * off180) % CRT_CC_SAMPLES]; +#else + cb = ccburst[t % CRT_CC_SAMPLES]; +#endif + line[t] = (BLANK_LEVEL + (cb * BURST_LEVEL)) >> 5; + iccf[t % CRT_CC_SAMPLES] = line[t]; + } + } + } + + for (y = 0; y < desth; y++) { + int field_offset; + int sy; + + field_offset = (s->field * s->h + desth) / desth / 2; + sy = (y * s->h) / desth; + + sy += field_offset; + + if (sy >= s->h) sy = s->h; + + sy *= s->w; + + reset_iir(&iirY); + reset_iir(&iirI); + reset_iir(&iirQ); + + for (x = 0; x < destw; x++) { + int fy, fi, fq; + int rA, gA, bA; + const unsigned char *pix; + int ire; /* composite signal */ + int xoff; + + pix = s->data + ((((x * s->w) / destw) + sy) * bpp); + switch (s->format) { + case CRT_PIX_FORMAT_RGB: + case CRT_PIX_FORMAT_RGBA: + rA = pix[0]; + gA = pix[1]; + bA = pix[2]; + break; + case CRT_PIX_FORMAT_BGR: + case CRT_PIX_FORMAT_BGRA: + rA = pix[2]; + gA = pix[1]; + bA = pix[0]; + break; + case CRT_PIX_FORMAT_ARGB: + rA = pix[1]; + gA = pix[2]; + bA = pix[3]; + break; + case CRT_PIX_FORMAT_ABGR: + rA = pix[3]; + gA = pix[2]; + bA = pix[1]; + break; + default: + rA = gA = bA = 0; + break; + } + + /* RGB to YIQ */ + fy = (19595 * rA + 38470 * gA + 7471 * bA) >> 14; + fi = (39059 * rA - 18022 * gA - 21103 * bA) >> 14; + fq = (13894 * rA - 34275 * gA + 20382 * bA) >> 14; + ire = BLACK_LEVEL + v->black_point; + + xoff = (x + xo) % CRT_CC_SAMPLES; + /* bandlimit Y,I,Q */ + fy = iirf(&iirY, fy); + fi = iirf(&iirI, fi) * ph * ccmodI[xoff] >> 4; + fq = iirf(&iirQ, fq) * ph * ccmodQ[xoff] >> 4; + ire += (fy + fi + fq) * (WHITE_LEVEL * v->white_point / 100) >> 10; + if (ire < 0) ire = 0; + if (ire > 110) ire = 110; + + v->analog[(x + xo) + (y + yo) * CRT_HRES] = ire; + } + } + for (n = 0; n < CRT_CC_VPER; n++) { + for (x = 0; x < CRT_CC_SAMPLES; x++) { + v->ccf[n][x] = iccf[x] << 7; + } + } +} +#endif diff --git a/gfx/video_filters/ntsc_crt_filter/crt_ntsc.h b/gfx/video_filters/ntsc_crt_filter/crt_ntsc.h new file mode 100644 index 00000000000..2a6f0e94770 --- /dev/null +++ b/gfx/video_filters/ntsc_crt_filter/crt_ntsc.h @@ -0,0 +1,130 @@ +/*****************************************************************************/ +/* + * NTSC/CRT - integer-only NTSC video signal encoding / decoding emulation + * + * by EMMIR 2018-2023 + * + * YouTube: https://www.youtube.com/@EMMIR_KC/videos + * Discord: https://discord.com/invite/hdYctSmyQJ + */ +/*****************************************************************************/ +#ifndef _CRT_NTSC_H_ +#define _CRT_NTSC_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +/* crt_ntsc.h + * + * An interface to convert a digital image to an analog NTSC signal. + * + */ +/* 0 = vertical chroma (228 chroma clocks per line) */ +/* 1 = checkered chroma (227.5 chroma clocks per line) */ +#define CRT_CHROMA_PATTERN 1 + +/* chroma clocks (subcarrier cycles) per line */ +#if (CRT_CHROMA_PATTERN == 1) +#define CRT_CC_LINE 2275 +#else +/* this will give the 'rainbow' effect in the famous waterfall scene */ +#define CRT_CC_LINE 2280 +#endif + +/* NOTE, in general, increasing CRT_CB_FREQ reduces blur and bleed */ +#define CRT_CB_FREQ 4 /* carrier frequency relative to sample rate */ +#define CRT_HRES (CRT_CC_LINE * CRT_CB_FREQ / 10) /* horizontal res */ +#define CRT_VRES 262 /* vertical resolution */ +#define CRT_INPUT_SIZE (CRT_HRES * CRT_VRES) + +#define CRT_TOP 21 /* first line with active video */ +#define CRT_BOT 261 /* final line with active video */ +#define CRT_LINES (CRT_BOT - CRT_TOP) /* number of active video lines */ + +#define CRT_CC_SAMPLES 4 /* samples per chroma period (samples per 360 deg) */ +#define CRT_CC_VPER 1 /* vertical period in which the artifacts repeat */ + +/* search windows, in samples */ +#define CRT_HSYNC_WINDOW 8 +#define CRT_VSYNC_WINDOW 8 + +/* accumulated signal threshold required for sync detection. + * Larger = more stable, until it's so large that it is never reached in which + * case the CRT won't be able to sync + */ +#define CRT_HSYNC_THRESH 4 +#define CRT_VSYNC_THRESH 94 + +/* + * FULL HORIZONTAL LINE SIGNAL (~63500 ns) + * |---------------------------------------------------------------------------| + * HBLANK (~10900 ns) ACTIVE VIDEO (~52600 ns) + * |-------------------||------------------------------------------------------| + * + * + * WITHIN HBLANK PERIOD: + * + * FP (~1500 ns) SYNC (~4700 ns) BW (~600 ns) CB (~2500 ns) BP (~1600 ns) + * |--------------||---------------||------------||-------------||-------------| + * BLANK SYNC BLANK BLANK BLANK + * + */ +#define LINE_BEG 0 +#define FP_ns 1500 /* front porch */ +#define SYNC_ns 4700 /* sync tip */ +#define BW_ns 600 /* breezeway */ +#define CB_ns 2500 /* color burst */ +#define BP_ns 1600 /* back porch */ +#define AV_ns 52600 /* active video */ +#define HB_ns (FP_ns + SYNC_ns + BW_ns + CB_ns + BP_ns) /* h blank */ +/* line duration should be ~63500 ns */ +#define LINE_ns (FP_ns + SYNC_ns + BW_ns + CB_ns + BP_ns + AV_ns) + +/* convert nanosecond offset to its corresponding point on the sampled line */ +#define ns2pos(ns) ((ns) * CRT_HRES / LINE_ns) +/* starting points for all the different pulses */ +#define FP_BEG ns2pos(0) +#define SYNC_BEG ns2pos(FP_ns) +#define BW_BEG ns2pos(FP_ns + SYNC_ns) +#define CB_BEG ns2pos(FP_ns + SYNC_ns + BW_ns) +#define BP_BEG ns2pos(FP_ns + SYNC_ns + BW_ns + CB_ns) +#define AV_BEG ns2pos(HB_ns) +#define AV_LEN ns2pos(AV_ns) + +/* somewhere between 7 and 12 cycles */ +#define CB_CYCLES 10 + +/* frequencies for bandlimiting */ +#define L_FREQ 1431818 /* full line */ +#define Y_FREQ 420000 /* Luma (Y) 4.2 MHz of the 14.31818 MHz */ +#define I_FREQ 150000 /* Chroma (I) 1.5 MHz of the 14.31818 MHz */ +#define Q_FREQ 55000 /* Chroma (Q) 0.55 MHz of the 14.31818 MHz */ + +/* IRE units (100 = 1.0V, -40 = 0.0V) */ +#define WHITE_LEVEL 100 +#define BURST_LEVEL 20 +#define BLACK_LEVEL 7 +#define BLANK_LEVEL 0 +#define SYNC_LEVEL -40 + +struct NTSC_SETTINGS { + const unsigned char *data; /* image data */ + int format; /* pix format (one of the CRT_PIX_FORMATs in crt_core.h) */ + int w, h; /* width and height of image */ + int raw; /* 0 = scale image to fit monitor, 1 = don't scale */ + int as_color; /* 0 = monochrome, 1 = full color */ + int field; /* 0 = even, 1 = odd */ + int frame; /* 0 = even, 1 = odd */ + int hue; /* 0-359 */ + int xoffset; /* x offset in sample space. 0 is minimum value */ + int yoffset; /* y offset in # of lines. 0 is minimum value */ + /* make sure your NTSC_SETTINGS struct is zeroed out before you do anything */ + int iirs_initialized; /* internal state */ +}; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/gfx/video_filters/ntsc_crt_filter/ntsc_crt.c b/gfx/video_filters/ntsc_crt_filter/ntsc_crt.c new file mode 100644 index 00000000000..2c70d8bec76 --- /dev/null +++ b/gfx/video_filters/ntsc_crt_filter/ntsc_crt.c @@ -0,0 +1,388 @@ +/* RetroArch CPU Filter - NTSC/CRT + * + * Original NTSC/CRT library by EMMIR 2018-2023 + * https://www.youtube.com/@EMMIR_KC/videos + * + * RetroArch softfilter wrapper 2024 + * + * Compile (Linux): + * gcc -O2 -shared -fPIC -std=c99 \ + * -o ntsc_crt.so \ + * ntsc_crt.c crt_core.c crt_ntsc.c \ + * -lm + * + * Compile (Windows cross): + * x86_64-w64-mingw32-gcc -O2 -shared -std=c99 \ + * -o ntsc_crt.dll \ + * ntsc_crt.c crt_core.c crt_ntsc.c \ + * -lm + * + * Install: + * Copy ntsc_crt.so (or .dll) + ntsc_crt.filt to: + * /filters/video/ + */ + +#include "softfilter.h" +#include "crt_core.h" + +#include +#include +#include + +#ifdef RARCH_INTERNAL +#define softfilter_get_implementation ntsc_crt_get_implementation +#define softfilter_thread_data ntsc_crt_softfilter_thread_data +#define filter_data ntsc_crt_filter_data +#endif + +/* ----------------------------------------------------------------------- + * Default tuning — these become the starting values shown in the .filt + * ----------------------------------------------------------------------- */ +#define DEFAULT_NOISE 2 /* 0 = clean signal, ~10 = noisy */ +#define DEFAULT_HUE 0 /* 0-359 */ +#define DEFAULT_BRIGHTNESS 0 /* monitor brightness offset */ +#define DEFAULT_CONTRAST 180 /* monitor contrast */ +#define DEFAULT_SATURATION 10 /* color saturation */ +#define DEFAULT_SCANLINES 0 /* 1 = dark gap between scanlines */ +#define DEFAULT_BLEND 1 /* 1 = blend fields (less flicker) */ +#define DEFAULT_AS_COLOR 1 /* 0 = monochrome, 1 = color */ +#define DEFAULT_BLACK_PT 0 /* black point adjustment */ +#define DEFAULT_WHITE_PT 100 /* white point adjustment */ + +/* ----------------------------------------------------------------------- + * Per-thread data (same pattern as scanline2x) + * ----------------------------------------------------------------------- */ +struct softfilter_thread_data +{ + void *out_data; + const void *in_data; + size_t out_pitch; + size_t in_pitch; + unsigned colfmt; + unsigned width; + unsigned height; + int first; + int last; +}; + +/* ----------------------------------------------------------------------- + * Main filter state + * ----------------------------------------------------------------------- */ +struct filter_data +{ + unsigned threads; + struct softfilter_thread_data *workers; + unsigned in_fmt; + + /* CRT library state */ + struct CRT crt; + struct NTSC_SETTINGS ntsc; + + /* output buffer (XRGB8888) */ + unsigned char *out_buf; + unsigned out_buf_w; + unsigned out_buf_h; + + /* user-settable params */ + int noise; + int hue; + int scanlines; + int blend; + int as_color; + + /* frame/field counter */ + int frame; + int field; +}; + +/* ----------------------------------------------------------------------- + * Helpers: XRGB8888 ↔ RGB packing + * ----------------------------------------------------------------------- */ + +/* Convert XRGB8888 source row to a packed RGB byte array for crt_modulate */ +static void xrgb8888_to_rgb(const uint32_t *src, unsigned char *dst, + unsigned w) +{ + unsigned x; + for (x = 0; x < w; x++) { + uint32_t c = src[x]; + dst[x * 3 + 0] = (c >> 16) & 0xff; /* R */ + dst[x * 3 + 1] = (c >> 8) & 0xff; /* G */ + dst[x * 3 + 2] = (c >> 0) & 0xff; /* B */ + } +} + +static void rgb565_to_rgb(const uint16_t *src, unsigned char *dst, + unsigned w) +{ + unsigned x; + for (x = 0; x < w; x++) { + uint16_t c = src[x]; + /* expand to 8-bit */ + dst[x * 3 + 0] = ((c >> 11) & 0x1f) << 3; + dst[x * 3 + 1] = ((c >> 5) & 0x3f) << 2; + dst[x * 3 + 2] = ((c >> 0) & 0x1f) << 3; + } +} + +/* Convert CRT XRGB output back to RetroArch XRGB8888 scanline */ +static void rgb_to_xrgb8888(const unsigned char *src, uint32_t *dst, + unsigned w) +{ + unsigned x; + for (x = 0; x < w; x++) { + dst[x] = 0xff000000u + | ((uint32_t)src[x * 3 + 0] << 16) + | ((uint32_t)src[x * 3 + 1] << 8) + | ((uint32_t)src[x * 3 + 2] << 0); + } +} + +/* ----------------------------------------------------------------------- + * softfilter API + * ----------------------------------------------------------------------- */ + +static unsigned ntsc_crt_input_fmts(void) +{ + return SOFTFILTER_FMT_XRGB8888 | SOFTFILTER_FMT_RGB565; +} + +static unsigned ntsc_crt_output_fmts(unsigned input_fmts) +{ + /* We always output XRGB8888 (CRT lib works in RGB) */ + (void)input_fmts; + return SOFTFILTER_FMT_XRGB8888; +} + +static unsigned ntsc_crt_threads(void *data) +{ + struct filter_data *filt = (struct filter_data*)data; + return filt->threads; +} + +/* Output is the same size as input — the CRT effect is spatial, not scaling */ +static void ntsc_crt_output_size(void *data, + unsigned *out_width, unsigned *out_height, + unsigned width, unsigned height) +{ + (void)data; + *out_width = width; + *out_height = height; +} + +static void *ntsc_crt_create(const struct softfilter_config *config, + unsigned in_fmt, unsigned out_fmt, + unsigned max_width, unsigned max_height, + unsigned threads, softfilter_simd_mask_t simd, void *userdata) +{ + struct filter_data *filt; + unsigned char *out_buf; + (void)out_fmt; + (void)simd; + (void)userdata; + + filt = (struct filter_data*)calloc(1, sizeof(*filt)); + if (!filt) return NULL; + + filt->workers = (struct softfilter_thread_data*) + calloc(1, sizeof(struct softfilter_thread_data)); + if (!filt->workers) { free(filt); return NULL; } + + /* single-threaded — CRT state is not thread-safe */ + filt->threads = 1; + filt->in_fmt = in_fmt; + + /* Allocate CRT output buffer: RGB (3 bpp) */ + out_buf = (unsigned char*)malloc(max_width * max_height * 3); + if (!out_buf) { free(filt->workers); free(filt); return NULL; } + + filt->out_buf = out_buf; + filt->out_buf_w = max_width; + filt->out_buf_h = max_height; + + /* Init CRT library */ + crt_init(&filt->crt, max_width, max_height, + CRT_PIX_FORMAT_RGB, out_buf); + + /* Read tunable params from .filt config (falls back to defaults) */ + if (config) { + config->get_int(userdata, "noise", &filt->noise, DEFAULT_NOISE); + config->get_int(userdata, "hue", &filt->hue, DEFAULT_HUE); + config->get_int(userdata, "as_color", &filt->as_color, DEFAULT_AS_COLOR); + config->get_int(userdata, "scanlines", &filt->scanlines, DEFAULT_SCANLINES); + config->get_int(userdata, "blend", &filt->blend, DEFAULT_BLEND); + config->get_int(userdata, "brightness", &filt->crt.brightness, DEFAULT_BRIGHTNESS); + config->get_int(userdata, "contrast", &filt->crt.contrast, DEFAULT_CONTRAST); + config->get_int(userdata, "saturation", &filt->crt.saturation, DEFAULT_SATURATION); + config->get_int(userdata, "black_point",&filt->crt.black_point, DEFAULT_BLACK_PT); + config->get_int(userdata, "white_point",&filt->crt.white_point, DEFAULT_WHITE_PT); + } else { + filt->noise = DEFAULT_NOISE; + filt->hue = DEFAULT_HUE; + filt->as_color = DEFAULT_AS_COLOR; + filt->scanlines = DEFAULT_SCANLINES; + filt->blend = DEFAULT_BLEND; + filt->crt.brightness = DEFAULT_BRIGHTNESS; + filt->crt.contrast = DEFAULT_CONTRAST; + filt->crt.saturation = DEFAULT_SATURATION; + filt->crt.black_point = DEFAULT_BLACK_PT; + filt->crt.white_point = DEFAULT_WHITE_PT; + } + + filt->crt.scanlines = filt->scanlines; + filt->crt.blend = filt->blend; + filt->crt.hue = filt->hue; + + /* NTSC_SETTINGS — zeroed by calloc, just set fields */ + memset(&filt->ntsc, 0, sizeof(filt->ntsc)); + filt->ntsc.as_color = filt->as_color; + filt->ntsc.hue = filt->hue; + filt->ntsc.format = CRT_PIX_FORMAT_RGB; + + filt->frame = 0; + filt->field = 0; + + return filt; +} + +static void ntsc_crt_destroy(void *data) +{ + struct filter_data *filt = (struct filter_data*)data; + if (!filt) return; + if (filt->out_buf) free(filt->out_buf); + free(filt->workers); + free(filt); +} + +/* ----------------------------------------------------------------------- + * The actual work callback — called from RetroArch's worker thread + * ----------------------------------------------------------------------- */ +static void ntsc_crt_work_cb(void *data, void *thread_data) +{ + struct filter_data *filt = (struct filter_data*)data; + struct softfilter_thread_data *thr = (struct softfilter_thread_data*)thread_data; + + unsigned width = thr->width; + unsigned height = thr->height; + size_t in_pitch = thr->in_pitch; + size_t out_pitch = thr->out_pitch; + + /* ---- Convert input to RGB byte-array for crt_modulate ---- */ + unsigned char *rgb_in = (unsigned char*)malloc(width * height * 3); + if (!rgb_in) return; + + if (thr->colfmt == SOFTFILTER_FMT_XRGB8888) { + unsigned y; + for (y = 0; y < height; y++) { + const uint32_t *row = (const uint32_t*) + ((const uint8_t*)thr->in_data + y * in_pitch); + xrgb8888_to_rgb(row, rgb_in + y * width * 3, width); + } + } else { + unsigned y; + for (y = 0; y < height; y++) { + const uint16_t *row = (const uint16_t*) + ((const uint8_t*)thr->in_data + y * in_pitch); + rgb565_to_rgb(row, rgb_in + y * width * 3, width); + } + } + + /* ---- Resize CRT output buffer if needed ---- */ + if (filt->out_buf_w != width || filt->out_buf_h != height) { + unsigned char *new_buf = (unsigned char*)realloc(filt->out_buf, + width * height * 3); + if (!new_buf) { free(rgb_in); return; } + filt->out_buf = new_buf; + filt->out_buf_w = width; + filt->out_buf_h = height; + crt_resize(&filt->crt, width, height, CRT_PIX_FORMAT_RGB, filt->out_buf); + } + filt->crt.out = filt->out_buf; + + /* ---- Set up NTSC_SETTINGS for this frame ---- */ + filt->ntsc.data = rgb_in; + filt->ntsc.format = CRT_PIX_FORMAT_RGB; + filt->ntsc.w = (int)width; + filt->ntsc.h = (int)height; + filt->ntsc.raw = 0; /* scale to fit */ + filt->ntsc.field = filt->field; + filt->ntsc.frame = filt->frame & 1; + filt->ntsc.hue = filt->hue; + + /* ---- Encode → signal → decode ---- */ + crt_modulate(&filt->crt, &filt->ntsc); + crt_demodulate(&filt->crt, filt->noise); + + /* ---- Advance field/frame counters ---- */ + filt->field ^= 1; + if (filt->field == 0) filt->frame++; + + /* ---- Copy RGB output → XRGB8888 destination ---- */ + { + unsigned y; + for (y = 0; y < height; y++) { + uint32_t *dst_row = (uint32_t*) + ((uint8_t*)thr->out_data + y * out_pitch); + const unsigned char *src_row = filt->out_buf + y * width * 3; + rgb_to_xrgb8888(src_row, dst_row, width); + } + } + + free(rgb_in); +} + +/* ----------------------------------------------------------------------- + * Packet submission (same structure as scanline2x) + * ----------------------------------------------------------------------- */ +static void ntsc_crt_packets(void *data, + struct softfilter_work_packet *packets, + void *output, size_t output_stride, + const void *input, unsigned width, unsigned height, + size_t input_stride) +{ + struct filter_data *filt = (struct filter_data*)data; + struct softfilter_thread_data *thr = &filt->workers[0]; + + thr->out_data = (uint8_t*)output; + thr->in_data = (const uint8_t*)input; + thr->out_pitch = output_stride; + thr->in_pitch = input_stride; + thr->width = width; + thr->height = height; + thr->colfmt = filt->in_fmt; + + packets[0].work = ntsc_crt_work_cb; + packets[0].thread_data = thr; +} + +/* ----------------------------------------------------------------------- + * Implementation descriptor — matches struct softfilter_implementation + * ----------------------------------------------------------------------- */ +static const struct softfilter_implementation ntsc_crt_impl = { + ntsc_crt_input_fmts, + ntsc_crt_output_fmts, + + ntsc_crt_create, + ntsc_crt_destroy, + + ntsc_crt_threads, + ntsc_crt_output_size, + ntsc_crt_packets, + + SOFTFILTER_API_VERSION, + "NTSC/CRT", + "ntsc_crt", +}; + +const struct softfilter_implementation *softfilter_get_implementation( + softfilter_simd_mask_t simd) +{ + (void)simd; + return &ntsc_crt_impl; +} + +#ifdef RARCH_INTERNAL +#undef softfilter_get_implementation +#undef softfilter_thread_data +#undef filter_data +#endif diff --git a/gfx/video_filters/ntsc_crt_filter/ntsc_crt.filt b/gfx/video_filters/ntsc_crt_filter/ntsc_crt.filt new file mode 100644 index 00000000000..fdb71df1fb0 --- /dev/null +++ b/gfx/video_filters/ntsc_crt_filter/ntsc_crt.filt @@ -0,0 +1,25 @@ +filter = ntsc_crt + +# --- Signal --- +# Amount of noise added to the analog signal (0 = clean, 10 = very noisy) +ntsc_crt_noise = 2 + +# Hue rotation in degrees (0-359) +ntsc_crt_hue = 0 + +# 0 = monochrome / 1 = full color +ntsc_crt_as_color = 1 + +# --- Monitor --- +ntsc_crt_brightness = 0 +ntsc_crt_contrast = 180 +ntsc_crt_saturation = 10 +ntsc_crt_black_point = 0 +ntsc_crt_white_point = 100 + +# --- Display --- +# 1 = dark gap between scanlines (classic CRT look) +ntsc_crt_scanlines = 0 + +# 1 = blend even/odd fields together (reduces flicker, softer look) +ntsc_crt_blend = 1