From edf8cf4dc6ce0fb4e0f98d6a0c439b442f227ef3 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 9 Sep 2020 19:28:38 -0400 Subject: [PATCH] Completes the set of with/without gamma, and ensures correct alpha selection. Also culls some other repetitive TODOs. --- .../Clock Signal/ScanTarget/CSScanTarget.mm | 58 +++++---- .../Clock Signal/ScanTarget/ScanTarget.metal | 120 +++++++++++------- 2 files changed, 108 insertions(+), 70 deletions(-) diff --git a/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm b/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm index 54521c592..1f64ae8e8 100644 --- a/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm +++ b/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm @@ -25,8 +25,6 @@ Source data is converted to 32bpp RGB or to composite directly from its input, at output resolution. Gamma correction is applied unless the inputs are 1bpp (e.g. Macintosh-style black/white, TTL-style RGB). - TODO: filtering when the output size is 'small'. - S-Video ------- @@ -364,8 +362,6 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; } - (void)updateSizeBuffersToSize:(CGSize)size { - // TODO: consider multisampling here? But it seems like you'd need another level of indirection - // in order to maintain an ongoing buffer that supersamples only at the end. const NSUInteger frameBufferWidth = NSUInteger(size.width * _view.layer.contentsScale); const NSUInteger frameBufferHeight = NSUInteger(size.height * _view.layer.contentsScale); @@ -619,7 +615,6 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; _pipeline = isSVideoOutput ? Pipeline::SVideo : Pipeline::CompositeColour; } - // TODO: factor in gamma, which may or may not be a factor (it isn't for 1-bit formats). struct FragmentSamplerDictionary { /// Fragment shader that outputs to the composition buffer for composite processing. NSString *const compositionComposite; @@ -636,29 +631,38 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; NSString *const directRGBWithGamma; }; const FragmentSamplerDictionary samplerDictionary[8] = { - {@"compositeSampleLuminance1", nullptr, @"sampleLuminance1", @"sampleLuminance1", nullptr, nullptr}, - {@"compositeSampleLuminance8", nullptr, @"sampleLuminance8", nullptr}, - {@"compositeSamplePhaseLinkedLuminance8", nullptr, @"samplePhaseLinkedLuminance8", nullptr}, - {@"compositeSampleLuminance8Phase8", @"sampleLuminance8Phase8", @"compositeSampleLuminance8Phase8", nullptr, nullptr, nullptr}, - {@"compositeSampleRed1Green1Blue1", @"svideoSampleRed1Green1Blue1", @"compositeSampleRed1Green1Blue1", nullptr, @"sampleRed1Green1Blue1", nullptr}, - {@"compositeSampleRed2Green2Blue2", @"svideoSampleRed2Green2Blue2", @"compositeSampleRed2Green2Blue2", nullptr, @"sampleRed2Green2Blue2", nullptr}, - {@"compositeSampleRed4Green4Blue4", @"svideoSampleRed4Green4Blue4", @"compositeSampleRed4Green4Blue4", nullptr, @"sampleRed4Green4Blue4", nullptr}, - {@"compositeSampleRed8Green8Blue8", @"svideoSampleRed8Green8Blue8", @"compositeSampleRed8Green8Blue8", nullptr, @"sampleRed8Green8Blue8", nullptr}, + // Composite formats. + {@"compositeSampleLuminance1", nil, @"sampleLuminance1", @"sampleLuminance1"}, + {@"compositeSampleLuminance8", nil, @"sampleLuminance8", @"sampleLuminance8WithGamma"}, + {@"compositeSamplePhaseLinkedLuminance8", nil, @"samplePhaseLinkedLuminance8", @"samplePhaseLinkedLuminance8WithGamma"}, + + // S-Video formats. + {@"compositeSampleLuminance8Phase8", @"sampleLuminance8Phase8", @"directCompositeSampleLuminance8Phase8", @"directCompositeSampleLuminance8Phase8WithGamma"}, + + // RGB formats. + {@"compositeSampleRed1Green1Blue1", @"svideoSampleRed1Green1Blue1", @"directCompositeSampleRed1Green1Blue1", @"directCompositeSampleRed1Green1Blue1WithGamma", @"sampleRed1Green1Blue1", @"sampleRed1Green1Blue1"}, + {@"compositeSampleRed2Green2Blue2", @"svideoSampleRed2Green2Blue2", @"directCompositeSampleRed2Green2Blue2", @"directCompositeSampleRed2Green2Blue2WithGamma", @"sampleRed2Green2Blue2", @"sampleRed2Green2Blue2WithGamma"}, + {@"compositeSampleRed4Green4Blue4", @"svideoSampleRed4Green4Blue4", @"directCompositeSampleRed4Green4Blue4", @"directCompositeSampleRed4Green4Blue4WithGamma", @"sampleRed4Green4Blue4", @"sampleRed4Green4Blue4WithGamma"}, + {@"compositeSampleRed8Green8Blue8", @"svideoSampleRed8Green8Blue8", @"directCompositeSampleRed8Green8Blue8", @"directCompositeSampleRed8Green8Blue8WithGamma", @"sampleRed8Green8Blue8", @"sampleRed8Green8Blue8WithGamma"}, }; #ifndef NDEBUG - // Do a quick check of the names entered above. I don't think this is possible at compile time. -// for(int c = 0; c < 8; ++c) { -// if(samplerDictionary[c].compositionComposite) assert([library newFunctionWithName:samplerDictionary[c].compositionComposite]); -// if(samplerDictionary[c].compositionSVideo) assert([library newFunctionWithName:samplerDictionary[c].compositionSVideo]); -// if(samplerDictionary[c].directComposite) assert([library newFunctionWithName:samplerDictionary[c].directComposite]); -// if(samplerDictionary[c].directRGB) assert([library newFunctionWithName:samplerDictionary[c].directRGB]); -// } + // Do a quick check that all the shaders named above are defined in the Metal code. I don't think this is possible at compile time. + for(int c = 0; c < 8; ++c) { +#define Test(x) if(samplerDictionary[c].x) assert([library newFunctionWithName:samplerDictionary[c].x]); + Test(compositionComposite); + Test(compositionSVideo); + Test(directComposite); + Test(directCompositeWithGamma); + Test(directRGB); + Test(directRGBWithGamma); +#undef Test + } #endif uniforms()->cyclesMultiplier = 1.0f; if(_pipeline != Pipeline::DirectToDisplay) { - // Pick a suitable cycle multiplier. TODO: can I reduce this from a multiple of 4? + // Pick a suitable cycle multiplier. const float minimumSize = 4.0f * float(modals.colour_cycle_numerator) / float(modals.colour_cycle_denominator); while(uniforms()->cyclesMultiplier * modals.cycles_per_line < minimumSize) { uniforms()->cyclesMultiplier += 1.0f; @@ -678,7 +682,6 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; // Generate the chrominance filter. { -// auto *const firCoefficients = uniforms()->chromaKernel; simd::float3 firCoefficients[8]; const auto chromaCoefficients = boxCoefficients(radiansPerPixel, 3.141592654f); _chromaKernelSize = 15; @@ -757,8 +760,14 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"interpolateFragment"]; } else { const bool isRGBOutput = modals.display_type == Outputs::Display::DisplayType::RGB; - pipelineDescriptor.fragmentFunction = - [library newFunctionWithName:isRGBOutput ? samplerDictionary[int(modals.input_data_type)].directRGB : samplerDictionary[int(modals.input_data_type)].directComposite]; + + NSString *shaderName; + if(isRGBOutput) { + shaderName = [self shouldApplyGamma] ? samplerDictionary[int(modals.input_data_type)].directRGBWithGamma : samplerDictionary[int(modals.input_data_type)].directRGB; + } else { + shaderName = [self shouldApplyGamma] ? samplerDictionary[int(modals.input_data_type)].directCompositeWithGamma : samplerDictionary[int(modals.input_data_type)].directComposite; + } + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:shaderName]; } // Enable blending. @@ -818,6 +827,7 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; [encoder setVertexTexture:_frameBuffer atIndex:0]; [encoder setFragmentTexture:_frameBuffer atIndex:0]; + [encoder setFragmentBuffer:_uniformsBuffer offset:0 atIndex:0]; [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; [encoder endEncoding]; diff --git a/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal b/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal index 900880c1e..d2c0cbbe9 100644 --- a/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal +++ b/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal @@ -10,9 +10,6 @@ using namespace metal; -// TODO: I'm being very loose, so far, in use of alpha. Sometimes it's 0.64, somtimes its 1.0. -// Apply some rigour, for crying out loud. - struct Uniforms { // This is used to scale scan positions, i.e. it provides the range // for mapping from scan-style integer positions into eye space. @@ -262,35 +259,42 @@ half4 composite(half level, half2 quadrature, half amplitude) { // composite format used for composition. Direct sampling is always for final output, so the two // 8-bit formats also provide a gamma option. -fragment half4 sampleLuminance1(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { - const half luminance = clamp(half(texture.sample(standardSampler, vert.textureCoordinates).r), half(0.0f), half(1.0f)) * uniforms.outputMultiplier; - return half4(half3(luminance), uniforms.outputAlpha); +half convertLuminance1(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { + return clamp(half(texture.sample(standardSampler, vert.textureCoordinates).r), half(0.0f), half(1.0f)); } -fragment half4 compositeSampleLuminance1(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { - return composite(texture.sample(standardSampler, vert.textureCoordinates).r, quadrature(vert.colourPhase), vert.colourAmplitude); +half convertLuminance8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { + return texture.sample(standardSampler, vert.textureCoordinates).r; } -fragment half4 sampleLuminance8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { - return half4(texture.sample(standardSampler, vert.textureCoordinates).rrr * uniforms.outputMultiplier, uniforms.outputAlpha); -} - -fragment half4 compositeSampleLuminance8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { - return composite(texture.sample(standardSampler, vert.textureCoordinates).r, quadrature(vert.colourPhase), vert.colourAmplitude); -} - -fragment half4 samplePhaseLinkedLuminance8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { +half convertPhaseLinkedLuminance8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { const int offset = int(vert.unitColourPhase * 4.0f) & 3; auto sample = texture.sample(standardSampler, vert.textureCoordinates); - return half4(half3(sample[offset]), uniforms.outputAlpha); + return sample[offset]; } -fragment half4 compositeSamplePhaseLinkedLuminance8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { - const int offset = int(vert.unitColourPhase * 4.0f) & 3; - const float snappedColourPhase = float(offset) * (0.5f * 3.141592654f); // TODO: plus machine-supplied offset. - auto sample = texture.sample(standardSampler, vert.textureCoordinates); - return composite(sample[offset], quadrature(snappedColourPhase), vert.colourAmplitude); -} + +#define CompositeSet(name, type) \ + fragment half4 sample##name(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \ + const half luminance = convert##name(vert, texture) * uniforms.outputMultiplier; \ + return half4(half3(luminance), uniforms.outputAlpha); \ + } \ + \ + fragment half4 sample##name##WithGamma(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \ + const half luminance = pow(convert##name(vert, texture) * uniforms.outputMultiplier, uniforms.outputGamma); \ + return half4(half3(luminance), uniforms.outputAlpha); \ + } \ + \ + fragment half4 compositeSample##name(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \ + const half luminance = convert##name(vert, texture) * uniforms.outputMultiplier; \ + return composite(luminance, quadrature(vert.colourPhase), vert.colourAmplitude); \ + } + +CompositeSet(Luminance1, ushort); +CompositeSet(Luminance8, half); +CompositeSet(PhaseLinkedLuminance8, half); + +#undef CompositeSet // The luminance/phase format can produce either composite or S-Video. @@ -302,54 +306,65 @@ half2 convertLuminance8Phase8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { + const half2 luminanceChroma = convertLuminance8Phase8(vert, texture); + const half luminance = mix(luminanceChroma.r, luminanceChroma.g, vert.colourAmplitude); + return composite(luminance, quadrature(vert.colourPhase), vert.colourAmplitude); +} + fragment half4 sampleLuminance8Phase8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { const half2 luminanceChroma = convertLuminance8Phase8(vert, texture); - const half2 qam = quadrature(vert.colourPhase) * 0.5f; + const half2 qam = quadrature(vert.colourPhase) * half(0.5f); return half4(luminanceChroma.r, half2(0.5f) + luminanceChroma.g*qam, half(1.0f)); } -fragment half4 compositeSampleLuminance8Phase8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { +fragment half4 directCompositeSampleLuminance8Phase8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { const half2 luminanceChroma = convertLuminance8Phase8(vert, texture); - const half level = mix(luminanceChroma.r, luminanceChroma.g, half(vert.colourAmplitude)); - return composite(level, quadrature(vert.colourPhase), vert.colourAmplitude); + const half luminance = mix(luminanceChroma.r * uniforms.outputMultiplier, luminanceChroma.g, vert.colourAmplitude); + return half4(half3(luminance), uniforms.outputAlpha); } -// All the RGB formats can produce RGB, composite or S-Video. -// -// Note on the below: in Metal you may not call a fragment function (so e.g. svideoSampleX can't just cann sampleX). -// Also I can find no functioning way to offer a templated fragment function. So I don't currently know how -// I could avoid the macro mess below. +fragment half4 directCompositeSampleLuminance8Phase8WithGamma(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { + const half2 luminanceChroma = convertLuminance8Phase8(vert, texture); + const half luminance = mix(pow(luminanceChroma.r * uniforms.outputMultiplier, uniforms.outputGamma), luminanceChroma.g, vert.colourAmplitude); + return half4(half3(luminance), uniforms.outputAlpha); +} + + +// All the RGB formats can produce RGB, composite or S-Video. -// TODO: is the calling convention here causing `vert` and `texture` to be copied? half3 convertRed8Green8Blue8(SourceInterpolator vert, texture2d texture) { return texture.sample(standardSampler, vert.textureCoordinates).rgb; } half3 convertRed4Green4Blue4(SourceInterpolator vert, texture2d texture) { const auto sample = texture.sample(standardSampler, vert.textureCoordinates).rg; - return half3(sample.r&15, (sample.g >> 4)&15, sample.g&15); + return clamp(half3(sample.r&15, (sample.g >> 4)&15, sample.g&15), half(0.0f), half(1.0f)); } half3 convertRed2Green2Blue2(SourceInterpolator vert, texture2d texture) { const auto sample = texture.sample(standardSampler, vert.textureCoordinates).r; - return half3((sample >> 4)&3, (sample >> 2)&3, sample&3); + return clamp(half3((sample >> 4)&3, (sample >> 2)&3, sample&3), half(0.0f), half(1.0f)); } half3 convertRed1Green1Blue1(SourceInterpolator vert, texture2d texture) { const auto sample = texture.sample(standardSampler, vert.textureCoordinates).r; - return half3(sample&4, sample&2, sample&1); + return clamp(half3(sample&4, sample&2, sample&1), half(0.0f), half(1.0f)); } -// TODO: don't hard code the 0.64 in sample##name. #define DeclareShaders(name, pixelType) \ - fragment half4 sample##name(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { \ - return half4(convert##name(vert, texture), 0.64); \ + fragment half4 sample##name(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \ + return half4(convert##name(vert, texture), uniforms.outputAlpha); \ + } \ + \ + fragment half4 sample##name##WithGamma(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \ + return half4(pow(convert##name(vert, texture), uniforms.outputGamma), uniforms.outputAlpha); \ } \ \ fragment half4 svideoSample##name(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \ - const auto colour = uniforms.fromRGB * clamp(convert##name(vert, texture), half(0.0f), half(1.0f)); \ + const auto colour = uniforms.fromRGB * convert##name(vert, texture); \ const half2 qam = quadrature(vert.colourPhase); \ const half chroma = dot(colour.gb, qam); \ return half4( \ @@ -359,11 +374,24 @@ half3 convertRed1Green1Blue1(SourceInterpolator vert, texture2d texture) ); \ } \ \ + half composite##name(SourceInterpolator vert, texture2d texture, constant Uniforms &uniforms, half2 colourSubcarrier) { \ + const auto colour = uniforms.fromRGB * convert##name(vert, texture); \ + return mix(colour.r, dot(colour.gb, colourSubcarrier), half(vert.colourAmplitude)); \ + } \ + \ fragment half4 compositeSample##name(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \ - const auto colour = uniforms.fromRGB * clamp(convert##name(vert, texture), half3(0.0f), half3(1.0f)); \ const half2 colourSubcarrier = quadrature(vert.colourPhase); \ - const half level = mix(colour.r, dot(colour.gb, colourSubcarrier), half(vert.colourAmplitude)); \ - return composite(level, colourSubcarrier, vert.colourAmplitude); \ + return composite(composite##name(vert, texture, uniforms, colourSubcarrier), colourSubcarrier, vert.colourAmplitude); \ + } \ + \ + fragment half4 directCompositeSample##name(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \ + const half level = composite##name(vert, texture, uniforms, quadrature(vert.colourPhase)); \ + return half4(half3(level), uniforms.outputAlpha); \ + } \ + \ + fragment half4 directCompositeSample##name##WithGamma(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \ + const half level = pow(composite##name(vert, texture, uniforms, quadrature(vert.colourPhase)), uniforms.outputGamma); \ + return half4(half3(level), uniforms.outputAlpha); \ } DeclareShaders(Red8Green8Blue8, half) @@ -379,8 +407,8 @@ fragment half4 interpolateFragment(CopyInterpolator vert [[stage_in]], texture2d return texture.sample(linearSampler, vert.textureCoordinates); } -fragment half4 clearFragment() { - return half4(0.0, 0.0, 0.0, 0.64); +fragment half4 clearFragment(constant Uniforms &uniforms [[buffer(0)]]) { + return half4(0.0, 0.0, 0.0, uniforms.outputAlpha); } // MARK: - Compute kernels