diff --git a/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm b/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm index de8eb60be..86ef39bb8 100644 --- a/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm +++ b/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm @@ -99,7 +99,7 @@ struct Uniforms { float zoom; simd::float2 offset; simd::float3 chromaCoefficients[8]; - float lumaCoefficients[8]; + simd::float2 lumaCoefficients[8]; float radiansPerPixel; float cyclesMultiplier; }; @@ -580,23 +580,39 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; } chromaCoefficients[7].x = 1.0f; - // Luminance is under-filtered during the separation phase in order not to subtract too much from chrominance; - // therefore an additional filtering is applied here. + // Luminance will be very soft as a result of the separation phase; apply a sharpen filter to try to undo that. if(!isSVideoOutput) { - SignalProcessing::FIRFilter luminancefilter(15, float(_lineBufferPixelsPerLine), 0.0f, colourCyclesPerLine * 0.8f); - const auto calculatedLumaCoefficients = luminancefilter.get_coefficients(); + constexpr float sharpen[] = { + 0.0042115543f, + 0.0f, + -0.0641804263f, + -0.252418578f, + -0.589709163f, + 0.987914681f, + 0.627704679f, + -0.426862389f, + 0.627704679f + }; for(size_t c = 0; c < 8; ++c) { - chromaCoefficients[c].x = calculatedLumaCoefficients[c]; + chromaCoefficients[c].x = sharpen[c]; } } } // Generate the luminance separation filter. { + // TODO: support separate high-low filters for chroma and luma, rather than treating that as purely subtractive. + auto *const luminanceCoefficients = uniforms()->lumaCoefficients; - SignalProcessing::FIRFilter luminancefilter(15, float(_lineBufferPixelsPerLine), 0.0f, colourCyclesPerLine); - const auto calculatedCoefficients = luminancefilter.get_coefficients(); - memcpy(luminanceCoefficients, calculatedCoefficients.data(), sizeof(float)*8); + SignalProcessing::FIRFilter lumaPart(15, float(_lineBufferPixelsPerLine), 0.0f, colourCyclesPerLine * 0.5f); + SignalProcessing::FIRFilter chromaPart(15, float(_lineBufferPixelsPerLine), 0.0f, colourCyclesPerLine * 1.1f); + + const auto lumaCoefficients = lumaPart.get_coefficients(); + const auto chromaCoefficients = chromaPart.get_coefficients(); + for(size_t c = 0; c < 8; ++c) { + luminanceCoefficients[c].x = lumaCoefficients[c]; + luminanceCoefficients[c].y = chromaCoefficients[c]; + } } // Store radians per pixel. TODO: is this now orphaned? Should I keep it anyway? diff --git a/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal b/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal index ac67ff1d8..9f0ee701b 100644 --- a/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal +++ b/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal @@ -38,7 +38,7 @@ struct Uniforms { // Describes the FIR filter in use for luma filtering; also 15 coefficients // symmetrical around the centre. - float lumaCoefficients[8]; + float2 lumaCoefficients[8]; // Maps from pixel offsets into the composition buffer to angular difference. float radiansPerPixel; @@ -416,26 +416,26 @@ kernel void separateLumaKernel( texture2d inTexture [[textu constant Uniforms &uniforms [[buffer(0)]], constant int &offset [[buffer(1)]]) { const float4 centreSample = inTexture.read(gid + uint2(7, offset)); - const float rawSamples[] = { - inTexture.read(gid + uint2(0, offset)).r, - inTexture.read(gid + uint2(1, offset)).r, - inTexture.read(gid + uint2(2, offset)).r, - inTexture.read(gid + uint2(3, offset)).r, - inTexture.read(gid + uint2(4, offset)).r, - inTexture.read(gid + uint2(5, offset)).r, - inTexture.read(gid + uint2(6, offset)).r, - centreSample.r, - inTexture.read(gid + uint2(8, offset)).r, - inTexture.read(gid + uint2(9, offset)).r, - inTexture.read(gid + uint2(10, offset)).r, - inTexture.read(gid + uint2(11, offset)).r, - inTexture.read(gid + uint2(12, offset)).r, - inTexture.read(gid + uint2(13, offset)).r, - inTexture.read(gid + uint2(14, offset)).r, + const float2 rawSamples[] = { + inTexture.read(gid + uint2(0, offset)).rr, + inTexture.read(gid + uint2(1, offset)).rr, + inTexture.read(gid + uint2(2, offset)).rr, + inTexture.read(gid + uint2(3, offset)).rr, + inTexture.read(gid + uint2(4, offset)).rr, + inTexture.read(gid + uint2(5, offset)).rr, + inTexture.read(gid + uint2(6, offset)).rr, + centreSample.rr, + inTexture.read(gid + uint2(8, offset)).rr, + inTexture.read(gid + uint2(9, offset)).rr, + inTexture.read(gid + uint2(10, offset)).rr, + inTexture.read(gid + uint2(11, offset)).rr, + inTexture.read(gid + uint2(12, offset)).rr, + inTexture.read(gid + uint2(13, offset)).rr, + inTexture.read(gid + uint2(14, offset)).rr, }; #define Sample(x, y) uniforms.lumaCoefficients[y] * rawSamples[x] - const float luminance = + const float2 luminance = Sample(0, 0) + Sample(1, 1) + Sample(2, 2) + Sample(3, 3) + Sample(4, 4) + Sample(5, 5) + Sample(6, 6) + Sample(7, 7) + Sample(8, 6) + Sample(9, 5) + Sample(10, 4) + Sample(11, 3) + Sample(12, 2) + Sample(13, 1) + Sample(14, 0); @@ -444,8 +444,8 @@ kernel void separateLumaKernel( texture2d inTexture [[textu // 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. outTexture.write(float4( - luminance / (1.0f - 0.15f), - (centreSample.gb - float2(0.5f)) * (centreSample.r - luminance) * 28.0f + float2(0.5f), + luminance.r / (1.0f - 0.15f), + (centreSample.gb - float2(0.5f)) * (centreSample.r - luminance.g) * 28.0f + float2(0.5f), 1.0f ), gid + uint2(7, offset));