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:
parent
e55945674d
commit
645c29f853
OSBindings/Mac
Clock Signal.xcodeproj
Clock Signal
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user