From 525233e10b6a5aee37537e8c5aa4082167c29eb4 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 10 Aug 2020 19:47:47 -0400 Subject: [PATCH] Ensures all input data types are parseable in Metal. Though now I need to think a bit more about the best way to compose signal-type conversions, and whether output-type calculations (i.e. gamma, brightness) are applied. --- .../xcschemes/Clock Signal.xcscheme | 2 +- .../Clock Signal/ScanTarget/CSScanTarget.mm | 49 +++++++-- .../Clock Signal/ScanTarget/ScanTarget.metal | 101 ++++++++++++++++-- Outputs/ScanTarget.hpp | 26 ++++- 4 files changed, 154 insertions(+), 24 deletions(-) diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme index 63be2c037..a99eb0a5d 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme +++ b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme @@ -67,7 +67,7 @@ input_data_type); - switch(_bytesPerInputPixel) { - default: - case 1: pixelFormat = MTLPixelFormatR8Unorm; break; - case 2: pixelFormat = MTLPixelFormatRG8Unorm; break; - case 4: pixelFormat = MTLPixelFormatRGBA8Unorm; break; + if(data_type_is_normalised(newModals->input_data_type)) { + switch(_bytesPerInputPixel) { + default: + case 1: pixelFormat = MTLPixelFormatR8Unorm; break; + case 2: pixelFormat = MTLPixelFormatRG8Unorm; break; + case 4: pixelFormat = MTLPixelFormatRGBA8Unorm; break; + } + } else { + switch(_bytesPerInputPixel) { + default: + case 1: pixelFormat = MTLPixelFormatR8Uint; break; + case 2: pixelFormat = MTLPixelFormatRG8Uint; break; + case 4: pixelFormat = MTLPixelFormatRGBA8Uint; break; + } } MTLTextureDescriptor *const textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:pixelFormat @@ -148,13 +157,33 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; pipelineDescriptor.colorAttachments[0].pixelFormat = view.colorPixelFormat; // TODO: logic somewhat more complicated than this, probably - pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"scanVertexMain"]; + pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"scanToDisplay"]; switch(newModals->input_data_type) { - default: - pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"scanFragmentMainRGB"]; - break; case Outputs::Display::InputDataType::Luminance1: - pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"scanFragmentMainL1"]; + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"sampleLuminance1"]; + break; + case Outputs::Display::InputDataType::Luminance8: + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"sampleLuminance8"]; + break; + case Outputs::Display::InputDataType::PhaseLinkedLuminance8: + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"samplePhaseLinkedLuminance8"]; + break; + + case Outputs::Display::InputDataType::Luminance8Phase8: + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"sampleLuminance8Phase8"]; + break; + + case Outputs::Display::InputDataType::Red1Green1Blue1: + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"sampleRed1Green1Blue1"]; + break; + case Outputs::Display::InputDataType::Red2Green2Blue2: + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"sampleRed2Green2Blue2"]; + break; + case Outputs::Display::InputDataType::Red4Green4Blue4: + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"sampleRed4Green4Blue4"]; + break; + case Outputs::Display::InputDataType::Red8Green8Blue8: + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"sampleRed8Green8Blue8"]; break; } diff --git a/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal b/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal index f2e81d258..0ab29335d 100644 --- a/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal +++ b/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal @@ -21,6 +21,8 @@ struct Uniforms { float aspectRatioMultiplier; }; +// MARK: - Structs used for receiving data from the emulation. + // This is intended to match the net effect of `Scan` as defined by the BufferingScanTarget. struct Scan { struct EndPoint { @@ -47,20 +49,25 @@ struct Line { uint8_t compositeAmplitude; }; +// MARK: - Intermediate structs. // This is an intermediate struct, which is TEMPORARY. -struct ColouredVertex { +struct SourceInterpolator { float4 position [[position]]; float2 textureCoordinates; + float colourPhase; + float colourAmplitude; }; // MARK: - Scan shaders; these do final output to the display. -vertex ColouredVertex scanVertexMain( constant Uniforms &uniforms [[buffer(1)]], - constant Scan *scans [[buffer(0)]], - uint instanceID [[instance_id]], - uint vertexID [[vertex_id]]) { +vertex SourceInterpolator scanToDisplay( constant Uniforms &uniforms [[buffer(1)]], + constant Scan *scans [[buffer(0)]], + uint instanceID [[instance_id]], + uint vertexID [[vertex_id]]) { + SourceInterpolator output; + // Get start and end vertices in regular float2 form. const float2 start = float2( float(scans[instanceID].endPoints[0].position[0]) / float(uniforms.scale.x), @@ -75,8 +82,15 @@ vertex ColouredVertex scanVertexMain( constant Uniforms &uniforms [[buffer(1)]], const float2 tangent = (end - start); const float2 normal = float2(-tangent.y, tangent.x) / length(tangent); + // Load up the colour details. + output.colourAmplitude = float(scans[instanceID].compositeAmplitude) / 255.0f; + output.colourPhase = mix( + float(scans[instanceID].endPoints[0].compositeAngle), + float(scans[instanceID].endPoints[1].compositeAngle), + float((vertexID&2) >> 1) + ) / 64.0; + // Hence determine this quad's real shape, using vertexID to pick a corner. - ColouredVertex output; output.position = float4( ((start + (float(vertexID&2) * 0.5) * tangent + (float(vertexID&1) - 0.5) * normal * uniforms.lineWidth) * float2(2.0, -2.0) + float2(-1.0, 1.0)) * float2(uniforms.aspectRatioMultiplier, 1.0), 0.0, @@ -96,12 +110,77 @@ constexpr sampler standardSampler( coord::pixel, } -// MARK: - Input formst to RGB conversions. +// MARK: - Various input format conversion samplers. -fragment half4 scanFragmentMainRGB (ColouredVertex vert [[stage_in]], texture2d texture [[texture(0)]]) { - return half4(texture.sample(standardSampler, vert.textureCoordinates)); +/* + Luminance1, // 1 byte/pixel; any bit set => white; no bits set => black. + Luminance8, // 1 byte/pixel; linear scale. + + PhaseLinkedLuminance8, // 4 bytes/pixel; each byte is an individual 8-bit luminance + // value and which value is output is a function of + // colour subcarrier phase — byte 0 defines the first quarter + // of each colour cycle, byte 1 the next quarter, etc. This + // format is intended to permit replay of sampled original data. + + // The luminance plus phase types describe a luminance and the phase offset + // of a colour subcarrier. So they can be used to generate a luminance signal, + // or an s-video pipeline. + + Luminance8Phase8, // 2 bytes/pixel; first is luminance, second is phase. + // Phase is encoded on a 192-unit circle; anything + // greater than 192 implies that the colour part of + // the signal should be omitted. + + // The RGB types can directly feed an RGB pipeline, naturally, or can be mapped + // to phase+luminance, or just to luminance. + + Red1Green1Blue1, // 1 byte/pixel; bit 0 is blue on or off, bit 1 is green, bit 2 is red. + Red2Green2Blue2, // 1 byte/pixel; bits 0 and 1 are blue, bits 2 and 3 are green, bits 4 and 5 are blue. + Red4Green4Blue4, // 2 bytes/pixel; first nibble is red, second is green, third is blue. + Red8Green8Blue8, // 4 bytes/pixel; first is red, second is green, third is blue, fourth is vacant. + + */ + + +// 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); } -fragment half4 scanFragmentMainL1(ColouredVertex vert [[stage_in]], texture2d texture [[texture(0)]]) { - return half4(half3(texture.sample(standardSampler, vert.textureCoordinates).r * 255.0), 1.0); +fragment float4 sampleLuminance8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { + return float4(float3(texture.sample(standardSampler, vert.textureCoordinates).r), 1.0); +} + +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); +} + +// The luminance/phase format can produce either composite or S-Video. + +fragment float4 sampleLuminance8Phase8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { + return float4(texture.sample(standardSampler, vert.textureCoordinates).rg, 0.0, 1.0); +} + +// All the RGB formats can produce RGB, composite or S-Video. + +fragment float4 sampleRed8Green8Blue8(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { + return float4(texture.sample(standardSampler, vert.textureCoordinates)); +} + +fragment float4 sampleRed1Green1Blue1(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { + const auto sample = texture.sample(standardSampler, vert.textureCoordinates).r; + return float4(sample&4, sample&2, sample&1, 1.0); +} + +fragment float4 sampleRed2Green2Blue2(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { + const auto sample = texture.sample(standardSampler, vert.textureCoordinates).r; + return float4((sample >> 4)&3, (sample >> 2)&3, sample&3, 3.0) / 3.0; +} + +fragment float4 sampleRed4Green4Blue4(SourceInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { + const auto sample = texture.sample(standardSampler, vert.textureCoordinates).rg; + return float4(sample.r&15, (sample.g >> 4)&15, sample.g&15, 15.0) / 15.0; } diff --git a/Outputs/ScanTarget.hpp b/Outputs/ScanTarget.hpp index 3284f5e03..e74c85289 100644 --- a/Outputs/ScanTarget.hpp +++ b/Outputs/ScanTarget.hpp @@ -89,7 +89,8 @@ enum class InputDataType { Red8Green8Blue8, // 4 bytes/pixel; first is red, second is green, third is blue, fourth is vacant. }; -inline size_t size_for_data_type(InputDataType data_type) { +/// @returns the number of bytes per sample for data of type @c data_type. +constexpr inline size_t size_for_data_type(InputDataType data_type) { switch(data_type) { case InputDataType::Luminance1: case InputDataType::Luminance8: @@ -110,7 +111,28 @@ inline size_t size_for_data_type(InputDataType data_type) { } } -inline DisplayType natural_display_type_for_data_type(InputDataType data_type) { +/// @returns @c true if this data type presents normalised data, i.e. each byte holds a +/// value in the range [0, 255] representing a real number in the range [0.0, 1.0]; @c false otherwise. +constexpr inline size_t data_type_is_normalised(InputDataType data_type) { + switch(data_type) { + case InputDataType::Luminance8: + case InputDataType::Luminance8Phase8: + case InputDataType::Red8Green8Blue8: + case InputDataType::PhaseLinkedLuminance8: + return true; + + default: + case InputDataType::Luminance1: + case InputDataType::Red1Green1Blue1: + case InputDataType::Red2Green2Blue2: + case InputDataType::Red4Green4Blue4: + return false; + } +} + +/// @returns The 'natural' display type for data of type @c data_type. The natural display is whichever would +/// display it with the least number of conversions. Caveat: a colour display is assumed for pure-composite data types. +constexpr inline DisplayType natural_display_type_for_data_type(InputDataType data_type) { switch(data_type) { default: case InputDataType::Luminance1: