From 645c29f85333894adaf2190156951e583d83ad0f Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 15 Aug 2020 21:24:10 -0400 Subject: [PATCH] Adds an intermediate buffer to correct inter-frame smoothing. Also goes someway back to the old scan output scheduling, albeit presently with limited thread safety. --- .../Clock Signal.xcodeproj/project.pbxproj | 2 + .../Documents/MachineDocument.swift | 7 +- .../Mac/Clock Signal/Machine/CSMachine.h | 3 - .../Mac/Clock Signal/Machine/CSMachine.mm | 22 +--- .../Clock Signal/ScanTarget/CSScanTarget.h | 4 + .../Clock Signal/ScanTarget/CSScanTarget.mm | 120 ++++++++++++++---- .../Clock Signal/ScanTarget/ScanTarget.metal | 32 ++++- .../Mac/Clock Signal/Views/CSScanTargetView.h | 8 +- .../Mac/Clock Signal/Views/CSScanTargetView.m | 43 ++----- 9 files changed, 150 insertions(+), 91 deletions(-) diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index fcc791b6c..deeab7a7a 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -5259,6 +5259,7 @@ GCC_WARN_UNUSED_LABEL = YES; INFOPLIST_FILE = "Clock Signal/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + MTL_TREAT_WARNINGS_AS_ERRORS = YES; OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-Wreorder", @@ -5306,6 +5307,7 @@ GCC_WARN_UNUSED_LABEL = YES; INFOPLIST_FILE = "Clock Signal/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + MTL_TREAT_WARNINGS_AS_ERRORS = YES; OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-Wreorder", diff --git a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift index 480d3c1c6..643e57d32 100644 --- a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift +++ b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift @@ -609,13 +609,10 @@ class MachineDocument: let url = pictursURL.appendingPathComponent(filename) // Obtain the machine's current display. - var imageRepresentation: NSBitmapImageRep? = nil - self.scanTargetView.perform { - imageRepresentation = self.machine.imageRepresentation - } + let imageRepresentation = self.machine.imageRepresentation // Encode as a PNG and save. - let pngData = imageRepresentation!.representation(using: .png, properties: [:]) + let pngData = imageRepresentation.representation(using: .png, properties: [:]) try! pngData?.write(to: url) } diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h index 09cd4b295..eea2c1f01 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h @@ -67,9 +67,6 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) { - (void)start; - (void)stop; -- (void)updateViewForPixelSize:(CGSize)pixelSize; -- (void)drawViewForPixelSize:(CGSize)pixelSize; - - (void)setKey:(uint16_t)key characters:(nullable NSString *)characters isPressed:(BOOL)isPressed; - (void)clearAllKeys; diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index aa783e070..a93328811 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -358,19 +358,6 @@ struct ActivityObserver: public Activity::Observer { _machine->scan_producer()->set_scan_target(_view.scanTarget.scanTarget); } -- (void)updateViewForPixelSize:(CGSize)pixelSize { -// _pixelSize = pixelSize; - -// @synchronized(self) { -// const auto scan_status = _machine->crt_machine()->get_scan_status(); -// NSLog(@"FPS (hopefully): %0.2f [retrace: %0.4f]", 1.0f / scan_status.field_duration, scan_status.retrace_duration); -// } -} - -- (void)drawViewForPixelSize:(CGSize)pixelSize { -// _scanTarget->draw((int)pixelSize.width, (int)pixelSize.height); -} - - (void)paste:(NSString *)paste { auto keyboardMachine = _machine->keyboard_machine(); if(keyboardMachine) @@ -831,13 +818,14 @@ struct ActivityObserver: public Activity::Observer { } if(!wasUpdating) { dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ - [self.view performWithGLContext:^{ + [self.view updateBacking]; +// [self.view performWithGLContext:^{ // self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height); - if(splitAndSync) { +// if(splitAndSync) { // self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height); - } - } flushDrawable:splitAndSync]; +// } +// } flushDrawable:splitAndSync]; self->_isUpdating.clear(); }); } diff --git a/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.h b/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.h index 93f9fb62c..e3f021ea3 100644 --- a/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.h +++ b/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.h @@ -16,4 +16,8 @@ - (nonnull instancetype)initWithView:(nonnull MTKView *)view; +// Draws all scans currently residing at the scan target to the backing store, +// ready for output when next requested. +- (void)updateFrameBuffer; + @end diff --git a/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm b/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm index cfb9dbc2f..9ec8b0ce8 100644 --- a/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm +++ b/OSBindings/Mac/Clock Signal/ScanTarget/CSScanTarget.mm @@ -42,7 +42,9 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; id _vertexShader; id _fragmentShader; + id _scanPipeline; + id _copyPipeline; // Buffers. id _uniformsBuffer; @@ -56,10 +58,16 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; size_t _bytesPerInputPixel; size_t _totalTextureBytes; + id _frameBuffer; + MTLRenderPassDescriptor *_frameBufferRenderPass; + // The scan target in C++-world terms and the non-GPU storage for it. BufferingScanTarget _scanTarget; BufferingScanTarget::LineMetadata _lineMetadataBuffer[NumBufferedLines]; std::atomic_bool _isDrawing; + + // The output view's aspect ratio. + __weak MTKView *_view; } - (nonnull instancetype)initWithView:(nonnull MTKView *)view { @@ -89,6 +97,7 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; _scanTarget.set_scan_buffer(reinterpret_cast(_scansBuffer.contents), NumBufferedScans); // Set initial aspect-ratio multiplier. + _view = view; [self mtkView:view drawableSizeWillChange:view.drawableSize]; } @@ -103,17 +112,41 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; @param size New drawable size in pixels */ - (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size { - uniforms()->aspectRatioMultiplier = float(_scanTarget.modals().aspect_ratio / (size.width / size.height)); + [self setAspectRatio]; + + // 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. + + // Generate a framebuffer and a pipeline that targets it. + MTLTextureDescriptor *const textureDescriptor = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:view.colorPixelFormat + width:NSUInteger(size.width * view.layer.contentsScale) + height:NSUInteger(size.height * view.layer.contentsScale) + mipmapped:NO]; + textureDescriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; + textureDescriptor.resourceOptions = MTLResourceStorageModePrivate; + _frameBuffer = [view.device newTextureWithDescriptor:textureDescriptor]; + + _frameBufferRenderPass = [[MTLRenderPassDescriptor alloc] init]; + _frameBufferRenderPass.colorAttachments[0].texture = _frameBuffer; + _frameBufferRenderPass.colorAttachments[0].loadAction = MTLLoadActionLoad; + _frameBufferRenderPass.colorAttachments[0].storeAction = MTLStoreActionStore; } -- (void)setModals:(const Outputs::Display::ScanTarget::Modals &)modals view:(nonnull MTKView *)view { +- (void)setAspectRatio { + uniforms()->aspectRatioMultiplier = float(_scanTarget.modals().aspect_ratio / (_view.bounds.size.width / _view.bounds.size.height)); +} + +- (void)setModals:(const Outputs::Display::ScanTarget::Modals &)modals { // // Populate uniforms. // uniforms()->scale[0] = modals.output_scale.x; uniforms()->scale[1] = modals.output_scale.y; - uniforms()->lineWidth = 0.75f / modals.expected_vertical_lines; // TODO: return to 1.0 (or slightly more), once happy. - uniforms()->aspectRatioMultiplier = float(modals.aspect_ratio / (view.bounds.size.width / view.bounds.size.height)); + uniforms()->lineWidth = 1.05f / modals.expected_vertical_lines; // TODO: return to 1.0 (or slightly more), once happy. + [self setAspectRatio]; const auto toRGB = to_rgb_matrix(modals.composite_colour_space); uniforms()->toRGB = simd::float3x3( @@ -172,11 +205,11 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; // - // Generate pipeline. + // Generate scan pipeline. // - id library = [view.device newDefaultLibrary]; + id library = [_view.device newDefaultLibrary]; MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; - pipelineDescriptor.colorAttachments[0].pixelFormat = view.colorPixelFormat; + pipelineDescriptor.colorAttachments[0].pixelFormat = _view.colorPixelFormat; // TODO: logic somewhat more complicated than this, probably pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"scanToDisplay"]; @@ -214,30 +247,40 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha; pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; - _scanPipeline = [view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil]; + _scanPipeline = [_view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil]; + + + + // + // Generate copy pipeline. + // + 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]; } -/*! - @method drawInMTKView: - @abstract Called on the delegate when it is asked to render into the view - @discussion Called on the delegate when it is asked to render into the view - */ -- (void)drawInMTKView:(nonnull MTKView *)view { - const Outputs::Display::ScanTarget::Modals *const newModals = _scanTarget.new_modals(); - if(newModals) { - [self setModals:*newModals view:view]; - } +- (void)checkModals { + // 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]; + } + }); +} - // Buy into framebuffer preservation. - // TODO: do I really need to do this on every draw? - MTLRenderPassDescriptor *const descriptor = view.currentRenderPassDescriptor; - descriptor.colorAttachments[0].loadAction = MTLLoadActionLoad; - descriptor.colorAttachments[0].storeAction = MTLStoreActionStore; - descriptor.colorAttachments[0].clearColor = MTLClearColorMake(1.0, 1.0, 0.0, 1.0); +//- (void)updateFrameBuffer { +//} + +- (void)updateFrameBuffer { + [self checkModals]; + if(!_frameBufferRenderPass) return; // Generate a command encoder for the view. - id commandBuffer = [_commandQueue commandBuffer]; - id encoder = [commandBuffer renderCommandEncoderWithDescriptor:descriptor]; + id commandBuffer = [_commandQueue commandBuffer]; + id encoder = [commandBuffer renderCommandEncoderWithDescriptor:_frameBufferRenderPass]; // Drawing. Just scans. [encoder setRenderPipelineState:_scanPipeline]; @@ -284,7 +327,30 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget; self->_scanTarget.complete_output_area(outputArea); }]; - // Register the drawable's presentation, finalise and commit. + // Commit the drawing. + [commandBuffer commit]; +} + +/*! + @method drawInMTKView: + @abstract Called on the delegate when it is asked to render into the view + @discussion Called on the delegate when it is asked to render into the view + */ +- (void)drawInMTKView:(nonnull MTKView *)view { + [self checkModals]; +// [self updateFrameBuffer]; + + // Schedule a copy from the current framebuffer to the view; blitting is unavailable as the target is a framebuffer texture. + id commandBuffer = [_commandQueue commandBuffer]; + id encoder = [commandBuffer renderCommandEncoderWithDescriptor:view.currentRenderPassDescriptor]; + + [encoder setRenderPipelineState:_copyPipeline]; + [encoder setVertexTexture:_frameBuffer atIndex:0]; + [encoder setFragmentTexture:_frameBuffer atIndex:0]; + + [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; + [encoder endEncoding]; + [commandBuffer presentDrawable:view.currentDrawable]; [commandBuffer commit]; } diff --git a/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal b/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal index 944222e87..5c0081e3f 100644 --- a/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal +++ b/OSBindings/Mac/Clock Signal/ScanTarget/ScanTarget.metal @@ -55,7 +55,6 @@ struct Line { // MARK: - Intermediate structs. -// This is an intermediate struct, which is TEMPORARY. struct SourceInterpolator { float4 position [[position]]; float2 textureCoordinates; @@ -201,3 +200,34 @@ DeclareShaders(Red8Green8Blue8, float) DeclareShaders(Red4Green4Blue4, ushort) DeclareShaders(Red2Green2Blue2, ushort) DeclareShaders(Red1Green1Blue1, ushort) + +// MARK: - Shaders for copying from a same-sized texture to an MTKView's frame buffer. + +struct CopyInterpolator { + float4 position [[position]]; + float2 textureCoordinates; +}; + +vertex CopyInterpolator copyVertex(uint vertexID [[vertex_id]], texture2d texture [[texture(0)]]) { + CopyInterpolator vert; + + const uint x = vertexID & 1; + const uint y = (vertexID >> 1) & 1; + + vert.textureCoordinates = float2( + x * texture.get_width(), + y * texture.get_height() + ); + vert.position = float4( + float(x) * 2.0 - 1.0, + 1.0 - float(y) * 2.0, + 0.0, + 1.0 + ); + + return vert; +} + +fragment float4 copyFragment(CopyInterpolator vert [[stage_in]], texture2d texture [[texture(0)]]) { + return texture.sample(standardSampler, vert.textureCoordinates); +} diff --git a/OSBindings/Mac/Clock Signal/Views/CSScanTargetView.h b/OSBindings/Mac/Clock Signal/Views/CSScanTargetView.h index 57e51b62f..a8d3f4ac3 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSScanTargetView.h +++ b/OSBindings/Mac/Clock Signal/Views/CSScanTargetView.h @@ -168,12 +168,8 @@ typedef NS_ENUM(NSInteger, CSScanTargetViewRedrawEvent) { /// The size in pixels of the OpenGL canvas, factoring in screen pixel density and view size in points. @property (nonatomic, readonly) CGSize backingSize; -/*! - Locks this view's OpenGL context and makes it current, performs @c action and then unlocks - the context. @c action is performed on the calling queue. -*/ -- (void)performWithGLContext:(nonnull dispatch_block_t)action flushDrawable:(BOOL)flushDrawable; -- (void)performWithGLContext:(nonnull dispatch_block_t)action; +- (void)updateBacking; +//- (void)performWithGLContext:(nonnull dispatch_block_t)action; /*! Instructs that the mouse cursor, if currently captured, should be released. diff --git a/OSBindings/Mac/Clock Signal/Views/CSScanTargetView.m b/OSBindings/Mac/Clock Signal/Views/CSScanTargetView.m index 8989fed90..4048b2745 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSScanTargetView.m +++ b/OSBindings/Mac/Clock Signal/Views/CSScanTargetView.m @@ -119,20 +119,20 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const } } -- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency { - [self redrawWithEvent:CSScanTargetViewRedrawEventTimer]; -} +//- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency { +// [self redrawWithEvent:CSScanTargetViewRedrawEventTimer]; +//} //- (void)drawRect:(NSRect)dirtyRect { // [self redrawWithEvent:CSScanTargetViewRedrawEventAppKit]; // NSLog(@"..."); //} -- (void)redrawWithEvent:(CSScanTargetViewRedrawEvent)event { - [self performWithGLContext:^{ -// [self.delegate openGLViewRedraw:self event:event]; - } flushDrawable:YES]; -} +//- (void)redrawWithEvent:(CSScanTargetViewRedrawEvent)event { +// [self performWithGLContext:^{ +//// [self.delegate openGLViewRedraw:self event:event]; +// } flushDrawable:YES]; +//} - (void)invalidate { _isInvalid = YES; @@ -174,17 +174,9 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const } } -//- (void)reshape { -// [super reshape]; -// @synchronized(self) { -// _backingSize = [self convertSizeToBacking:self.bounds.size]; -// } -// -// [self performWithGLContext:^{ -// CGSize viewSize = [self backingSize]; -// glViewport(0, 0, (GLsizei)viewSize.width, (GLsizei)viewSize.height); -// } flushDrawable:NO]; -//} +- (void)updateBacking { + [_scanTarget updateFrameBuffer]; +} - (void)awakeFromNib { // Use the preferred device if available. @@ -202,19 +194,6 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const [self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]]; } -- (void)performWithGLContext:(dispatch_block_t)action flushDrawable:(BOOL)flushDrawable { -// CGLLockContext([[self openGLContext] CGLContextObj]); -// [self.openGLContext makeCurrentContext]; -// action(); -// CGLUnlockContext([[self openGLContext] CGLContextObj]); -// -// if(flushDrawable) CGLFlushDrawable([[self openGLContext] CGLContextObj]); -} - -- (void)performWithGLContext:(nonnull dispatch_block_t)action { -// [self performWithGLContext:action flushDrawable:NO]; -} - #pragma mark - NSResponder - (BOOL)acceptsFirstResponder {