New NTSC shader by PlainOldPants (#627)

* Add Patchy NTSC

* Patchy NTSC - Improve Genesis/MegaDrive settings

Still matched 100% by eye

* Patchy NTSC - Last minute fixes

Moved patchy-mesen to the nes_raw_palette folder, and added some optional, unfinished gamut correction LUTs.
The LUT step is skipped by default and is clearly labelled as "work in progress" in the settings.
The purpose of including the gamut correction inside an NTSC shader is to encourage the use of more complex methods for gamut conversion. Up to this point, Grade has been the most popular option for that, yet Grade has some less favorable compromises due to the entire gamut conversion process being done in real time.

* Delete ntsc/patchy-mesen-raw-palette.slangp

* Patchy NTSC - Compress LUTs (except P22_80s_D65)

* Update P22-80s (not compressed)

All the LUTs that were compressed had also been updated prior to compressing. This one was updated too, but it was unable to be compressed. It was mistakenly omitted in the commit to update the LUTs.

* Remove notice about work-in-progress LUTs

Just ONE more thing
This commit is contained in:
PlainOldPants 2024-08-27 23:49:40 +00:00 committed by GitHub
parent ef68606c0e
commit 4cf85d3147
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 3715 additions and 0 deletions

View File

@ -0,0 +1,116 @@
shaders = "7"
feedback_pass = "0"
shader0 = "../ntsc/shaders/patchy-ntsc/patchy-ntsc-pass1.slang"
filter_linear0 = "false"
wrap_mode0 = "clamp_to_border"
frame_count_mod0 = "1000"
mipmap_input0 = "false"
alias0 = ""
float_framebuffer0 = "true"
srgb_framebuffer0 = "false"
scale_type_x0 = "source"
scale_x0 = "8.000000"
scale_type_y0 = "source"
scale_y0 = "1.000000"
shader1 = "../ntsc/shaders/patchy-ntsc/patchy-ntsc-pass2.slang"
filter_linear1 = "false"
wrap_mode1 = "clamp_to_border"
frame_count_mod1 = "1000"
mipmap_input1 = "false"
alias1 = ""
float_framebuffer1 = "true"
srgb_framebuffer1 = "false"
scale_type_x1 = "source"
scale_x1 = "1.000000"
scale_type_y1 = "source"
scale_y1 = "1.000000"
shader2 = "../ntsc/shaders/patchy-ntsc/patchy-ntsc-pass3.slang"
filter_linear2 = "false"
wrap_mode2 = "clamp_to_border"
frame_count_mod2 = "1000"
mipmap_input2 = "false"
alias2 = ""
float_framebuffer2 = "true"
srgb_framebuffer2 = "false"
scale_type_x2 = "source"
scale_x2 = "1.000000"
scale_type_y2 = "source"
scale_y2 = "1.000000"
shader3 = "../ntsc/shaders/patchy-ntsc/patchy-ntsc-pass4.slang"
filter_linear3 = "false"
wrap_mode3 = "clamp_to_border"
frame_count_mod3 = "1000"
mipmap_input3 = "false"
alias3 = ""
float_framebuffer3 = "true"
srgb_framebuffer3 = "false"
scale_type_x3 = "source"
scale_x3 = "1.000000"
scale_type_y3 = "source"
scale_y3 = "1.000000"
shader4 = "../ntsc/shaders/patchy-ntsc/patchy-ntsc-pass5.slang"
filter_linear4 = "false"
wrap_mode4 = "clamp_to_border"
frame_count_mod4 = "1000"
mipmap_input4 = "false"
alias4 = ""
float_framebuffer4 = "true"
srgb_framebuffer4 = "false"
scale_type_x4 = "source"
scale_x4 = "1.000000"
scale_type_y4 = "source"
scale_y4 = "1.000000"
shader5 = "../ntsc/shaders/patchy-ntsc/trilinearLUT-switchable.slang"
filter_linear5 = "false"
wrap_mode5 = "clamp_to_border"
frame_count_mod5 = "1000"
mipmap_input5 = "false"
alias5 = ""
float_framebuffer5 = "true"
srgb_framebuffer5 = "false"
scale_type_x5 = "source"
scale_x5 = "1.000000"
scale_type_y5 = "source"
scale_y5 = "1.000000"
shader6 = "../ntsc/shaders/patchy-ntsc/linear-to-srgb.slang"
filter_linear6 = "false"
wrap_mode6 = "clamp_to_border"
frame_count_mod6 = "1000"
mipmap_input6 = "false"
alias6 = ""
float_framebuffer6 = "true"
srgb_framebuffer6 = "false"
scale_type_x6 = "source"
scale_x6 = "1.000000"
scale_type_y6 = "source"
scale_y6 = "1.000000"
pn_knob_contrast = "0.725000"
pn_knob_saturation = "1.150000"
pn_knob_tint = "-5.000000"
pn_rgb_smear_enable = "1.000000"
pn_width_uncropped = "256.000000"
pn_height_uncropped = "240.000000"
pn_nes_enable = "1.000000"
textures = "PhosphorSamplerLUT1;PhosphorSamplerLUT2;PhosphorSamplerLUT3;PhosphorSamplerLUT4"
PhosphorSamplerLUT1 = "../ntsc/shaders/patchy-ntsc/P22_80s_D65.png"
PhosphorSamplerLUT1_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT1_mipmap = "false"
PhosphorSamplerLUT1_linear = "false"
PhosphorSamplerLUT2 = "../ntsc/shaders/patchy-ntsc/P22_90s_D65.png"
PhosphorSamplerLUT2_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT2_mipmap = "false"
PhosphorSamplerLUT2_linear = "false"
PhosphorSamplerLUT3 = "../ntsc/shaders/patchy-ntsc/P22_J_D65.png"
PhosphorSamplerLUT3_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT3_mipmap = "false"
PhosphorSamplerLUT3_linear = "false"
PhosphorSamplerLUT4 = "../ntsc/shaders/patchy-ntsc/TrinitronP22_D65.png"
PhosphorSamplerLUT4_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT4_mipmap = "false"
PhosphorSamplerLUT4_linear = "false"

117
ntsc/patchy-blastem.slangp Normal file
View File

@ -0,0 +1,117 @@
shaders = "7"
feedback_pass = "0"
shader0 = "shaders/patchy-ntsc/patchy-ntsc-pass1.slang"
filter_linear0 = "false"
wrap_mode0 = "clamp_to_border"
frame_count_mod0 = "1000"
mipmap_input0 = "false"
alias0 = ""
float_framebuffer0 = "true"
srgb_framebuffer0 = "false"
scale_type_x0 = "source"
scale_x0 = "8.000000"
scale_type_y0 = "source"
scale_y0 = "1.000000"
shader1 = "shaders/patchy-ntsc/patchy-ntsc-pass2.slang"
filter_linear1 = "false"
wrap_mode1 = "clamp_to_border"
frame_count_mod1 = "1000"
mipmap_input1 = "false"
alias1 = ""
float_framebuffer1 = "true"
srgb_framebuffer1 = "false"
scale_type_x1 = "source"
scale_x1 = "1.000000"
scale_type_y1 = "source"
scale_y1 = "1.000000"
shader2 = "shaders/patchy-ntsc/patchy-ntsc-pass3.slang"
filter_linear2 = "false"
wrap_mode2 = "clamp_to_border"
frame_count_mod2 = "1000"
mipmap_input2 = "false"
alias2 = ""
float_framebuffer2 = "true"
srgb_framebuffer2 = "false"
scale_type_x2 = "source"
scale_x2 = "1.000000"
scale_type_y2 = "source"
scale_y2 = "1.000000"
shader3 = "shaders/patchy-ntsc/patchy-ntsc-pass4.slang"
filter_linear3 = "false"
wrap_mode3 = "clamp_to_border"
frame_count_mod3 = "1000"
mipmap_input3 = "false"
alias3 = ""
float_framebuffer3 = "true"
srgb_framebuffer3 = "false"
scale_type_x3 = "source"
scale_x3 = "1.000000"
scale_type_y3 = "source"
scale_y3 = "1.000000"
shader4 = "shaders/patchy-ntsc/patchy-ntsc-pass5.slang"
filter_linear4 = "false"
wrap_mode4 = "clamp_to_border"
frame_count_mod4 = "1000"
mipmap_input4 = "false"
alias4 = ""
float_framebuffer4 = "true"
srgb_framebuffer4 = "false"
scale_type_x4 = "source"
scale_x4 = "1.000000"
scale_type_y4 = "source"
scale_y4 = "1.000000"
shader5 = "shaders/patchy-ntsc/trilinearLUT-switchable.slang"
filter_linear5 = "false"
wrap_mode5 = "clamp_to_border"
frame_count_mod5 = "1000"
mipmap_input5 = "false"
alias5 = ""
float_framebuffer5 = "true"
srgb_framebuffer5 = "false"
scale_type_x5 = "source"
scale_x5 = "1.000000"
scale_type_y5 = "source"
scale_y5 = "1.000000"
shader6 = "shaders/patchy-ntsc/linear-to-srgb.slang"
filter_linear6 = "false"
wrap_mode6 = "clamp_to_border"
frame_count_mod6 = "1000"
mipmap_input6 = "false"
alias6 = ""
float_framebuffer6 = "true"
srgb_framebuffer6 = "false"
scale_type_x6 = "source"
scale_x6 = "1.000000"
scale_type_y6 = "source"
scale_y6 = "1.000000"
pn_knob_saturation = "1.150000"
pn_knob_tint = "-5.000000"
pn_rgb_smear_enable = "1.000000"
pn_genesis_jailbar_enable = "1.000000"
pn_genesis_jailbar_offset = "0.312500"
pn_scanline_dur = "47.699997"
pn_color_init_offset = "0.700000"
pn_modulator_luma_filter_type = "2.000000"
pn_modulator_luma_res = "220.000000"
textures = "PhosphorSamplerLUT1;PhosphorSamplerLUT2;PhosphorSamplerLUT3;PhosphorSamplerLUT4"
PhosphorSamplerLUT1 = "shaders/patchy-ntsc/P22_80s_D65.png"
PhosphorSamplerLUT1_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT1_mipmap = "false"
PhosphorSamplerLUT1_linear = "false"
PhosphorSamplerLUT2 = "shaders/patchy-ntsc/P22_90s_D65.png"
PhosphorSamplerLUT2_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT2_mipmap = "false"
PhosphorSamplerLUT2_linear = "false"
PhosphorSamplerLUT3 = "shaders/patchy-ntsc/P22_J_D65.png"
PhosphorSamplerLUT3_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT3_mipmap = "false"
PhosphorSamplerLUT3_linear = "false"
PhosphorSamplerLUT4 = "shaders/patchy-ntsc/TrinitronP22_D65.png"
PhosphorSamplerLUT4_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT4_mipmap = "false"
PhosphorSamplerLUT4_linear = "false"

View File

@ -0,0 +1,118 @@
shaders = "7"
feedback_pass = "0"
shader0 = "shaders/patchy-ntsc/patchy-ntsc-pass1.slang"
filter_linear0 = "false"
wrap_mode0 = "clamp_to_border"
frame_count_mod0 = "1000"
mipmap_input0 = "false"
alias0 = ""
float_framebuffer0 = "true"
srgb_framebuffer0 = "false"
scale_type_x0 = "source"
scale_x0 = "8.000000"
scale_type_y0 = "source"
scale_y0 = "1.000000"
shader1 = "shaders/patchy-ntsc/patchy-ntsc-pass2.slang"
filter_linear1 = "false"
wrap_mode1 = "clamp_to_border"
frame_count_mod1 = "1000"
mipmap_input1 = "false"
alias1 = ""
float_framebuffer1 = "true"
srgb_framebuffer1 = "false"
scale_type_x1 = "source"
scale_x1 = "1.000000"
scale_type_y1 = "source"
scale_y1 = "1.000000"
shader2 = "shaders/patchy-ntsc/patchy-ntsc-pass3.slang"
filter_linear2 = "false"
wrap_mode2 = "clamp_to_border"
frame_count_mod2 = "1000"
mipmap_input2 = "false"
alias2 = ""
float_framebuffer2 = "true"
srgb_framebuffer2 = "false"
scale_type_x2 = "source"
scale_x2 = "1.000000"
scale_type_y2 = "source"
scale_y2 = "1.000000"
shader3 = "shaders/patchy-ntsc/patchy-ntsc-pass4.slang"
filter_linear3 = "false"
wrap_mode3 = "clamp_to_border"
frame_count_mod3 = "1000"
mipmap_input3 = "false"
alias3 = ""
float_framebuffer3 = "true"
srgb_framebuffer3 = "false"
scale_type_x3 = "source"
scale_x3 = "1.000000"
scale_type_y3 = "source"
scale_y3 = "1.000000"
shader4 = "shaders/patchy-ntsc/patchy-ntsc-pass5.slang"
filter_linear4 = "false"
wrap_mode4 = "clamp_to_border"
frame_count_mod4 = "1000"
mipmap_input4 = "false"
alias4 = ""
float_framebuffer4 = "true"
srgb_framebuffer4 = "false"
scale_type_x4 = "source"
scale_x4 = "1.000000"
scale_type_y4 = "source"
scale_y4 = "1.000000"
shader5 = "shaders/patchy-ntsc/trilinearLUT-switchable.slang"
filter_linear5 = "false"
wrap_mode5 = "clamp_to_border"
frame_count_mod5 = "1000"
mipmap_input5 = "false"
alias5 = ""
float_framebuffer5 = "true"
srgb_framebuffer5 = "false"
scale_type_x5 = "source"
scale_x5 = "1.000000"
scale_type_y5 = "source"
scale_y5 = "1.000000"
shader6 = "shaders/patchy-ntsc/linear-to-srgb.slang"
filter_linear6 = "false"
wrap_mode6 = "clamp_to_border"
frame_count_mod6 = "1000"
mipmap_input6 = "false"
alias6 = ""
float_framebuffer6 = "true"
srgb_framebuffer6 = "false"
scale_type_x6 = "source"
scale_x6 = "1.000000"
scale_type_y6 = "source"
scale_y6 = "1.000000"
pn_knob_saturation = "1.150000"
pn_knob_tint = "-5.000000"
pn_rgb_smear_enable = "1.000000"
pn_genesis_palette = "1.0"
pn_genesis_jailbar_enable = "1.000000"
pn_genesis_jailbar_offset = "0.312500"
pn_scanline_dur = "47.699997"
pn_color_init_offset = "0.700000"
pn_modulator_luma_filter_type = "2.000000"
pn_modulator_luma_res = "220.000000"
textures = "PhosphorSamplerLUT1;PhosphorSamplerLUT2;PhosphorSamplerLUT3;PhosphorSamplerLUT4"
PhosphorSamplerLUT1 = "shaders/patchy-ntsc/P22_80s_D65.png"
PhosphorSamplerLUT1_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT1_mipmap = "false"
PhosphorSamplerLUT1_linear = "false"
PhosphorSamplerLUT2 = "shaders/patchy-ntsc/P22_90s_D65.png"
PhosphorSamplerLUT2_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT2_mipmap = "false"
PhosphorSamplerLUT2_linear = "false"
PhosphorSamplerLUT3 = "shaders/patchy-ntsc/P22_J_D65.png"
PhosphorSamplerLUT3_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT3_mipmap = "false"
PhosphorSamplerLUT3_linear = "false"
PhosphorSamplerLUT4 = "shaders/patchy-ntsc/TrinitronP22_D65.png"
PhosphorSamplerLUT4_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT4_mipmap = "false"
PhosphorSamplerLUT4_linear = "false"

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -0,0 +1,2 @@
./gamutthingy -i 64.png -o P22.png -g linear -s ntscjp22trinitron -d srgb --map-mode compress --gma vpr --safe-zone-type const-detail --remap-factor 0.4 --remap-limit 0.9 --knee soft --knee-factor 0.4 --di false --sc false

View File

@ -0,0 +1,56 @@
#version 450
/*
Patchy NTSC
Copyright (C) 2024 Patchy68k/PlainOldPants
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// This pass converts from linear sRGB to gamma-corrected sRGB.
layout(push_constant) uniform Push
{
vec4 SourceSize;
vec4 OriginalSize;
vec4 OutputSize;
uint FrameCount;
vec4 FinalViewportSize;
} params;
layout(std140, set = 0, binding = 0) uniform UBO
{
mat4 MVP;
} global;
#include "../../../../shaders_slang/include/colorspace-tools.h"
#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
void main()
{
gl_Position = global.MVP * Position;
vTexCoord = TexCoord;
}
#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;
void main()
{
vec3 rgb = texture(Source, vTexCoord).rgb;
rgb = linear_to_sRGB(rgb, 2.4);
FragColor = vec4(rgb, 1.0);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
shaders = "3"
feedback_pass = "0"
shader0 = "patchy-color.slang"
filter_linear0 = "false"
wrap_mode0 = "clamp_to_border"
mipmap_input0 = "false"
alias0 = ""
float_framebuffer0 = "false"
srgb_framebuffer0 = "false"
scale_type_x0 = "source"
scale_x0 = "1.000000"
scale_type_y0 = "source"
scale_y0 = "1.000000"
shader1 = "../lut/trilinearLUT-switchable.slang"
filter_linear1 = "false"
wrap_mode1 = "clamp_to_border"
mipmap_input1 = "false"
alias1 = ""
float_framebuffer1 = "true"
srgb_framebuffer1 = "false"
scale_type_x1 = "source"
scale_x1 = "1.000000"
scale_type_y1 = "source"
scale_y1 = "1.000000"
shader2 = "linear-to-srgb.slang"
filter_linear2 = "false"
wrap_mode2 = "clamp_to_border"
mipmap_input2 = "false"
alias2 = ""
float_framebuffer2 = "false"
srgb_framebuffer2 = "false"
scale_type_x2 = "source"
scale_x2 = "1.000000"
scale_type_y2 = "source"
scale_y2 = "1.000000"
pc_console = "1.000000"
pc_genesis_needfix = "1.000000"
pc_demodulator = "8.000000"
lut_chroma_adapt = "0.000000"
textures = "PhosphorSamplerLUT1;PhosphorSamplerLUT2;PhosphorSamplerLUT3;PhosphorSamplerLUT4;PhosphorSamplerLUT5"
PhosphorSamplerLUT1 = "../lut/SMPTEC_TrinitronP22_LinearRBG_to_sRGB_LinearRGB.png"
PhosphorSamplerLUT1_linear = "false"
PhosphorSamplerLUT1_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT1_mipmap = "false"
PhosphorSamplerLUT2 = "../lut/NTSCU_TrinitronP22_LinearRBG_to_sRGB_LinearRGB.png"
PhosphorSamplerLUT2_linear = "false"
PhosphorSamplerLUT2_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT2_mipmap = "false"
PhosphorSamplerLUT3 = "../lut/NTSCJ_TrinitronP22_LinearRBG_to_sRGB_LinearRGB.png"
PhosphorSamplerLUT3_linear = "false"
PhosphorSamplerLUT3_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT3_mipmap = "false"
PhosphorSamplerLUT4 = "../lut/SMPTEC_P2280s_LinearRBG_to_sRGB_LinearRGB.png"
PhosphorSamplerLUT4_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT4_mipmap = "false"
PhosphorSamplerLUT4_linear = "false"
PhosphorSamplerLUT5 = "../lut/SMPTEC_P2290s_LinearRBG_to_sRGB_LinearRGB.png"
PhosphorSamplerLUT5_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT5_mipmap = "false"
PhosphorSamplerLUT5_linear = "false"

View File

@ -0,0 +1,686 @@
/*
Patchy NTSC
Copyright (C) 2024 Patchy68k/PlainOldPants
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef __patchy_ntsc_inc_filters_inc__
#define __patchy_ntsc_inc_filters_inc__
#include "patchy-ntsc-inc-params.inc"
///////////////////////
// General functions //
///////////////////////
vec2 realPixelCoord() {
vec2 pixelCoord = vTexCoord * params.OutputSize.xy / vec2(SIGNAL_RES, 1) + (vec2(global.pn_width_uncropped, global.pn_height_uncropped) - params.OutputSize.xy / vec2(SIGNAL_RES, 1)) / 2;
pixelCoord.y = floor(pixelCoord.y);
return pixelCoord;
}
int getPhaseIntNES() {
vec2 pixelCoord = realPixelCoord();
return int(0.1 + 8 * pixelCoord.x + 4 * pixelCoord.y + 4 * (int(params.FrameCount + 0.1) % int(2.1 + global.pn_nes_phase_mod)));;
}
float getPhase() {
if(global.pn_nes_enable > 0.5) {
int p = getPhaseIntNES();
return 2 * pi * (p + 3) / 12.0;
} else {
vec2 pixelCoord = realPixelCoord();
float realOffX = pixelCoord.x / global.pn_width_uncropped;
return 2 * pi * (realOffX * global.pn_scanline_dur * global.pn_color_freq + global.pn_color_init_offset + global.pn_color_line_offset * pixelCoord.y + params.FrameCount * global.pn_color_screen_offset);
}
}
float getFreqFactor() {
if(global.pn_nes_enable > 0.5) {
// https://www.nesdev.org/wiki/NTSC_video as of August 15, 2024
// Each NES pixel is 186 nanoseconds.
return 0.186 / 8.0 * 2 * pi;
} else {
return global.pn_scanline_dur / global.pn_width_uncropped / SIGNAL_RES * 2 * pi;
}
}
float getBurstRate() {
if(global.pn_nes_enable > 0.5) {
return 2 * pi / 12.0;
} else {
return 2 * pi / SIGNAL_RES / global.pn_width_uncropped * global.pn_scanline_dur * global.pn_color_freq;
}
}
//////////////////////////////////
// Common functions for filters //
//////////////////////////////////
float sinc(float x) {
if(x == 0.0)
return 1.0;
else
return sin(x) / x;
}
// Hann window, or a Hamming window with a0 = 0.5
// https://en.wikipedia.org/wiki/Window_function
float hamming(int samplePos, int width) {
return pow(cos(pi * samplePos / width / 2), 2);
}
/////////////////////////////////
// Three band equalizer filter //
/////////////////////////////////
// Three band equalizer filter, directly ported from NTSC-CRT by EMMIR.
// It worked well in NTSC-CRT because that was written in plain C,
// but it is terrible in a shader.
/* f_lo_mhz - low cutoff frequency (MHz)
* f_hi_mhz - high cutoff frequency (MHz)
* g_lo, g_mid, g_hi - gains
*/
vec4 filterEQF(float f_lo_mhz, float f_hi_mhz,
float g_lo, float g_mid, float g_hi,
int rgbIndex, float maxDistFailsafe, float overallOffset) {
// https://www.nesdev.org/wiki/NTSC_video as of August 15, 2024
// Each NES pixel is 186 nanoseconds.
#define L_FREQ (global.pn_nes_enable > 0.5 ? 8 / 0.186 : SIGNAL_RES * global.pn_width_uncropped / global.pn_scanline_dur)
#define CRT_HRES (global.pn_width_uncropped * SIGNAL_RES)
#define MHz2L(MHz) (CRT_HRES * ((MHz)) / L_FREQ)
#define HISTLEN 7
#define HISTOLD (HISTLEN - 1) /* oldest entry */
#define HISTNEW 0 /* newest entry */
// struct EQF
float lf, hf; /* fractions */
float g[3]; /* gains */
float fL[4];
float fH[4];
float h[HISTLEN]; /* history */
// init_eq(struct EQF *f,
// int f_lo, int f_hi, int rate,
// int g_lo, int g_mid, int g_hi)
float rate = CRT_HRES;
float f_lo = MHz2L(f_lo_mhz);
float f_hi = MHz2L(f_hi_mhz);
g[0] = g_lo;
g[1] = g_mid;
g[2] = g_hi;
lf = 2 * sin(pi * f_lo / rate);
hf = 2 * sin(pi * f_hi / rate);
// reset_eq(struct EQF *f)
for(int i = 0; i < HISTLEN; i++)
h[i] = 0;
for(int i = 0; i < 4; i++)
fL[i] = fH[i] = 0;
float r[3];
int maxDistI = int(0.5 + maxDistFailsafe / 100.0 * SIGNAL_RES * global.pn_width_uncropped);
overallOffset /= SIGNAL_RES;
for(int offset = -maxDistI; offset <= 0; offset++) {
float s = texture(Source, vTexCoord + vec2((offset - overallOffset) * params.OutputSize.z, 0))[rgbIndex];
// eqf(struct EQF *f, int s)
fL[0] += lf * (s - fL[0]);
fH[0] += lf * (s - fH[0]);
for(int i = 1; i < 4; i++) {
fL[i] += lf * (fL[i - 1] - fL[i]);
fH[i] += lf * (fH[i - 1] - fH[i]);
}
r[0] = fL[3];
r[1] = fH[3] - fL[3];
r[2] = h[HISTOLD] - fH[3];
for(int i = 0; i < 3; i++) {
r[i] = r[i] * g[i];
}
for(int i = HISTOLD; i > 0; i--) {
h[i] = h[i - 1];
}
h[HISTNEW] = s;
}
return vec4(r[0] + r[1] + r[2]);
#undef L_FREQ
#undef CRT_HRES
#undef kHz2L
#undef HISTLEN
#undef HISTOLD
#undef HISTNEW
}
///////////////////////
// Lowpass functions //
///////////////////////
// https://www.mathworks.com/help/signal/ug/fir-filter-design.html as of August 9, 2024
// Lowpass using the "Windowing Method" as described on that website.
// I took this idea from artifact-colors, which has a similar lowpass.
vec4 lowpass(float halfWidth, float level) {
float freqFactor = getFreqFactor();
float levelFixed = level * freqFactor;
int halfWidthInt = int(floor(halfWidth * SIGNAL_RES + 0.5)); // Round to nearest integer.
float totalMax = 0.;
vec4 lowPassed = vec4(0);
for(int i = -halfWidthInt; i <= halfWidthInt; i++) {
vec4 samp = texture(Source, vTexCoord + vec2(i * params.OutputSize.z, 0));
float factor = levelFixed * sinc(levelFixed * i) * hamming(i, halfWidthInt);
totalMax += factor;
lowPassed += factor * samp;
}
return lowPassed / totalMax;
}
vec4 lowpassGaussian(float sigma, float radius) {
// Copied and pasted from Guest.r's gaussian blur shader
float SIGMA_H = sigma * SIGNAL_RES;
float SIZEH = radius * SIGNAL_RES; // 2.6 standard deviations = 99%
float invsqrsigma = 1.0/(2.0*SIGMA_H*SIGMA_H);
float f = fract(params.OutputSize.x * vTexCoord.x);
f = 0.5 - f;
vec2 tex = floor(params.OutputSize.xy * vTexCoord)*params.OutputSize.zw + 0.5*params.OutputSize.zw;
vec4 color = vec4(0.0);
vec2 dx = vec2(params.OutputSize.z, 0.0);
vec4 baseTex = texture(Source, tex);
float w;
float wsum = 0.0;
vec4 pixel;
float n = -SIZEH;
do
{
pixel = texture(Source, tex + n*dx);
w = exp(-pow(n+f, 2)*invsqrsigma);
color = color + w * pixel;
wsum = wsum + w;
n = n + 1.0;
} while (n <= SIZEH);
color = color / wsum;
return color;
}
float d(float x, float b)
{
return (pi*b*min(abs(x)+0.5,1.0/b));
}
float e(float x, float b)
{
return (pi*b*min(max(abs(x)-0.5,-1.0/b),1.0/b));
}
vec4 lowpassGTU(float signalRes) {
// Taken from GTU-Famicom by aliaspider
float offset = fract((vTexCoord.x * params.OutputSize.x) - 0.5);
float range = ceil(0.5 + global.pn_width_uncropped * SIGNAL_RES / signalRes);
float Y = signalRes / global.pn_width_uncropped / SIGNAL_RES; // Remember to compensate for cropped overscan. The user might or might not be cropping it.
float X;
vec4 c;
vec4 combined = vec4(0);
float i;
// for (i=-range;i<range+2.0;i++){
for (i = 1.0 - range; i < range + 1.0; i += 1)
{
X = offset - i;
c = texture(Source, vec2(vTexCoord.x - X * params.OutputSize.z, vTexCoord.y));
c *= ((d(X, Y) + sin(d(X, Y)) - e(X, Y) - sin(e(X, Y))) / (2.0 * pi));
combined += c;
}
return combined;
}
vec4 twoPointCombLuma() {
float halfPhase;
if(global.pn_nes_enable > 0.5) {
halfPhase = 6 * params.OutputSize.z;
} else {
halfPhase = params.OutputSize.z * 0.5 / (global.pn_scanline_dur * global.pn_color_freq / global.pn_width_uncropped / SIGNAL_RES);
}
// vec2 coord = vTexCoord + vec2(floor(halfPhase / params.OutputSize.z / 2) * params.OutputSize.z, 0); // Make sure we step by a multiple of an input pixel. Stepping by a fraction doesn't filter as good.
// return (texture(Source, coord) + texture(Source, coord - vec2(halfPhase, 0))) / 2;
// return vec4(0);
return (texture(Source, vTexCoord) + texture(Source, vTexCoord - vec2(halfPhase, 0))) / 2;
}
vec4 lowpassPickable(float type,
float halfWidth, float level, // artifact-colors sinc window
float gaussSigma, float gaussRadius, // guest(r) gaussian blur
float signalRes, // aliaspider gtu
float f_lo, float f_hi,
float g_lo, float g_mid, float g_hi,
float maxDistFailsafe, float overallOffset) // EMMIR NTSC-CRT
{
if(type < -0.5) {
return texture(Source, vTexCoord); // No low pass
} else if(type < 0.5) {
return lowpass(halfWidth, level); // sinc window
} else if(type < 1.5) {
return lowpassGaussian(gaussSigma, gaussRadius);
} else if (type < 2.5) {
return lowpassGTU(signalRes);
} else if (type < 3.5) {
return filterEQF(f_lo, f_hi, g_lo, g_mid, g_hi, 0, maxDistFailsafe, overallOffset);
} else {
// This filter is only for the demodulator, so it's at the end.
// The lowpass filter selection for the modulator only goes up to 3 to prevent
// this one from being picked for that.
return twoPointCombLuma();
}
}
////////////////////////
// Bandpass functions //
////////////////////////
// https://www.mathworks.com/help/signal/ug/fir-filter-design.html as of August 9, 2024
// This method of bandpassing is also taken from artifact-colors. Uses the "Windowing Method".
vec4 bandpass(float halfWidth, float levelLo, float levelHi) {
float freqFactor = getFreqFactor();
float levelFixedLo = (levelLo) * freqFactor;
float levelFixedHi = (levelHi) * freqFactor;
int halfWidthInt = int(floor(halfWidth * SIGNAL_RES + 0.5)); // Round to nearest integer.
float burstRate = getBurstRate();
vec4 bandPassed = vec4(0);
float maxSum = 0.0;
for(int i = -halfWidthInt; i <= halfWidthInt; i++) {
vec4 samp = texture(Source, vTexCoord + vec2(i * params.OutputSize.z, 0));
float window = hamming(i, halfWidthInt);
float factorLo = levelFixedLo * sinc(levelFixedLo * i);
float factorHi = levelFixedHi * sinc(levelFixedHi * i);
float factor = factorHi - factorLo;
bandPassed += window * factor * samp;
maxSum += window * factor * cos(burstRate * i);
}
return bandPassed / maxSum;
}
vec4 twoPointCombChroma() {
float halfPhase;
if(global.pn_nes_enable < 0.5) {
halfPhase = params.OutputSize.z * SIGNAL_RES / 2 / (global.pn_scanline_dur * global.pn_color_freq / global.pn_width_uncropped);
} else {
halfPhase = params.OutputSize.z * 6;
}
return (texture(Source, vTexCoord + vec2(halfPhase / 2, 0)) - texture(Source, vTexCoord - vec2(halfPhase / 2, 0))) / 2;
}
vec4 bandpassPickable(float type,
float halfWidth, float level, float levelDiff,
float f_lo, float f_hi,
float g_lo, float g_mid, float g_hi, float maxDistFailsafe, float overallOffset, int index)
{
if(type < -0.5) {
return texture(Source, vTexCoord);
} else if(type < 0.5) {
return bandpass(halfWidth, level, levelDiff);
} else if(type < 1.5) {
return filterEQF(f_lo, f_hi, g_lo, g_mid, g_hi, index, maxDistFailsafe, overallOffset) * 2;
} else {
return twoPointCombChroma();
}
}
/////////////////////////
// Chroma demodulation //
/////////////////////////
mat3x2 yuvAxisPoints() {
float r_off, r_max, g_off, g_max, b_off, b_max;
const float Uupscale = 2.03206187221989;
const float angFix = -123.0 / 180.0 * 3.14159265359;
if(global.pn_demodulator_std < 0.5) {
// Standard BT.470 matrix, used for NTSC and PAL SDTV.
r_off = (123 - 90) / 180.0 * pi; // 33 deg
r_max = 1.13983;
g_off = -1.979; // -113 deg
g_max = 0.70203;
b_off = 123 / 180.0 * pi; // 123 deg
b_max = 2.03211;
} else if(global.pn_demodulator_std < 1.5) {
// Later standard BT.709 matrix, used for NTSC and PAL HDTV.
r_off = (123 - 90) / 180.0 * 3.14159265359;
r_max = 1.28033;
g_off = -2.085;
g_max = 0.43703;
b_off = 123 / 180.0 * 3.14159265359;
b_max = 2.12798;
} else if(global.pn_demodulator_std < 2.5) {
// Sony CXA2025AS (JP axis) from official documentation, assuming blue is 33 degrees
r_off = (123 - 95) / 180.0 * 3.14159265359;
r_max = 0.78;
g_off = (123 - 240) / 180.0 * 3.14159265359;
g_max = 0.3;
b_off = 123 / 180.0 * 3.14159265359;
b_max = 1;
} else if(global.pn_demodulator_std < 3.5) {
// CXA1464AS (JP) from official data sheet
r_off = (123 - 98) / 180.0 * 3.14159265359;
r_max = 0.78;
g_off = (123 - 243) / 180.0 * 3.14159265359;
g_max = 0.31;
b_off = 123 / 180.0 * 3.14159265359;
b_max = 1;
} else if(global.pn_demodulator_std < 4.5) {
// Panasonic AN5367FB
r_off = (123 - 104) / 180.0 * 3.14159265359;
r_max = 0.96;
g_off = (123 - 235) / 180.0 * 3.14159265359;
g_max = 0.36;
b_off = 123 / 180.0 * 3.14159265359;
b_max = 1;
} else if(global.pn_demodulator_std < 5.5) {
// Toshiba TA8867BN, from official data sheet
r_off = (123 - 104) / 180.0 * 3.14159265359;
r_max = 0.91;
g_off = (123 - 240) / 180.0 * 3.14159265359;
g_max = 0.31;
b_off = 123 / 180.0 * 3.14159265359;
b_max = 1;
} else if(global.pn_demodulator_std < 6.5) {
// Toshiba TA8867AN, taken from the official data sheet
r_off = (123 - 112) / 180.0 * 3.14159265359;
r_max = 0.84;
g_off = (123 - 237) / 180.0 * 3.14159265359;
g_max = 0.33;
b_off = 123 / 180.0 * 3.14159265359;
b_max = 1;
} else if(global.pn_demodulator_std < 7.5) {
// Sony CXA2025AS (US axis mode) from the official data sheet
r_off = (123 - 112) / 180.0 * 3.14159265359; // 112 deg or 11 deg
r_max = 0.83;
g_off = (123 - 252) / 180.0 * 3.14159265359; // 252 deg or -129 deg
g_max = 0.3;
b_off = 123 / 180.0 * 3.14159265359; // 0 deg or 123.5 deg
b_max = 1.0;
} else if(global.pn_demodulator_std < 8.5) {
// Sony CXA1465AS from the official data sheet
r_off = (123 - 114) / 180.0 * 3.14159265359;
r_max = 0.78;
g_off = (123 - 255) / 180.0 * 3.14159265359;
g_max = 0.31;
b_off = 123 / 180.0 * 3.14159265359;
b_max = 1;
} else if(global.pn_demodulator_std < 9.5) {
// Custom from the user, in the same format as in a data sheet.
r_off = (123 - global.pn_demodulator_r_off) / 180.0 * pi;
g_off = (123 - global.pn_demodulator_g_off) / 180.0 * pi;
b_off = 123.0 / 180.0 * pi;
r_max = global.pn_demodulator_r_amp * Uupscale;
g_max = global.pn_demodulator_g_amp * Uupscale;
b_max = Uupscale;
}
// PAL matrix from TA8867AN and -BN. Notice that green is more saturated.
// I'm now assuming that the oversaturated green is a mistake in the data sheet that doesn't appear in real hardware.
// r_off = (123 - 90) / 180.0 * 3.14159265359;
// r_max = 0.56 * 1.4;
// g_off = (123 - 235) / 180.0 * 3.14159265359;
// g_max = 0.38 * 1.4;
// b_off = 123 / 180.0 * 3.14159265359;
// b_max = 1 * 1.4;
r_max *= Uupscale / b_max; // This can be hard-coded into the presets easily.
g_max *= Uupscale / b_max;
b_max *= Uupscale / b_max;
r_off += angFix; // This can also be hard-coded into the presets easily.
g_off += angFix;
b_off += angFix;
r_off *= -1;
g_off *= -1;
b_off *= -1;
return mat3x2(r_off, r_max,
g_off, g_max,
b_off, b_max);
}
mat3x2 yuvAxisPointsSynced() {
// Do not automatically sync the SMPTE-C and Rec. 709 matrices. Just return the default.
// Automatic syncing had to be done for jungle chips because their defaults are not listed in their data sheets.
mat3x2 yuvAxisPts = yuvAxisPoints();
if(global.pn_demodulator_std < 1.5) {
return yuvAxisPts;
}
// if(global.eztvcol3_cmp_console < 1.5) {
// // This code is made using the mathematical formulas outlined by Chthon at https://forums.libretro.com/t/dogways-grading-shader-slang/27148/561
// // The result is an RGB to RGB matrix.
//
// float xr, yr, xg, yg, xb, yb;
// xr = yuvAxisPts[0][1] * cos(yuvAxisPts[0][0]);
// yr = yuvAxisPts[0][1] * sin(yuvAxisPts[0][0]);
// xg = yuvAxisPts[1][1] * cos(yuvAxisPts[1][0]);
// yg = yuvAxisPts[1][1] * sin(yuvAxisPts[1][0]);
// xb = yuvAxisPts[2][1] * cos(yuvAxisPts[2][0]);
// yb = yuvAxisPts[2][1] * sin(yuvAxisPts[2][0]);
//
// float wr = 0.298911657927057;
// float wg = 0.586610718748869;
// float wb = 0.114477623324074;
//
// float ud = 0.492111;
// float vd = 0.877283;
// float uu = 2.03206187221989;
// float vu = 1.13988302520395;
//
// mat2 matB = mat2(
// (1-wr)/vu, -wg/vu,
// -wr/uu, -wg/uu
// );
//
// vec2 cr = matB * vec2(yr, xr);
// vec2 cg = matB * vec2(yg, xg);
// vec2 cb = matB * vec2(yb, xb);
//
// return mat3(
// cr.r + wr, cg.r + wr, cb.r + wr,
// cr.g + wg, cg.g + wg, cb.g + wg,
// 1 - (cr.r + wr + cr.g + wg), 1 - (cg.r + wr + cg.g + wg), 1 - (cb.r + wr + cb.g + wg)
// );
// } else {
// This simpler code generates a YUV to RGB matrix.
float xr, yr, xg, yg, xb, yb;
xr = yuvAxisPts[0][1] * cos(yuvAxisPts[0][0]);
yr = yuvAxisPts[0][1] * sin(yuvAxisPts[0][0]);
xg = yuvAxisPts[1][1] * cos(yuvAxisPts[1][0]);
yg = yuvAxisPts[1][1] * sin(yuvAxisPts[1][0]);
xb = yuvAxisPts[2][1] * cos(yuvAxisPts[2][0]);
yb = yuvAxisPts[2][1] * sin(yuvAxisPts[2][0]);
mat3 toRgb = mat3(1, 1, 1,
xr, xg, xb,
yr, yg, yb);
mat3 fromRgb = inverse(toRgb);
mat3 fromRgbStd;
// if(global.pn_modulator_std < 0.5) {
// Rec. 601 YUV matrix
// This is used in the Genesis/MegaDrive and SNES/SFC
fromRgbStd = mat3x3(
0.299, -0.14713, 0.615,
0.587, -0.28886, -0.51499,
0.114, 0.436, -0.10001
);
// } else {
// // Rec. 709 YUV matrix
// // Don't know any specific consoles using this, but I assume this *eventually* became common.
// fromRgbStd = mat3(0.2126, -0.09991, 0.615,
// 0.7152, -0.33609, -0.55861,
// 0.0722, 0.436, -0.05639);
// }
// Basic linear regression to match the hue rotation (tint) to standard color bars.
// Simply average up the offsets, and that's the ideal hue rotation.
// On US CRTs, many people probably rotated this further to get greener greens, but then, some green would become very cyan, and some reds and pinks would become very orange.
// Other people on US CRTs, especially on the NES, probably rotated a little in the opposite direction to get browner browns.
// A problem with this method is that this is not a perceptually uniform color space. I will address that sometime else.
float residSum = 0;
for(int i = 1; i <= 6; i++) {
vec3 colorBar = vec3(i & 1, (i >> 1) & 1, i >> 2);
vec3 yuvJungle = fromRgb * colorBar;
vec3 yuvStd = fromRgbStd * colorBar;
float resid = atan(yuvJungle.b, yuvJungle.g) - atan(yuvStd.b, yuvStd.g);
while(resid > pi)
resid -= 2 * pi;
while(resid < -pi)
resid += 2 * pi;
residSum += resid;
}
float hueOff = -residSum / 6;
for(int i = 0; i < 3; i++)
yuvAxisPts[i][0] += hueOff;
xr = yuvAxisPts[0][1] * cos(yuvAxisPts[0][0]);
yr = yuvAxisPts[0][1] * sin(yuvAxisPts[0][0]);
xg = yuvAxisPts[1][1] * cos(yuvAxisPts[1][0]);
yg = yuvAxisPts[1][1] * sin(yuvAxisPts[1][0]);
xb = yuvAxisPts[2][1] * cos(yuvAxisPts[2][0]);
yb = yuvAxisPts[2][1] * sin(yuvAxisPts[2][0]);
toRgb = mat3(1, 1, 1,
xr, xg, xb,
yr, yg, yb);
fromRgb = inverse(toRgb);
// Old least-squares linear regression to match the saturation to the color bars.
// We have an equation R = Summation for each of 6 bars: ((This Bar's Saturation in Jungle Chip) * (Saturation Factor) - (This Bar's Standard Saturation))^2
// Meaning, R is the sum of squared differences in saturations. It's a subtraction, not a ratio.
// Leaving (Saturation Factor) as a variable and simplifying down, the equation is in the format R = A(Saturation Factor)^2 + B(Saturation Factor) + C.
// The value of (Saturation Factor) that minimizes R is in the center of the parabola, or -B/(2*A). The value C is never used, so we don't calculate it.
// float Aval = 0;
// float Bval = 0;
// for(int i = 1; i <= 6; i++) {
// vec3 colorBar = vec3(i & 1, (i >> 1) & 1, i >> 2);
// vec3 yuvJungle = fromRgb * colorBar;
// vec3 yuvStd = fromRgbStd * colorBar;
// Aval += pow(yuvJungle.g, 2) + pow(yuvJungle.b, 2);
// Bval += 2 * sqrt(pow(yuvJungle.g, 2) + pow(yuvJungle.b, 2)) * -sqrt(pow(yuvStd.g, 2) + pow(yuvStd.b, 2));
// }
//
// float satFix = -Bval / (2 * Aval);
// toRgb[0] *= 1; // Have to add this line here because some GPU drivers force you to multiply all 3 columns together.
// toRgb[1] /= satFix;
// toRgb[2] /= satFix;
// New geometric mean to match the saturation to the color bars.
// Take the proportion of the change on each color bar.
// Then, take the geometric mean of those six proportions.
// This has a better result than the linear regression above because saturation is not linear.
// A problem with this method is that this is not a perceptually uniform color space. I will address that sometime else.
float prop = 1.0;
for(int i = 1; i <= 6; i++) {
vec3 colorBar = vec3(i & 1, (i >> 1) & 1, i >> 2);
vec3 yuvJungle = fromRgb * colorBar;
vec3 yuvStd = fromRgbStd * colorBar;
float satJungle = sqrt(pow(yuvJungle.g, 2) + pow(yuvJungle.b, 2));
float satStd = sqrt(pow(yuvStd.g, 2) + pow(yuvStd.b, 2));
prop *= satJungle / satStd; // What saturation do we set to make this bar match in saturation?
}
prop = pow(prop, 1.0/6.0); // Geometric mean of those proportional errors.
for(int i = 0; i < 3; i++)
yuvAxisPts[i][1] *= prop;
return yuvAxisPts;
// }
}
mat3 YBmyRmyToRGBMatrix() {
mat3x2 yuvAxisPts = yuvAxisPointsSynced();
float xr, yr, xg, yg, xb, yb;
xr = yuvAxisPts[0][1] * cos(yuvAxisPts[0][0]);
yr = yuvAxisPts[0][1] * sin(yuvAxisPts[0][0]);
xg = yuvAxisPts[1][1] * cos(yuvAxisPts[1][0]);
yg = yuvAxisPts[1][1] * sin(yuvAxisPts[1][0]);
xb = yuvAxisPts[2][1] * cos(yuvAxisPts[2][0]);
yb = yuvAxisPts[2][1] * sin(yuvAxisPts[2][0]);
// YUV to RGB matrix
mat3 toRgb = mat3(1, 1, 1,
xr, xg, xb,
yr, yg, yb);
// YUV to Y B-Y R-Y matrix
mat3 toYBmyRmy = mat3(1, 0, 0,
0, xb, xr,
0, yb, yr);
// Y B-Y R-Y to YUV to RGB matrix
return toRgb * inverse(toYBmyRmy);
}
vec2 uvDemodPickable(float type, float chroma, float phase, float tint) {
mat3x2 axes = yuvAxisPointsSynced();
if(type < -0.5) {
return 2 * chroma * vec2(axes[2][1] * sin(phase + axes[2][0] + tint), axes[0][1] * sin(phase + axes[0][0] + tint));
} else if(type < 1.5) { // either 0 or 1
return 2 * chroma * vec2(axes[2][1] * sin(phase + axes[2][0] + tint), axes[0][1] * sin(phase + axes[0][0] + tint));
} else {
return 2 * chroma * vec2(axes[2][1] * sin(phase + pi / 2 + axes[2][0] + tint), axes[0][1] * sin(phase + pi / 2 + axes[0][0] + tint));
}
}
#endif // __patchy_ntsc_inc_filters_inc__

View File

@ -0,0 +1,228 @@
/*
Patchy NTSC
Copyright (C) 2024 Patchy68k/PlainOldPants
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef __patchy_ntsc_inc_params_inc__
#define __patchy_ntsc_inc_params_inc__
// NES simulation requires SIGNAL_RES to be exactly 8. Not 16. Not 4. Only 8.
// Some code involving NES has 8 hardcoded, and some code uses SIGNAL_RES instead.
#define SIGNAL_RES 8
#define pi 3.14159265358
layout(std140, set = 0, binding = 0) uniform UBO
{
mat4 MVP;
float pn_test_pattern,
pn_width_uncropped,
pn_height_uncropped,
pn_scanline_dur,
pn_color_amplitude,
pn_color_freq,
pn_color_init_offset,
pn_color_line_offset,
pn_color_screen_offset,
pn_nes_enable,
pn_nes_phase_mod,
pn_nes_hue_skew,
pn_genesis_palette,
pn_genesis_jailbar_enable,
pn_genesis_jailbar_offset,
pn_genesis_jailbar_amplitude,
pn_modulator_std,
pn_modulator_luma_filter_type,
pn_modulator_luma_filter_width,
pn_modulator_luma_filter_level,
pn_modulator_luma_radius,
pn_modulator_luma_sigma,
pn_modulator_luma_res,
pn_modulator_luma_eq_f_lo,
pn_modulator_luma_eq_f_hi,
pn_modulator_luma_eq_g_lo,
pn_modulator_luma_eq_g_mid,
pn_modulator_luma_eq_g_hi,
pn_modulator_luma_eq_off,
pn_modulator_luma_eq_dist,
pn_modulator_chroma_filter_type,
pn_modulator_chroma_filter_width,
pn_modulator_chroma_filter_level,
pn_modulator_chroma_filter_level_diff,
pn_modulator_chroma_eq_f_lo,
pn_modulator_chroma_eq_f_hi,
pn_modulator_chroma_eq_g_lo,
pn_modulator_chroma_eq_g_mid,
pn_modulator_chroma_eq_g_hi,
pn_modulator_chroma_eq_off,
pn_modulator_chroma_eq_dist,
pn_demodulator_std,
pn_demodulator_r_off,
pn_demodulator_r_amp,
pn_demodulator_g_off,
pn_demodulator_g_amp,
pn_demodulator_luma_filter_type,
pn_demodulator_luma_filter_width,
pn_demodulator_luma_filter_level,
pn_demodulator_luma_radius,
pn_demodulator_luma_sigma,
pn_demodulator_luma_res,
pn_demodulator_luma_eq_f_lo,
pn_demodulator_luma_eq_f_hi,
pn_demodulator_luma_eq_g_lo,
pn_demodulator_luma_eq_g_mid,
pn_demodulator_luma_eq_g_hi,
pn_demodulator_luma_eq_off,
pn_demodulator_luma_eq_dist,
pn_demodulator_chroma_filter_type,
pn_demodulator_chroma_filter_width,
pn_demodulator_chroma_filter_level,
pn_demodulator_chroma_filter_level_diff,
pn_demodulator_chroma_eq_f_lo,
pn_demodulator_chroma_eq_f_hi,
pn_demodulator_chroma_eq_g_lo,
pn_demodulator_chroma_eq_g_mid,
pn_demodulator_chroma_eq_g_hi,
pn_demodulator_chroma_eq_off,
pn_demodulator_chroma_eq_dist,
pn_demodulator_uv_filter_width,
pn_demodulator_uv_filter_level,
pn_knob_contrast,
pn_knob_brightness,
pn_knob_saturation,
pn_knob_tint,
pn_rgb_smear_enable,
pn_rgb_smear_clamp,
pn_rgb_smear_rate,
pn_rgb_smear_limit,
pn_rgb_smear_res,
pn_gamma_type,
pn_g_CRT_l,
pn_power_gamma;
} global;
#pragma parameter pn_comment_header "======= Patchy NTSC Settings =======" 0 0 0 1
#pragma parameter pn_comment_test_pattern "=== Test Patterns ===" 0 0 0 1
#pragma parameter pn_test_pattern "Test Pattern: Color Bars, Ramps, HSV, Focused HSV, NES Full" 0 0 5 1
#pragma parameter pn_comment_resolution "=== Auto detect cropped overscan ===" 0 0 0 1
#pragma parameter pn_width_uncropped "Console Horizontal Resolution (pixels)" 320 128 720 1
#pragma parameter pn_height_uncropped "Console Vertical Resolution (pixels)" 224 128 480 1
#pragma parameter pn_comment_nes "=== NES Settings ===" 0 0 0 1
#pragma parameter pn_comment_nes_palette "== Note: Use Mesen (not FCEUmm) and change palette to Raw ==" 0 0 0 1
#pragma parameter pn_nes_enable "Enable NES Raw Palette Mode" 0 0 1 1
#pragma parameter pn_nes_phase_mod "NES Battletoads / Battletoads Double Dragon Phase Cycle" 0 0 1 1
#pragma parameter pn_nes_hue_skew "NES colors emulated PPU version: 2C02G | 2C02E" 0 0 1 1
#pragma parameter pn_comment_genesis_jailbar "=== Genesis Settings ===" 0 0 0 1
#pragma parameter pn_genesis_palette "Genesis Plus GX color fix (not for BlastEm or other consoles)" 0 0 1 1
#pragma parameter pn_genesis_jailbar_enable "Enable Genesis/MegaDrive jailbars on solid backgrounds" 0 0 1 1
#pragma parameter pn_genesis_jailbar_offset "Genesis Jailbars Offset" 0 0 1 0.0625
#pragma parameter pn_genesis_jailbar_amplitude "Genesis Jailbars Severity" 1.0 0.1 3.0 0.1
#pragma parameter pn_comment_color_carrier "=== Color carrier settings ===" 0 0 0 1
#pragma parameter pn_scanline_dur "Scanline Duration (uSec)" 47.6 1.0 300.0 0.1
#pragma parameter pn_color_amplitude "Color Carrier Amplitude" 1.0 0.025 1.5 0.025
#pragma parameter pn_color_freq "Color Carrier Frequency (MHz)" 3.5795454 3.47954 3.67954 0.00001
#pragma parameter pn_color_init_offset "Color Carrier Initial Offset" 0.1 0 1 0.02
#pragma parameter pn_color_line_offset "Color Carrier Per-Line Offset" 0 0 1 0.02
#pragma parameter pn_color_screen_offset "Color Carrier Per-Frame Offset" 0 0 1 0.02
#pragma parameter pn_comment_modulator_std "=== Console video standard (Most are Rec. 601) ===" 0 0 0 1
#pragma parameter pn_modulator_std "Modulator standard: Rec. 601 | Rec. 709" 0 0 1 1
#pragma parameter pn_comment_mod_luma_lowpass "=== (Not NES) Console's filtering luma/chroma before adding ===" 0 0 0 1
#pragma parameter pn_modulator_luma_filter_type "Mod. Luma Lowpass Type" 0 -1 3 1
#pragma parameter pn_modulator_luma_filter_width "Mod. Luma Lowpass (0) Sinc Half-Width" 9 0 30 0.25
#pragma parameter pn_modulator_luma_filter_level "Mod. Luma Lowpass (0) Sinc Cutoff Frequency (MHz)" 3.05 0.5 40.0 0.025
#pragma parameter pn_modulator_luma_radius "Mod. Luma Lowpass (1) Gaussian Blur Radius" 3.0 0.1 15.0 0.1
#pragma parameter pn_modulator_luma_sigma "Mod. Luma Lowpass (1) Gaussian Blur Sigma" 1.5 0.1 15.0 0.01
#pragma parameter pn_modulator_luma_res "Mod. Luma Lowpass (2) GTU Signal Res Y" 180 10 800 2
#pragma parameter pn_modulator_luma_eq_f_lo "Mod. Luma 3 band eq (3) Low freq (MHz)" 1.5 0.025 5.5 0.025
#pragma parameter pn_modulator_luma_eq_f_hi "Mod. Luma 3 band eq (3) Hi freq (MHz)" 3.0 0.025 5.5 0.025
#pragma parameter pn_modulator_luma_eq_g_lo "Mod. Luma 3 band eq (3) Low gain" 1.0 0.025 1.0 0.025
#pragma parameter pn_modulator_luma_eq_g_mid "Mod. Luma 3 band eq (3) Mid gain" 0.125 0.025 1.0 0.025
#pragma parameter pn_modulator_luma_eq_g_hi "Mod. Luma 3 band eq (3) High gain" 0.15 0.025 1.0 0.025
#pragma parameter pn_modulator_luma_eq_off "Mod. Luma 3 band eq (3) offset (pixels)" 0.5 0 4 0.0125
#pragma parameter pn_modulator_luma_eq_dist "Mod. Luma 3 band eq (3) max distance (% screen)" 20 5 40.0 0.25
#pragma parameter pn_modulator_chroma_filter_type "Mod. Chroma Bandpass Type" 0 -1 1 1
#pragma parameter pn_modulator_chroma_filter_width "Mod. Chroma Bandpass (0) Half-Width" 5 0 30 0.25
#pragma parameter pn_modulator_chroma_filter_level "Mod. Chroma Bandpass (0) Low Frequency (MHz)" 3.55 0.5 6.0 0.025
#pragma parameter pn_modulator_chroma_filter_level_diff "Mod. Chroma Bandpass (0) High Frequency (MHz)" 3.6 0.5 6.0 0.025
#pragma parameter pn_modulator_chroma_eq_f_lo "Mod. Chroma 3 band eq (1) Low freq (MHz)" 3.2 0.025 5.5 0.025
#pragma parameter pn_modulator_chroma_eq_f_hi "Mod. Chroma 3 band eq (1) Hi freq (MHz)" 4.5 0.025 5.5 0.025
#pragma parameter pn_modulator_chroma_eq_g_lo "Mod. Chroma 3 band eq (1) Low gain" 0.1 0.025 1.0 0.025
#pragma parameter pn_modulator_chroma_eq_g_mid "Mod. Chroma 3 band eq (1) Mid gain" 1.0 0.025 1.0 0.025
#pragma parameter pn_modulator_chroma_eq_g_hi "Mod. Chroma 3 band eq (1) High gain" 0.1 0.025 1.0 0.025
#pragma parameter pn_modulator_chroma_eq_off "Mod. Chroma 3 band eq (3) offset (pixels)" 0.5 0 4 0.0125
#pragma parameter pn_modulator_chroma_eq_dist "Mod. Chroma 3 band eq (1) max distance (% screen)" 20 5 40.0 0.25
#pragma parameter pn_comment_demod_filter "=== CRT Chroma/Luma Separation (a.k.a. comb filter) ===" 0 0 0 1
#pragma parameter pn_demodulator_luma_filter_type "Demodulator Luma Lowpass Type" 0 -1 4 1
#pragma parameter pn_demodulator_luma_filter_width "Demod. Luma Lowpass (0) Sinc Half-Width" 5 0 30 0.25
#pragma parameter pn_demodulator_luma_filter_level "Demod. Luma Lowpass (0) Sinc Cutoff Frequency (MHz)" 2.35 0.5 4.0 0.025
#pragma parameter pn_demodulator_luma_radius "Demod. Luma Lowpass (1) Gaussian Blur Radius" 4.5 0.1 15.0 0.1
#pragma parameter pn_demodulator_luma_sigma "Demod. Luma Lowpass (1) Gaussian Blur Sigma" 1.9 0.1 15.0 0.01
#pragma parameter pn_demodulator_luma_res "Demod. Luma Lowpass (2) GTU Signal Res Y" 180 10 800 2
#pragma parameter pn_demodulator_luma_eq_f_lo "Demod. Luma 3 band eq (3) Low freq (MHz)" 1.5 0.025 5.5 0.025
#pragma parameter pn_demodulator_luma_eq_f_hi "Demod. Luma 3 band eq (3) Hi freq (MHz)" 3.0 0.025 5.5 0.025
#pragma parameter pn_demodulator_luma_eq_g_lo "Demod. Luma 3 band eq (3) Low gain" 1.0 0.025 1.0 0.025
#pragma parameter pn_demodulator_luma_eq_g_mid "Demod. Luma 3 band eq (3) Mid gain" 0.125 0.025 1.0 0.025
#pragma parameter pn_demodulator_luma_eq_g_hi "Demod. Luma 3 band eq (3) High gain" 0.15 0.025 1.0 0.025
#pragma parameter pn_demodulator_luma_eq_off "Demod. Luma 3 band eq (3) offset (pixels)" 0.5 0 4 0.0125
#pragma parameter pn_demodulator_luma_eq_dist "Demod. Luma 3 band eq (3) max distance (% screen)" 20 5 40.0 0.25
#pragma parameter pn_demodulator_chroma_filter_type "Demod. Chroma Bandpass Type" 0 -1 2 1
#pragma parameter pn_demodulator_chroma_filter_width "Demod. Chroma Bandpass (0) Half-Width" 5 0 30 0.25
#pragma parameter pn_demodulator_chroma_filter_level "Demod. Chroma Bandpass (0) Low Frequency (MHz)" 3.55 0.5 6.0 0.025
#pragma parameter pn_demodulator_chroma_filter_level_diff "Demod. Chroma Bandpass (0) High Frequency (MHz)" 3.6 0.5 6.0 0.025
#pragma parameter pn_demodulator_chroma_eq_f_lo "Demod. Chroma 3 band eq (1) Low freq (MHz)" 3.2 0.025 5.5 0.025
#pragma parameter pn_demodulator_chroma_eq_f_hi "Demod. Chroma 3 band eq (1) Hi freq (MHz)" 4.5 0.025 5.5 0.025
#pragma parameter pn_demodulator_chroma_eq_g_lo "Demod. Chroma 3 band eq (1) Low gain" 0.1 0.025 1.0 0.025
#pragma parameter pn_demodulator_chroma_eq_g_mid "Demod. Chroma 3 band eq (1) Mid gain" 1.0 0.025 1.0 0.025
#pragma parameter pn_demodulator_chroma_eq_g_hi "Demod. Chroma 3 band eq (1) High gain" 0.1 0.025 1.0 0.025
#pragma parameter pn_demodulator_chroma_eq_off "Demod. Chroma 3 band eq (3) offset (pixels)" 0.5 0 4 0.0125
#pragma parameter pn_demodulator_chroma_eq_dist "Demod. Chroma 3 band eq (1) max distance (% screen)" 20 5 40.0 0.25
//#pragma parameter pn_comment_uv_filter "=== CRT U and V lowpass - Little need to change this ===" 0 0 0 1
//#pragma parameter pn_demodulator_uv_filter_width "Demodulator U and V Lowpass Half-Width" 6 0 30 0.25
//#pragma parameter pn_demodulator_uv_filter_level "Demodulator U and V Lowpass Cutoff Frequency (MHz)" 4 0.5 40.0 0.025
#pragma parameter pn_comment_demod_std "=== CRT Jungle Chip: US are reverse chrono order thru 1990s ===" 0 0 0 1
#pragma parameter pn_demodulator_std "Demodulator: Rec. 601, Rec. 709, 2*JP, 5*US, Custom" 8 0 9 1
#pragma parameter pn_demodulator_r_off "Custom R-Y offset" 112 90 130 1
#pragma parameter pn_demodulator_r_amp "Custom R-Y gain" 0.84 0.0 1.00 0.01
#pragma parameter pn_demodulator_g_off "Custom G-Y offset" 235 200 300 1
#pragma parameter pn_demodulator_g_amp "Custom G-Y gain" 0.33 0.0 1.00 0.01
#pragma parameter pn_comment_knobs "=== CRT End User Color Knobs ===" 0 0 0 1
#pragma parameter pn_knob_contrast "CRT Contrast (Picture)" 0.8 0 2 0.025
#pragma parameter pn_knob_brightness "CRT Brightness (Black Level)" 0 -0.5 0.5 0.01
#pragma parameter pn_knob_saturation "CRT Color (Saturation)" 1.0 0 5 0.025
#pragma parameter pn_knob_tint "CRT Tint (Hue Rotation)" 0 -45 45 0.5
#pragma parameter pn_comment_smearing "=== Red (and some blue) smearing to the right ===" 0 0 0 1
#pragma parameter pn_rgb_smear_enable "Enable Smearing (Should be off unless your CRT is wearing out)" 0 0 1 1
#pragma parameter pn_rgb_smear_clamp "RGB Clamp Level (any value above this becomes equal to this)" 1.5 1.0 3.0 0.05
#pragma parameter pn_rgb_smear_rate "RGB Smear Level (after clamping, anything above this smears)" 1.0 1.0 3.0 0.05
#pragma parameter pn_rgb_smear_limit "Max Smear Detection Distance (% of screen)" 30 0 100 0.25
#pragma parameter pn_rgb_smear_res "Smear Resolution (Higher is more intense but more accurate)" 1 1 3 1
#pragma parameter pn_comment_phosphor "=== CRT Gamma and Phosphor Gamut ===" 0 0 0 1
#pragma parameter pn_gamma_type "Gamma: BT.1886 + Grade black lift fix | BT.1886 | Power | sRGB" 1 0 3 1
#pragma parameter pn_g_CRT_l "Black lift fix approximate gamma" 2.50 2.30 2.60 0.01
#pragma parameter pn_power_gamma "Power gamma" 2.4 2.2 3.0 0.01
#endif // __patchy_ntsc_inc_params_inc__

View File

@ -0,0 +1,349 @@
#version 450
/*
Patchy NTSC
Copyright (C) 2024 Patchy68k/PlainOldPants
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// This pass generates the raw, unfiltered luma and chroma signals separately. An optional fix for Genesis colors is included.
layout(push_constant) uniform Push
{
vec4 SourceSize;
vec4 OriginalSize;
vec4 OutputSize;
uint FrameCount;
vec4 FinalViewportSize;
} params;
// Includes constants, global, and all parameters
#include "patchy-ntsc-inc-params.inc"
#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
void main()
{
gl_Position = global.MVP * Position;
vTexCoord = TexCoord;
}
#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;
// Includes lowpass and bandpass functions and some shared code
#include "patchy-ntsc-inc-filters.inc"
vec3 fixGenColor(vec3 src) {
// Code in BlastEm looks like this:
// static uint8_t levels[] = {0, 27, 49, 71, 87, 103, 119, 130, 146, 157, 174, 190, 206, 228, 255};
// b = levels[(color >> 8) & 0xE];
// g = levels[(color >> 4) & 0xE];
// r = levels[color & 0xE];
// whereas Genesis Plus GX has this:
// /* 8:8:8 RGB */
//#elif defined(USE_32BPP_RENDERING)
//#define MAKE_PIXEL(r,g,b) ((0xff << 24) | (r) << 20 | (r) << 16 | (g) << 12 | (g) << 8 | (b) << 4 | (b))
//#endif
//
///* Initialize Mode 5 pixel color look-up tables */
// for (i = 0; i < 0x200; i++)
// {
// /* CRAM 9-bit value (BBBGGGRRR) */
// r = (i >> 0) & 7;
// g = (i >> 3) & 7;
// b = (i >> 6) & 7;
//
// /* Convert to output pixel format */
// pixel_lut[0][i] = MAKE_PIXEL(r,g,b);
// pixel_lut[1][i] = MAKE_PIXEL(r<<1,g<<1,b<<1);
// pixel_lut[2][i] = MAKE_PIXEL(r+7,g+7,b+7);
// }
float levels[] = {0, 27, 49, 71, 87, 103, 119, 130, 146, 157, 174, 190, 206, 228, 255};
for(int i = 0; i < 3; i++) {
src[i] = levels[int(min(src[i] * 16 + 0.5, 14.5))] / 255.0;
}
return src;
// Old code from Dogway's Grading Shader (grade.slang) which does not work as of August 2024
// float lo, hi;
// for(int i = 0; i < 5; i++) {
// float cont = 2.578419881;
// float pivot = 0.520674;
// float color = (i < 3 ? src[i] : i - 3.0);
//
// cont = pow(cont - 1., 3.);
//
// float knee = 1. / (1. + exp (cont * pivot));
// float shldr = 1. / (1. + exp (cont * (pivot - 1.)));
//
// float res = pivot - log(1. / (color * (shldr - knee) + knee) - 1.) / cont;
// if(i < 3)
// src[i] = res;
// else if(i == 3)
// lo = res;
// else
// hi = res;
// }
// return (src - lo) / (hi - lo);
}
bool InColorp (int p, int color)
{
return ((color + p) % 12 < 6);
}
#define TO_INT2(X) int(floor(((X) * 3.0) + 0.5))
#define TO_INT3(X) int(floor(((X) * 7.0) + 0.5))
#define TO_INT4(X) int(floor(((X) * 15.0) + 0.5))
vec3 smpteBars(vec2 pos) {
float wholeRamp = pos.x * 8;
float flooredWholeRamp = floor(pos.x * 8);
if(pos.y < 0.75) {
// Standard color bars
int index = 7 - int(flooredWholeRamp + 0.5);
return vec3((index & 2) >> 1, index >> 2, index & 1);
} else if(pos.x > 0.875) {
// Standard mini-pluge for syncing the black level
float partialRamp = wholeRamp - flooredWholeRamp;
float steppedPartialRamp = floor(partialRamp * 3);
return vec3((steppedPartialRamp - 1.0) * 0.075);
} else {
// Deeper pluge for setting a nonstandard brightness
float normPos = pos.x / 0.875;
float normY = (pos.y - 0.75) / 0.25;
float steppedPos = floor(normPos * 21);
float finalRamp = (steppedPos - 10) / 10;
if(normY < 0.25) {
return vec3(finalRamp, 0, 0);
} else if(normY < 0.5) {
return vec3(0, finalRamp, 0);
} else if(normY < 0.75) {
return vec3(0, 0, finalRamp);
} else {
return vec3(finalRamp);
}
}
}
vec3 colorRamps(vec2 pos) {
int x = int(pos.x * 17);
int y = int(pos.y * 15);
if(x == 0 || x == 16 || (y & 1) == 0) {
return vec3(0);
} else {
// Red, green, blue, and white are grouped together on purpose.
vec3 primaries[] = {
vec3(1, 1, 1),
vec3(1, 0, 0),
vec3(0, 1, 0),
vec3(0, 0, 1),
vec3(0, 1, 1),
vec3(1, 0, 1),
vec3(1, 1, 0)
};
return mix(vec3(0), primaries[y >> 1], x / 15.0);
}
}
vec3 hsvSpectrum(vec2 pos, bool widenKeyAreas) {
vec3 black = vec3(0);
vec3 white = vec3(1);
vec3 primaries[] = {
vec3(1, 0, 1),
vec3(1, 0, 0),
vec3(1, 1, 0),
vec3(0, 1, 0),
vec3(0, 1, 1),
vec3(0, 0, 1),
vec3(1, 0, 1), // Colors are repeated intentionally
vec3(1, 0, 0),
vec3(1, 1, 0),
};
float scaledY = pos.y * 7.0 + 0.5;
int lowIndex = int(floor(scaledY) + 0.1);
int highIndex = int(ceil(scaledY) + 0.1);
float interpolation = scaledY - lowIndex;
if(widenKeyAreas)
interpolation = min(interpolation * 1.5, 1.0);
vec3 mixedCenter = mix(primaries[lowIndex], primaries[highIndex], interpolation);
float scaledX = pos.x * 2;
if(scaledX < 1) {
if(widenKeyAreas)
scaledX = min(scaledX * 1.5, 1.0);
return mix(black, mixedCenter, scaledX);
} else {
return mix(mixedCenter, white, scaledX - 1);
}
}
vec3 nesFullPalette(vec2 pos) {
float hue = floor(pos.x * 16.0) / 15.0;
float emphasis = floor(pos.y * 8.0) / 7.0;
float level = floor(4.0 * (pos.y * 8.0 - floor(pos.y * 8.0))) / 3.0;
return vec3(hue, level, emphasis);
}
vec3 testPattern(vec2 pos) {
if(global.pn_test_pattern < 1.5) {
return smpteBars(pos);
} else if(global.pn_test_pattern < 2.5) {
return colorRamps(pos);
} else if(global.pn_test_pattern < 3.5) {
return hsvSpectrum(pos, false);
} else if(global.pn_test_pattern < 4.5) {
return hsvSpectrum(pos, true);
} else if(global.pn_test_pattern < 5.5) {
return nesFullPalette(pos);
}
}
void main()
{
vec4 colorIn = texture(Source, vTexCoord);
if(global.pn_test_pattern > 0.5) {
colorIn.rgb = testPattern(vTexCoord);
}
// Compensate for cropped overscan, assuming equal crop on both sides. We don't know whether the user is cropping overscan or not.
// Current position in pixels. X is a decimal number. Y is floored to an integer.
vec2 pixelCoord = vTexCoord * params.OutputSize.xy / vec2(SIGNAL_RES, 1) + (vec2(global.pn_width_uncropped, global.pn_height_uncropped) - params.OutputSize.xy / vec2(SIGNAL_RES, 1)) / 2;
pixelCoord.y = floor(pixelCoord.y);
if(global.pn_nes_enable > 0.5) {
// This code is taken from both the nesdev wiki and from GTU-famicom, the latter created by aliaspider.
int p = getPhaseIntNES();
int color = TO_INT4(colorIn.x);
int level = TO_INT2(colorIn.y);
int emphasis = TO_INT3(colorIn.z);
if(color > 13)
level = 1;
float black = 0.312;
float white = 1.100;
// These new levels originate from lidnariq's post on nesdev in 2015.
// https://forums.nesdev.org/viewtopic.php?p=159266#p159266
float levels[16] = float[] (
0.228f, 0.312f, 0.552f, 0.880f, // Signal low
0.616f, 0.840f, 1.100f, 1.100f, // Signal high
0.192f, 0.256f, 0.448f, 0.712f, // Signal low, attenuated
0.500f, 0.676f, 0.896f, 0.896f // Signal high, attenuated
);
int amtAtten = 0;
if(((bool(emphasis & 1) && InColorp(p, 0)) ||
(bool(emphasis & 2) && InColorp(p, 4)) ||
(bool(emphasis & 4) && InColorp(p, 8))) && color < 14)
{
amtAtten = 8;
}
float low = levels[0 + level + amtAtten];
float high = levels[4 + level + amtAtten];
if(color == 0)
low = high;
else if(color > 12)
high = low;
float sig = InColorp(p, color) ? high : low;
sig = (sig - black) / (white - black);
FragColor = vec4(sig, 0, 0, 1);
} else {
if(global.pn_genesis_palette > 0.5) {
colorIn.rgb = fixGenColor(colorIn.rgb);
}
float phase = getPhase();
mat3 yuvMat;
if(global.pn_modulator_std < 0.5) {
// Rec. 601 YUV matrix
// This is used in the Genesis/MegaDrive and SNES/SFC
yuvMat = mat3x3(
0.299, -0.14713, 0.615,
0.587, -0.28886, -0.51499,
0.114, 0.436, -0.10001
);
} else if(global.pn_modulator_std < 1.5) {
// Rec. 709 YUV matrix
// Don't know any specific consoles using this, but I assume this *eventually* became common.
yuvMat = mat3(0.2126, -0.09991, 0.615,
0.7152, -0.33609, -0.55861,
0.0722, 0.436, -0.05639);
} else {
// This matrix was removed because it's too similar to Rec. 601, and it is less precise due to
// the data sheets only having two decimal places of precision.
// float redShift, redAmp, redY,
// greenShift, greenAmp, greenY,
// blueShift, blueAmp, blueY,
// whiteY;
//
// // NTSC color encoding
// // Genesis/MegaDrive's Sony CXA1145
// // Taken from Sony's official documentation
// // Also, the CXA1645, MB3514, and KA2195D all have these same values too.
// // Same values are also found in the hard-to-find datasheets for SNES video encoders.
// // It's safe to assume almost any 90s console uses these values for composite.
// // The factor 0.2 comes from the standard colurburst amplitude being 0.2.
// // This can be verified against the data sheets, as their colorburst amplitude is 0.29 / 0.71 / 2.
// redShift = (104.0) / 180.0 * pi;
// redAmp = 3.16 * 0.2;
// redY = 0.21;
// greenShift = (-119.0) / 180.0 * pi; // -119 deg = 241 deg
// greenAmp = 2.95 * 0.2;
// greenY = 0.42;
// blueShift = (-13.0) / 180.0 * pi; // -13 deg = 347 deg
// blueAmp = 2.24 * 0.2;
// blueY = 0.08;
// whiteY = 0.71;
//
// yuvMat = mat3(
// redY/whiteY, redAmp*cos(redShift), redAmp*sin(redShift),
// greenY/whiteY, greenAmp*cos(greenShift), greenAmp*sin(greenShift),
// blueY/whiteY, blueAmp*cos(blueShift), blueAmp*sin(blueShift)
// );
}
vec3 yuvCol = yuvMat * colorIn.rgb;
float luma = yuvCol.r / global.pn_color_amplitude;
float chroma = (yuvCol.g * sin(phase) + yuvCol.b * cos(phase));
FragColor = vec4(luma, chroma, 0, 1);
}
}

View File

@ -0,0 +1,86 @@
#version 450
/*
Patchy NTSC
Copyright (C) 2024 Patchy68k/PlainOldPants
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// This pass filters the luma signal and adds the chroma signal to get the composite signal.
layout(push_constant) uniform Push
{
vec4 SourceSize;
vec4 OriginalSize;
vec4 OutputSize;
uint FrameCount;
vec4 FinalViewportSize;
} params;
// Includes constants, global, and all parameters
#include "patchy-ntsc-inc-params.inc"
#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
void main()
{
gl_Position = global.MVP * Position;
vTexCoord = TexCoord;
}
#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;
// Includes lowpass and bandpass functions
#include "patchy-ntsc-inc-filters.inc"
void main()
{
if(global.pn_nes_enable > 0.5) {
FragColor = texture(Source, vTexCoord);
} else {
float composite = 0.0;
// Filter luma before combining with chroma.
composite += lowpassPickable(global.pn_modulator_luma_filter_type,
global.pn_modulator_luma_filter_width, global.pn_modulator_luma_filter_level,
global.pn_modulator_luma_sigma, global.pn_modulator_luma_radius,
global.pn_modulator_luma_res,
global.pn_modulator_luma_eq_f_lo, global.pn_modulator_luma_eq_f_hi,
global.pn_modulator_luma_eq_g_lo, global.pn_modulator_luma_eq_g_mid, global.pn_modulator_luma_eq_g_hi,
global.pn_modulator_luma_eq_dist, global.pn_modulator_luma_eq_off).r;
composite += bandpassPickable(global.pn_modulator_chroma_filter_type,
global.pn_modulator_chroma_filter_width, global.pn_modulator_chroma_filter_level, global.pn_modulator_chroma_filter_level_diff,
global.pn_modulator_chroma_eq_f_lo, global.pn_modulator_chroma_eq_f_hi,
global.pn_modulator_chroma_eq_g_lo, global.pn_modulator_chroma_eq_g_mid, global.pn_modulator_chroma_eq_g_hi,
global.pn_modulator_chroma_eq_dist, global.pn_modulator_chroma_eq_off, 1 // 1 means chroma is in the G channel of the vector
).g;
if(global.pn_genesis_jailbar_enable > 0.5) {
// Compensate for cropped overscan, assuming equal crop on both sides.
// Current position in pixels, both x and y being decimal numbers.
vec2 pixelCoord = realPixelCoord();
// This is not to be confused with the rainbow banding effect seen elsewhere on the Genesis, such as in the waterfall in Sonic 1.
// I don't even know what actually is causing the jailbars on the Model 1 Genesis, but this looks about right.
// From what I've read, these jailbars are largely missing on the Genesis model 1 VA6 revision onward, and it can be fixed on earlier model 1s by adjusting the capacitors.
// 0.0075 matched by eye, not precise.
composite += 0.0075 * global.pn_genesis_jailbar_amplitude * -cos(pi * pixelCoord.x + 2 * pi * global.pn_genesis_jailbar_offset);
}
FragColor = vec4(composite, 0, 0, 1);
}
}

View File

@ -0,0 +1,124 @@
#version 450
/*
Patchy NTSC
Copyright (C) 2024 Patchy68k/PlainOldPants
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// This pass separates luma and chroma from each other and demodulates chroma into B-Y and R-Y, using B-Y and R-Y offsets and multipliers taken from real jungle chips, taking into account the Tint knob/OSD setting.
layout(push_constant) uniform Push
{
vec4 SourceSize;
vec4 OriginalSize;
vec4 OutputSize;
uint FrameCount;
vec4 FinalViewportSize;
} params;
// Includes constants, global, and all parameters
#include "patchy-ntsc-inc-params.inc"
#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
void main()
{
gl_Position = global.MVP * Position;
vTexCoord = TexCoord;
}
#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;
// Includes lowpass and bandpass functions
#include "patchy-ntsc-inc-filters.inc"
void main()
{
float luma = 0.0;
float chroma = 0.0;
float phase = getPhase();
// Filter the signal to isolate luma
luma += lowpassPickable(global.pn_demodulator_luma_filter_type,
global.pn_demodulator_luma_filter_width, global.pn_demodulator_luma_filter_level,
global.pn_demodulator_luma_radius, global.pn_demodulator_luma_sigma,
global.pn_demodulator_luma_res,
global.pn_demodulator_luma_eq_f_lo, global.pn_demodulator_luma_eq_f_hi,
global.pn_demodulator_luma_eq_g_lo, global.pn_demodulator_luma_eq_g_mid, global.pn_demodulator_luma_eq_g_hi,
global.pn_demodulator_luma_eq_dist, global.pn_demodulator_luma_eq_off).r;
// Filter the signal to isolate chroma
chroma += bandpassPickable(global.pn_demodulator_chroma_filter_type,
global.pn_demodulator_chroma_filter_width, global.pn_demodulator_chroma_filter_level, global.pn_demodulator_chroma_filter_level_diff,
global.pn_demodulator_chroma_eq_f_lo, global.pn_demodulator_chroma_eq_f_hi,
global.pn_demodulator_chroma_eq_g_lo, global.pn_demodulator_chroma_eq_g_mid, global.pn_demodulator_chroma_eq_g_hi,
global.pn_demodulator_chroma_eq_dist, global.pn_demodulator_chroma_eq_dist, 0 // 0 means the chroma signal is in the R channel of the vector
).r;
if(global.pn_nes_enable < 0.5) {
luma *= global.pn_color_amplitude; // Correction by the color carrier amplitude
} else {
// From lidnariq's measurements at http://forums.nesdev.org/viewtopic.php?p=159266#p159266
// 30 = colorburst max
// -23 = colorburst min
// 110 = white
// 0 = black
float nesAmp = (30.0 - -23.0) / 2.0 / (110.0 - 0.0);
float stdAmp = 0.2; // Standard colorburst amplitude in NTSC
chroma *= stdAmp / nesAmp;
}
float tint = global.pn_knob_tint * pi / 180.0;
if(global.pn_nes_enable > 0.5) {
float saturatedLevels[4] = {((0.228 + 0.616) / 2. - 0.312) / (1.100 - 0.312), ((0.312 + 0.840) / 2. - 0.312) / (1.100 - 0.312), ((0.552 + 1.100) / 2. - 0.312) / (1.100 - 0.312), ((0.880 + 1.100) / 2. - 0.312) / (1.100 - 0.312)}; // Calculated from DAC.slang from GTU-famicom
int satLevelI = -1; // Integer part. Can be 0, 1, or 2. If the level is 3 or higher, it is represented as 2 with a fractional part greater than 1.
float satLevelF; // Fractional part. If I=0, -1<F<1. If I=1, 0<F<1. If I=2, 0<F<2.
do {
if(satLevelI < 0)
satLevelI = 0;
else
satLevelI++;
float satYRemaining = luma - saturatedLevels[satLevelI];
satLevelF = satYRemaining / (saturatedLevels[satLevelI + 1] - saturatedLevels[satLevelI]);
} while(satLevelI < 2 && satLevelF > 1.0);
// Skew amounts taken from Drag's NES palette generator.
// I remember seeing these on the nesdev forums somewhere too.
// The change is done here instead of in the video signal for better precision.
float skew;
if(global.pn_nes_hue_skew < 0.5) {
// 2C02G
skew = (satLevelI + satLevelF) * -5.0 / 180.0 * pi; // -5 degrees
} else {
// 2C02E
skew = (satLevelI + satLevelF) * -2.5 / 180.0 * pi; // -2.5 degrees
}
// 15 degree rotation was a very close match against real hardware video capture.
// It makes sense because it's half of an NES PPU cycle.
// The change is done here instead of in the video signal for better precision, due to the video signal sample resolution being the same as the NES PPU cycle rate.
tint += -skew - 15.0 / 180.0 * pi;
}
vec2 bmyRmy = uvDemodPickable(global.pn_demodulator_chroma_filter_type, chroma, phase, tint);
FragColor = vec4(luma, bmyRmy, 1);
}

View File

@ -0,0 +1,64 @@
#version 450
/*
Patchy NTSC
Copyright (C) 2024 Patchy68k/PlainOldPants
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// This pass lowpasses R-Y and B-Y and combines the result with Y to get RGB with corrections taken from jungle chips that actually existed, taking the knobs/on-screen-display settings into account.
layout(push_constant) uniform Push
{
vec4 SourceSize;
vec4 OriginalSize;
vec4 OutputSize;
uint FrameCount;
vec4 FinalViewportSize;
} params;
// Includes constants, global, and all parameters
#include "patchy-ntsc-inc-params.inc"
#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
void main()
{
gl_Position = global.MVP * Position;
vTexCoord = TexCoord;
}
#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;
// Includes lowpass and bandpass functions
#include "patchy-ntsc-inc-filters.inc"
void main()
{
float y = texture(Source, vTexCoord).r;
// A slight lowpass on B-Y and R-Y after demodulating
vec2 rmybmy = lowpassGTU(240).gb;
vec3 rgb = YBmyRmyToRGBMatrix() * vec3(y, rmybmy * global.pn_knob_saturation);
rgb *= global.pn_knob_contrast;
// rgb += global.pn_knob_brightness; // Brightness will be handled later.
if(global.pn_rgb_smear_enable > 0.5)
rgb = min(vec3(global.pn_rgb_smear_clamp), rgb);
FragColor = vec4(rgb, 1);
// FragColor = vec4(y, u, v, 1);
}

View File

@ -0,0 +1,166 @@
#version 450
/*
Patchy NTSC
Copyright (C) 2024 Patchy68k/PlainOldPants
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// This pass performs two steps: (Optionally) smear over the bright colors, then linearize the final RGB to prepare for a gamut fix.
// This pass optionally makes bright colors smear over because they aren't being clamped low enough.
// I'm no expert, but my assumption is that this happens when a CRT has weakened after being used for a very long time.
// CRTs that are working properly don't have this problem. Even my 1989 RCA Colortrak doesn't have this problem at all.
layout(push_constant) uniform Push
{
vec4 SourceSize;
vec4 OriginalSize;
vec4 OutputSize;
uint FrameCount;
vec4 FinalViewportSize;
} params;
// Includes constants, global, and all parameters
#include "patchy-ntsc-inc-params.inc"
#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
void main()
{
gl_Position = global.MVP * Position;
vTexCoord = TexCoord;
}
#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;
// Includes lowpass and bandpass functions
#include "patchy-ntsc-inc-filters.inc"
// Includes sRGB_to_linear
#include "../../../../shaders_slang/include/colorspace-tools.h"
// CRT EOTF Function
// Taken from Grade
//----------------------------------------------------------------------
#define CRT_l -(100000.*log((72981.-500000./(3.*max(2.3,global.pn_g_CRT_l)))/9058.))/945461.
float EOTF_1886a(float color, float bl, float brightness, float contrast) {
// Defaults:
// Black Level = 0.1
// Brightness = 0
// Contrast = 100
const float wl = 100.0;
float b = pow(bl, 1./2.4);
float a = pow(wl, 1./2.4)-b;
// b = (brightness-50.) / 250. + b/a; // -0.20 to +0.20
b = (brightness * 100.) / 250. + b/a; // -0.20 to +0.20
//a = contrast!=50. ? pow(2.,(contrast-50.)/50.) : 1.; // 0.50 to +2.00
a = contrast;
const float Vc = 0.35; // Offset
float Lw = wl/100. * a; // White level
float Lb = min( b * a,Vc); // Black level
const float a1 = 2.6; // Shoulder gamma
const float a2 = 3.0; // Knee gamma
float k = Lw /pow(1. + Lb, a1);
float sl = k * pow(Vc + Lb, a1-a2); // Slope for knee gamma
color = color >= Vc ? k * pow(color + Lb, a1 ) : sl * pow(color + Lb, a2 );
// Black lift compensation
float bc = 0.00446395*pow(bl,1.23486);
color = min(max(color-bc,0.0)*(1.0/(1.0-bc)), 1.0); // Undo Lift
color = pow(color,1.0-0.00843283*pow(bl,1.22744)); // Restore Gamma from 'Undo Lift'
return color;
}
vec3 EOTF_1886a_f3( vec3 color, float BlackLevel, float brightness, float contrast) {
color.r = EOTF_1886a( color.r, BlackLevel, brightness, contrast);
color.g = EOTF_1886a( color.g, BlackLevel, brightness, contrast);
color.b = EOTF_1886a( color.b, BlackLevel, brightness, contrast);
return color.rgb;
}
float EOTF_1886a_default(float color, float brightness, float contrast) {
// From Rec. ITU-R BT.1886
float Vc = 0.35, // Vc, a1, and a2 are directly from the paper
a1 = 2.6,
a2 = 3.0,
Lw = contrast;
float V = color,
b = brightness, // The paper recommends about either 0 or 0.1.
k = Lw / pow(1.0 + b, a1);
if(V < Vc) {
return k * pow(Vc + b, a1 - a2) * pow(V + b, a2);
} else {
return k * pow(V + b, a1);
}
}
vec3 EOTF_1886a_default_f3(vec3 color, float brightness, float contrast) {
color.r = EOTF_1886a_default(color.r, brightness, contrast);
color.g = EOTF_1886a_default(color.g, brightness, contrast);
color.b = EOTF_1886a_default(color.b, brightness, contrast);
return color;
}
void main()
{
vec3 rgb;
// Before this pass, all RGB values are clamped under global.pn_rgb_smear_rate.
// If that value is 1.0, then there will be no smearing, so this entire pass can be skipped.
if(global.pn_rgb_smear_enable < 0.5) {
rgb = texture(Source, vTexCoord).rgb;
} else {
vec3 col = vec3(0);
vec3 maxes = vec3(global.pn_rgb_smear_rate); // If a value goes over this, it'll smear.
float i = max(0.0, vTexCoord.x - floor(params.OutputSize.x * global.pn_rgb_smear_limit / 100.0) * params.OutputSize.z);
while(i < vTexCoord.x - 0.5 * params.OutputSize.z) {
col = max(col + texture(Source, vec2(i, vTexCoord.y)).rgb - maxes, vec3(0));
i += max(params.OutputSize.z * SIGNAL_RES / pow(2, global.pn_rgb_smear_res), params.OutputSize.z);
}
col += texture(Source, vTexCoord).rgb;
col = min(col, maxes);
rgb = min(col, 1);
}
rgb /= global.pn_knob_contrast;
if(global.pn_gamma_type < 0.5) {
FragColor = vec4(EOTF_1886a_f3(rgb, CRT_l, global.pn_knob_brightness, global.pn_knob_contrast), 1.0);
} else if(global.pn_gamma_type < 1.5) {
FragColor = vec4(clamp(EOTF_1886a_default_f3(rgb, global.pn_knob_brightness, global.pn_knob_contrast), 0, 1), 1.0);
} else if(global.pn_gamma_type < 2.5) {
rgb = max(rgb * (1.0 - global.pn_knob_brightness) + global.pn_knob_brightness, 0);
rgb *= pow(global.pn_knob_contrast, 1.0 / global.pn_power_gamma);
FragColor = vec4(pow(min(rgb, vec3(1)), vec3(global.pn_power_gamma)), 1.0);
} else {
// The different values 2.2 and 2.4 are not a mistake.
rgb = max(rgb * (1.0 - global.pn_knob_brightness) + global.pn_knob_brightness, 0);
rgb *= global.pn_knob_contrast <= 1.0 ? linear_to_sRGB(vec3(global.pn_knob_contrast), 2.4).r : pow(global.pn_knob_contrast, 1.0 / 2.2);
FragColor = vec4(sRGB_to_linear(rgb, 2.4), 1.0);
}
}

View File

@ -0,0 +1,107 @@
shaders = "7"
shader0 = "patchy-ntsc-pass1.slang"
filter_linear0 = "false"
wrap_mode0 = "clamp_to_border"
frame_count_mod0 = "1000"
mipmap_input0 = "false"
alias0 = ""
float_framebuffer0 = "true"
srgb_framebuffer0 = "false"
scale_type_x0 = "source"
scale_x0 = "8.000000"
scale_type_y0 = "source"
scale_y0 = "1.000000"
shader1 = "patchy-ntsc-pass2.slang"
filter_linear1 = "false"
wrap_mode1 = "clamp_to_border"
frame_count_mod1 = "1000"
mipmap_input1 = "false"
alias1 = ""
float_framebuffer1 = "true"
srgb_framebuffer1 = "false"
scale_type_x1 = "source"
scale_x1 = "1.000000"
scale_type_y1 = "source"
scale_y1 = "1.000000"
shader2 = "patchy-ntsc-pass3.slang"
filter_linear2 = "false"
wrap_mode2 = "clamp_to_border"
frame_count_mod2 = "1000"
mipmap_input2 = "false"
alias2 = ""
float_framebuffer2 = "true"
srgb_framebuffer2 = "false"
scale_type_x2 = "source"
scale_x2 = "1.000000"
scale_type_y2 = "source"
scale_y2 = "1.000000"
shader3 = "patchy-ntsc-pass4.slang"
filter_linear3 = "false"
wrap_mode3 = "clamp_to_border"
frame_count_mod3 = "1000"
mipmap_input3 = "false"
alias3 = ""
float_framebuffer3 = "true"
srgb_framebuffer3 = "false"
scale_type_x3 = "source"
scale_x3 = "1.000000"
scale_type_y3 = "source"
scale_y3 = "1.000000"
shader4 = "patchy-ntsc-pass5.slang"
filter_linear4 = "false"
wrap_mode4 = "clamp_to_border"
frame_count_mod4 = "1000"
mipmap_input4 = "false"
alias4 = ""
float_framebuffer4 = "true"
srgb_framebuffer4 = "false"
scale_type_x4 = "source"
scale_x4 = "1.000000"
scale_type_y4 = "source"
scale_y4 = "1.000000"
shader5 = "../lut/trilinearLUT-switchable.slang"
filter_linear5 = "false"
wrap_mode5 = "clamp_to_border"
frame_count_mod5 = "1000"
mipmap_input5 = "false"
alias5 = ""
float_framebuffer5 = "true"
srgb_framebuffer5 = "false"
scale_type_x5 = "source"
scale_x5 = "1.000000"
scale_type_y5 = "source"
scale_y5 = "1.000000"
shader6 = "linear-to-srgb.slang"
filter_linear6 = "false"
wrap_mode6 = "clamp_to_border"
frame_count_mod6 = "1000"
mipmap_input6 = "false"
alias6 = ""
float_framebuffer6 = "true"
srgb_framebuffer6 = "false"
scale_type_x6 = "source"
scale_x6 = "1.000000"
scale_type_y6 = "source"
scale_y6 = "1.000000"
textures = "PhosphorSamplerLUT1;PhosphorSamplerLUT2;PhosphorSamplerLUT3;PhosphorSamplerLUT4;PhosphorSamplerLUT5"
PhosphorSamplerLUT1 = "../lut/SMPTEC_TrinitronP22_LinearRBG_to_sRGB_LinearRGB.png"
PhosphorSamplerLUT1_linear = "false"
PhosphorSamplerLUT1_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT1_mipmap = "false"
PhosphorSamplerLUT2 = "../lut/NTSCU_TrinitronP22_LinearRBG_to_sRGB_LinearRGB.png"
PhosphorSamplerLUT2_linear = "false"
PhosphorSamplerLUT2_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT2_mipmap = "false"
PhosphorSamplerLUT3 = "../lut/NTSCJ_TrinitronP22_LinearRBG_to_sRGB_LinearRGB.png"
PhosphorSamplerLUT3_linear = "false"
PhosphorSamplerLUT3_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT3_mipmap = "false"
PhosphorSamplerLUT4 = "../lut/SMPTEC_P2280s_LinearRBG_to_sRGB_LinearRGB.png"
PhosphorSamplerLUT4_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT4_mipmap = "false"
PhosphorSamplerLUT4_linear = "false"
PhosphorSamplerLUT5 = "../lut/SMPTEC_P2290s_LinearRBG_to_sRGB_LinearRGB.png"
PhosphorSamplerLUT5_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT5_mipmap = "false"
PhosphorSamplerLUT5_linear = "false"

View File

@ -0,0 +1,203 @@
#version 450
/*
Trilinear LUT
Slightly more accurate, but slower, than reshade/shaders/LUT/LUT.slang
Compatible with the images in that directory, or this one.
Expects n^2 x n dimensions with green on the vertical axis, red on the minor horizontal axis, blue on the major horizontal axis.
Linear filtering for the LUT image should be FALSE. (Reshade's LUT expects true.)
Copyright (C) 2023 ChthonVII
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
layout(push_constant) uniform Push
{
vec4 SourceSize;
vec4 OriginalSize;
vec4 OutputSize;
uint FrameCount;
float lut_toggle, lut_index, lut_chroma_adapt;
} params;
layout(std140, set = 0, binding = 0) uniform UBO
{
mat4 MVP;
} global;
#pragma parameter lut_toggle "Gamut: sRGB, P22-80s, P22-90s, P22-J, Trinitron P22" 0 0 4 1
#pragma parameter lut_index "Reference White: SMPTE-C (D65) | NTSC-U (C) | NTSC-J (D93)" 1 0 2 1
//#pragma parameter lut_chroma_adapt "(For Trinitron Only) Chromatic Adaptation to D65" 0 0 1 1
#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
void main()
{
gl_Position = global.MVP * Position;
//vTexCoord = TexCoord * 1.0001;
vTexCoord = TexCoord;
}
#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;
layout(set = 0, binding = 3) uniform sampler2D PhosphorSamplerLUT1;
layout(set = 0, binding = 4) uniform sampler2D PhosphorSamplerLUT2;
layout(set = 0, binding = 5) uniform sampler2D PhosphorSamplerLUT3;
layout(set = 0, binding = 6) uniform sampler2D PhosphorSamplerLUT4;
//layout(set = 0, binding = 7) uniform sampler2D PhosphorSamplerLUT5;
#define saturate(c) clamp(c, 0.0, 1.0)
void main()
{
vec2 LUT_Size = textureSize(PhosphorSamplerLUT1, 0); // All are the same size... right?
vec3 imgColor = texture(Source, vTexCoord.xy).rgb;
// Force disable chromatic adaptation if a chroma-adapted LUT is missing.
// The current version lacks any chroma-adapted LUTs whatsoever, so the check is commented out.
// if(params.lut_chroma_adapt < 0.5 || params.lut_toggle < 2.5) {
// Change the white balance ourselves, and use the D65-to-D65 LUT.
float Wx, Wy;
if(params.lut_index < 0.5) {
// D65
Wx = 0.31266142;
Wy = 0.3289589;
} else if(params.lut_index < 1.5) {
Wx = 0.31; // C
Wy = 0.316;
} else {
// grade.slang D93
// I do not know where this originates from, but it was in Dogway's grading shader.
// It is not the same 9300K as the lookup table from gamutthingy.
Wx = 0.281;
Wy = 0.311;
}
vec3 d65 = vec3(0.31266142, 0.3289589, 1 - 0.31266142 - 0.3289589) / 0.3289589;
vec3 c = vec3(Wx, Wy, 1 - Wx - Wy) / Wy;
mat3 primaries;
if(params.lut_toggle < 0.5) {
primaries = mat3(0.64, 0.30, 0.15, // sRGB phosphors
0.33, 0.60, 0.06,
0.03, 0.10, 0.79);
} else if(params.lut_toggle < 1.5) {
primaries = mat3( // P22-80s, from Grade
0.6470, 0.2820, 0.1472,
0.3430, 0.6200, 0.0642,
0.0100, 0.0980, 0.7886);
} else if(params.lut_toggle < 2.5) {
primaries = mat3( // P22-90s, from Grade
0.6661, 0.3134, 0.1472,
0.3329, 0.6310, 0.0642,
0.0010, 0.0556, 0.7886);
} else if(params.lut_toggle < 3.5) {
primaries = mat3( // P22-J, from Grade
0.625, 0.280, 0.152,
0.350, 0.605, 0.062,
0.025, 0.115, 0.786);
} else {
primaries = mat3(0.621, 0.281, 0.152, // Trinitron phosphors
0.330, 0.606, 0.067,
0.049, 0.113, 0.781);
}
mat3 unbalanced = mat3(primaries[0][0] / primaries[1][0], 1.0, primaries[2][0] / primaries[1][0],
primaries[0][1] / primaries[1][1], 1.0, primaries[2][1] / primaries[1][1],
primaries[0][2] / primaries[1][2], 1.0, primaries[2][2] / primaries[1][2]);
mat3 unbInv = inverse(unbalanced);
vec3 d65NormConsts = unbInv * d65;
vec3 cNormConsts = unbInv * c;
vec3 normConsts = cNormConsts / d65NormConsts;
normConsts /= max(normConsts.r, max(normConsts.g, normConsts.b));
imgColor *= normConsts; // Scale R, G, and B to get the white point that we want.
// }
if(params.lut_toggle < 0.5) {
FragColor = vec4(imgColor, 1.0);
return;
}
// find rgb values and their ceilings and floors on a scale on 0-63
vec3 temp = imgColor.rgb * (vec3(LUT_Size.y - 1.0));
vec3 floors = floor(imgColor.rgb * (vec3(LUT_Size.y - 1.0)));
vec3 ceils = ceil(imgColor.rgb * (vec3(LUT_Size.y - 1.0)));
// how close are we to the ceiling on a scale of 0-1?
vec3 weights = saturate(temp - floors);
// retroarch can't correctly sample a 1.0 coordinate
// so we are going to add a just-under-half-step offset to red and green, then increase their divisors by 1
// This should get us a slightly lower coordinate within the same pixel
// This is where reshade's LUT is slightly inaccurate because it's taking a linearly filtered sample from the wrong spot.
floors = floors + vec3(0.4999, 0.4999, 0.0);
ceils = ceils + vec3(0.4999, 0.4999, 0.0);
floors = floors / vec3((LUT_Size.y * LUT_Size.y), LUT_Size.y, LUT_Size.y);
ceils = ceils / vec3((LUT_Size.y * LUT_Size.y), LUT_Size.y, LUT_Size.y);
// this would be correct, if retroarch sampled correctly.
//floors = saturate(floors / vec3(((LUT_Size.y * LUT_Size.y) - 1.0), LUT_Size.y - 1.0, LUT_Size.y));
//ceils = saturate(ceils / vec3(((LUT_Size.y * LUT_Size.y) - 1.0), LUT_Size.y - 1.0, LUT_Size.y));
// take 8 samples
vec3 RfGfBf, RfGfBc, RfGcBf, RfGcBc, RcGfBf, RcGfBc, RcGcBf, RcGcBc;
#define eightSamples(SamplerLUT) \
RfGfBf = (texture(SamplerLUT, vec2(floors.b + floors.r, floors.g))).xyz; \
RfGfBc = (texture(SamplerLUT, vec2(ceils.b + floors.r, floors.g))).xyz; \
RfGcBf = (texture(SamplerLUT, vec2(floors.b + floors.r, ceils.g))).xyz; \
RfGcBc = (texture(SamplerLUT, vec2(ceils.b + floors.r, ceils.g))).xyz; \
RcGfBf = (texture(SamplerLUT, vec2(floors.b + ceils.r, floors.g))).xyz; \
RcGfBc = (texture(SamplerLUT, vec2(ceils.b + ceils.r, floors.g))).xyz; \
RcGcBf = (texture(SamplerLUT, vec2(floors.b + ceils.r, ceils.g))).xyz; \
RcGcBc = (texture(SamplerLUT, vec2(ceils.b + ceils.r, ceils.g))).xyz;
if(params.lut_toggle < 1.5) {
eightSamples(PhosphorSamplerLUT1);
} else if(params.lut_toggle < 2.5) {
eightSamples(PhosphorSamplerLUT2);
} else if (params.lut_toggle < 3.5) {
eightSamples(PhosphorSamplerLUT3);
} else {
eightSamples(PhosphorSamplerLUT4);
}
#undef eightSamples
// merge down to 4 samples along blue axis
vec3 RfGf = saturate(mix(RfGfBf, RfGfBc, vec3(weights.b)));
vec3 RfGc = saturate(mix(RfGcBf, RfGcBc, vec3(weights.b)));
vec3 RcGf = saturate(mix(RcGfBf, RcGfBc, vec3(weights.b)));
vec3 RcGc = saturate(mix(RcGcBf, RcGcBc, vec3(weights.b)));
// merge down to 2 samples along green axis
vec3 Rf = saturate(mix(RfGf, RfGc, vec3(weights.g)));
vec3 Rc = saturate(mix(RcGf, RcGc, vec3(weights.g)));
// merge down to one color along red axis
vec3 finalcolor = saturate(mix(Rf, Rc, vec3(weights.r)));
// output
FragColor = vec4(finalcolor.rgb, 1.0);
}

View File

@ -0,0 +1,28 @@
shaders = 1
shader0 = "trilinearLUT-switchable.slang"
filter_linear0 = false
float_framebuffer0 = true
textures = "PhosphorSamplerLUT1;PhosphorSamplerLUT2;PhosphorSamplerLUT3;PhosphorSamplerLUT4"
PhosphorSamplerLUT1 = "P22_80s_D65.png"
PhosphorSamplerLUT1_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT1_mipmap = "false"
PhosphorSamplerLUT1_linear = "false"
PhosphorSamplerLUT2 = "P22_90s_D65.png"
PhosphorSamplerLUT2_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT2_mipmap = "false"
PhosphorSamplerLUT2_linear = "false"
PhosphorSamplerLUT3 = "P22_J_D65.png"
PhosphorSamplerLUT3_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT3_mipmap = "false"
PhosphorSamplerLUT3_linear = "false"
PhosphorSamplerLUT4 = "TrinitronP22_D65.png"
PhosphorSamplerLUT4_wrap_mode = "clamp_to_border"
PhosphorSamplerLUT4_mipmap = "false"
PhosphorSamplerLUT4_linear = "false"

View File

@ -0,0 +1,113 @@
#version 450
/*
Trilinear LUT
Slightly more accurate, but slower, than reshade/shaders/LUT/LUT.slang
Compatible with the images in that directory, or this one.
Expects n^2 x n dimensions with green on the vertical axis, red on the minor horizontal axis, blue on the major horizontal axis.
Linear filtering for the LUT image should be FALSE. (Reshade's LUT expects true.)
Copyright (C) 2023 ChthonVII
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
layout(push_constant) uniform Push
{
vec4 SourceSize;
vec4 OriginalSize;
vec4 OutputSize;
uint FrameCount;
} params;
layout(std140, set = 0, binding = 0) uniform UBO
{
mat4 MVP;
} global;
#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
void main()
{
gl_Position = global.MVP * Position;
//vTexCoord = TexCoord * 1.0001;
vTexCoord = TexCoord;
}
#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;
layout(set = 0, binding = 3) uniform sampler2D SamplerLUT;
#define saturate(c) clamp(c, 0.0, 1.0)
void main()
{
vec2 LUT_Size = textureSize(SamplerLUT, 0);
vec3 imgColor = texture(Source, vTexCoord.xy).rgb;
// find rgb values and their ceilings and floors on a scale on 0-63
vec3 temp = imgColor.rgb * (vec3(LUT_Size.y - 1.0));
vec3 floors = floor(imgColor.rgb * (vec3(LUT_Size.y - 1.0)));
vec3 ceils = ceil(imgColor.rgb * (vec3(LUT_Size.y - 1.0)));
// how close are we to the ceiling on a scale of 0-1?
vec3 weights = saturate(temp - floors);
// retroarch can't correctly sample a 1.0 coordinate
// so we are going to add a just-under-half-step offset to red and green, then increase their divisors by 1
// This should get us a slightly lower coordinate within the same pixel
// This is where reshade's LUT is slightly inaccurate because it's taking a linearly filtered sample from the wrong spot.
floors = floors + vec3(0.4999, 0.4999, 0.0);
ceils = ceils + vec3(0.4999, 0.4999, 0.0);
floors = floors / vec3((LUT_Size.y * LUT_Size.y), LUT_Size.y, LUT_Size.y);
ceils = ceils / vec3((LUT_Size.y * LUT_Size.y), LUT_Size.y, LUT_Size.y);
// this would be correct, if retroarch sampled correctly.
//floors = saturate(floors / vec3(((LUT_Size.y * LUT_Size.y) - 1.0), LUT_Size.y - 1.0, LUT_Size.y));
//ceils = saturate(ceils / vec3(((LUT_Size.y * LUT_Size.y) - 1.0), LUT_Size.y - 1.0, LUT_Size.y));
// take 8 samples
vec3 RfGfBf = (texture(SamplerLUT, vec2(floors.b + floors.r, floors.g))).xyz;
vec3 RfGfBc = (texture(SamplerLUT, vec2(ceils.b + floors.r, floors.g))).xyz;
vec3 RfGcBf = (texture(SamplerLUT, vec2(floors.b + floors.r, ceils.g))).xyz;
vec3 RfGcBc = (texture(SamplerLUT, vec2(ceils.b + floors.r, ceils.g))).xyz;
vec3 RcGfBf = (texture(SamplerLUT, vec2(floors.b + ceils.r, floors.g))).xyz;
vec3 RcGfBc = (texture(SamplerLUT, vec2(ceils.b + ceils.r, floors.g))).xyz;
vec3 RcGcBf = (texture(SamplerLUT, vec2(floors.b + ceils.r, ceils.g))).xyz;
vec3 RcGcBc = (texture(SamplerLUT, vec2(ceils.b + ceils.r, ceils.g))).xyz;
// merge down to 4 samples along blue axis
vec3 RfGf = saturate(mix(RfGfBf, RfGfBc, vec3(weights.b)));
vec3 RfGc = saturate(mix(RfGcBf, RfGcBc, vec3(weights.b)));
vec3 RcGf = saturate(mix(RcGfBf, RcGfBc, vec3(weights.b)));
vec3 RcGc = saturate(mix(RcGcBf, RcGcBc, vec3(weights.b)));
// merge down to 2 samples along green axis
vec3 Rf = saturate(mix(RfGf, RfGc, vec3(weights.g)));
vec3 Rc = saturate(mix(RcGf, RcGc, vec3(weights.g)));
// merge down to one color along red axis
vec3 finalcolor = saturate(mix(Rf, Rc, vec3(weights.r)));
// output
FragColor = vec4(finalcolor.rgb, 1.0);
}