1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-08-07 23:25:00 +00:00

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.
This commit is contained in:
Thomas Harte
2020-08-15 21:24:10 -04:00
parent e55945674d
commit 645c29f853
9 changed files with 150 additions and 91 deletions

View File

@@ -5259,6 +5259,7 @@
GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_LABEL = YES;
INFOPLIST_FILE = "Clock Signal/Info.plist"; INFOPLIST_FILE = "Clock Signal/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MTL_TREAT_WARNINGS_AS_ERRORS = YES;
OTHER_CPLUSPLUSFLAGS = ( OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)", "$(OTHER_CFLAGS)",
"-Wreorder", "-Wreorder",
@@ -5306,6 +5307,7 @@
GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_LABEL = YES;
INFOPLIST_FILE = "Clock Signal/Info.plist"; INFOPLIST_FILE = "Clock Signal/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MTL_TREAT_WARNINGS_AS_ERRORS = YES;
OTHER_CPLUSPLUSFLAGS = ( OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)", "$(OTHER_CFLAGS)",
"-Wreorder", "-Wreorder",

View File

@@ -609,13 +609,10 @@ class MachineDocument:
let url = pictursURL.appendingPathComponent(filename) let url = pictursURL.appendingPathComponent(filename)
// Obtain the machine's current display. // Obtain the machine's current display.
var imageRepresentation: NSBitmapImageRep? = nil let imageRepresentation = self.machine.imageRepresentation
self.scanTargetView.perform {
imageRepresentation = self.machine.imageRepresentation
}
// Encode as a PNG and save. // 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) try! pngData?.write(to: url)
} }

View File

@@ -67,9 +67,6 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
- (void)start; - (void)start;
- (void)stop; - (void)stop;
- (void)updateViewForPixelSize:(CGSize)pixelSize;
- (void)drawViewForPixelSize:(CGSize)pixelSize;
- (void)setKey:(uint16_t)key characters:(nullable NSString *)characters isPressed:(BOOL)isPressed; - (void)setKey:(uint16_t)key characters:(nullable NSString *)characters isPressed:(BOOL)isPressed;
- (void)clearAllKeys; - (void)clearAllKeys;

View File

@@ -358,19 +358,6 @@ struct ActivityObserver: public Activity::Observer {
_machine->scan_producer()->set_scan_target(_view.scanTarget.scanTarget); _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 { - (void)paste:(NSString *)paste {
auto keyboardMachine = _machine->keyboard_machine(); auto keyboardMachine = _machine->keyboard_machine();
if(keyboardMachine) if(keyboardMachine)
@@ -831,13 +818,14 @@ struct ActivityObserver: public Activity::Observer {
} }
if(!wasUpdating) { if(!wasUpdating) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ 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); // self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height);
if(splitAndSync) { // if(splitAndSync) {
// self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height); // self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height);
} // }
} flushDrawable:splitAndSync]; // } flushDrawable:splitAndSync];
self->_isUpdating.clear(); self->_isUpdating.clear();
}); });
} }

View File

@@ -16,4 +16,8 @@
- (nonnull instancetype)initWithView:(nonnull MTKView *)view; - (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 @end

View File

@@ -42,7 +42,9 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
id<MTLFunction> _vertexShader; id<MTLFunction> _vertexShader;
id<MTLFunction> _fragmentShader; id<MTLFunction> _fragmentShader;
id<MTLRenderPipelineState> _scanPipeline; id<MTLRenderPipelineState> _scanPipeline;
id<MTLRenderPipelineState> _copyPipeline;
// Buffers. // Buffers.
id<MTLBuffer> _uniformsBuffer; id<MTLBuffer> _uniformsBuffer;
@@ -56,10 +58,16 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
size_t _bytesPerInputPixel; size_t _bytesPerInputPixel;
size_t _totalTextureBytes; size_t _totalTextureBytes;
id<MTLTexture> _frameBuffer;
MTLRenderPassDescriptor *_frameBufferRenderPass;
// 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];
std::atomic_bool _isDrawing; std::atomic_bool _isDrawing;
// The output view's aspect ratio.
__weak MTKView *_view;
} }
- (nonnull instancetype)initWithView:(nonnull MTKView *)view { - (nonnull instancetype)initWithView:(nonnull MTKView *)view {
@@ -89,6 +97,7 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
_scanTarget.set_scan_buffer(reinterpret_cast<BufferingScanTarget::Scan *>(_scansBuffer.contents), NumBufferedScans); _scanTarget.set_scan_buffer(reinterpret_cast<BufferingScanTarget::Scan *>(_scansBuffer.contents), NumBufferedScans);
// Set initial aspect-ratio multiplier. // Set initial aspect-ratio multiplier.
_view = view;
[self mtkView:view drawableSizeWillChange:view.drawableSize]; [self mtkView:view drawableSizeWillChange:view.drawableSize];
} }
@@ -103,17 +112,41 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
@param size New drawable size in pixels @param size New drawable size in pixels
*/ */
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size { - (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. // Populate uniforms.
// //
uniforms()->scale[0] = modals.output_scale.x; uniforms()->scale[0] = modals.output_scale.x;
uniforms()->scale[1] = modals.output_scale.y; 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()->lineWidth = 1.05f / 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)); [self setAspectRatio];
const auto toRGB = to_rgb_matrix(modals.composite_colour_space); const auto toRGB = to_rgb_matrix(modals.composite_colour_space);
uniforms()->toRGB = simd::float3x3( uniforms()->toRGB = simd::float3x3(
@@ -172,11 +205,11 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
// //
// Generate pipeline. // Generate scan pipeline.
// //
id<MTLLibrary> library = [view.device newDefaultLibrary]; id<MTLLibrary> library = [_view.device newDefaultLibrary];
MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; 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 // TODO: logic somewhat more complicated than this, probably
pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"scanToDisplay"]; pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"scanToDisplay"];
@@ -214,30 +247,40 @@ 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;
_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];
} }
/*! - (void)checkModals {
@method drawInMTKView: // TODO: rethink BufferingScanTarget::perform. Is it now really just for guarding the modals?
@abstract Called on the delegate when it is asked to render into the view _scanTarget.perform([=] {
@discussion Called on the delegate when it is asked to render into the view const Outputs::Display::ScanTarget::Modals *const newModals = _scanTarget.new_modals();
*/ if(newModals) {
- (void)drawInMTKView:(nonnull MTKView *)view { [self setModals:*newModals];
const Outputs::Display::ScanTarget::Modals *const newModals = _scanTarget.new_modals(); }
if(newModals) { });
[self setModals:*newModals view:view]; }
}
// Buy into framebuffer preservation. //- (void)updateFrameBuffer {
// TODO: do I really need to do this on every draw? //}
MTLRenderPassDescriptor *const descriptor = view.currentRenderPassDescriptor;
descriptor.colorAttachments[0].loadAction = MTLLoadActionLoad; - (void)updateFrameBuffer {
descriptor.colorAttachments[0].storeAction = MTLStoreActionStore; [self checkModals];
descriptor.colorAttachments[0].clearColor = MTLClearColorMake(1.0, 1.0, 0.0, 1.0); if(!_frameBufferRenderPass) return;
// Generate a command encoder for the view. // Generate a command encoder for the view.
id <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer]; id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
id <MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor:descriptor]; id<MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor:_frameBufferRenderPass];
// Drawing. Just scans. // Drawing. Just scans.
[encoder setRenderPipelineState:_scanPipeline]; [encoder setRenderPipelineState:_scanPipeline];
@@ -284,7 +327,30 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
self->_scanTarget.complete_output_area(outputArea); 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<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
id<MTLRenderCommandEncoder> 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 presentDrawable:view.currentDrawable];
[commandBuffer commit]; [commandBuffer commit];
} }

View File

@@ -55,7 +55,6 @@ struct Line {
// MARK: - Intermediate structs. // MARK: - Intermediate structs.
// This is an intermediate struct, which is TEMPORARY.
struct SourceInterpolator { struct SourceInterpolator {
float4 position [[position]]; float4 position [[position]];
float2 textureCoordinates; float2 textureCoordinates;
@@ -201,3 +200,34 @@ DeclareShaders(Red8Green8Blue8, float)
DeclareShaders(Red4Green4Blue4, ushort) DeclareShaders(Red4Green4Blue4, ushort)
DeclareShaders(Red2Green2Blue2, ushort) DeclareShaders(Red2Green2Blue2, ushort)
DeclareShaders(Red1Green1Blue1, 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<float> 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<float> texture [[texture(0)]]) {
return texture.sample(standardSampler, vert.textureCoordinates);
}

View File

@@ -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. /// The size in pixels of the OpenGL canvas, factoring in screen pixel density and view size in points.
@property (nonatomic, readonly) CGSize backingSize; @property (nonatomic, readonly) CGSize backingSize;
/*! - (void)updateBacking;
Locks this view's OpenGL context and makes it current, performs @c action and then unlocks //- (void)performWithGLContext:(nonnull dispatch_block_t)action;
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;
/*! /*!
Instructs that the mouse cursor, if currently captured, should be released. Instructs that the mouse cursor, if currently captured, should be released.

View File

@@ -119,20 +119,20 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
} }
} }
- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency { //- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency {
[self redrawWithEvent:CSScanTargetViewRedrawEventTimer]; // [self redrawWithEvent:CSScanTargetViewRedrawEventTimer];
} //}
//- (void)drawRect:(NSRect)dirtyRect { //- (void)drawRect:(NSRect)dirtyRect {
// [self redrawWithEvent:CSScanTargetViewRedrawEventAppKit]; // [self redrawWithEvent:CSScanTargetViewRedrawEventAppKit];
// NSLog(@"..."); // NSLog(@"...");
//} //}
- (void)redrawWithEvent:(CSScanTargetViewRedrawEvent)event { //- (void)redrawWithEvent:(CSScanTargetViewRedrawEvent)event {
[self performWithGLContext:^{ // [self performWithGLContext:^{
// [self.delegate openGLViewRedraw:self event:event]; //// [self.delegate openGLViewRedraw:self event:event];
} flushDrawable:YES]; // } flushDrawable:YES];
} //}
- (void)invalidate { - (void)invalidate {
_isInvalid = YES; _isInvalid = YES;
@@ -174,17 +174,9 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
} }
} }
//- (void)reshape { - (void)updateBacking {
// [super reshape]; [_scanTarget updateFrameBuffer];
// @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)awakeFromNib { - (void)awakeFromNib {
// Use the preferred device if available. // Use the preferred device if available.
@@ -202,19 +194,6 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
[self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]]; [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 #pragma mark - NSResponder
- (BOOL)acceptsFirstResponder { - (BOOL)acceptsFirstResponder {