changeset 2329:06d5e9b08bdb

Add NTSC composite shader by Sik
author Michael Pavone <pavone@retrodev.com>
date Wed, 23 Aug 2023 21:38:39 -0700
parents 7f8d0fdc5bca
children 959a3e9aaac5
files Makefile segacd.c shaders/ntsc.f.glsl
diffstat 3 files changed, 185 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Wed Aug 23 21:36:09 2023 -0700
+++ b/Makefile	Wed Aug 23 21:38:39 2023 -0700
@@ -92,7 +92,7 @@
 LDFLAGS:=-lm $(GLES_LIB)
 else
 CFLAGS+= -DGLEW_STATIC -Iglew/include
-LDFLAGS:=-lm glew/lib/libGLEW.a
+LDFLAGS:=-lm glew/lib/libGLEW.a -lEGL
 endif
 
 ifeq ($(OS),Darwin)
--- a/segacd.c	Wed Aug 23 21:36:09 2023 -0700
+++ b/segacd.c	Wed Aug 23 21:38:39 2023 -0700
@@ -1472,6 +1472,38 @@
 	return main_gate_write16(address, vcontext, value16);
 }
 
+uint8_t laseractive_regs[256];
+
+static uint16_t laseractive_read16(uint32_t address, void *vcontext)
+{
+	printf("LaserActive 16-bit register read %X\n", address);
+	return 0xFFFF;
+}
+
+static uint8_t laseractive_read8(uint32_t address, void *vcontext)
+{
+	printf("LaserActive 8-bit register read %X\n", address);
+	if (address == 0xFDFE81) {
+		return 0x80 | (laseractive_regs[0x41] & 1);
+	} else if (address >= 0xFDFE41 && address < 0xFDFE80 && (address & 1)) {
+		return laseractive_regs[address & 0xFF];
+	}
+	return 0xFF;
+}
+
+static void *laseractive_write16(uint32_t address, void *vcontext, uint16_t value)
+{
+	printf("LaserActive 16-bit register write %X: %X\n", address, value);
+	return vcontext;
+}
+
+static void *laseractive_write8(uint32_t address, void *vcontext, uint8_t value)
+{
+	printf("LaserActive 8-bit register write %X: %X\n", address, value);
+	laseractive_regs[address & 0xFF] = value;
+	return vcontext;
+}
+
 segacd_context *alloc_configure_segacd(system_media *media, uint32_t opts, uint8_t force_region, rom_info *info)
 {
 	static memmap_chunk sub_cpu_map[] = {
@@ -1483,7 +1515,8 @@
 			.read_16 = word_ram_1M_read16, .write_16 = word_ram_1M_write16, .read_8 = word_ram_1M_read8, .write_8 = word_ram_1M_write8, .shift = 1},
 		{0xFE0000, 0xFF0000, 0x003FFF, .flags=MMAP_READ | MMAP_WRITE | MMAP_ONLY_ODD},
 		{0xFF0000, 0xFF8000, 0x003FFF, .read_16 = pcm_read16, .write_16 = pcm_write16, .read_8 = pcm_read8, .write_8 = pcm_write8},
-		{0xFF8000, 0xFF8200, 0x0001FF, .read_16 = sub_gate_read16, .write_16 = sub_gate_write16, .read_8 = sub_gate_read8, .write_8 = sub_gate_write8}
+		{0xFF8000, 0xFF8200, 0x0001FF, .read_16 = sub_gate_read16, .write_16 = sub_gate_write16, .read_8 = sub_gate_read8, .write_8 = sub_gate_write8},
+		{0xFD0000, 0xFE0000, 0xFFFFFF, .read_16 = laseractive_read16, .write_16 = laseractive_write16, .read_8 = laseractive_read8, .write_8 = laseractive_write8}
 	};
 
 	segacd_context *cd = calloc(sizeof(segacd_context), 1);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/shaders/ntsc.f.glsl	Wed Aug 23 21:38:39 2023 -0700
@@ -0,0 +1,150 @@
+//******************************************************************************
+// NTSC composite simulator for BlastEm
+// Shader by Sik, based on BlastEm's default shader
+//
+// It works by converting from RGB to YIQ and then encoding it into NTSC, then
+// trying to decode it back. The lossy nature of the encoding process results in
+// the rainbow effect. It also accounts for the differences between H40 and H32
+// mode as it computes the exact colorburst cycle length.
+//
+// This shader tries to work around the inability to keep track of previous
+// pixels by sampling seven points (in 0.25 colorburst cycle intervals), that
+// seems to be enough to give decent filtering (four samples are used for
+// low-pass filtering, but we need seven because decoding chroma also requires
+// four samples so we're filtering over overlapping samples... just see the
+// comments in the I/Q code to understand).
+//******************************************************************************
+
+uniform mediump float width;
+uniform sampler2D textures[2];
+uniform mediump vec2 texsize;
+varying mediump vec2 texcoord;
+
+// Converts from RGB to YIQ
+mediump vec3 rgba2yiq(vec4 rgba)
+{
+	return vec3(
+		rgba[0] * 0.3 + rgba[1] * 0.59 + rgba[2] * 0.11,
+		rgba[0] * 0.599 + rgba[1] * -0.2773 + rgba[2] * -0.3217,
+		rgba[0] * 0.213 + rgba[1] * -0.5251 + rgba[2] * 0.3121
+	);
+}
+
+// Encodes YIQ into composite
+mediump float yiq2raw(vec3 yiq, float phase)
+{
+	return yiq[0] + yiq[1] * sin(phase) + yiq[2] * cos(phase);
+}
+
+// Converts from YIQ to RGB
+mediump vec4 yiq2rgba(vec3 yiq)
+{
+	return vec4(
+		yiq[0] + yiq[1] * 0.9469 + yiq[2] * 0.6236,
+		yiq[0] - yiq[1] * 0.2748 - yiq[2] * 0.6357,
+		yiq[0] - yiq[1] * 1.1 + yiq[2] * 1.7,
+		1.0
+	);
+}
+
+void main()
+{
+	// Use first pair of lines for hard line edges
+	// Use second pair of lines for soft line edges
+	mediump float modifiedY0 = (floor(texcoord.y * texsize.y + 0.25) + 0.5) / texsize.y;
+	mediump float modifiedY1 = (floor(texcoord.y * texsize.y - 0.25) + 0.5) / texsize.y;
+	//mediump float modifiedY0 = (texcoord.y * texsize.y + 0.75) / texsize.y;
+	//mediump float modifiedY1 = (texcoord.y * texsize.y + 0.25) / texsize.y;
+	
+	// Used by the mixing when fetching texels, related to the way BlastEm
+	// handles interlaced mode (nothing to do with composite)
+	mediump float factorY = (sin(texcoord.y * texsize.y * 6.283185307) + 1.0) * 0.5;
+	
+	// Horizontal distance of half a colorburst cycle
+	mediump float factorX = (1.0 / texsize.x) / 170.667 * 0.5 * (width - 27.0);
+	
+	// Where we store the sampled pixels.
+	// [0] = current pixel
+	// [1] = 1/4 colorburst cycles earlier
+	// [2] = 2/4 colorburst cycles earlier
+	// [3] = 3/4 colorburst cycles earlier
+	// [4] = 1 colorburst cycle earlier
+	// [5] = 1 1/4 colorburst cycles earlier
+	// [6] = 1 2/4 colorburst cycles earlier
+	mediump float phase[7];		// Colorburst phase (in radians)
+	mediump float raw[7];		// Raw encoded composite signal
+	
+	// Sample all the pixels we're going to use
+	mediump float x = texcoord.x;
+	for (int n = 0; n < 7; n++, x -= factorX * 0.5) {
+		// Compute colorburst phase at this point
+		phase[n] = x / factorX * 3.1415926;
+		
+		// Decode RGB into YIQ and then into composite
+		// Reading two textures is a BlastEm thing :P (the two fields in
+		// interlaced mode, that's taken as-is from the stock shaders)
+		raw[n] = yiq2raw(mix(
+			rgba2yiq(texture2D(textures[1], vec2(x, modifiedY1))),
+			rgba2yiq(texture2D(textures[0], vec2(x, modifiedY0))),
+			factorY
+		), phase[n]);
+	}
+	
+	// Decode Y by averaging over the the whole sampled cycle (effectively
+	// filtering anything above the colorburst frequency)
+	mediump float y_mix = (raw[0] + raw[1] + raw[2] + raw[3]) * 0.25;
+	
+	// Decode I and Q (see page below to understand what's going on)
+	// https://codeandlife.com/2012/10/09/composite-video-decoding-theory-and-practice/
+	//
+	// Retrieving I and Q out of the raw signal is done like this
+	// (use sin for I and cos for Q):
+	//
+	//    0.5 * raw[0] * sin(phase[0]) + 0.5 * raw[1] * sin(phase[1]) +
+	//    0.5 * raw[2] * sin(phase[2]) + 0.5 * raw[3] * sin(phase[3])
+	//
+	// i.e. multiply each of the sampled quarter cycles against the reference
+	// wave and average them (actually double that because for some reason
+	// that's needed to get the correct scale, hence 0.5 instead of 0.25)
+	//
+	// That turns out to be blocky tho, so we opt to filter down the chroma...
+	// which requires doing the above *four* times if we do it the same way as
+	// we did for luminance (note that 0.125 = 1/4 of 0.5):
+	//
+	//    0.125 * raw[0] * sin(phase[0]) + 0.125 * raw[1] * sin(phase[1]) +
+	//    0.125 * raw[2] * sin(phase[2]) + 0.125 * raw[3] * sin(phase[3]) +
+	//    0.125 * raw[1] * sin(phase[1]) + 0.125 * raw[2] * sin(phase[2]) +
+	//    0.125 * raw[3] * sin(phase[3]) + 0.125 * raw[4] * sin(phase[4]) +
+	//    0.125 * raw[2] * sin(phase[2]) + 0.125 * raw[3] * sin(phase[3]) +
+	//    0.125 * raw[4] * sin(phase[4]) + 0.125 * raw[5] * sin(phase[5]) +
+	//    0.125 * raw[3] * sin(phase[3]) + 0.125 * raw[4] * sin(phase[4]) +
+	//    0.125 * raw[5] * sin(phase[5]) + 0.125 * raw[6] * sin(phase[6])
+	//
+	// There are a lot of repeated values there that could be merged into one,
+	// what you see below is the resulting simplification.
+	
+	mediump float i_mix =
+		0.125 * raw[0] * sin(phase[0]) +
+		0.25  * raw[1] * sin(phase[1]) +
+		0.375 * raw[2] * sin(phase[2]) +
+		0.5   * raw[3] * sin(phase[3]) +
+		0.375 * raw[4] * sin(phase[4]) +
+		0.25  * raw[5] * sin(phase[5]) +
+		0.125 * raw[6] * sin(phase[6]);
+	
+	mediump float q_mix =
+		0.125 * raw[0] * cos(phase[0]) +
+		0.25  * raw[1] * cos(phase[1]) +
+		0.375 * raw[2] * cos(phase[2]) +
+		0.5   * raw[3] * cos(phase[3]) +
+		0.375 * raw[4] * cos(phase[4]) +
+		0.25  * raw[5] * cos(phase[5]) +
+		0.125 * raw[6] * cos(phase[6]);
+	
+	// Convert YIQ back to RGB and output it
+	gl_FragColor = yiq2rgba(vec3(y_mix, i_mix, q_mix));
+	
+	// If you're curious to see what the raw composite signal looks like,
+	// comment out the above and uncomment the line below instead
+	//gl_FragColor = vec4(raw[0], raw[0], raw[0], 1.0);
+}