mirror of
https://github.com/TomHarte/CLK.git
synced 2024-11-29 12:50:28 +00:00
Introduces a stencil buffer plus the inter-frame clearing it allows.
This commit is contained in:
parent
940e9e037e
commit
745797b596
@ -45,6 +45,7 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
|
|||||||
|
|
||||||
id<MTLRenderPipelineState> _scanPipeline;
|
id<MTLRenderPipelineState> _scanPipeline;
|
||||||
id<MTLRenderPipelineState> _copyPipeline;
|
id<MTLRenderPipelineState> _copyPipeline;
|
||||||
|
id<MTLRenderPipelineState> _clearPipeline;
|
||||||
|
|
||||||
// Buffers.
|
// Buffers.
|
||||||
id<MTLBuffer> _uniformsBuffer;
|
id<MTLBuffer> _uniformsBuffer;
|
||||||
@ -61,6 +62,10 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
|
|||||||
id<MTLTexture> _frameBuffer;
|
id<MTLTexture> _frameBuffer;
|
||||||
MTLRenderPassDescriptor *_frameBufferRenderPass;
|
MTLRenderPassDescriptor *_frameBufferRenderPass;
|
||||||
|
|
||||||
|
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 scan target in C++-world terms and the non-GPU storage for it.
|
// The scan target in C++-world terms and the non-GPU storage for it.
|
||||||
BufferingScanTarget _scanTarget;
|
BufferingScanTarget _scanTarget;
|
||||||
BufferingScanTarget::LineMetadata _lineMetadataBuffer[NumBufferedLines];
|
BufferingScanTarget::LineMetadata _lineMetadataBuffer[NumBufferedLines];
|
||||||
@ -100,13 +105,25 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
|
|||||||
_view = view;
|
_view = view;
|
||||||
[self mtkView:view drawableSizeWillChange:view.drawableSize];
|
[self mtkView:view drawableSizeWillChange:view.drawableSize];
|
||||||
|
|
||||||
// Generate copy pipeline.
|
// Generate copy and clear pipelines.
|
||||||
id<MTLLibrary> library = [_view.device newDefaultLibrary];
|
id<MTLLibrary> library = [_view.device newDefaultLibrary];
|
||||||
MTLRenderPipelineDescriptor *const pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
|
MTLRenderPipelineDescriptor *const pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
|
||||||
pipelineDescriptor.colorAttachments[0].pixelFormat = _view.colorPixelFormat;
|
pipelineDescriptor.colorAttachments[0].pixelFormat = _view.colorPixelFormat;
|
||||||
pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"copyVertex"];
|
pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"copyVertex"];
|
||||||
pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"copyFragment"];
|
pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"copyFragment"];
|
||||||
_copyPipeline = [_view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil];
|
_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;
|
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
|
// 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.
|
// in order to maintain an ongoing buffer that supersamples only at the end.
|
||||||
|
|
||||||
// TODO: attach a stencil buffer.
|
|
||||||
|
|
||||||
@synchronized(self) {
|
@synchronized(self) {
|
||||||
// Generate a framebuffer and a pipeline that targets it.
|
// Generate a framebuffer and a stencil.
|
||||||
MTLTextureDescriptor *const textureDescriptor = [MTLTextureDescriptor
|
MTLTextureDescriptor *const textureDescriptor = [MTLTextureDescriptor
|
||||||
texture2DDescriptorWithPixelFormat:view.colorPixelFormat
|
texture2DDescriptorWithPixelFormat:view.colorPixelFormat
|
||||||
width:NSUInteger(size.width * view.layer.contentsScale)
|
width:NSUInteger(size.width * view.layer.contentsScale)
|
||||||
@ -138,11 +153,34 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
|
|||||||
textureDescriptor.resourceOptions = MTLResourceStorageModePrivate;
|
textureDescriptor.resourceOptions = MTLResourceStorageModePrivate;
|
||||||
_frameBuffer = [view.device newTextureWithDescriptor:textureDescriptor];
|
_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 = [[MTLRenderPassDescriptor alloc] init];
|
||||||
_frameBufferRenderPass.colorAttachments[0].texture = _frameBuffer;
|
_frameBufferRenderPass.colorAttachments[0].texture = _frameBuffer;
|
||||||
_frameBufferRenderPass.colorAttachments[0].loadAction = MTLLoadActionLoad;
|
_frameBufferRenderPass.colorAttachments[0].loadAction = MTLLoadActionLoad;
|
||||||
_frameBufferRenderPass.colorAttachments[0].storeAction = MTLStoreActionStore;
|
_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.
|
// TODO: old framebuffer should be resized onto the new one.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -259,27 +297,15 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
|
|||||||
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
|
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
|
||||||
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
|
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
|
||||||
|
|
||||||
|
// Set stencil format.
|
||||||
|
pipelineDescriptor.stencilAttachmentPixelFormat = MTLPixelFormatStencil8;
|
||||||
|
|
||||||
|
// Finish.
|
||||||
_scanPipeline = [_view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil];
|
_scanPipeline = [_view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)checkModals {
|
- (void)outputScansFrom:(size_t)start to:(size_t)end commandBuffer:(id<MTLCommandBuffer>)commandBuffer {
|
||||||
// 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();
|
|
||||||
if(newModals) {
|
|
||||||
[self setModals:*newModals];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)updateFrameBuffer {
|
|
||||||
[self checkModals];
|
|
||||||
|
|
||||||
@synchronized(self) {
|
|
||||||
if(!_frameBufferRenderPass) return;
|
|
||||||
|
|
||||||
// Generate a command encoder for the view.
|
// Generate a command encoder for the view.
|
||||||
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
|
|
||||||
id<MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor:_frameBufferRenderPass];
|
id<MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor:_frameBufferRenderPass];
|
||||||
|
|
||||||
// Drawing. Just scans.
|
// Drawing. Just scans.
|
||||||
@ -290,6 +316,57 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
|
|||||||
[encoder setVertexBuffer:_uniformsBuffer offset:0 atIndex:1];
|
[encoder setVertexBuffer:_uniformsBuffer offset:0 atIndex:1];
|
||||||
[encoder setFragmentBuffer:_uniformsBuffer offset:0 atIndex:0];
|
[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<MTLCommandBuffer>)commandBuffer {
|
||||||
|
// Generate a command encoder for the view.
|
||||||
|
id<MTLRenderCommandEncoder> 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();
|
||||||
|
if(newModals) {
|
||||||
|
[self setModals:*newModals];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
@synchronized(self) {
|
||||||
|
if(!_frameBufferRenderPass) return;
|
||||||
|
|
||||||
const auto outputArea = _scanTarget.get_output_area();
|
const auto outputArea = _scanTarget.get_output_area();
|
||||||
|
|
||||||
// Ensure texture changes are noted.
|
// Ensure texture changes are noted.
|
||||||
@ -307,27 +384,43 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TEMPORARY: just draw the scans.
|
// Obtain a source for render command encoders.
|
||||||
if(outputArea.start.scan != outputArea.end.scan) {
|
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete encoding.
|
//
|
||||||
[encoder endEncoding];
|
// 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.
|
||||||
|
//
|
||||||
|
|
||||||
// Add a callback to update the buffer.
|
// 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];
|
||||||
|
|
||||||
|
// Add a callback to update the scan target buffer and commit the drawing.
|
||||||
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull) {
|
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull) {
|
||||||
self->_scanTarget.complete_output_area(outputArea);
|
self->_scanTarget.complete_output_area(outputArea);
|
||||||
}];
|
}];
|
||||||
|
|
||||||
// Commit the drawing.
|
|
||||||
[commandBuffer commit];
|
[commandBuffer commit];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,9 @@
|
|||||||
#include <metal_stdlib>
|
#include <metal_stdlib>
|
||||||
using namespace metal;
|
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 {
|
struct Uniforms {
|
||||||
// This is used to scale scan positions, i.e. it provides the range
|
// This is used to scale scan positions, i.e. it provides the range
|
||||||
// for mapping from scan-style integer positions into eye space.
|
// 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.
|
// Calculate the tangent and normal.
|
||||||
const float2 tangent = (end - start);
|
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.
|
// Load up the colour details.
|
||||||
output.colourAmplitude = float(scans[instanceID].compositeAmplitude) / 255.0f;
|
output.colourAmplitude = float(scans[instanceID].compositeAmplitude) / 255.0f;
|
||||||
@ -231,3 +234,7 @@ vertex CopyInterpolator copyVertex(uint vertexID [[vertex_id]], texture2d<float>
|
|||||||
fragment float4 copyFragment(CopyInterpolator vert [[stage_in]], texture2d<float> texture [[texture(0)]]) {
|
fragment float4 copyFragment(CopyInterpolator vert [[stage_in]], texture2d<float> texture [[texture(0)]]) {
|
||||||
return texture.sample(standardSampler, vert.textureCoordinates);
|
return texture.sample(standardSampler, vert.textureCoordinates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fragment float4 clearFragment(CopyInterpolator vert [[stage_in]], texture2d<float> texture [[texture(0)]]) {
|
||||||
|
return float4(0.0, 0.0, 0.0, 0.64);
|
||||||
|
}
|
||||||
|
@ -221,7 +221,7 @@ void BufferingScanTarget::announce(Event event, bool is_visible, const Outputs::
|
|||||||
is_first_in_frame_ = false;
|
is_first_in_frame_ = false;
|
||||||
|
|
||||||
// Sanity check.
|
// 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.
|
// Store actual line data.
|
||||||
Line &active_line = line_buffer_[size_t(write_pointers_.line)];
|
Line &active_line = line_buffer_[size_t(write_pointers_.line)];
|
||||||
|
Loading…
Reference in New Issue
Block a user