#version 450

/*
   A port of DeadlyRedCube's Cathode-Retro shader to slang format
   based on a snapshot from https://github.com/DeadlyRedCube/Cathode-Retro circa Nov. 2023
   ported by hunterk
   license: MIT
*/

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This shader takes an RGB image and turns it into either an S-Video or Composite signal (Based on whether g_compositeBlend is 0 or 1).
//  We might also be generating a PAIR of these, using two different sets of phase inputs (if g_scanlinePhases is a two-component input),
//  for purposes of temporal aliasing reduction.


#include "slang_params.inc"
#include "cathode-retro-util-tracking-instability.inc"
#define signalTexCoord vTexCoord

// This is the RGB input texture. It is expected to be g_inputWidth x g_scanlineCount in size.
// This sampler should be set up with linear filtering, and either clamp or border addressing.
//DECLARE_TEXTURE2D(g_sourceTexture, g_sourceSampler);
layout(set = 0, binding = 2) uniform sampler2D g_sourceTexture;

// This is the scanline phases texture, generated by GeneratePhaseTexture. It is g_scanlineCount x 1 in size, and each texel component in
//  it represents the phase offset of the NTSC colorburst for the corresponding scanline, in multiples of the colorburst wavelength.
// This sampler should be set up with linear filtering, and either clamp or border addressing.
//DECLARE_TEXTURE2D(g_scanlinePhases, g_scanlinePhasesSampler);
layout(set = 0, binding = 3) uniform sampler2D g_scanlinePhases;

  // The number of texels that the output texture will contain for each color cycle wave (i.e. the wavelength in output samples of
  //  the color carrier wave).
  uint g_outputTexelsPerColorburstCycle = uint(global.cb_samples);

  // The width of the input texture.
  uint g_inputWidth = uint(params.OriginalSize.x);

  // The width of the output render target.
  uint g_outputWidth = uint(params.OutputSize.x);

  // The number of scanlines in the current field of video (the height of the input texture).
  uint g_scanlineCount = (params.OriginalSize.y < 400.) ? uint(params.OriginalSize.y) : uint(params.OriginalSize.y * 0.5);

  // This is whether we're blending the generated luma/chroma into a single output channel or not. It is expected to be 0 or 1 (no
  //  intermediate values), where "0" means "keep luma and chroma separate, like an S-Video signal" and "1" means "add the two together,
  //  like a composite signal".
  float g_compositeBlend = global.composite;

  // The scale of any picture instability (horizontal scanline-by-scanline tracking issues). This is used to offset our texture sampling
  //  when generating the output so the picture tracking is imperfect.  Must match the similarly-named value in GeneratePhaseTexture.
  float g_instabilityScale = global.horz_track_scale;

  // A seed for the noise used to generate the scanline-by-scanline picture instability. Must match the simiarly-named value in
  //  GeneratePhaseTexture.
  uint g_noiseSeed = uint(mod(params.FrameCount, 13637));

    // the number of output texels to pad on either side of the signal texture (so that filtering won't have visible artifacts on the left
    //  and right sides).
  uint g_sidePaddingTexelCount = uint(global.sig_pad);


const float pi = 3.141592653;

void main()
{
  uvec2 signalTexelIndex = uvec2(floor(signalTexCoord * vec2(g_outputWidth, g_scanlineCount)));

  // The texcoord we're using to sample the input texture neesd to be adjusted slightly. The 0.25 offset ensures that our generated texture
  //  is centered on the RGB texture.
  vec2 texCoord = (vec2(signalTexelIndex) * vec2(float(g_inputWidth) / float(g_outputWidth), 1.) + vec2(0.25, 0.5))
    / vec2(g_inputWidth, g_scanlineCount);

  // Expand our sampling a little bit to adjust for the padding we want on the sides.
  uint effectiveOutputWidth = g_outputWidth - g_sidePaddingTexelCount;
  texCoord.x = (texCoord.x - 0.5) * float(g_outputWidth) / float(effectiveOutputWidth) + 0.5;

  // Add in the instability to our x coordinate to wiggle our generated texture around.
  float instability = CalculateTrackingInstabilityOffset(
    signalTexelIndex.y,
    g_noiseSeed,
    g_instabilityScale,
    g_outputWidth);
  texCoord.x += instability;

  vec3 rgb = texture(g_sourceTexture, texCoord).rgb;

  // Convert RGB to YIQ using the NTSC standard (SMPTE C) conversion (from https://en.wikipedia.org/wiki/YIQ)
  vec3 yiq;

  yiq.r = dot(rgb, vec3(0.3000,  0.5900,  0.1100));
  yiq.g = dot(rgb, vec3(0.5990, -0.2773, -0.3217));
  yiq.b = dot(rgb, vec3(0.2130, -0.5251,  0.3121));

  // Do some gamma adjustments to counter for the gamma that will be used at decode time.
  yiq.x = pow(saturate(yiq.x), 2.2 / 2.0);
  float iqSat = saturate(length(yiq.yz));
  yiq.yz *= pow(iqSat, 2.2 / 2.0) / max(0.00001, iqSat);

  // Separate the YIQ channels out because YIQ.y ended up being confusing (is it Y? no it's I! but also it's y!)
  float Y = yiq.x;
  float I = yiq.y;
  float Q = yiq.z;

  // Calculate the phase for our current x position on the current scanline.
  vec2 scanlinePhase = texture(
    g_scanlinePhases,
    (vec2(0.0, signalTexelIndex.y + 0.5) / g_scanlineCount)).xy;
  vec2 phase = scanlinePhase + signalTexelIndex.x / float(g_outputTexelsPerColorburstCycle);

  // Now we need to encode our IQ component in the carrier wave at the correct phase. This is QAM modulation.
  vec2 s, c;
  sincos(2.0 * pi * phase, s, c);

  vec2 luma = vec2(Y, Y);
  vec2 chroma = s * I - c * Q;

  if (g_compositeBlend > 0.5)
  {
    // We are outputting a composite signal so combine luma and chroma and output it into our expected 1- or 2-channel texture.
    FragColor = (luma + chroma).xyxy;
  }
  else
  {
    // Outputting svideo, so don't combine luma and chroma, and write them out into our expected 2- or 4-channel texture.
    FragColor = vec4(luma, chroma).xzyw;
  }
}
