From 74d0acdaecb1342eb6befc57339ed2b36fd490a3 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 3 Sep 2020 19:04:17 -0400 Subject: [PATCH] Fixes non-RGB colour composite generation. The hard-coded sharpen filter proves to be a really bad fit for the Apple II though. --- .../Clock Signal/ScanTarget/CSScanTarget.mm | 4 +- .../Clock Signal/ScanTarget/ScanTarget.metal | 98 +++++++++---------- 2 files changed, 48 insertions(+), 54 deletions(-) diff --git a/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm b/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm index 86ef39bb8..b1b404848 100644 --- a/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm +++ b/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm @@ -604,8 +604,8 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; // TODO: support separate high-low filters for chroma and luma, rather than treating that as purely subtractive. auto *const luminanceCoefficients = uniforms()->lumaCoefficients; - SignalProcessing::FIRFilter lumaPart(15, float(_lineBufferPixelsPerLine), 0.0f, colourCyclesPerLine * 0.5f); - SignalProcessing::FIRFilter chromaPart(15, float(_lineBufferPixelsPerLine), 0.0f, colourCyclesPerLine * 1.1f); + SignalProcessing::FIRFilter lumaPart(15, float(_lineBufferPixelsPerLine), 0.0f, colourCyclesPerLine * 0.75f); + SignalProcessing::FIRFilter chromaPart(15, float(_lineBufferPixelsPerLine), 0.0f, colourCyclesPerLine * 1.0f); const auto lumaCoefficients = lumaPart.get_coefficients(); const auto chromaCoefficients = chromaPart.get_coefficients(); diff --git a/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal b/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal index 9f0ee701b..1bb8ea326 100644 --- a/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal +++ b/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal @@ -96,6 +96,11 @@ struct SourceInterpolator { float colourAmplitude [[flat]]; }; +struct CopyInterpolator { + float4 position [[position]]; + float2 textureCoordinates; +}; + // MARK: - Vertex shaders. float2 textureLocation(constant Line *line, float offset, constant Uniforms &uniforms) { @@ -210,6 +215,25 @@ vertex SourceInterpolator scanToComposition( constant Uniforms &uniforms [[buffe return result; } +vertex CopyInterpolator copyVertex(uint vertexID [[vertex_id]], texture2d texture [[texture(0)]]) { + CopyInterpolator vert; + + const uint x = vertexID & 1; + const uint y = (vertexID >> 1) & 1; + + vert.textureCoordinates = float2( + x * texture.get_width(), + y * texture.get_height() + ); + vert.position = float4( + float(x) * 2.0 - 1.0, + 1.0 - float(y) * 2.0, + 0.0, + 1.0 + ); + + return vert; +} // MARK: - Various input format conversion samplers. @@ -217,20 +241,28 @@ float2 quadrature(float phase) { return float2(cos(phase), sin(phase)); } +float4 composite(float level, float2 quadrature, float amplitude) { + return float4( + level, + float2(0.5f) + quadrature*0.5f, + amplitude + ); +} + // There's only one meaningful way to sample the luminance formats. fragment float4 sampleLuminance1(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { - return float4(float3(texture.sample(standardSampler, vert.textureCoordinates).r), 1.0); + return composite(texture.sample(standardSampler, vert.textureCoordinates).r, quadrature(vert.colourPhase), vert.colourAmplitude); } fragment float4 sampleLuminance8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { - return float4(float3(texture.sample(standardSampler, vert.textureCoordinates).r), 1.0); + return composite(texture.sample(standardSampler, vert.textureCoordinates).r, quadrature(vert.colourPhase), vert.colourAmplitude); } fragment float4 samplePhaseLinkedLuminance8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { const int offset = int(vert.colourPhase * 4.0); auto sample = texture.sample(standardSampler, vert.textureCoordinates); - return float4(float3(sample[offset]), 1.0); + return composite(sample[offset], quadrature(vert.colourPhase), vert.colourAmplitude); } // The luminance/phase format can produce either composite or S-Video. @@ -254,12 +286,7 @@ fragment float4 sampleLuminance8Phase8(SourceInterpolator vert [[stage_in]], tex fragment float4 compositeSampleLuminance8Phase8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { const float2 luminanceChroma = convertLuminance8Phase8(vert, texture); const float level = mix(luminanceChroma.r, luminanceChroma.g, vert.colourAmplitude); - return float4( - level, - 0.5 + 0.5*level*cos(vert.colourPhase), - 0.5 + 0.5*level*sin(vert.colourPhase), - 1.0 - ); + return composite(level, quadrature(vert.colourPhase), vert.colourAmplitude); } // All the RGB formats can produce RGB, composite or S-Video. @@ -309,11 +336,7 @@ float3 convertRed1Green1Blue1(SourceInterpolator vert, texture2d texture const auto colour = uniforms.fromRGB * clamp(convert##name(vert, texture), float3(0.0f), float3(1.0f)); \ const float2 colourSubcarrier = quadrature(vert.colourPhase); \ const float level = mix(colour.r, dot(colour.gb, colourSubcarrier), vert.colourAmplitude); \ - return float4( \ - level, \ - float2(0.5f) + quadrature(vert.colourPhase)*0.5f, \ - vert.colourPhase \ - ); \ + return composite(level, colourSubcarrier, vert.colourAmplitude); \ } DeclareShaders(Red8Green8Blue8, float) @@ -321,48 +344,19 @@ DeclareShaders(Red4Green4Blue4, ushort) DeclareShaders(Red2Green2Blue2, ushort) DeclareShaders(Red1Green1Blue1, ushort) -// MARK: - Shaders for copying from a same-sized texture to an MTKView's frame buffer. - -struct CopyInterpolator { - float4 position [[position]]; - float2 textureCoordinates; -}; - -vertex CopyInterpolator copyVertex(uint vertexID [[vertex_id]], texture2d texture [[texture(0)]]) { - CopyInterpolator vert; - - const uint x = vertexID & 1; - const uint y = (vertexID >> 1) & 1; - - vert.textureCoordinates = float2( - x * texture.get_width(), - y * texture.get_height() - ); - vert.position = float4( - float(x) * 2.0 - 1.0, - 1.0 - float(y) * 2.0, - 0.0, - 1.0 - ); - - return vert; -} - fragment float4 copyFragment(CopyInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { return texture.sample(standardSampler, vert.textureCoordinates); } +fragment float4 interpolateFragment(CopyInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { + return texture.sample(linearSampler, vert.textureCoordinates); +} + fragment float4 clearFragment() { return float4(0.0, 0.0, 0.0, 0.64); } -// MARK: - - -fragment float4 interpolateFragment(CopyInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { - return texture.sample(linearSampler, vert.textureCoordinates); -} - -// MARK: - Kernel functions +// MARK: - Compute kernels /// Given input pixels of the form (luminance, 0.5 + 0.5*chrominance*cos(phase), 0.5 + 0.5*chrominance*sin(phase)), applies a lowpass /// filter to the two chrominance parts, then uses the toRGB matrix to convert to RGB and stores. @@ -441,11 +435,11 @@ kernel void separateLumaKernel( texture2d inTexture [[textu Sample(8, 6) + Sample(9, 5) + Sample(10, 4) + Sample(11, 3) + Sample(12, 2) + Sample(13, 1) + Sample(14, 0); #undef Sample - // TODO: determine why centreSample.a doesn't seem to be giving the real composite amplitude, and stop - // hard-coding 0.15f and 7.0f below. + // The mix/steps below ensures that the absence of a colour burst leads the colour subcarrier to be discarded. + const float isColour = step(0.01, centreSample.a); outTexture.write(float4( - luminance.r / (1.0f - 0.15f), - (centreSample.gb - float2(0.5f)) * (centreSample.r - luminance.g) * 28.0f + float2(0.5f), + mix(luminance.g, luminance.r / (1.0f - centreSample.a), isColour), + isColour * (centreSample.gb - float2(0.5f)) * (centreSample.r - luminance.g) / mix(1.0f, centreSample.a, isColour) + float2(0.5f), 1.0f ), gid + uint2(7, offset));