From 745797b596b2792eb80e05a0118ac1432c2730ff Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 16 Aug 2020 16:42:32 -0400 Subject: [PATCH] Introduces a stencil buffer plus the inter-frame clearing it allows. --- .../Clock Signal/ScanTarget/CSScanTarget.mm | 165 ++++++++++++++---- .../Clock Signal/ScanTarget/ScanTarget.metal | 9 +- Outputs/ScanTargets/BufferingScanTarget.cpp | 2 +- 3 files changed, 138 insertions(+), 38 deletions(-) diff --git a/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm b/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm index b95aab15c..292be240d 100644 --- a/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm +++ b/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm @@ -45,6 +45,7 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; id _scanPipeline; id _copyPipeline; + id _clearPipeline; // Buffers. id _uniformsBuffer; @@ -61,6 +62,10 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; id _frameBuffer; MTLRenderPassDescriptor *_frameBufferRenderPass; + id _frameBufferStencil; + id _drawStencilState; // Always draws, sets stencil to 1. + id _clearStencilState; // Draws only where stencil is 0, clears all to 0. + // The scan target in C++-world terms and the non-GPU storage for it. BufferingScanTarget _scanTarget; BufferingScanTarget::LineMetadata _lineMetadataBuffer[NumBufferedLines]; @@ -100,13 +105,25 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; _view = view; [self mtkView:view drawableSizeWillChange:view.drawableSize]; - // Generate copy pipeline. + // Generate copy and clear pipelines. id library = [_view.device newDefaultLibrary]; MTLRenderPipelineDescriptor *const pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; pipelineDescriptor.colorAttachments[0].pixelFormat = _view.colorPixelFormat; pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"copyVertex"]; pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"copyFragment"]; _copyPipeline = [_view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil]; + + pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"clearFragment"]; + pipelineDescriptor.stencilAttachmentPixelFormat = MTLPixelFormatStencil8; + _clearPipeline = [_view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil]; + + // Clear stencil: always write the reference value (of 0), but draw only where the stencil already + // had that value. + MTLDepthStencilDescriptor *depthStencilDescriptor = [[MTLDepthStencilDescriptor alloc] init]; + depthStencilDescriptor.frontFaceStencil.stencilCompareFunction = MTLCompareFunctionEqual; + depthStencilDescriptor.frontFaceStencil.depthStencilPassOperation = MTLStencilOperationReplace; + depthStencilDescriptor.frontFaceStencil.stencilFailureOperation = MTLStencilOperationReplace; + _clearStencilState = [view.device newDepthStencilStateWithDescriptor:depthStencilDescriptor]; } return self; @@ -125,10 +142,8 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; // 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. - // TODO: attach a stencil buffer. - @synchronized(self) { - // Generate a framebuffer and a pipeline that targets it. + // Generate a framebuffer and a stencil. MTLTextureDescriptor *const textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:view.colorPixelFormat width:NSUInteger(size.width * view.layer.contentsScale) @@ -138,11 +153,34 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; textureDescriptor.resourceOptions = MTLResourceStorageModePrivate; _frameBuffer = [view.device newTextureWithDescriptor:textureDescriptor]; + MTLTextureDescriptor *const stencilTextureDescriptor = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatStencil8 + width:NSUInteger(size.width * view.layer.contentsScale) + height:NSUInteger(size.height * view.layer.contentsScale) + mipmapped:NO]; + stencilTextureDescriptor.usage = MTLTextureUsageRenderTarget; + stencilTextureDescriptor.resourceOptions = MTLResourceStorageModePrivate; + _frameBufferStencil = [view.device newTextureWithDescriptor:stencilTextureDescriptor]; + + // Generate a render pass with that framebuffer and stencil. _frameBufferRenderPass = [[MTLRenderPassDescriptor alloc] init]; _frameBufferRenderPass.colorAttachments[0].texture = _frameBuffer; _frameBufferRenderPass.colorAttachments[0].loadAction = MTLLoadActionLoad; _frameBufferRenderPass.colorAttachments[0].storeAction = MTLStoreActionStore; + _frameBufferRenderPass.stencilAttachment.clearStencil = 0; + _frameBufferRenderPass.stencilAttachment.texture = _frameBufferStencil; + _frameBufferRenderPass.stencilAttachment.loadAction = MTLLoadActionLoad; + _frameBufferRenderPass.stencilAttachment.storeAction = MTLStoreActionStore; + + // Establish intended stencil useage; it's only to track which pixels haven't been painted + // at all at the end of every frame. So: always paint, and replace the stored stencil value + // (which is seeded as 0) with the nominated one (a 1). + MTLDepthStencilDescriptor *depthStencilDescriptor = [[MTLDepthStencilDescriptor alloc] init]; + depthStencilDescriptor.frontFaceStencil.stencilCompareFunction = MTLCompareFunctionAlways; + depthStencilDescriptor.frontFaceStencil.depthStencilPassOperation = MTLStencilOperationReplace; + _drawStencilState = [view.device newDepthStencilStateWithDescriptor:depthStencilDescriptor]; + // TODO: old framebuffer should be resized onto the new one. } } @@ -259,10 +297,65 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha; pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + // Set stencil format. + pipelineDescriptor.stencilAttachmentPixelFormat = MTLPixelFormatStencil8; + + // Finish. _scanPipeline = [_view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil]; } -- (void)checkModals { +- (void)outputScansFrom:(size_t)start to:(size_t)end commandBuffer:(id)commandBuffer { + // Generate a command encoder for the view. + id encoder = [commandBuffer renderCommandEncoderWithDescriptor:_frameBufferRenderPass]; + + // Drawing. Just scans. + [encoder setRenderPipelineState:_scanPipeline]; + + [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]; + + [encoder setDepthStencilState:_drawStencilState]; + [encoder setStencilReferenceValue:1]; +#ifndef NDEBUG + // Quick aid for debugging: the stencil test is predicated on front-facing pixels, so make sure they're + // being generated. + [encoder setCullMode:MTLCullModeBack]; +#endif + + if(start != end) { + 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]; + if(end) { + [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4 instanceCount:end]; + } + } + } + + // Complete encoding and return. + [encoder endEncoding]; +} + +- (void)outputFrameCleanerToCommandBuffer:(id)commandBuffer { + // Generate a command encoder for the view. + id encoder = [commandBuffer renderCommandEncoderWithDescriptor:_frameBufferRenderPass]; + + // Drawing. Just scans. + [encoder setRenderPipelineState:_clearPipeline]; + [encoder setDepthStencilState:_clearStencilState]; + [encoder setStencilReferenceValue:0]; + + [encoder setVertexTexture:_frameBuffer atIndex:0]; + [encoder setFragmentTexture:_frameBuffer atIndex:0]; + + [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; + [encoder endEncoding]; +} + +- (void)updateFrameBuffer { // TODO: rethink BufferingScanTarget::perform. Is it now really just for guarding the modals? _scanTarget.perform([=] { const Outputs::Display::ScanTarget::Modals *const newModals = _scanTarget.new_modals(); @@ -270,26 +363,10 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; [self setModals:*newModals]; } }); -} - -- (void)updateFrameBuffer { - [self checkModals]; @synchronized(self) { if(!_frameBufferRenderPass) return; - // Generate a command encoder for the view. - id commandBuffer = [_commandQueue commandBuffer]; - id encoder = [commandBuffer renderCommandEncoderWithDescriptor:_frameBufferRenderPass]; - - // Drawing. Just scans. - [encoder setRenderPipelineState:_scanPipeline]; - - [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]; - const auto outputArea = _scanTarget.get_output_area(); // Ensure texture changes are noted. @@ -307,27 +384,43 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; } - // TEMPORARY: just draw the scans. - if(outputArea.start.scan != outputArea.end.scan) { - if(outputArea.start.scan < outputArea.end.scan) { - [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4 instanceCount:outputArea.end.scan - outputArea.start.scan baseInstance:outputArea.start.scan]; - } else { - [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4 instanceCount:NumBufferedScans - outputArea.start.scan baseInstance:outputArea.start.scan]; - if(outputArea.end.scan) { - [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4 instanceCount:outputArea.end.scan]; - } + // Obtain a source for render command encoders. + id commandBuffer = [_commandQueue commandBuffer]; + + // + // Drawing algorithm used below, in broad terms: + // + // Maintain a persistent buffer of current CRT state. + // + // During each frame, paint to the persistent buffer anything new. Update a stencil buffer to track + // every pixel so-far touched. + // + // At the end of the frame, draw a 'frame cleaner', which is a whole-screen rect that paints over + // only those areas that the stencil buffer indicates weren't painted this frame. + // + // 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. + + // 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; } + line = (line + 1) % NumBufferedLines; } + [self outputScansFrom:scan to:outputArea.end.scan commandBuffer:commandBuffer]; - // Complete encoding. - [encoder endEncoding]; - - // Add a callback to update the buffer. + // Add a callback to update the scan target buffer and commit the drawing. [commandBuffer addCompletedHandler:^(id _Nonnull) { self->_scanTarget.complete_output_area(outputArea); }]; - - // Commit the drawing. [commandBuffer commit]; } } diff --git a/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal b/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal index 1fb85a801..fa780d6c2 100644 --- a/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal +++ b/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal @@ -9,6 +9,9 @@ #include 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. @@ -83,7 +86,7 @@ vertex SourceInterpolator scanToDisplay( constant Uniforms &uniforms [[buffer(1) // Calculate the tangent and normal. const float2 tangent = (end - start); - const float2 normal = float2(-tangent.y, tangent.x) / length(tangent); + const float2 normal = float2(tangent.y, -tangent.x) / length(tangent); // Load up the colour details. output.colourAmplitude = float(scans[instanceID].compositeAmplitude) / 255.0f; @@ -231,3 +234,7 @@ vertex CopyInterpolator copyVertex(uint vertexID [[vertex_id]], texture2d fragment float4 copyFragment(CopyInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { return texture.sample(standardSampler, vert.textureCoordinates); } + +fragment float4 clearFragment(CopyInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { + return float4(0.0, 0.0, 0.0, 0.64); +} diff --git a/Outputs/ScanTargets/BufferingScanTarget.cpp b/Outputs/ScanTargets/BufferingScanTarget.cpp index cc8b0ecf9..684b1611b 100644 --- a/Outputs/ScanTargets/BufferingScanTarget.cpp +++ b/Outputs/ScanTargets/BufferingScanTarget.cpp @@ -221,7 +221,7 @@ void BufferingScanTarget::announce(Event event, bool is_visible, const Outputs:: is_first_in_frame_ = false; // Sanity check. - assert(((metadata.first_scan + provided_scans_) % scan_buffer_size_) == write_pointers_.scan); + assert(((metadata.first_scan + size_t(provided_scans_)) % scan_buffer_size_) == write_pointers_.scan); // Store actual line data. Line &active_line = line_buffer_[size_t(write_pointers_.line)];