From 3597f687de7a0d0e474af0077b7090ded897d23f Mon Sep 17 00:00:00 2001
From: Thomas Harte <thomas.harte@gmail.com>
Date: Wed, 19 Aug 2020 21:20:06 -0400
Subject: [PATCH] Continues sidling towards composite & S-Video handling.

---
 Machines/Apple/Macintosh/Video.cpp            |   2 +-
 .../Clock Signal/ScanTarget/CSScanTarget.mm   | 186 +++++++++++++-----
 .../Clock Signal/ScanTarget/ScanTarget.metal  |  15 +-
 3 files changed, 145 insertions(+), 58 deletions(-)

diff --git a/Machines/Apple/Macintosh/Video.cpp b/Machines/Apple/Macintosh/Video.cpp
index 211401e36..8d1bfcdf1 100644
--- a/Machines/Apple/Macintosh/Video.cpp
+++ b/Machines/Apple/Macintosh/Video.cpp
@@ -29,7 +29,7 @@ Video::Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulato
  	crt_(704, 1, 370, 6, Outputs::Display::InputDataType::Luminance1) {
 
  	crt_.set_display_type(Outputs::Display::DisplayType::RGB);
-	crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f));
+//	crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f));
 	crt_.set_aspect_ratio(1.73f);	// The Mac uses a non-standard scanning area.
 }
 
diff --git a/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm b/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm
index 6a049b065..893949848 100644
--- a/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm	
+++ b/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm	
@@ -45,7 +45,8 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
 	id<MTLFunction> _vertexShader;
 	id<MTLFunction> _fragmentShader;
 
-	id<MTLRenderPipelineState> _scanPipeline;
+	id<MTLRenderPipelineState> _composePipeline;
+	id<MTLRenderPipelineState> _outputPipeline;
 	id<MTLRenderPipelineState> _copyPipeline;
 	id<MTLRenderPipelineState> _clearPipeline;
 
@@ -64,16 +65,22 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
 	id<MTLTexture> _frameBuffer;
 	MTLRenderPassDescriptor *_frameBufferRenderPass;
 
+	id<MTLTexture> _compositionTexture;
+
+	// The stencil buffer and the two ways it's used.
 	id<MTLTexture> _frameBufferStencil;
 	id<MTLDepthStencilState> _drawStencilState;		// Always draws, sets stencil to 1.
 	id<MTLDepthStencilState> _clearStencilState;	// Draws only where stencil is 0, clears all to 0.
 
+	// The composition pipeline, and whether it's in use.
+	BOOL _isUsingCompositionPipeline;
+
 	// The scan target in C++-world terms and the non-GPU storage for it.
 	BufferingScanTarget _scanTarget;
 	BufferingScanTarget::LineMetadata _lineMetadataBuffer[NumBufferedLines];
-	std::atomic_bool _isDrawing;
+	std::atomic_flag _isDrawing;
 
-	// The output view's aspect ratio.
+	// The output view.
 	__weak MTKView *_view;
 }
 
@@ -126,6 +133,19 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
 		depthStencilDescriptor.frontFaceStencil.depthStencilPassOperation = MTLStencilOperationReplace;
 		depthStencilDescriptor.frontFaceStencil.stencilFailureOperation = MTLStencilOperationReplace;
 		_clearStencilState = [view.device newDepthStencilStateWithDescriptor:depthStencilDescriptor];
+
+		// Create a composition texture up front.
+		MTLTextureDescriptor *const textureDescriptor = [MTLTextureDescriptor
+			texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
+			width:2048		// This 'should do'.
+			height:NumBufferedLines
+			mipmapped:NO];
+		textureDescriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;
+		textureDescriptor.resourceOptions = MTLResourceStorageModePrivate;
+		_compositionTexture = [view.device newTextureWithDescriptor:textureDescriptor];
+
+		// Ensure the is-drawing flag is initially clear.
+		_isDrawing.clear();
 	}
 
 	return self;
@@ -274,37 +294,75 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
 	//
 	id<MTLLibrary> library = [_view.device newDefaultLibrary];
 	MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
+
+	// Occasions when the composition buffer isn't required are slender:
+	//
+	//	(i) input and output are both RGB; or
+	//	(i) output is composite monochrome.
+	_isUsingCompositionPipeline =
+		(
+			(natural_display_type_for_data_type(modals.input_data_type) != Outputs::Display::DisplayType::RGB) ||
+			(modals.display_type != Outputs::Display::DisplayType::RGB)
+		) && modals.display_type != Outputs::Display::DisplayType::CompositeMonochrome;
+
+	struct FragmentSamplerDictionary {
+		/// Fragment shader that outputs to the composition buffer for composite processing.
+		NSString *const compositionComposite;
+		/// Fragment shader that outputs to the composition buffer for S-Video processing.
+		NSString *const compositionSVideo;
+
+		/// Fragment shader that outputs directly as monochrome composite.
+		NSString *const directComposite;
+		/// Fragment shader that outputs directly as RGB.
+		NSString *const directRGB;
+	};
+
+	// TODO: create fragment shaders to apply composite multiplication.
+	// TODO: incorporate gamma correction into all direct outputters.
+	const FragmentSamplerDictionary samplerDictionary[8] = {
+		// Luminance1
+		{@"sampleLuminance1", nullptr, @"sampleLuminance1", nullptr},
+		{@"sampleLuminance8", nullptr, @"sampleLuminance8", nullptr},
+		{@"samplePhaseLinkedLuminance8", nullptr, @"samplePhaseLinkedLuminance8", nullptr},
+		{@"compositeSampleLuminance8Phase8", @"sampleLuminance8Phase8", @"compositeSampleLuminance8Phase8", nullptr},
+		{@"compositeSampleRed1Green1Blue1", @"svideoSampleRed1Green1Blue1", @"compositeSampleRed1Green1Blue1", @"sampleRed1Green1Blue1"},
+		{@"compositeSampleRed2Green2Blue2", @"svideoSampleRed2Green2Blue2", @"compositeSampleRed2Green2Blue2", @"sampleRed2Green2Blue2"},
+		{@"compositeSampleRed4Green4Blue4", @"svideoSampleRed4Green4Blue4", @"compositeSampleRed4Green4Blue4", @"sampleRed4Green4Blue4"},
+		{@"compositeSampleRed8Green8Blue8", @"svideoSampleRed8Green8Blue8", @"compositeSampleRed8Green8Blue8", @"sampleRed8Green8Blue8"},
+	};
+
+#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]);
+	}
+#endif
+
+	// Build the composition pipeline if one is in use.
+	if(_isUsingCompositionPipeline) {
+		const bool isSVideoOutput = modals.display_type == Outputs::Display::DisplayType::SVideo;
+
+		pipelineDescriptor.colorAttachments[0].pixelFormat = _compositionTexture.pixelFormat;
+		pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"scanToComposition"];
+		pipelineDescriptor.fragmentFunction =
+			[library newFunctionWithName:isSVideoOutput ? samplerDictionary[int(modals.input_data_type)].compositionSVideo : samplerDictionary[int(modals.input_data_type)].compositionComposite];
+
+		_composePipeline = [_view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil];
+	}
+
+	// Build the output pipeline.
 	pipelineDescriptor.colorAttachments[0].pixelFormat = _view.colorPixelFormat;
+	pipelineDescriptor.vertexFunction = [library newFunctionWithName:_isUsingCompositionPipeline ? @"lineToDisplay" : @"scanToDisplay"];
 
-	// TODO: logic somewhat more complicated than this, probably
-	pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"scanToDisplay"];
-	switch(modals.input_data_type) {
-		case Outputs::Display::InputDataType::Luminance1:
-			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;
+	if(_isUsingCompositionPipeline) {
+		pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"sampleRed8Green8Blue8"];
+	} 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];
 	}
 
 	// Enable blending.
@@ -316,18 +374,23 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
 	pipelineDescriptor.stencilAttachmentPixelFormat = MTLPixelFormatStencil8;
 
 	// Finish.
-	_scanPipeline = [_view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil];
+	_outputPipeline = [_view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil];
 }
 
-- (void)outputScansFrom:(size_t)start to:(size_t)end commandBuffer:(id<MTLCommandBuffer>)commandBuffer {
+- (void)outputFrom:(size_t)start to:(size_t)end commandBuffer:(id<MTLCommandBuffer>)commandBuffer {
 	// Generate a command encoder for the view.
 	id<MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor:_frameBufferRenderPass];
 
-	// Drawing. Just scans.
-	[encoder setRenderPipelineState:_scanPipeline];
+	// Final output. Could be scans or lines.
+	[encoder setRenderPipelineState:_outputPipeline];
 
-	[encoder setFragmentTexture:_writeAreaTexture atIndex:0];
-	[encoder setVertexBuffer:_scansBuffer offset:0 atIndex:0];
+	if(_isUsingCompositionPipeline) {
+		[encoder setFragmentTexture:_compositionTexture atIndex:0];
+		[encoder setVertexBuffer:_linesBuffer offset:0 atIndex:0];
+	} else {
+		[encoder setFragmentTexture:_writeAreaTexture atIndex:0];
+		[encoder setVertexBuffer:_scansBuffer offset:0 atIndex:0];
+	}
 	[encoder setVertexBuffer:_uniformsBuffer offset:0 atIndex:1];
 	[encoder setFragmentBuffer:_uniformsBuffer offset:0 atIndex:0];
 
@@ -343,7 +406,7 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
 		if(start < end) {
 			[encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4 instanceCount:end - start baseInstance:start];
 		} else {
-			[encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4 instanceCount:NumBufferedScans - start baseInstance:start];
+			[encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4 instanceCount:(_isUsingCompositionPipeline ? NumBufferedLines : NumBufferedScans) - start baseInstance:start];
 			if(end) {
 				[encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4 instanceCount:end];
 			}
@@ -416,21 +479,35 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
 		// Hence every pixel is touched every frame, regardless of the machine's output.
 		//
 
-		// TODO: proceed as per the below inly if doing a scan-centric output.
-		// Draw scans to a composition buffer and from there to the display as lines otherwise.
+		if(_isUsingCompositionPipeline) {
+			// TODO: Draw scans to a composition buffer and from there to the display as lines otherwise.
 
-		// Break up scans by frame.
-		size_t line = outputArea.start.line;
-		size_t scan = outputArea.start.scan;
-		while(line != outputArea.end.line) {
-			if(_lineMetadataBuffer[line].is_first_in_frame && _lineMetadataBuffer[line].previous_frame_was_complete) {
-				[self outputScansFrom:scan to:_lineMetadataBuffer[line].first_scan commandBuffer:commandBuffer];
-				[self outputFrameCleanerToCommandBuffer:commandBuffer];
-				scan = _lineMetadataBuffer[line].first_scan;
+			// Output lines, broken up by frame.
+			size_t startLine = outputArea.start.line;
+			size_t line = outputArea.start.line;
+			while(line != outputArea.end.line) {
+				if(_lineMetadataBuffer[line].is_first_in_frame && _lineMetadataBuffer[line].previous_frame_was_complete) {
+					[self outputFrom:startLine to:line commandBuffer:commandBuffer];
+					[self outputFrameCleanerToCommandBuffer:commandBuffer];
+					startLine = line;
+				}
+				line = (line + 1) % NumBufferedLines;
 			}
-			line = (line + 1) % NumBufferedLines;
+			[self outputFrom:startLine to:outputArea.end.line commandBuffer:commandBuffer];
+		} else {
+			// Output scans directly, broken up by frame.
+			size_t line = outputArea.start.line;
+			size_t scan = outputArea.start.scan;
+			while(line != outputArea.end.line) {
+				if(_lineMetadataBuffer[line].is_first_in_frame && _lineMetadataBuffer[line].previous_frame_was_complete) {
+					[self outputFrom:scan to:_lineMetadataBuffer[line].first_scan commandBuffer:commandBuffer];
+					[self outputFrameCleanerToCommandBuffer:commandBuffer];
+					scan = _lineMetadataBuffer[line].first_scan;
+				}
+				line = (line + 1) % NumBufferedLines;
+			}
+			[self outputFrom:scan to:outputArea.end.scan commandBuffer:commandBuffer];
 		}
-		[self outputScansFrom:scan to:outputArea.end.scan commandBuffer:commandBuffer];
 
 		// Add a callback to update the scan target buffer and commit the drawing.
 		[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull) {
@@ -446,6 +523,10 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
  @discussion Called on the delegate when it is asked to render into the view
  */
 - (void)drawInMTKView:(nonnull MTKView *)view {
+	if(_isDrawing.test_and_set()) {
+		return;
+	}
+
 	// Schedule a copy from the current framebuffer to the view; blitting is unavailable as the target is a framebuffer texture.
 	id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
 
@@ -461,6 +542,9 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
 	[encoder endEncoding];
 
 	[commandBuffer presentDrawable:view.currentDrawable];
+	[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull) {
+		self->_isDrawing.clear();
+	}];
 	[commandBuffer commit];
 }
 
diff --git a/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal b/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal
index 4d69f3bc7..0dee02f91 100644
--- a/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal	
+++ b/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal	
@@ -161,20 +161,23 @@ vertex SourceInterpolator lineToDisplay(	constant Uniforms &uniforms [[buffer(1)
 
 // This assumes that it needs to generate endpoints for a line segment.
 
-vertex SourceInterpolator scanToCompound(	constant Uniforms &uniforms [[buffer(1)]],
-											constant Scan *scans [[buffer(0)]],
-											uint instanceID [[instance_id]],
-											uint vertexID [[vertex_id]],
-											texture2d<float> texture [[texture(0)]]) {
+vertex SourceInterpolator scanToComposition(	constant Uniforms &uniforms [[buffer(1)]],
+												constant Scan *scans [[buffer(0)]],
+												uint instanceID [[instance_id]],
+												uint vertexID [[vertex_id]],
+												texture2d<float> texture [[texture(0)]]) {
 	SourceInterpolator result;
 
 	// Populate result as if direct texture access were available.
 	result.position.x = mix(scans[instanceID].endPoints[0].cyclesSinceRetrace, scans[instanceID].endPoints[1].cyclesSinceRetrace, float(vertexID));
 	result.position.y = scans[instanceID].line;
+	result.position.wz = float2(0.0, 1.0);
 	result.textureCoordinates.x = mix(scans[instanceID].endPoints[0].dataOffset, scans[instanceID].endPoints[1].dataOffset, float(vertexID));
 	result.textureCoordinates.y = scans[instanceID].dataY;
 
-	// TODO: map position into eye space, allowing for target texture dimensions.
+	// Map position into eye space, allowing for target texture dimensions.
+	// TODO: is this really necessary? Is there nothing like coord::pixel that applies here?
+	result.position.xy = ((result.position.xy + float2(0.5)) / float2(texture.get_width(), texture.get_height())) * float2(2.0) - float2(1.0);
 
 	return result;
 }