From 256f4a6679ce4bd2cfccc66951fe716477d26382 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 2 Feb 2020 21:29:22 -0500 Subject: [PATCH 01/25] Fixes -invalidate: cancel the dispatch source, don't just suspend it, and wait until that is done. --- .../High Precision Timer/CSHighPrecisionTimer.m | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/OSBindings/Mac/Clock Signal/High Precision Timer/CSHighPrecisionTimer.m b/OSBindings/Mac/Clock Signal/High Precision Timer/CSHighPrecisionTimer.m index 0947e5620..46a233a22 100644 --- a/OSBindings/Mac/Clock Signal/High Precision Timer/CSHighPrecisionTimer.m +++ b/OSBindings/Mac/Clock Signal/High Precision Timer/CSHighPrecisionTimer.m @@ -31,7 +31,15 @@ } - (void)invalidate { - dispatch_suspend(_timer); + NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0]; + + dispatch_source_set_cancel_handler(_timer, ^{ + [lock lock]; + [lock unlockWithCondition:1]; + }); + + dispatch_source_cancel(_timer); + [lock lockWhenCondition:1]; } @end From 0f2783075fe12447be2c6e8e87348f5463d7df13 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 2 Feb 2020 21:39:20 -0500 Subject: [PATCH 02/25] Moves responsibility for timed updates to CSMachine, which gives the CSHighPrecisionTimer a shot. --- .../Documents/MachineDocument.swift | 30 ++----------------- .../Mac/Clock Signal/Machine/CSMachine.h | 3 ++ .../Mac/Clock Signal/Machine/CSMachine.mm | 18 ++++++++++- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift index 66f7d7293..bd4048923 100644 --- a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift +++ b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift @@ -24,8 +24,6 @@ class MachineDocument: private let actionLock = NSLock() /// Ensures exclusive access between calls to machine.updateView and machine.drawView, and close(). private let drawLock = NSLock() - /// Ensures exclusive access to the best-effort updater. - private let bestEffortLock = NSLock() // MARK: - Machine details. @@ -43,9 +41,6 @@ class MachineDocument: /// The output audio queue, if any. private var audioQueue: CSAudioQueue! - /// The best-effort updater. - private var bestEffortUpdater: CSBestEffortUpdater? - // MARK: - Main NIB connections. /// The OpenGL view to receive this machine's display. @@ -89,19 +84,14 @@ class MachineDocument: } override func close() { + machine.stop() + activityPanel?.setIsVisible(false) activityPanel = nil optionsPanel?.setIsVisible(false) optionsPanel = nil - bestEffortLock.lock() - if let bestEffortUpdater = bestEffortUpdater { - bestEffortUpdater.flush() - self.bestEffortUpdater = nil - } - bestEffortLock.unlock() - actionLock.lock() drawLock.lock() machine = nil @@ -200,7 +190,6 @@ class MachineDocument: } machine.delegate = self - self.bestEffortUpdater = CSBestEffortUpdater() // Callbacks from the OpenGL may come on a different thread, immediately following the .delegate set; // hence the full setup of the best-effort updater prior to setting self as a delegate. @@ -220,7 +209,7 @@ class MachineDocument: openGLView.window!.makeFirstResponder(openGLView) // Start forwarding best-effort updates. - self.bestEffortUpdater!.setMachine(machine) + machine.start() } } @@ -251,24 +240,11 @@ class MachineDocument: /// Responds to the CSAudioQueueDelegate dry-queue warning message by requesting a machine update. final func audioQueueIsRunningDry(_ audioQueue: CSAudioQueue) { - bestEffortLock.lock() - bestEffortUpdater?.update(with: .audioNeeded) - bestEffortLock.unlock() } /// Responds to the CSOpenGLViewDelegate redraw message by requesting a machine update if this is a timed /// request, and ordering a redraw regardless of the motivation. final func openGLViewRedraw(_ view: CSOpenGLView, event redrawEvent: CSOpenGLViewRedrawEvent) { - if redrawEvent == .timer { - bestEffortLock.lock() - if let bestEffortUpdater = bestEffortUpdater { - bestEffortLock.unlock() - bestEffortUpdater.update() - } else { - bestEffortLock.unlock() - } - } - if drawLock.try() { if redrawEvent == .timer { machine.updateView(forPixelSize: view.backingSize) diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h index 83f6d072b..18bda6979 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h @@ -64,6 +64,9 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) { - (void)setView:(nullable CSOpenGLView *)view aspectRatio:(float)aspectRatio; +- (void)start; +- (void)stop; + - (void)updateViewForPixelSize:(CGSize)pixelSize; - (void)drawViewForPixelSize:(CGSize)pixelSize; diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index 2864167c9..15fc721fe 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -10,6 +10,7 @@ #import "CSMachine+Target.h" #include "CSROMFetcher.hpp" +#import "CSHighPrecisionTimer.h" #include "MediaTarget.hpp" #include "JoystickMachine.hpp" @@ -149,6 +150,8 @@ struct ActivityObserver: public Activity::Observer { std::bitset<65536> _depressedKeys; NSMutableArray *_leds; + CSHighPrecisionTimer *_timer; + std::unique_ptr _scanTarget; } @@ -326,7 +329,7 @@ struct ActivityObserver: public Activity::Observer { } - (void)updateViewForPixelSize:(CGSize)pixelSize { - _scanTarget->update((int)pixelSize.width, (int)pixelSize.height); + self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height); // @synchronized(self) { // const auto scan_status = _machine->crt_machine()->get_scan_status(); @@ -694,4 +697,17 @@ struct ActivityObserver: public Activity::Observer { return _leds; } +#pragma mark - Timer + +- (void)start { + _timer = [[CSHighPrecisionTimer alloc] initWithTask:^{ + self->_machine->crt_machine()->run_for(2500000.0 / 1000000000.0); + } interval:2500000]; +} + +- (void)stop { + [_timer invalidate]; + _timer = nil; +} + @end From cf9729c74ff74980b2caa34cbd5a41bb7d42a5d9 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 3 Feb 2020 21:58:29 -0500 Subject: [PATCH 03/25] Takes a first shot at running OpenGL work throughout a frame. Rather than en masse at the end. But it seems I've been lazy with my threading. Work to do! --- .../Documents/MachineDocument.swift | 4 +-- .../Mac/Clock Signal/Machine/CSMachine.mm | 32 ++++++++++++++++--- .../Mac/Clock Signal/Views/CSOpenGLView.h | 25 +++++++++++++++ .../Mac/Clock Signal/Views/CSOpenGLView.m | 30 ++++++++++------- 4 files changed, 72 insertions(+), 19 deletions(-) diff --git a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift index bd4048923..72bfef8c2 100644 --- a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift +++ b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift @@ -177,9 +177,7 @@ class MachineDocument: if let machine = self.machine, let openGLView = self.openGLView { // Establish the output aspect ratio and audio. let aspectRatio = self.aspectRatio() - openGLView.perform(glContext: { - machine.setView(openGLView, aspectRatio: Float(aspectRatio.width / aspectRatio.height)) - }) + machine.setView(openGLView, aspectRatio: Float(aspectRatio.width / aspectRatio.height)) // Attach an options panel if one is available. if let optionsPanelNibName = self.machineDescription?.optionsPanelNibName { diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index 15fc721fe..b4780eec1 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -33,7 +33,7 @@ #include "../../../../Outputs/OpenGL/ScanTarget.hpp" #include "../../../../Outputs/OpenGL/Screenshot.hpp" -@interface CSMachine() +@interface CSMachine() - (void)speaker:(Outputs::Speaker::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length; - (void)speakerDidChangeInputClock:(Outputs::Speaker::Speaker *)speaker; - (void)addLED:(NSString *)led; @@ -151,6 +151,7 @@ struct ActivityObserver: public Activity::Observer { NSMutableArray *_leds; CSHighPrecisionTimer *_timer; + CGSize _pixelSize; std::unique_ptr _scanTarget; } @@ -318,9 +319,10 @@ struct ActivityObserver: public Activity::Observer { - (void)setView:(CSOpenGLView *)view aspectRatio:(float)aspectRatio { _view = view; + _view.displayLinkDelegate = self; [view performWithGLContext:^{ [self setupOutputWithAspectRatio:aspectRatio]; - }]; + } flushDrawable:NO]; } - (void)setupOutputWithAspectRatio:(float)aspectRatio { @@ -329,7 +331,7 @@ struct ActivityObserver: public Activity::Observer { } - (void)updateViewForPixelSize:(CGSize)pixelSize { - self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height); +// _pixelSize = pixelSize; // @synchronized(self) { // const auto scan_status = _machine->crt_machine()->get_scan_status(); @@ -699,9 +701,31 @@ struct ActivityObserver: public Activity::Observer { #pragma mark - Timer +- (void)openGLView:(CSOpenGLView *)view didUpdateDisplayLink:(CVDisplayLinkRef)displayLink { +} + +- (void)openGLViewDisplayLinkDidFire:(CSOpenGLView *)view { + @synchronized(self) { + _pixelSize = view.backingSize; + } + [self.view performWithGLContext:^{ + self->_scanTarget->draw((int)self->_pixelSize.width, (int)self->_pixelSize.height); + } flushDrawable:YES]; +} + - (void)start { _timer = [[CSHighPrecisionTimer alloc] initWithTask:^{ - self->_machine->crt_machine()->run_for(2500000.0 / 1000000000.0); + CGSize pixelSize; + @synchronized(self) { + self->_machine->crt_machine()->run_for(2500000.0 / 1000000000.0); + pixelSize = self->_pixelSize; + } + + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ + [self.view performWithGLContext:^{ + self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height); + } flushDrawable:NO]; + }); } interval:2500000]; } diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h index 25920e578..a46d64752 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h @@ -101,6 +101,29 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { @end +/*! + Although I'm still on the fence about this as a design decision, CSOpenGLView is itself responsible + for creating and destroying a CVDisplayLink. There's a practical reason for this: you'll get real synchronisation + only if a link is explicitly tied to a particular display, and the CSOpenGLView therefore owns the knowledge + necessary to decide when to create and modify them. It doesn't currently just propagate "did change screen"-type + messages because I haven't yet found a way to track that other than polling, in which case I might as well put + that into the display link callback. +*/ +@protocol CSOpenGLViewDisplayLinkDelegate + +/*! + Informs the delegate that from now on, the display link @c displayLink will be used for update notifications + and/or that the frequency or phase or @c displayLink has changed. +*/ +- (void)openGLView:(nonnull CSOpenGLView *)view didUpdateDisplayLink:(nonnull CVDisplayLinkRef)displayLink; + +/*! + Informs the delegate that the display link has fired. +*/ +- (void)openGLViewDisplayLinkDidFire:(nonnull CSOpenGLView *)view; + +@end + /*! Provides an OpenGL canvas with a refresh-linked update timer that can forward a subset of typical first-responder actions. @@ -109,6 +132,7 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { @property (atomic, weak, nullable) id delegate; @property (nonatomic, weak, nullable) id responderDelegate; +@property (atomic, weak, nullable) id displayLinkDelegate; /// Determines whether the view offers mouse capturing — i.e. if the user clicks on the view then /// then the system cursor is disabled and the mouse events defined by CSOpenGLViewResponderDelegate @@ -139,6 +163,7 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { 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; /*! diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m index c1a57dee0..eb783f3a1 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m @@ -30,11 +30,6 @@ // Note the initial screen. _currentScreen = self.window.screen; - // Synchronize buffer swaps with vertical refresh rate. - // TODO: discard this, once scheduling is sufficiently intelligent? - GLint swapInt = 1; - [[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval]; - // set the clear colour [self.openGLContext makeCurrentContext]; glClearColor(0.0, 0.0, 0.0, 1.0); @@ -68,6 +63,9 @@ CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj]; CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(_displayLink, cglContext, cglPixelFormat); + // Give a shout-out. + [self.displayLinkDelegate openGLView:self didUpdateDisplayLink:_displayLink]; + // Activate the display link CVDisplayLinkStart(_displayLink); } @@ -75,7 +73,8 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) { CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext; - [view drawAtTime:now frequency:CVDisplayLinkGetActualOutputVideoRefreshPeriod(displayLink)]; + [view checkDisplayLink]; + [view.displayLinkDelegate openGLViewDisplayLinkDidFire:view]; /* Do not touch the display link from after this call; there's a bit of a race condition with setupDisplayLink. Specifically: Apple provides CVDisplayLinkStop but a call to that merely prevents future calls to the callback, @@ -89,7 +88,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt return kCVReturnSuccess; } -- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency { +- (void)checkDisplayLink { // Test now whether the screen this view is on has changed since last time it was checked. // There's likely a callback available for this, on NSWindow if nowhere else, or an NSNotification, // but since this method is going to be called repeatedly anyway, and the test is cheap, polling @@ -105,7 +104,9 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt // the window is actually on, and at its rate. [self setupDisplayLink]; } +} +- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency { [self redrawWithEvent:CSOpenGLViewRedrawEventTimer]; } @@ -113,11 +114,10 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt [self redrawWithEvent:CSOpenGLViewRedrawEventAppKit]; } -- (void)redrawWithEvent:(CSOpenGLViewRedrawEvent)event { +- (void)redrawWithEvent:(CSOpenGLViewRedrawEvent)event { [self performWithGLContext:^{ [self.delegate openGLViewRedraw:self event:event]; - CGLFlushDrawable([[self openGLContext] CGLContextObj]); - }]; + } flushDrawable:YES]; } - (void)invalidate { @@ -145,7 +145,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt [self performWithGLContext:^{ CGSize viewSize = [self backingSize]; glViewport(0, 0, (GLsizei)viewSize.width, (GLsizei)viewSize.height); - }]; + } flushDrawable:NO]; } - (void)awakeFromNib { @@ -178,11 +178,17 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt [self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]]; } -- (void)performWithGLContext:(dispatch_block_t)action { +- (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 From 96769c52f686e7d3b02adcdbc798ac389fd67722 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 3 Feb 2020 22:08:07 -0500 Subject: [PATCH 04/25] Prevents an endless queue of backlogged updates. --- .../Mac/Clock Signal/Machine/CSMachine.mm | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index b4780eec1..a747d9693 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -25,6 +25,7 @@ #import "NSBundle+DataResource.h" #import "NSData+StdVector.h" +#include #include #import @@ -152,6 +153,7 @@ struct ActivityObserver: public Activity::Observer { CSHighPrecisionTimer *_timer; CGSize _pixelSize; + std::atomic_flag is_updating; std::unique_ptr _scanTarget; } @@ -205,6 +207,7 @@ struct ActivityObserver: public Activity::Observer { _speakerDelegate.machineAccessLock = _delegateMachineAccessLock; _joystickMachine = _machine->joystick_machine(); + is_updating.clear(); } return self; } @@ -705,11 +708,12 @@ struct ActivityObserver: public Activity::Observer { } - (void)openGLViewDisplayLinkDidFire:(CSOpenGLView *)view { + CGSize pixelSize = view.backingSize; @synchronized(self) { - _pixelSize = view.backingSize; + _pixelSize = pixelSize; } [self.view performWithGLContext:^{ - self->_scanTarget->draw((int)self->_pixelSize.width, (int)self->_pixelSize.height); + self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height); } flushDrawable:YES]; } @@ -721,11 +725,14 @@ struct ActivityObserver: public Activity::Observer { pixelSize = self->_pixelSize; } - dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ - [self.view performWithGLContext:^{ - self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height); - } flushDrawable:NO]; - }); + if(!self->is_updating.test_and_set()) { + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ + [self.view performWithGLContext:^{ + self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height); + } flushDrawable:NO]; + self->is_updating.clear(); + }); + } } interval:2500000]; } From 01fd1b1a2e09baf2e5a7c1dfe3f9d7988d4ee2a1 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Mon, 3 Feb 2020 22:44:39 -0500 Subject: [PATCH 05/25] Pulls out ticks as a macro constant. For playing. --- OSBindings/Mac/Clock Signal/Machine/CSMachine.mm | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index a747d9693..f1e3a31a7 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -717,11 +717,13 @@ struct ActivityObserver: public Activity::Observer { } flushDrawable:YES]; } +#define TICKS 600 + - (void)start { _timer = [[CSHighPrecisionTimer alloc] initWithTask:^{ CGSize pixelSize; @synchronized(self) { - self->_machine->crt_machine()->run_for(2500000.0 / 1000000000.0); + self->_machine->crt_machine()->run_for(1.0 / double(TICKS)); pixelSize = self->_pixelSize; } @@ -733,9 +735,11 @@ struct ActivityObserver: public Activity::Observer { self->is_updating.clear(); }); } - } interval:2500000]; + } interval:uint64_t(1000000000) / uint64_t(TICKS)]; } +#undef TICKS + - (void)stop { [_timer invalidate]; _timer = nil; From 709c229cd7cc137df83e6239b977295281727330 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 4 Feb 2020 20:19:46 -0500 Subject: [PATCH 06/25] Gets a bit more explicit with ScanTarget documentation. --- Outputs/ScanTarget.hpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Outputs/ScanTarget.hpp b/Outputs/ScanTarget.hpp index 139ddaa15..b4932939b 100644 --- a/Outputs/ScanTarget.hpp +++ b/Outputs/ScanTarget.hpp @@ -273,17 +273,21 @@ struct ScanTarget { /// data and scan allocations should be invalidated. virtual void will_change_owner() {} - /// Marks the end of an atomic set of data. Drawing is best effort, so the scan target should either: + /// Acts as a fence, marking the end of an atomic set of [begin/end]_[scan/data] calls] — all future pieces of + /// data will have no relation to scans prior to the submit() and all future scans will similarly have no relation to + /// prior runs of data. + /// + /// Drawing is defined to be best effort, so the scan target should either: /// /// (i) output everything received since the previous submit; or - /// (ii) output nothing. + /// (ii) output nothing. /// /// If there were any allocation failures — i.e. any nullptr responses to begin_data or /// begin_scan — then (ii) is a required response. But a scan target may also need to opt for (ii) /// for any other reason. /// /// The ScanTarget isn't bound to take any drawing action immediately; it may sit on submitted data for - /// as long as it feels is appropriate subject to an @c flush. + /// as long as it feels is appropriate, subject to a @c flush. virtual void submit() = 0; @@ -303,6 +307,12 @@ struct ScanTarget { /*! Provides a hint that the named event has occurred. + Guarantee: + * any announce acts as an implicit fence on data/scans, much as a submit(). + + Permitted ScanTarget implementation: + * ignore all output during retrace periods. + @param event The event. @param is_visible @c true if the output stream is visible immediately after this event; @c false otherwise. @param location The location of the event. From b41920990fbbff3f24e836c1ad88566d531e176c Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 4 Feb 2020 22:15:20 -0500 Subject: [PATCH 07/25] Moves `submit` step to end of line, rather than end of scan. --- Outputs/OpenGL/ScanTarget.cpp | 47 ++++++++++++----------------------- Outputs/OpenGL/ScanTarget.hpp | 2 -- Outputs/ScanTarget.hpp | 2 +- 3 files changed, 17 insertions(+), 34 deletions(-) diff --git a/Outputs/OpenGL/ScanTarget.cpp b/Outputs/OpenGL/ScanTarget.cpp index d70a54a0e..83fe1613b 100644 --- a/Outputs/OpenGL/ScanTarget.cpp +++ b/Outputs/OpenGL/ScanTarget.cpp @@ -251,21 +251,6 @@ void ScanTarget::will_change_owner() { vended_scan_ = nullptr; } -void ScanTarget::submit() { - if(allocation_has_failed_) { - // Reset all pointers to where they were; this also means - // the stencil won't be properly populated. - write_pointers_ = submit_pointers_.load(); - frame_is_complete_ = false; - } else { - // Advance submit pointer. - submit_pointers_.store(write_pointers_); - } - - // Continue defaulting to a failed allocation for as long as there isn't a line available. - allocation_has_failed_ = line_allocation_has_failed_; -} - void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t composite_amplitude) { // Forward the event to the display metrics tracker. display_metrics_.announce_event(event); @@ -273,10 +258,8 @@ void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display:: if(event == ScanTarget::Event::EndVerticalRetrace) { // The previous-frame-is-complete flag is subject to a two-slot queue because // measurement for *this* frame needs to begin now, meaning that the previous - // result needs to be put somewhere. Setting frame_is_complete_ back to true - // only after it has been put somewhere also doesn't work, since if the first - // few lines of a frame are skipped for any reason, there'll be nowhere to - // put it. + // result needs to be put somewhere — it'll be attached to the first successful + // line output. is_first_in_frame_ = true; previous_frame_was_complete_ = frame_is_complete_; frame_is_complete_ = true; @@ -299,24 +282,13 @@ void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display:: // Attempt to allocate a new line; note allocation failure if necessary. const auto next_line = uint16_t((write_pointers_.line + 1) % LineBufferHeight); if(next_line == read_pointers.line) { - line_allocation_has_failed_ = allocation_has_failed_ = true; + allocation_has_failed_ = true; active_line_ = nullptr; } else { - line_allocation_has_failed_ = false; write_pointers_.line = next_line; active_line_ = &line_buffer_[size_t(write_pointers_.line)]; } provided_scans_ = 0; - } else { - // Just check whether a new line is available now, if waiting. - if(line_allocation_has_failed_) { - const auto next_line = uint16_t((write_pointers_.line + 1) % LineBufferHeight); - if(next_line != read_pointers.line) { - line_allocation_has_failed_ = false; - write_pointers_.line = next_line; - active_line_ = &line_buffer_[size_t(write_pointers_.line)]; - } - } } if(active_line_) { @@ -329,6 +301,7 @@ void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display:: } } else { if(active_line_) { + // A successfully-allocated line is ending. active_line_->end_points[1].x = location.x; active_line_->end_points[1].y = location.y; active_line_->end_points[1].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace; @@ -345,6 +318,18 @@ void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display:: } #endif } + + // A line is complete; submit latest updates if nothing failed. + if(allocation_has_failed_) { + // Reset all pointers to where they were; this also means + // the stencil won't be properly populated. + write_pointers_ = submit_pointers_.load(); + frame_is_complete_ = false; + } else { + // Advance submit pointer. + submit_pointers_.store(write_pointers_); + } + allocation_has_failed_ = false; } output_is_visible_ = is_visible; } diff --git a/Outputs/OpenGL/ScanTarget.hpp b/Outputs/OpenGL/ScanTarget.hpp index a3531cdda..46300a0ce 100644 --- a/Outputs/OpenGL/ScanTarget.hpp +++ b/Outputs/OpenGL/ScanTarget.hpp @@ -77,7 +77,6 @@ class ScanTarget: public Outputs::Display::ScanTarget { void end_scan() final; uint8_t *begin_data(size_t required_length, size_t required_alignment) final; void end_data(size_t actual_length) final; - void submit() final; void announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t colour_burst_amplitude) final; void will_change_owner() final; @@ -188,7 +187,6 @@ class ScanTarget: public Outputs::Display::ScanTarget { // Track allocation failures. bool data_is_allocated_ = false; bool allocation_has_failed_ = false; - bool line_allocation_has_failed_ = false; // Receives scan target modals. Modals modals_; diff --git a/Outputs/ScanTarget.hpp b/Outputs/ScanTarget.hpp index b4932939b..94529142b 100644 --- a/Outputs/ScanTarget.hpp +++ b/Outputs/ScanTarget.hpp @@ -288,7 +288,7 @@ struct ScanTarget { /// /// The ScanTarget isn't bound to take any drawing action immediately; it may sit on submitted data for /// as long as it feels is appropriate, subject to a @c flush. - virtual void submit() = 0; + virtual void submit() {} /* From 5caf74b93025d7d378ca1cb98f25ca6bcd1b6301 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 4 Feb 2020 22:24:37 -0500 Subject: [PATCH 08/25] Corrects typo. --- Concurrency/BestEffortUpdater.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Concurrency/BestEffortUpdater.cpp b/Concurrency/BestEffortUpdater.cpp index 6eb3635a2..68ee39f97 100644 --- a/Concurrency/BestEffortUpdater.cpp +++ b/Concurrency/BestEffortUpdater.cpp @@ -78,9 +78,9 @@ void BestEffortUpdater::update_loop() { // Cap running at 1/5th of a second, to avoid doing a huge amount of work after any // brief system interruption. const double duration = std::min(double(integer_duration) / 1e9, 0.2); - const double elapsed_duraation = delegate->update(this, duration, has_skipped_, flags); + const double elapsed_duration = delegate->update(this, duration, has_skipped_, flags); - previous_time_point_ += int64_t(elapsed_duraation * 1e9); + previous_time_point_ += int64_t(elapsed_duration * 1e9); has_skipped_ = false; } } From e7410b8ed8b3bc821fb93c7bf359953ee6387a5a Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 4 Feb 2020 22:24:54 -0500 Subject: [PATCH 09/25] Uses objective clock for updates. --- OSBindings/Mac/Clock Signal/Machine/CSMachine.mm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index f1e3a31a7..82c407710 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -720,10 +720,16 @@ struct ActivityObserver: public Activity::Observer { #define TICKS 600 - (void)start { + __block auto lastTime = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + _timer = [[CSHighPrecisionTimer alloc] initWithTask:^{ + const auto timeNow = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + const auto duration = timeNow - lastTime; + lastTime = timeNow; + CGSize pixelSize; @synchronized(self) { - self->_machine->crt_machine()->run_for(1.0 / double(TICKS)); + self->_machine->crt_machine()->run_for((double)duration / 1e9); pixelSize = self->_pixelSize; } From e1cbad0b6d22dabad149abb9d905c435425ddc44 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 4 Feb 2020 23:08:25 -0500 Subject: [PATCH 10/25] Ensures new displayLinkDelegates get a nudge with the initial display link. --- .../Mac/Clock Signal/Views/CSOpenGLView.m | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m index eb783f3a1..3538ff655 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m @@ -22,6 +22,8 @@ NSTrackingArea *_mouseTrackingArea; NSTimer *_mouseHideTimer; BOOL _mouseIsCaptured; + + id _displayLinkDelegate; } - (void)prepareOpenGL { @@ -70,6 +72,23 @@ CVDisplayLinkStart(_displayLink); } +- (void)setDisplayLinkDelegate:(id)displayLinkDelegate { + @synchronized(self) { + _displayLinkDelegate = displayLinkDelegate; + + // Seed with the current _displayLink, if any. + if(_displayLink) { + [displayLinkDelegate openGLView:self didUpdateDisplayLink:_displayLink]; + } + } +} + +- (id)displayLinkDelegate { + @synchronized(self) { + return _displayLinkDelegate; + } +} + static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) { CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext; From 14d976eecbf18d2d36b6ddf8c1450ed116300285 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 4 Feb 2020 23:08:54 -0500 Subject: [PATCH 11/25] Starts towards an implementation of time warping. --- .../Mac/Clock Signal/Machine/CSMachine.mm | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index 82c407710..18e62cd47 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -153,7 +153,9 @@ struct ActivityObserver: public Activity::Observer { CSHighPrecisionTimer *_timer; CGSize _pixelSize; - std::atomic_flag is_updating; + std::atomic_flag _isUpdating; + int64_t _syncTime; + double _refreshPeriod; std::unique_ptr _scanTarget; } @@ -207,7 +209,7 @@ struct ActivityObserver: public Activity::Observer { _speakerDelegate.machineAccessLock = _delegateMachineAccessLock; _joystickMachine = _machine->joystick_machine(); - is_updating.clear(); + _isUpdating.clear(); } return self; } @@ -705,11 +707,14 @@ struct ActivityObserver: public Activity::Observer { #pragma mark - Timer - (void)openGLView:(CSOpenGLView *)view didUpdateDisplayLink:(CVDisplayLinkRef)displayLink { + _refreshPeriod = CVDisplayLinkGetActualOutputVideoRefreshPeriod(displayLink); + NSLog(@"Refresh period: %0.5f (%0.5f)", _refreshPeriod, 1.0 / _refreshPeriod); } - (void)openGLViewDisplayLinkDidFire:(CSOpenGLView *)view { CGSize pixelSize = view.backingSize; @synchronized(self) { + _syncTime = std::chrono::high_resolution_clock::now().time_since_epoch().count(); _pixelSize = pixelSize; } [self.view performWithGLContext:^{ @@ -725,22 +730,48 @@ struct ActivityObserver: public Activity::Observer { _timer = [[CSHighPrecisionTimer alloc] initWithTask:^{ const auto timeNow = std::chrono::high_resolution_clock::now().time_since_epoch().count(); const auto duration = timeNow - lastTime; - lastTime = timeNow; CGSize pixelSize; + int64_t syncTime; @synchronized(self) { + syncTime = self->_syncTime; + + // If this tick includes vsync then inspect the machine. + BOOL splitAndSync = NO; + if(timeNow >= self->_syncTime && lastTime < self->_syncTime) { + // Grab the scan status and check out the machine's current frame time. + // If it's stable and within 3% of a non-zero integer multiple of the + // display rate, consider an adjustment. + const auto scan_status = self->_machine->crt_machine()->get_scan_status(); + if(scan_status.field_duration_gradient < 0.00001) { + auto ratio = self->_refreshPeriod / scan_status.field_duration; + if(ratio > 1.5) { + ratio = fmod(ratio, 1.0); + if(ratio < 0.5) ratio += 1.0; + } + splitAndSync = ratio <= 1.03 && ratio >= 0.97; +// if(splitAndSync) { +// NSLog(@"+: %0.3f %0.3f [%0.5f]", scan_status.field_duration / self->_refreshPeriod, ratio, scan_status.field_duration_gradient); +// } else { +// NSLog(@"-: %0.3f %0.3f [%0.5f]", scan_status.field_duration / self->_refreshPeriod, ratio, scan_status.field_duration_gradient); +// } + } // else NSLog(@"e"); + } + self->_machine->crt_machine()->run_for((double)duration / 1e9); pixelSize = self->_pixelSize; } - if(!self->is_updating.test_and_set()) { + if(!self->_isUpdating.test_and_set()) { dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ [self.view performWithGLContext:^{ self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height); } flushDrawable:NO]; - self->is_updating.clear(); + self->_isUpdating.clear(); }); } + + lastTime = timeNow; } interval:uint64_t(1000000000) / uint64_t(TICKS)]; } From f95b07efeaac9a3f7726a0856843c4036fd1ac3c Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 6 Feb 2020 23:35:03 -0500 Subject: [PATCH 12/25] Continues edging towards raster racing and/or time warping. --- .../Mac/Clock Signal/Machine/CSMachine.mm | 72 ++++++++++++++----- .../Mac/Clock Signal/Views/CSOpenGLView.h | 2 +- .../Mac/Clock Signal/Views/CSOpenGLView.m | 2 +- Outputs/CRT/CRT.cpp | 5 +- Outputs/CRT/Internals/Flywheel.hpp | 17 +++++ 5 files changed, 73 insertions(+), 25 deletions(-) diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index 18e62cd47..ee709e9d1 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -155,6 +155,7 @@ struct ActivityObserver: public Activity::Observer { CGSize _pixelSize; std::atomic_flag _isUpdating; int64_t _syncTime; + int64_t _timeDiff; double _refreshPeriod; std::unique_ptr _scanTarget; @@ -711,12 +712,26 @@ struct ActivityObserver: public Activity::Observer { NSLog(@"Refresh period: %0.5f (%0.5f)", _refreshPeriod, 1.0 / _refreshPeriod); } -- (void)openGLViewDisplayLinkDidFire:(CSOpenGLView *)view { +- (void)openGLViewDisplayLinkDidFire:(CSOpenGLView *)view now:(const CVTimeStamp *)now outputTime:(const CVTimeStamp *)outputTime { + // First order of business: grab a timestamp. + const auto timeNow = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + CGSize pixelSize = view.backingSize; @synchronized(self) { - _syncTime = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + // Store a means to map from CVTimeStamp.hostTime to std::chrono::high_resolution_clock; + // there is an extremely dodgy assumption here that both are in the same units (and, below, that both as in ns). + if(!_timeDiff) { + _timeDiff = int64_t(now->hostTime) - int64_t(timeNow); + } + + // Store the next end-of-frame time. TODO: and start of next and implied visible duration, if raster racing? + _syncTime = int64_t(now->hostTime) + _timeDiff; + + // Also crib the current view pixel size. _pixelSize = pixelSize; } + + // Draw the current output. (TODO: do this within the timer if either raster racing or, at least, sync matching). [self.view performWithGLContext:^{ self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height); } flushDrawable:YES]; @@ -728,44 +743,63 @@ struct ActivityObserver: public Activity::Observer { __block auto lastTime = std::chrono::high_resolution_clock::now().time_since_epoch().count(); _timer = [[CSHighPrecisionTimer alloc] initWithTask:^{ + // Grab the time now and, therefore, the amount of time since the timer last fired. const auto timeNow = std::chrono::high_resolution_clock::now().time_since_epoch().count(); const auto duration = timeNow - lastTime; + CGSize pixelSize; - int64_t syncTime; + BOOL splitAndSync = NO; @synchronized(self) { - syncTime = self->_syncTime; - // If this tick includes vsync then inspect the machine. - BOOL splitAndSync = NO; if(timeNow >= self->_syncTime && lastTime < self->_syncTime) { // Grab the scan status and check out the machine's current frame time. // If it's stable and within 3% of a non-zero integer multiple of the - // display rate, consider an adjustment. + // display rate, mark this time window to be split over the sync. const auto scan_status = self->_machine->crt_machine()->get_scan_status(); if(scan_status.field_duration_gradient < 0.00001) { auto ratio = self->_refreshPeriod / scan_status.field_duration; - if(ratio > 1.5) { - ratio = fmod(ratio, 1.0); - if(ratio < 0.5) ratio += 1.0; + const double integerRatio = round(ratio); + if(integerRatio > 0.0) { + ratio /= integerRatio; + splitAndSync = ratio <= 1.03 && ratio >= 0.97; } - splitAndSync = ratio <= 1.03 && ratio >= 0.97; -// if(splitAndSync) { -// NSLog(@"+: %0.3f %0.3f [%0.5f]", scan_status.field_duration / self->_refreshPeriod, ratio, scan_status.field_duration_gradient); -// } else { -// NSLog(@"-: %0.3f %0.3f [%0.5f]", scan_status.field_duration / self->_refreshPeriod, ratio, scan_status.field_duration_gradient); -// } - } // else NSLog(@"e"); + } } - self->_machine->crt_machine()->run_for((double)duration / 1e9); + // If the time window is being split, run up to the split, then check out machine speed, possibly + // adjusting multiplier, then run after the split. + if(splitAndSync) { + self->_machine->crt_machine()->run_for((double)(self->_syncTime - lastTime) / 1e9); +// NSLog(@"%0.4f [%d / %0.4f]\n", self->_machine->crt_machine()->get_scan_status().current_position, apple_cycles, CRTMachine::Machine::machine_duration); + apple_cycles = 0; + CRTMachine::Machine::machine_duration = 0.0; + self->_machine->crt_machine()->run_for((double)(timeNow - self->_syncTime) / 1e9); + } else { + self->_machine->crt_machine()->run_for((double)duration / 1e9); + } pixelSize = self->_pixelSize; } - if(!self->_isUpdating.test_and_set()) { + // If this was not a split-and-sync then dispatch the update request asynchronously, unless + // there is an earlier one not yet finished, in which case don't worry about it for now. + // + // If it was a split-and-sync then spin until it is safe to dispatch, and dispatch with + // a concluding draw. Implicit assumption here: whatever is left to be done in the final window + // can be done within the retrace period. + auto wasUpdating = self->_isUpdating.test_and_set(); +// if(wasUpdating && splitAndSync) { +// while(self->_isUpdating.test_and_set()); +// wasUpdating = false; +// } + if(!wasUpdating) { dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ [self.view performWithGLContext:^{ self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height); + +// if(splitAndSync) { +// self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height); +// } } flushDrawable:NO]; self->_isUpdating.clear(); }); diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h index a46d64752..3e1919304 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h @@ -120,7 +120,7 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { /*! Informs the delegate that the display link has fired. */ -- (void)openGLViewDisplayLinkDidFire:(nonnull CSOpenGLView *)view; +- (void)openGLViewDisplayLinkDidFire:(nonnull CSOpenGLView *)view now:(nonnull const CVTimeStamp *)now outputTime:(nonnull const CVTimeStamp *)outputTime; @end diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m index 3538ff655..2cbf7ed67 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m @@ -93,7 +93,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext; [view checkDisplayLink]; - [view.displayLinkDelegate openGLViewDisplayLinkDidFire:view]; + [view.displayLinkDelegate openGLViewDisplayLinkDidFire:view now:now outputTime:outputTime]; /* Do not touch the display link from after this call; there's a bit of a race condition with setupDisplayLink. Specifically: Apple provides CVDisplayLinkStop but a call to that merely prevents future calls to the callback, diff --git a/Outputs/CRT/CRT.cpp b/Outputs/CRT/CRT.cpp index 8d8c4f3fb..b624fa6cf 100644 --- a/Outputs/CRT/CRT.cpp +++ b/Outputs/CRT/CRT.cpp @@ -489,10 +489,7 @@ Outputs::Display::ScanStatus CRT::get_scaled_scan_status() const { status.field_duration = float(vertical_flywheel_->get_locked_period()) / float(time_multiplier_); status.field_duration_gradient = float(vertical_flywheel_->get_last_period_adjustment()) / float(time_multiplier_); status.retrace_duration = float(vertical_flywheel_->get_retrace_period()) / float(time_multiplier_); - status.current_position = - std::max(0.0f, - float(vertical_flywheel_->get_current_output_position()) / (float(vertical_flywheel_->get_locked_period()) * float(time_multiplier_)) - ); + status.current_position = float(vertical_flywheel_->get_current_phase()) / float(vertical_flywheel_->get_locked_scan_period()); status.hsync_count = vertical_flywheel_->get_number_of_retraces(); return status; } diff --git a/Outputs/CRT/Internals/Flywheel.hpp b/Outputs/CRT/Internals/Flywheel.hpp index 471fad8f3..43b612800 100644 --- a/Outputs/CRT/Internals/Flywheel.hpp +++ b/Outputs/CRT/Internals/Flywheel.hpp @@ -139,6 +139,16 @@ struct Flywheel { return counter_ - retrace_time_; } + /*! + Returns the current 'phase' — 0 is the start of the display; a count up to 0 from a negative number represents + the retrace period and it will then count up to get_locked_scan_period(). + + @returns The current output position. + */ + inline int get_current_phase() { + return counter_ - retrace_time_; + } + /*! @returns the amount of time since retrace last began. Time then counts monotonically up from zero. */ @@ -160,6 +170,13 @@ struct Flywheel { return standard_period_ - retrace_time_; } + /*! + @returns the actual length of the scan period (excluding retrace). + */ + inline int get_locked_scan_period() { + return expected_next_sync_ - retrace_time_; + } + /*! @returns the expected length of a complete scan and retrace cycle. */ From 09132306e43196909920f6a3babde2e5080b045f Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 6 Feb 2020 23:35:23 -0500 Subject: [PATCH 13/25] Removes two temporary debugging steps. --- OSBindings/Mac/Clock Signal/Machine/CSMachine.mm | 2 -- 1 file changed, 2 deletions(-) diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index ee709e9d1..a8a3a8395 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -772,8 +772,6 @@ struct ActivityObserver: public Activity::Observer { if(splitAndSync) { self->_machine->crt_machine()->run_for((double)(self->_syncTime - lastTime) / 1e9); // NSLog(@"%0.4f [%d / %0.4f]\n", self->_machine->crt_machine()->get_scan_status().current_position, apple_cycles, CRTMachine::Machine::machine_duration); - apple_cycles = 0; - CRTMachine::Machine::machine_duration = 0.0; self->_machine->crt_machine()->run_for((double)(timeNow - self->_syncTime) / 1e9); } else { self->_machine->crt_machine()->run_for((double)duration / 1e9); From f615d096ca2adea98f673c08c083beffce5ea962 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 8 Feb 2020 15:03:18 -0500 Subject: [PATCH 14/25] Switch to obtaining refresh periods ephemerally. Which simplifies the necessary delegate protocol. --- .../Mac/Clock Signal/Machine/CSMachine.mm | 8 +++---- .../Mac/Clock Signal/Views/CSOpenGLView.h | 6 ----- .../Mac/Clock Signal/Views/CSOpenGLView.m | 22 ------------------- 3 files changed, 3 insertions(+), 33 deletions(-) diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index a8a3a8395..c88e2fab6 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -707,11 +707,6 @@ struct ActivityObserver: public Activity::Observer { #pragma mark - Timer -- (void)openGLView:(CSOpenGLView *)view didUpdateDisplayLink:(CVDisplayLinkRef)displayLink { - _refreshPeriod = CVDisplayLinkGetActualOutputVideoRefreshPeriod(displayLink); - NSLog(@"Refresh period: %0.5f (%0.5f)", _refreshPeriod, 1.0 / _refreshPeriod); -} - - (void)openGLViewDisplayLinkDidFire:(CSOpenGLView *)view now:(const CVTimeStamp *)now outputTime:(const CVTimeStamp *)outputTime { // First order of business: grab a timestamp. const auto timeNow = std::chrono::high_resolution_clock::now().time_since_epoch().count(); @@ -729,6 +724,9 @@ struct ActivityObserver: public Activity::Observer { // Also crib the current view pixel size. _pixelSize = pixelSize; + + // Set the current refresh period. + _refreshPeriod = double(now->videoRefreshPeriod) / double(now->videoTimeScale); } // Draw the current output. (TODO: do this within the timer if either raster racing or, at least, sync matching). diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h index 3e1919304..80c64f280 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h @@ -111,12 +111,6 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { */ @protocol CSOpenGLViewDisplayLinkDelegate -/*! - Informs the delegate that from now on, the display link @c displayLink will be used for update notifications - and/or that the frequency or phase or @c displayLink has changed. -*/ -- (void)openGLView:(nonnull CSOpenGLView *)view didUpdateDisplayLink:(nonnull CVDisplayLinkRef)displayLink; - /*! Informs the delegate that the display link has fired. */ diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m index 2cbf7ed67..a974c05bb 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m @@ -22,8 +22,6 @@ NSTrackingArea *_mouseTrackingArea; NSTimer *_mouseHideTimer; BOOL _mouseIsCaptured; - - id _displayLinkDelegate; } - (void)prepareOpenGL { @@ -65,30 +63,10 @@ CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj]; CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(_displayLink, cglContext, cglPixelFormat); - // Give a shout-out. - [self.displayLinkDelegate openGLView:self didUpdateDisplayLink:_displayLink]; - // Activate the display link CVDisplayLinkStart(_displayLink); } -- (void)setDisplayLinkDelegate:(id)displayLinkDelegate { - @synchronized(self) { - _displayLinkDelegate = displayLinkDelegate; - - // Seed with the current _displayLink, if any. - if(_displayLink) { - [displayLinkDelegate openGLView:self didUpdateDisplayLink:_displayLink]; - } - } -} - -- (id)displayLinkDelegate { - @synchronized(self) { - return _displayLinkDelegate; - } -} - static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) { CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext; From 7c0f3bb2378c3873f320c3bd6c6b1e9eaf421065 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 8 Feb 2020 18:01:48 -0500 Subject: [PATCH 15/25] Gets to slightly adjusting execution speed and matching up respective vertical syncs. I probably still need to move the ->draw inline. --- .../xcschemes/Clock Signal.xcscheme | 2 +- .../Mac/Clock Signal/Machine/CSMachine.mm | 34 +++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme index a99eb0a5d..63be2c037 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme +++ b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme @@ -67,7 +67,7 @@ _machine->crt_machine()->get_scan_status(); + double ratio = 1.0; if(scan_status.field_duration_gradient < 0.00001) { - auto ratio = self->_refreshPeriod / scan_status.field_duration; + ratio = self->_refreshPeriod / scan_status.field_duration; const double integerRatio = round(ratio); if(integerRatio > 0.0) { ratio /= integerRatio; - splitAndSync = ratio <= 1.03 && ratio >= 0.97; + + constexpr double maximumAdjustment = 1.03; + splitAndSync = ratio <= maximumAdjustment && ratio >= 1 / maximumAdjustment; } } + + // If the time window is being split, run up to the split, then check out machine speed, possibly + // adjusting multiplier, then run after the split. + if(splitAndSync) { + self->_machine->crt_machine()->run_for((double)(self->_syncTime - lastTime) / 1e9); + + // The host versus emulated ratio is calculated based on the current perceived frame duration of the machine. + // Either that number is exactly correct or it's already the result of some sort of low-pass filter. So there's + // no benefit to second guessing it here — just take it to be correct. + // + // ... with one slight caveat, which is that it is desireable to adjust phase here, to align vertical sync points. + // So the set speed multiplier may be adjusted slightly to aim for that. + double speed_multiplier = 1.0 / ratio; + if(scan_status.current_position > 0.0) { + constexpr double adjustmentRatio = 1.01; + if(scan_status.current_position < 0.5) speed_multiplier /= adjustmentRatio; + else speed_multiplier *= adjustmentRatio; + } + self->_machine->crt_machine()->set_speed_multiplier(speed_multiplier); + self->_machine->crt_machine()->run_for((double)(timeNow - self->_syncTime) / 1e9); + } } // If the time window is being split, run up to the split, then check out machine speed, possibly // adjusting multiplier, then run after the split. - if(splitAndSync) { - self->_machine->crt_machine()->run_for((double)(self->_syncTime - lastTime) / 1e9); -// NSLog(@"%0.4f [%d / %0.4f]\n", self->_machine->crt_machine()->get_scan_status().current_position, apple_cycles, CRTMachine::Machine::machine_duration); - self->_machine->crt_machine()->run_for((double)(timeNow - self->_syncTime) / 1e9); - } else { + if(!splitAndSync) { self->_machine->crt_machine()->run_for((double)duration / 1e9); } pixelSize = self->_pixelSize; From b76a5870b3e066b34531951accb90656e47af325 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 8 Feb 2020 18:07:13 -0500 Subject: [PATCH 16/25] Moves drawing into the next timer tick after retrace if sync locked. ... which should mean it occurs within 1/600th of a second of announced retrace, which I assume always will be less than the retrace period. So: does the frame buffer update during retrace. This should completely eliminate tearing for machines that can be synced to the native output rate. --- .../xcschemes/Clock Signal.xcscheme | 2 +- .../Mac/Clock Signal/Machine/CSMachine.mm | 30 ++++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme index 63be2c037..a99eb0a5d 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme +++ b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme @@ -67,7 +67,7 @@ _scanTarget; } @@ -712,6 +713,7 @@ struct ActivityObserver: public Activity::Observer { const auto timeNow = std::chrono::high_resolution_clock::now().time_since_epoch().count(); CGSize pixelSize = view.backingSize; + BOOL isSyncLocking; @synchronized(self) { // Store a means to map from CVTimeStamp.hostTime to std::chrono::high_resolution_clock; // there is an extremely dodgy assumption here that both are in the same units (and, below, that both as in ns). @@ -727,12 +729,17 @@ struct ActivityObserver: public Activity::Observer { // Set the current refresh period. _refreshPeriod = double(now->videoRefreshPeriod) / double(now->videoTimeScale); + + // Determine where responsibility lies for drawing. + isSyncLocking = _isSyncLocking; } // Draw the current output. (TODO: do this within the timer if either raster racing or, at least, sync matching). - [self.view performWithGLContext:^{ - self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height); - } flushDrawable:YES]; + if(!isSyncLocking) { + [self.view performWithGLContext:^{ + self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height); + } flushDrawable:YES]; + } } #define TICKS 600 @@ -766,6 +773,7 @@ struct ActivityObserver: public Activity::Observer { splitAndSync = ratio <= maximumAdjustment && ratio >= 1 / maximumAdjustment; } } + self->_isSyncLocking = splitAndSync; // If the time window is being split, run up to the split, then check out machine speed, possibly // adjusting multiplier, then run after the split. @@ -804,19 +812,19 @@ struct ActivityObserver: public Activity::Observer { // a concluding draw. Implicit assumption here: whatever is left to be done in the final window // can be done within the retrace period. auto wasUpdating = self->_isUpdating.test_and_set(); -// if(wasUpdating && splitAndSync) { -// while(self->_isUpdating.test_and_set()); -// wasUpdating = false; -// } + if(wasUpdating && splitAndSync) { + while(self->_isUpdating.test_and_set()); + wasUpdating = false; + } if(!wasUpdating) { dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ [self.view performWithGLContext:^{ self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height); -// if(splitAndSync) { -// self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height); -// } - } flushDrawable:NO]; + if(splitAndSync) { + self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height); + } + } flushDrawable:splitAndSync]; self->_isUpdating.clear(); }); } From c26c8992ae37f89a80891a44a91fc55d1615a441 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 8 Feb 2020 21:27:04 -0500 Subject: [PATCH 17/25] Reintroduces joystick support; eliminates CSBestEffortUpdater. --- .../Clock Signal.xcodeproj/project.pbxproj | 14 --- .../ClockSignal-Bridging-Header.h | 1 - .../Mac/Clock Signal/Machine/CSMachine.h | 2 - .../Mac/Clock Signal/Machine/CSMachine.mm | 108 ++++++++++-------- .../Updater/CSBestEffortUpdater.h | 27 ----- .../Updater/CSBestEffortUpdater.mm | 51 --------- 6 files changed, 61 insertions(+), 142 deletions(-) delete mode 100644 OSBindings/Mac/Clock Signal/Updater/CSBestEffortUpdater.h delete mode 100644 OSBindings/Mac/Clock Signal/Updater/CSBestEffortUpdater.mm diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index f4165bef8..751840b21 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -812,7 +812,6 @@ 4BD4A8D01E077FD20020D856 /* PCMTrackTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */; }; 4BD5D2682199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */; }; 4BD5D2692199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */; }; - 4BD5F1951D13528900631CD1 /* CSBestEffortUpdater.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5F1941D13528900631CD1 /* CSBestEffortUpdater.mm */; }; 4BD61664206B2AC800236112 /* QuickLoadOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BD61662206B2AC700236112 /* QuickLoadOptions.xib */; }; 4BD67DCB209BE4D700AB2146 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD67DCA209BE4D600AB2146 /* StaticAnalyser.cpp */; }; 4BD67DCC209BE4D700AB2146 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD67DCA209BE4D600AB2146 /* StaticAnalyser.cpp */; }; @@ -1689,8 +1688,6 @@ 4BD468F61D8DF41D0084958B /* 1770.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = 1770.hpp; path = 1770/1770.hpp; sourceTree = ""; }; 4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PCMTrackTests.mm; sourceTree = ""; }; 4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ScanTargetGLSLFragments.cpp; sourceTree = ""; }; - 4BD5F1931D13528900631CD1 /* CSBestEffortUpdater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CSBestEffortUpdater.h; path = Updater/CSBestEffortUpdater.h; sourceTree = ""; }; - 4BD5F1941D13528900631CD1 /* CSBestEffortUpdater.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = CSBestEffortUpdater.mm; path = Updater/CSBestEffortUpdater.mm; sourceTree = ""; }; 4BD601A920D89F2A00CBCE57 /* Log.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Log.hpp; path = ../../Outputs/Log.hpp; sourceTree = ""; }; 4BD61663206B2AC700236112 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/QuickLoadOptions.xib"; sourceTree = SOURCE_ROOT; }; 4BD67DC9209BE4D600AB2146 /* StaticAnalyser.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = StaticAnalyser.hpp; sourceTree = ""; }; @@ -3276,7 +3273,6 @@ 4BB73EAA1B587A5100552FC2 /* MainMenu.xib */, 4BE5F85A1C3E1C2500C43F01 /* Resources */, 4BDA00DB22E60EE900AC3CD0 /* ROMRequester */, - 4BD5F1961D1352A000631CD1 /* Updater */, 4B55CE5A1C3B7D6F0093A61B /* Views */, ); path = "Clock Signal"; @@ -3624,15 +3620,6 @@ name = 1770; sourceTree = ""; }; - 4BD5F1961D1352A000631CD1 /* Updater */ = { - isa = PBXGroup; - children = ( - 4BD5F1931D13528900631CD1 /* CSBestEffortUpdater.h */, - 4BD5F1941D13528900631CD1 /* CSBestEffortUpdater.mm */, - ); - name = Updater; - sourceTree = ""; - }; 4BD67DC8209BE4D600AB2146 /* DiskII */ = { isa = PBXGroup; children = ( @@ -4493,7 +4480,6 @@ 4B4518A41F75FD1C00926311 /* OricMFMDSK.cpp in Sources */, 4B4B1A3C200198CA00A0F866 /* KonamiSCC.cpp in Sources */, 4BB0A65B2044FD3000FB3688 /* SN76489.cpp in Sources */, - 4BD5F1951D13528900631CD1 /* CSBestEffortUpdater.mm in Sources */, 4B894532201967B4007DE474 /* 6502.cpp in Sources */, 4BDB61EC203285AE0048AF91 /* Atari2600OptionsPanel.swift in Sources */, 4BBB70A8202014E2002FE009 /* MultiCRTMachine.cpp in Sources */, diff --git a/OSBindings/Mac/Clock Signal/ClockSignal-Bridging-Header.h b/OSBindings/Mac/Clock Signal/ClockSignal-Bridging-Header.h index c39778ffd..ebbf066d9 100644 --- a/OSBindings/Mac/Clock Signal/ClockSignal-Bridging-Header.h +++ b/OSBindings/Mac/Clock Signal/ClockSignal-Bridging-Header.h @@ -13,7 +13,6 @@ #import "CSOpenGLView.h" #import "CSROMReceiverView.h" -#import "CSBestEffortUpdater.h" #import "CSJoystickManager.h" #import "NSData+CRC32.h" diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h index 18bda6979..844f20243 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h @@ -57,8 +57,6 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) { */ - (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result missingROMs:(nullable inout NSMutableArray *)missingROMs NS_DESIGNATED_INITIALIZER; -- (NSTimeInterval)runForInterval:(NSTimeInterval)interval untilEvent:(int)events; - - (float)idealSamplingRateFromRange:(NSRange)range; - (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize; diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index 19f8d1292..dad64bee8 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -159,6 +159,8 @@ struct ActivityObserver: public Activity::Observer { double _refreshPeriod; BOOL _isSyncLocking; + NSTimer *_joystickTimer; + std::unique_ptr _scanTarget; } @@ -211,6 +213,7 @@ struct ActivityObserver: public Activity::Observer { _speakerDelegate.machineAccessLock = _delegateMachineAccessLock; _joystickMachine = _machine->joystick_machine(); + [self updateJoystickTimer]; _isUpdating.clear(); } return self; @@ -225,6 +228,8 @@ struct ActivityObserver: public Activity::Observer { } - (void)dealloc { + [_joystickTimer invalidate]; + // The two delegate's references to this machine are nilled out here because close_output may result // in a data flush, which might cause an audio callback, which could cause the audio queue to decide // that it's out of data, resulting in an attempt further to run the machine while it is dealloc'ing. @@ -270,57 +275,64 @@ struct ActivityObserver: public Activity::Observer { } } -- (NSTimeInterval)runForInterval:(NSTimeInterval)interval untilEvent:(int)events { +- (void)updateJoystickTimer { + // Joysticks updates are scheduled for a nominal 200 polls/second, using a plain old NSTimer. + if(_joystickMachine && _joystickManager) { + _joystickTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / 200.0 target:self selector:@selector(updateJoysticks) userInfo:nil repeats:YES]; + } else { + [_joystickTimer invalidate]; + _joystickTimer = nil; + } +} + +- (void)updateJoysticks { + [_joystickManager update]; + + // TODO: configurable mapping from physical joypad inputs to machine inputs. + // Until then, apply a default mapping. + @synchronized(self) { - if(_joystickMachine && _joystickManager) { - [_joystickManager update]; + size_t c = 0; + auto &machine_joysticks = _joystickMachine->get_joysticks(); + for(CSJoystick *joystick in _joystickManager.joysticks) { + size_t target = c % machine_joysticks.size(); + ++c; - // TODO: configurable mapping from physical joypad inputs to machine inputs. - // Until then, apply a default mapping. - - size_t c = 0; - auto &machine_joysticks = _joystickMachine->get_joysticks(); - for(CSJoystick *joystick in _joystickManager.joysticks) { - size_t target = c % machine_joysticks.size(); - ++c; - - // Post the first two analogue axes presented by the controller as horizontal and vertical inputs, - // unless the user seems to be using a hat. - // SDL will return a value in the range [-32768, 32767], so map from that to [0, 1.0] - if(!joystick.hats.count || !joystick.hats[0].direction) { - if(joystick.axes.count > 0) { - const float x_axis = joystick.axes[0].position; - machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Horizontal), x_axis); - } - if(joystick.axes.count > 1) { - const float y_axis = joystick.axes[1].position; - machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Vertical), y_axis); - } - } else { - // Forward hats as directions; hats always override analogue inputs. - for(CSJoystickHat *hat in joystick.hats) { - machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Up), !!(hat.direction & CSJoystickHatDirectionUp)); - machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Down), !!(hat.direction & CSJoystickHatDirectionDown)); - machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Left), !!(hat.direction & CSJoystickHatDirectionLeft)); - machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Right), !!(hat.direction & CSJoystickHatDirectionRight)); - } + // Post the first two analogue axes presented by the controller as horizontal and vertical inputs, + // unless the user seems to be using a hat. + // SDL will return a value in the range [-32768, 32767], so map from that to [0, 1.0] + if(!joystick.hats.count || !joystick.hats[0].direction) { + if(joystick.axes.count > 0) { + const float x_axis = joystick.axes[0].position; + machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Horizontal), x_axis); } + if(joystick.axes.count > 1) { + const float y_axis = joystick.axes[1].position; + machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Vertical), y_axis); + } + } else { + // Forward hats as directions; hats always override analogue inputs. + for(CSJoystickHat *hat in joystick.hats) { + machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Up), !!(hat.direction & CSJoystickHatDirectionUp)); + machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Down), !!(hat.direction & CSJoystickHatDirectionDown)); + machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Left), !!(hat.direction & CSJoystickHatDirectionLeft)); + machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Right), !!(hat.direction & CSJoystickHatDirectionRight)); + } + } - // Forward all fire buttons, mapping as a function of index. - if(machine_joysticks[target]->get_number_of_fire_buttons()) { - std::vector button_states((size_t)machine_joysticks[target]->get_number_of_fire_buttons()); - for(CSJoystickButton *button in joystick.buttons) { - if(button.isPressed) button_states[(size_t)(((int)button.index - 1) % machine_joysticks[target]->get_number_of_fire_buttons())] = true; - } - for(size_t index = 0; index < button_states.size(); ++index) { - machine_joysticks[target]->set_input( - Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Fire, index), - button_states[index]); - } + // Forward all fire buttons, mapping as a function of index. + if(machine_joysticks[target]->get_number_of_fire_buttons()) { + std::vector button_states((size_t)machine_joysticks[target]->get_number_of_fire_buttons()); + for(CSJoystickButton *button in joystick.buttons) { + if(button.isPressed) button_states[(size_t)(((int)button.index - 1) % machine_joysticks[target]->get_number_of_fire_buttons())] = true; + } + for(size_t index = 0; index < button_states.size(); ++index) { + machine_joysticks[target]->set_input( + Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Fire, index), + button_states[index]); } } } - return _machine->crt_machine()->run_until(interval, events); } } @@ -387,15 +399,17 @@ struct ActivityObserver: public Activity::Observer { } - (void)setJoystickManager:(CSJoystickManager *)joystickManager { - @synchronized(self) { - _joystickManager = joystickManager; - if(_joystickMachine) { + _joystickManager = joystickManager; + if(_joystickMachine) { + @synchronized(self) { auto &machine_joysticks = _joystickMachine->get_joysticks(); for(const auto &joystick: machine_joysticks) { joystick->reset_all_inputs(); } } } + + [self updateJoystickTimer]; } - (void)setKey:(uint16_t)key characters:(NSString *)characters isPressed:(BOOL)isPressed { diff --git a/OSBindings/Mac/Clock Signal/Updater/CSBestEffortUpdater.h b/OSBindings/Mac/Clock Signal/Updater/CSBestEffortUpdater.h deleted file mode 100644 index 970104107..000000000 --- a/OSBindings/Mac/Clock Signal/Updater/CSBestEffortUpdater.h +++ /dev/null @@ -1,27 +0,0 @@ -// -// CSBestEffortUpdater.h -// Clock Signal -// -// Created by Thomas Harte on 16/06/2016. -// Copyright 2016 Thomas Harte. All rights reserved. -// - -#import -#import - -#import "CSMachine.h" - -// The following is coupled to the definitions in CRTMachine.hpp, but exposed here -// for the benefit of Swift. -typedef NS_ENUM(NSInteger, CSBestEffortUpdaterEvent) { - CSBestEffortUpdaterEventAudioNeeded = 1 << 0 -}; - -@interface CSBestEffortUpdater : NSObject - -- (void)update; -- (void)updateWithEvent:(CSBestEffortUpdaterEvent)event; -- (void)flush; -- (void)setMachine:(CSMachine *)machine; - -@end diff --git a/OSBindings/Mac/Clock Signal/Updater/CSBestEffortUpdater.mm b/OSBindings/Mac/Clock Signal/Updater/CSBestEffortUpdater.mm deleted file mode 100644 index a0ba3e21f..000000000 --- a/OSBindings/Mac/Clock Signal/Updater/CSBestEffortUpdater.mm +++ /dev/null @@ -1,51 +0,0 @@ -// -// CSBestEffortUpdater.m -// Clock Signal -// -// Created by Thomas Harte on 16/06/2016. -// Copyright 2016 Thomas Harte. All rights reserved. -// - -#import "CSBestEffortUpdater.h" - -#include "BestEffortUpdater.hpp" - -struct UpdaterDelegate: public Concurrency::BestEffortUpdater::Delegate { - __weak CSMachine *machine; - - Time::Seconds update(Concurrency::BestEffortUpdater *updater, Time::Seconds seconds, bool did_skip_previous_update, int flags) final { - return [machine runForInterval:seconds untilEvent:flags]; - } -}; - -@implementation CSBestEffortUpdater { - Concurrency::BestEffortUpdater _updater; - UpdaterDelegate _updaterDelegate; -} - -- (instancetype)init { - self = [super init]; - if(self) { - _updater.set_delegate(&_updaterDelegate); - } - return self; -} - -- (void)update { - _updater.update(); -} - -- (void)updateWithEvent:(CSBestEffortUpdaterEvent)event { - _updater.update((int)event); -} - -- (void)flush { - _updater.flush(); -} - -- (void)setMachine:(CSMachine *)machine { - _updater.flush(); - _updaterDelegate.machine = machine; -} - -@end From dac217c98cee10002c551b28d3d6091edb6d9eb9 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 8 Feb 2020 22:08:27 -0500 Subject: [PATCH 18/25] Defers starting the macOS audio queue, and attempts to restart it upon packet loss. Hopefully forever to vanish permanent audio loss? --- OSBindings/Mac/Clock Signal/Audio/CSAudioQueue.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/OSBindings/Mac/Clock Signal/Audio/CSAudioQueue.m b/OSBindings/Mac/Clock Signal/Audio/CSAudioQueue.m index 398cc01ae..b2769381e 100644 --- a/OSBindings/Mac/Clock Signal/Audio/CSAudioQueue.m +++ b/OSBindings/Mac/Clock Signal/Audio/CSAudioQueue.m @@ -117,7 +117,6 @@ static void audioOutputCallback( kCFRunLoopCommonModes, 0, &_audioQueue)) { - AudioQueueStart(_audioQueue, NULL); } } @@ -175,6 +174,10 @@ static void audioOutputCallback( AudioQueueEnqueueBuffer(_audioQueue, newBuffer, 0, NULL); [_storedBuffersLock unlock]; + + // 'Start' the queue. This is documented to be a no-op if the queue is already started, + // and it's better to defer starting it until at least some data is available. + AudioQueueStart(_audioQueue, NULL); } #pragma mark - Sampling Rate getters From b5d6126a2df5eaaac0522f1aeac9f383a3231575 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 9 Feb 2020 16:32:32 -0500 Subject: [PATCH 19/25] Avoids unnecessary filter recalculation. --- Outputs/Speaker/Implementation/LowpassSpeaker.hpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp index 686e00d25..53d1d3c74 100644 --- a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp +++ b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp @@ -60,6 +60,10 @@ template class LowpassSpeaker: public Speaker { // Implemented as per Speaker. void set_computed_output_rate(float cycles_per_second, int buffer_size) final { std::lock_guard lock_guard(filter_parameters_mutex_); + if(filter_parameters_.output_cycles_per_second == cycles_per_second && size_t(buffer_size) == output_buffer_.size()) { + return; + } + filter_parameters_.output_cycles_per_second = cycles_per_second; filter_parameters_.parameters_are_dirty = true; output_buffer_.resize(std::size_t(buffer_size)); @@ -70,6 +74,9 @@ template class LowpassSpeaker: public Speaker { */ void set_input_rate(float cycles_per_second) { std::lock_guard lock_guard(filter_parameters_mutex_); + if(filter_parameters_.input_cycles_per_second == cycles_per_second) { + return; + } filter_parameters_.input_cycles_per_second = cycles_per_second; filter_parameters_.parameters_are_dirty = true; filter_parameters_.input_rate_changed = true; @@ -83,6 +90,9 @@ template class LowpassSpeaker: public Speaker { */ void set_high_frequency_cutoff(float high_frequency) { std::lock_guard lock_guard(filter_parameters_mutex_); + if(filter_parameters_.high_frequency_cutoff == high_frequency) { + return; + } filter_parameters_.high_frequency_cutoff = high_frequency; filter_parameters_.parameters_are_dirty = true; } From 2ea1e059a89a120ce77c78c3e8b0e3bd1d3e91dd Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 9 Feb 2020 16:34:13 -0500 Subject: [PATCH 20/25] Softens swings in emulated machine speed. --- OSBindings/Mac/Clock Signal/Machine/CSMachine.mm | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index dad64bee8..aca5a0753 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -158,6 +158,7 @@ struct ActivityObserver: public Activity::Observer { int64_t _timeDiff; double _refreshPeriod; BOOL _isSyncLocking; + double _speedMultiplier; NSTimer *_joystickTimer; @@ -168,6 +169,7 @@ struct ActivityObserver: public Activity::Observer { self = [super init]; if(self) { _analyser = result; + _speedMultiplier = 1.0; Machine::Error error; std::vector missing_roms; @@ -802,11 +804,12 @@ struct ActivityObserver: public Activity::Observer { // So the set speed multiplier may be adjusted slightly to aim for that. double speed_multiplier = 1.0 / ratio; if(scan_status.current_position > 0.0) { - constexpr double adjustmentRatio = 1.01; + constexpr double adjustmentRatio = 1.005; if(scan_status.current_position < 0.5) speed_multiplier /= adjustmentRatio; else speed_multiplier *= adjustmentRatio; } - self->_machine->crt_machine()->set_speed_multiplier(speed_multiplier); + self->_speedMultiplier = (self->_speedMultiplier * 0.95) + (speed_multiplier * 0.05); + self->_machine->crt_machine()->set_speed_multiplier(self->_speedMultiplier); self->_machine->crt_machine()->run_for((double)(timeNow - self->_syncTime) / 1e9); } } From 6ae42d07a722034a4ffefdea041f59285dc69211 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 9 Feb 2020 16:42:07 -0500 Subject: [PATCH 21/25] Retains existing output when switching filter coefficients. This eliminates an issue with dynamic rate matching and throwing away the beginnings of buffers. --- Outputs/Speaker/Implementation/LowpassSpeaker.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp index 53d1d3c74..afde67693 100644 --- a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp +++ b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp @@ -228,7 +228,6 @@ template class LowpassSpeaker: public Speaker { ); number_of_taps = (number_of_taps * 2) | 1; - output_buffer_pointer_ = 0; stepper_ = std::make_unique( uint64_t(filter_parameters.input_cycles_per_second), uint64_t(filter_parameters.output_cycles_per_second)); From 24340d1d4f5356cef7c9fcb02296d7656412f604 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 9 Feb 2020 17:04:49 -0500 Subject: [PATCH 22/25] Resolves fetch errors. --- Machines/Atari/ST/Video.cpp | 4 ++-- Machines/Atari/ST/Video.hpp | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Machines/Atari/ST/Video.cpp b/Machines/Atari/ST/Video.cpp index ca84ed3c2..030c8a41e 100644 --- a/Machines/Atari/ST/Video.cpp +++ b/Machines/Atari/ST/Video.cpp @@ -194,8 +194,8 @@ void Video::run_for(HalfCycles duration) { // There will be pixels this line, subject to the shifter pipeline. // Divide into 8-[half-]cycle windows; at the start of each window fetch a word, // and during the rest of the window, shift out. - int start_column = since_load >> 3; - const int end_column = (since_load + run_length) >> 3; + int start_column = (since_load - 1) >> 3; + const int end_column = (since_load + run_length - 1) >> 3; while(start_column != end_column) { data_latch_[data_latch_position_] = ram_[current_address_ & 262143]; diff --git a/Machines/Atari/ST/Video.hpp b/Machines/Atari/ST/Video.hpp index a13ad8af1..34a0f94c0 100644 --- a/Machines/Atari/ST/Video.hpp +++ b/Machines/Atari/ST/Video.hpp @@ -133,7 +133,6 @@ class Video { int current_address_ = 0; uint16_t *ram_ = nullptr; - uint16_t line_buffer_[256]; int x_ = 0, y_ = 0, next_y_ = 0; bool load_ = false; From 85dcdbfe9e5fd3151ece15e1e5bf16af390d8c01 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 9 Feb 2020 19:12:44 -0500 Subject: [PATCH 23/25] Adopts a log prefix for the Master System. --- Machines/MasterSystem/MasterSystem.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Machines/MasterSystem/MasterSystem.cpp b/Machines/MasterSystem/MasterSystem.cpp index 1810f2eb3..ce98a0bf8 100644 --- a/Machines/MasterSystem/MasterSystem.cpp +++ b/Machines/MasterSystem/MasterSystem.cpp @@ -21,6 +21,8 @@ #include "../../ClockReceiver/JustInTime.hpp" #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" + +#define LOG_PREFIX "[SMS] " #include "../../Outputs/Log.hpp" #include "../../Analyser/Static/Sega/Target.hpp" From b04daca98e843b4818171c8af560abf3d5a4a676 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 9 Feb 2020 19:13:21 -0500 Subject: [PATCH 24/25] Picks a safer default construction. --- Outputs/OpenGL/ScanTarget.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Outputs/OpenGL/ScanTarget.hpp b/Outputs/OpenGL/ScanTarget.hpp index 46300a0ce..f7eeb816e 100644 --- a/Outputs/OpenGL/ScanTarget.hpp +++ b/Outputs/OpenGL/ScanTarget.hpp @@ -108,7 +108,7 @@ class ScanTarget: public Outputs::Display::ScanTarget { // The sizes below might be less hassle as something more natural like ints, // but squeezing this struct into 64 bits makes the std::atomics more likely // to be lock free; they are under LLVM x86-64. - int write_area = 0; + int write_area = 1; // By convention this points to the vended area. Which is preceded by a guard pixel. So a sensible default construction is write_area = 1. uint16_t scan_buffer = 0; uint16_t line = 0; }; From 6bda4034c67effb49b591b63e8150cb8c0d0ee5a Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 9 Feb 2020 19:14:25 -0500 Subject: [PATCH 25/25] Ensures no input data is dropped when changing output rates. I think this 'completely' deals with the problem. At least until someone wants dynamic output buffer sizes or something like that. We'll see. --- .../Speaker/Implementation/LowpassSpeaker.hpp | 146 +++++++++++------- 1 file changed, 92 insertions(+), 54 deletions(-) diff --git a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp index afde67693..da1cc692b 100644 --- a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp +++ b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp @@ -109,6 +109,12 @@ template class LowpassSpeaker: public Speaker { } private: + enum class Conversion { + ResampleSmaller, + Copy, + ResampleLarger + } conversion_ = Conversion::Copy; + /*! Advances by the number of cycles specified, obtaining data from the sample source supplied at construction, filtering it and passing it on to the speaker's delegate if there is one. @@ -131,69 +137,41 @@ template class LowpassSpeaker: public Speaker { delegate_->speaker_did_change_input_clock(this); } - // If input and output rates exactly match, and no additional cut-off has been specified, - // just accumulate results and pass on. - if( filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second && - filter_parameters.high_frequency_cutoff < 0.0) { - while(cycles_remaining) { - const auto cycles_to_read = std::min(output_buffer_.size() - output_buffer_pointer_, cycles_remaining); + switch(conversion_) { + case Conversion::Copy: + while(cycles_remaining) { + const auto cycles_to_read = std::min(output_buffer_.size() - output_buffer_pointer_, cycles_remaining); - sample_source_.get_samples(cycles_to_read, &output_buffer_[output_buffer_pointer_]); - output_buffer_pointer_ += cycles_to_read; + sample_source_.get_samples(cycles_to_read, &output_buffer_[output_buffer_pointer_]); + output_buffer_pointer_ += cycles_to_read; - // announce to delegate if full - if(output_buffer_pointer_ == output_buffer_.size()) { - output_buffer_pointer_ = 0; - did_complete_samples(this, output_buffer_); - } - - cycles_remaining -= cycles_to_read; - } - - return; - } - - // If the output rate is less than the input rate, or an additional cut-off has been specified, use the filter. - if( filter_parameters.input_cycles_per_second > filter_parameters.output_cycles_per_second || - (filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second && filter_parameters.high_frequency_cutoff >= 0.0)) { - while(cycles_remaining) { - const auto cycles_to_read = std::min(cycles_remaining, input_buffer_.size() - input_buffer_depth_); - sample_source_.get_samples(cycles_to_read, &input_buffer_[input_buffer_depth_]); - cycles_remaining -= cycles_to_read; - input_buffer_depth_ += cycles_to_read; - - if(input_buffer_depth_ == input_buffer_.size()) { - output_buffer_[output_buffer_pointer_] = filter_->apply(input_buffer_.data()); - output_buffer_pointer_++; - - // Announce to delegate if full. + // announce to delegate if full if(output_buffer_pointer_ == output_buffer_.size()) { output_buffer_pointer_ = 0; did_complete_samples(this, output_buffer_); } - // If the next loop around is going to reuse some of the samples just collected, use a memmove to - // preserve them in the correct locations (TODO: use a longer buffer to fix that) and don't skip - // anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse. - const auto steps = stepper_->step(); - if(steps < input_buffer_.size()) { - auto *const input_buffer = input_buffer_.data(); - std::memmove( input_buffer, - &input_buffer[steps], - sizeof(int16_t) * (input_buffer_.size() - steps)); - input_buffer_depth_ -= steps; - } else { - if(steps > input_buffer_.size()) - sample_source_.skip_samples(steps - input_buffer_.size()); - input_buffer_depth_ = 0; + cycles_remaining -= cycles_to_read; + } + break; + + case Conversion::ResampleSmaller: + while(cycles_remaining) { + const auto cycles_to_read = std::min(cycles_remaining, input_buffer_.size() - input_buffer_depth_); + sample_source_.get_samples(cycles_to_read, &input_buffer_[input_buffer_depth_]); + cycles_remaining -= cycles_to_read; + input_buffer_depth_ += cycles_to_read; + + if(input_buffer_depth_ == input_buffer_.size()) { + resample_input_buffer(); } } - } + break; - return; + case Conversion::ResampleLarger: + // TODO: input rate is less than output rate. + break; } - - // TODO: input rate is less than output rate } T &sample_source_; @@ -239,8 +217,68 @@ template class LowpassSpeaker: public Speaker { high_pass_frequency, SignalProcessing::FIRFilter::DefaultAttenuation); - input_buffer_.resize(std::size_t(number_of_taps)); - input_buffer_depth_ = 0; + + // Pick the new conversion function. + if( filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second && + filter_parameters.high_frequency_cutoff < 0.0) { + // If input and output rates exactly match, and no additional cut-off has been specified, + // just accumulate results and pass on. + conversion_ = Conversion::Copy; + } else if( filter_parameters.input_cycles_per_second > filter_parameters.output_cycles_per_second || + (filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second && filter_parameters.high_frequency_cutoff >= 0.0)) { + // If the output rate is less than the input rate, or an additional cut-off has been specified, use the filter. + conversion_ = Conversion::ResampleSmaller; + } else { + conversion_ = Conversion::ResampleLarger; + } + + // Do something sensible with any dangling input, if necessary. + switch(conversion_) { + // Neither direct copying nor resampling larger currently use any temporary input. + // Although in the latter case that's just because it's unimplemented. But, regardless, + // that means nothing to do. + default: break; + + case Conversion::ResampleSmaller: + // Reize the input buffer only if absolutely necessary; if sizing downward + // such that a sample would otherwise be lost then output it now. Keep anything + // currently in the input buffer that hasn't yet been processed. + if(input_buffer_.size() != size_t(number_of_taps)) { + if(input_buffer_depth_ >= size_t(number_of_taps)) { + resample_input_buffer(); + input_buffer_depth_ %= size_t(number_of_taps); + } + input_buffer_.resize(size_t(number_of_taps)); + } + break; + } + } + + inline void resample_input_buffer() { + output_buffer_[output_buffer_pointer_] = filter_->apply(input_buffer_.data()); + output_buffer_pointer_++; + + // Announce to delegate if full. + if(output_buffer_pointer_ == output_buffer_.size()) { + output_buffer_pointer_ = 0; + did_complete_samples(this, output_buffer_); + } + + // If the next loop around is going to reuse some of the samples just collected, use a memmove to + // preserve them in the correct locations (TODO: use a longer buffer to fix that?) and don't skip + // anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse. + const auto steps = stepper_->step(); + if(steps < input_buffer_.size()) { + auto *const input_buffer = input_buffer_.data(); + std::memmove( input_buffer, + &input_buffer[steps], + sizeof(int16_t) * (input_buffer_.size() - steps)); + input_buffer_depth_ -= steps; + } else { + if(steps > input_buffer_.size()) + sample_source_.skip_samples(steps - input_buffer_.size()); + input_buffer_depth_ = 0; + } } };