1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-04-09 15:39:08 +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

@ -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",

@ -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)
}

@ -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;

@ -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();
});
}

@ -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

@ -42,7 +42,9 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
id<MTLFunction> _vertexShader;
id<MTLFunction> _fragmentShader;
id<MTLRenderPipelineState> _scanPipeline;
id<MTLRenderPipelineState> _copyPipeline;
// Buffers.
id<MTLBuffer> _uniformsBuffer;
@ -56,10 +58,16 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
size_t _bytesPerInputPixel;
size_t _totalTextureBytes;
id<MTLTexture> _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<BufferingScanTarget::Scan *>(_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<MTLLibrary> library = [view.device newDefaultLibrary];
id<MTLLibrary> 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 <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
id <MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor:descriptor];
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
id<MTLRenderCommandEncoder> 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<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 commit];
}

@ -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<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);
}

@ -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.

@ -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 {