1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-06 01:28:57 +00:00

Merge pull request #746 from TomHarte/LatencyChop

Reduces latency in macOS, improves concurrency
This commit is contained in:
Thomas Harte 2020-02-09 21:07:40 -05:00 committed by GitHub
commit f1cd35fa16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 424 additions and 291 deletions

View File

@ -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 // Cap running at 1/5th of a second, to avoid doing a huge amount of work after any
// brief system interruption. // brief system interruption.
const double duration = std::min(double(integer_duration) / 1e9, 0.2); 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; has_skipped_ = false;
} }
} }

View File

@ -194,8 +194,8 @@ void Video::run_for(HalfCycles duration) {
// There will be pixels this line, subject to the shifter pipeline. // 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, // Divide into 8-[half-]cycle windows; at the start of each window fetch a word,
// and during the rest of the window, shift out. // and during the rest of the window, shift out.
int start_column = since_load >> 3; int start_column = (since_load - 1) >> 3;
const int end_column = (since_load + run_length) >> 3; const int end_column = (since_load + run_length - 1) >> 3;
while(start_column != end_column) { while(start_column != end_column) {
data_latch_[data_latch_position_] = ram_[current_address_ & 262143]; data_latch_[data_latch_position_] = ram_[current_address_ & 262143];

View File

@ -133,7 +133,6 @@ class Video {
int current_address_ = 0; int current_address_ = 0;
uint16_t *ram_ = nullptr; uint16_t *ram_ = nullptr;
uint16_t line_buffer_[256];
int x_ = 0, y_ = 0, next_y_ = 0; int x_ = 0, y_ = 0, next_y_ = 0;
bool load_ = false; bool load_ = false;

View File

@ -21,6 +21,8 @@
#include "../../ClockReceiver/JustInTime.hpp" #include "../../ClockReceiver/JustInTime.hpp"
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#define LOG_PREFIX "[SMS] "
#include "../../Outputs/Log.hpp" #include "../../Outputs/Log.hpp"
#include "../../Analyser/Static/Sega/Target.hpp" #include "../../Analyser/Static/Sega/Target.hpp"

View File

@ -812,7 +812,6 @@
4BD4A8D01E077FD20020D856 /* PCMTrackTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */; }; 4BD4A8D01E077FD20020D856 /* PCMTrackTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */; };
4BD5D2682199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */; }; 4BD5D2682199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */; };
4BD5D2692199148100DDF17D /* 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 */; }; 4BD61664206B2AC800236112 /* QuickLoadOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BD61662206B2AC700236112 /* QuickLoadOptions.xib */; };
4BD67DCB209BE4D700AB2146 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD67DCA209BE4D600AB2146 /* StaticAnalyser.cpp */; }; 4BD67DCB209BE4D700AB2146 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD67DCA209BE4D600AB2146 /* StaticAnalyser.cpp */; };
4BD67DCC209BE4D700AB2146 /* 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 = "<group>"; }; 4BD468F61D8DF41D0084958B /* 1770.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = 1770.hpp; path = 1770/1770.hpp; sourceTree = "<group>"; };
4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PCMTrackTests.mm; sourceTree = "<group>"; }; 4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PCMTrackTests.mm; sourceTree = "<group>"; };
4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ScanTargetGLSLFragments.cpp; sourceTree = "<group>"; }; 4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ScanTargetGLSLFragments.cpp; sourceTree = "<group>"; };
4BD5F1931D13528900631CD1 /* CSBestEffortUpdater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CSBestEffortUpdater.h; path = Updater/CSBestEffortUpdater.h; sourceTree = "<group>"; };
4BD5F1941D13528900631CD1 /* CSBestEffortUpdater.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = CSBestEffortUpdater.mm; path = Updater/CSBestEffortUpdater.mm; sourceTree = "<group>"; };
4BD601A920D89F2A00CBCE57 /* Log.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Log.hpp; path = ../../Outputs/Log.hpp; sourceTree = "<group>"; }; 4BD601A920D89F2A00CBCE57 /* Log.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Log.hpp; path = ../../Outputs/Log.hpp; sourceTree = "<group>"; };
4BD61663206B2AC700236112 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/QuickLoadOptions.xib"; sourceTree = SOURCE_ROOT; }; 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 = "<group>"; }; 4BD67DC9209BE4D600AB2146 /* StaticAnalyser.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = StaticAnalyser.hpp; sourceTree = "<group>"; };
@ -3276,7 +3273,6 @@
4BB73EAA1B587A5100552FC2 /* MainMenu.xib */, 4BB73EAA1B587A5100552FC2 /* MainMenu.xib */,
4BE5F85A1C3E1C2500C43F01 /* Resources */, 4BE5F85A1C3E1C2500C43F01 /* Resources */,
4BDA00DB22E60EE900AC3CD0 /* ROMRequester */, 4BDA00DB22E60EE900AC3CD0 /* ROMRequester */,
4BD5F1961D1352A000631CD1 /* Updater */,
4B55CE5A1C3B7D6F0093A61B /* Views */, 4B55CE5A1C3B7D6F0093A61B /* Views */,
); );
path = "Clock Signal"; path = "Clock Signal";
@ -3624,15 +3620,6 @@
name = 1770; name = 1770;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
4BD5F1961D1352A000631CD1 /* Updater */ = {
isa = PBXGroup;
children = (
4BD5F1931D13528900631CD1 /* CSBestEffortUpdater.h */,
4BD5F1941D13528900631CD1 /* CSBestEffortUpdater.mm */,
);
name = Updater;
sourceTree = "<group>";
};
4BD67DC8209BE4D600AB2146 /* DiskII */ = { 4BD67DC8209BE4D600AB2146 /* DiskII */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -4493,7 +4480,6 @@
4B4518A41F75FD1C00926311 /* OricMFMDSK.cpp in Sources */, 4B4518A41F75FD1C00926311 /* OricMFMDSK.cpp in Sources */,
4B4B1A3C200198CA00A0F866 /* KonamiSCC.cpp in Sources */, 4B4B1A3C200198CA00A0F866 /* KonamiSCC.cpp in Sources */,
4BB0A65B2044FD3000FB3688 /* SN76489.cpp in Sources */, 4BB0A65B2044FD3000FB3688 /* SN76489.cpp in Sources */,
4BD5F1951D13528900631CD1 /* CSBestEffortUpdater.mm in Sources */,
4B894532201967B4007DE474 /* 6502.cpp in Sources */, 4B894532201967B4007DE474 /* 6502.cpp in Sources */,
4BDB61EC203285AE0048AF91 /* Atari2600OptionsPanel.swift in Sources */, 4BDB61EC203285AE0048AF91 /* Atari2600OptionsPanel.swift in Sources */,
4BBB70A8202014E2002FE009 /* MultiCRTMachine.cpp in Sources */, 4BBB70A8202014E2002FE009 /* MultiCRTMachine.cpp in Sources */,

View File

@ -117,7 +117,6 @@ static void audioOutputCallback(
kCFRunLoopCommonModes, kCFRunLoopCommonModes,
0, 0,
&_audioQueue)) { &_audioQueue)) {
AudioQueueStart(_audioQueue, NULL);
} }
} }
@ -175,6 +174,10 @@ static void audioOutputCallback(
AudioQueueEnqueueBuffer(_audioQueue, newBuffer, 0, NULL); AudioQueueEnqueueBuffer(_audioQueue, newBuffer, 0, NULL);
[_storedBuffersLock unlock]; [_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 #pragma mark - Sampling Rate getters

View File

@ -13,7 +13,6 @@
#import "CSOpenGLView.h" #import "CSOpenGLView.h"
#import "CSROMReceiverView.h" #import "CSROMReceiverView.h"
#import "CSBestEffortUpdater.h"
#import "CSJoystickManager.h" #import "CSJoystickManager.h"
#import "NSData+CRC32.h" #import "NSData+CRC32.h"

View File

@ -24,8 +24,6 @@ class MachineDocument:
private let actionLock = NSLock() private let actionLock = NSLock()
/// Ensures exclusive access between calls to machine.updateView and machine.drawView, and close(). /// Ensures exclusive access between calls to machine.updateView and machine.drawView, and close().
private let drawLock = NSLock() private let drawLock = NSLock()
/// Ensures exclusive access to the best-effort updater.
private let bestEffortLock = NSLock()
// MARK: - Machine details. // MARK: - Machine details.
@ -43,9 +41,6 @@ class MachineDocument:
/// The output audio queue, if any. /// The output audio queue, if any.
private var audioQueue: CSAudioQueue! private var audioQueue: CSAudioQueue!
/// The best-effort updater.
private var bestEffortUpdater: CSBestEffortUpdater?
// MARK: - Main NIB connections. // MARK: - Main NIB connections.
/// The OpenGL view to receive this machine's display. /// The OpenGL view to receive this machine's display.
@ -89,19 +84,14 @@ class MachineDocument:
} }
override func close() { override func close() {
machine.stop()
activityPanel?.setIsVisible(false) activityPanel?.setIsVisible(false)
activityPanel = nil activityPanel = nil
optionsPanel?.setIsVisible(false) optionsPanel?.setIsVisible(false)
optionsPanel = nil optionsPanel = nil
bestEffortLock.lock()
if let bestEffortUpdater = bestEffortUpdater {
bestEffortUpdater.flush()
self.bestEffortUpdater = nil
}
bestEffortLock.unlock()
actionLock.lock() actionLock.lock()
drawLock.lock() drawLock.lock()
machine = nil machine = nil
@ -187,9 +177,7 @@ class MachineDocument:
if let machine = self.machine, let openGLView = self.openGLView { if let machine = self.machine, let openGLView = self.openGLView {
// Establish the output aspect ratio and audio. // Establish the output aspect ratio and audio.
let aspectRatio = self.aspectRatio() 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. // Attach an options panel if one is available.
if let optionsPanelNibName = self.machineDescription?.optionsPanelNibName { if let optionsPanelNibName = self.machineDescription?.optionsPanelNibName {
@ -200,7 +188,6 @@ class MachineDocument:
} }
machine.delegate = self machine.delegate = self
self.bestEffortUpdater = CSBestEffortUpdater()
// Callbacks from the OpenGL may come on a different thread, immediately following the .delegate set; // 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. // hence the full setup of the best-effort updater prior to setting self as a delegate.
@ -220,7 +207,7 @@ class MachineDocument:
openGLView.window!.makeFirstResponder(openGLView) openGLView.window!.makeFirstResponder(openGLView)
// Start forwarding best-effort updates. // Start forwarding best-effort updates.
self.bestEffortUpdater!.setMachine(machine) machine.start()
} }
} }
@ -251,24 +238,11 @@ class MachineDocument:
/// Responds to the CSAudioQueueDelegate dry-queue warning message by requesting a machine update. /// Responds to the CSAudioQueueDelegate dry-queue warning message by requesting a machine update.
final func audioQueueIsRunningDry(_ audioQueue: CSAudioQueue) { 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 /// 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. /// request, and ordering a redraw regardless of the motivation.
final func openGLViewRedraw(_ view: CSOpenGLView, event redrawEvent: CSOpenGLViewRedrawEvent) { 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 drawLock.try() {
if redrawEvent == .timer { if redrawEvent == .timer {
machine.updateView(forPixelSize: view.backingSize) machine.updateView(forPixelSize: view.backingSize)

View File

@ -31,7 +31,15 @@
} }
- (void)invalidate { - (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 @end

View File

@ -57,13 +57,14 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
*/ */
- (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result missingROMs:(nullable inout NSMutableArray<CSMissingROM *> *)missingROMs NS_DESIGNATED_INITIALIZER; - (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result missingROMs:(nullable inout NSMutableArray<CSMissingROM *> *)missingROMs NS_DESIGNATED_INITIALIZER;
- (NSTimeInterval)runForInterval:(NSTimeInterval)interval untilEvent:(int)events;
- (float)idealSamplingRateFromRange:(NSRange)range; - (float)idealSamplingRateFromRange:(NSRange)range;
- (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize; - (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize;
- (void)setView:(nullable CSOpenGLView *)view aspectRatio:(float)aspectRatio; - (void)setView:(nullable CSOpenGLView *)view aspectRatio:(float)aspectRatio;
- (void)start;
- (void)stop;
- (void)updateViewForPixelSize:(CGSize)pixelSize; - (void)updateViewForPixelSize:(CGSize)pixelSize;
- (void)drawViewForPixelSize:(CGSize)pixelSize; - (void)drawViewForPixelSize:(CGSize)pixelSize;

View File

@ -10,6 +10,7 @@
#import "CSMachine+Target.h" #import "CSMachine+Target.h"
#include "CSROMFetcher.hpp" #include "CSROMFetcher.hpp"
#import "CSHighPrecisionTimer.h"
#include "MediaTarget.hpp" #include "MediaTarget.hpp"
#include "JoystickMachine.hpp" #include "JoystickMachine.hpp"
@ -24,6 +25,7 @@
#import "NSBundle+DataResource.h" #import "NSBundle+DataResource.h"
#import "NSData+StdVector.h" #import "NSData+StdVector.h"
#include <atomic>
#include <bitset> #include <bitset>
#import <OpenGL/OpenGL.h> #import <OpenGL/OpenGL.h>
@ -32,7 +34,7 @@
#include "../../../../Outputs/OpenGL/ScanTarget.hpp" #include "../../../../Outputs/OpenGL/ScanTarget.hpp"
#include "../../../../Outputs/OpenGL/Screenshot.hpp" #include "../../../../Outputs/OpenGL/Screenshot.hpp"
@interface CSMachine() @interface CSMachine() <CSOpenGLViewDisplayLinkDelegate>
- (void)speaker:(Outputs::Speaker::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length; - (void)speaker:(Outputs::Speaker::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length;
- (void)speakerDidChangeInputClock:(Outputs::Speaker::Speaker *)speaker; - (void)speakerDidChangeInputClock:(Outputs::Speaker::Speaker *)speaker;
- (void)addLED:(NSString *)led; - (void)addLED:(NSString *)led;
@ -149,6 +151,17 @@ struct ActivityObserver: public Activity::Observer {
std::bitset<65536> _depressedKeys; std::bitset<65536> _depressedKeys;
NSMutableArray<NSString *> *_leds; NSMutableArray<NSString *> *_leds;
CSHighPrecisionTimer *_timer;
CGSize _pixelSize;
std::atomic_flag _isUpdating;
int64_t _syncTime;
int64_t _timeDiff;
double _refreshPeriod;
BOOL _isSyncLocking;
double _speedMultiplier;
NSTimer *_joystickTimer;
std::unique_ptr<Outputs::Display::OpenGL::ScanTarget> _scanTarget; std::unique_ptr<Outputs::Display::OpenGL::ScanTarget> _scanTarget;
} }
@ -156,6 +169,7 @@ struct ActivityObserver: public Activity::Observer {
self = [super init]; self = [super init];
if(self) { if(self) {
_analyser = result; _analyser = result;
_speedMultiplier = 1.0;
Machine::Error error; Machine::Error error;
std::vector<ROMMachine::ROM> missing_roms; std::vector<ROMMachine::ROM> missing_roms;
@ -201,6 +215,8 @@ struct ActivityObserver: public Activity::Observer {
_speakerDelegate.machineAccessLock = _delegateMachineAccessLock; _speakerDelegate.machineAccessLock = _delegateMachineAccessLock;
_joystickMachine = _machine->joystick_machine(); _joystickMachine = _machine->joystick_machine();
[self updateJoystickTimer];
_isUpdating.clear();
} }
return self; return self;
} }
@ -214,6 +230,8 @@ struct ActivityObserver: public Activity::Observer {
} }
- (void)dealloc { - (void)dealloc {
[_joystickTimer invalidate];
// The two delegate's references to this machine are nilled out here because close_output may result // 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 // 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. // that it's out of data, resulting in an attempt further to run the machine while it is dealloc'ing.
@ -259,65 +277,73 @@ 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) { @synchronized(self) {
if(_joystickMachine && _joystickManager) { size_t c = 0;
[_joystickManager update]; 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. // Post the first two analogue axes presented by the controller as horizontal and vertical inputs,
// Until then, apply a default mapping. // 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]
size_t c = 0; if(!joystick.hats.count || !joystick.hats[0].direction) {
auto &machine_joysticks = _joystickMachine->get_joysticks(); if(joystick.axes.count > 0) {
for(CSJoystick *joystick in _joystickManager.joysticks) { const float x_axis = joystick.axes[0].position;
size_t target = c % machine_joysticks.size(); machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Horizontal), x_axis);
++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));
}
} }
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. // Forward all fire buttons, mapping as a function of index.
if(machine_joysticks[target]->get_number_of_fire_buttons()) { if(machine_joysticks[target]->get_number_of_fire_buttons()) {
std::vector<bool> button_states((size_t)machine_joysticks[target]->get_number_of_fire_buttons()); std::vector<bool> button_states((size_t)machine_joysticks[target]->get_number_of_fire_buttons());
for(CSJoystickButton *button in joystick.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; 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) { for(size_t index = 0; index < button_states.size(); ++index) {
machine_joysticks[target]->set_input( machine_joysticks[target]->set_input(
Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Fire, index), Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Fire, index),
button_states[index]); button_states[index]);
}
} }
} }
} }
return _machine->crt_machine()->run_until(interval, events);
} }
} }
- (void)setView:(CSOpenGLView *)view aspectRatio:(float)aspectRatio { - (void)setView:(CSOpenGLView *)view aspectRatio:(float)aspectRatio {
_view = view; _view = view;
_view.displayLinkDelegate = self;
[view performWithGLContext:^{ [view performWithGLContext:^{
[self setupOutputWithAspectRatio:aspectRatio]; [self setupOutputWithAspectRatio:aspectRatio];
}]; } flushDrawable:NO];
} }
- (void)setupOutputWithAspectRatio:(float)aspectRatio { - (void)setupOutputWithAspectRatio:(float)aspectRatio {
@ -326,7 +352,7 @@ struct ActivityObserver: public Activity::Observer {
} }
- (void)updateViewForPixelSize:(CGSize)pixelSize { - (void)updateViewForPixelSize:(CGSize)pixelSize {
_scanTarget->update((int)pixelSize.width, (int)pixelSize.height); // _pixelSize = pixelSize;
// @synchronized(self) { // @synchronized(self) {
// const auto scan_status = _machine->crt_machine()->get_scan_status(); // const auto scan_status = _machine->crt_machine()->get_scan_status();
@ -375,15 +401,17 @@ struct ActivityObserver: public Activity::Observer {
} }
- (void)setJoystickManager:(CSJoystickManager *)joystickManager { - (void)setJoystickManager:(CSJoystickManager *)joystickManager {
@synchronized(self) { _joystickManager = joystickManager;
_joystickManager = joystickManager; if(_joystickMachine) {
if(_joystickMachine) { @synchronized(self) {
auto &machine_joysticks = _joystickMachine->get_joysticks(); auto &machine_joysticks = _joystickMachine->get_joysticks();
for(const auto &joystick: machine_joysticks) { for(const auto &joystick: machine_joysticks) {
joystick->reset_all_inputs(); joystick->reset_all_inputs();
} }
} }
} }
[self updateJoystickTimer];
} }
- (void)setKey:(uint16_t)key characters:(NSString *)characters isPressed:(BOOL)isPressed { - (void)setKey:(uint16_t)key characters:(NSString *)characters isPressed:(BOOL)isPressed {
@ -694,4 +722,139 @@ struct ActivityObserver: public Activity::Observer {
return _leds; return _leds;
} }
#pragma mark - Timer
- (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;
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).
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;
// 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).
if(!isSyncLocking) {
[self.view performWithGLContext:^{
self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height);
} flushDrawable:YES];
}
}
#define TICKS 600
- (void)start {
__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;
BOOL splitAndSync = NO;
@synchronized(self) {
// If this tick includes vsync then inspect the machine.
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, mark this time window to be split over the sync.
const auto scan_status = self->_machine->crt_machine()->get_scan_status();
double ratio = 1.0;
if(scan_status.field_duration_gradient < 0.00001) {
ratio = self->_refreshPeriod / scan_status.field_duration;
const double integerRatio = round(ratio);
if(integerRatio > 0.0) {
ratio /= integerRatio;
constexpr double maximumAdjustment = 1.03;
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.
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.005;
if(scan_status.current_position < 0.5) speed_multiplier /= adjustmentRatio;
else speed_multiplier *= adjustmentRatio;
}
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);
}
}
// 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)duration / 1e9);
}
pixelSize = self->_pixelSize;
}
// 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:splitAndSync];
self->_isUpdating.clear();
});
}
lastTime = timeNow;
} interval:uint64_t(1000000000) / uint64_t(TICKS)];
}
#undef TICKS
- (void)stop {
[_timer invalidate];
_timer = nil;
}
@end @end

View File

@ -1,27 +0,0 @@
//
// CSBestEffortUpdater.h
// Clock Signal
//
// Created by Thomas Harte on 16/06/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreVideo/CoreVideo.h>
#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

View File

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

View File

@ -101,6 +101,23 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
@end @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 the display link has fired.
*/
- (void)openGLViewDisplayLinkDidFire:(nonnull CSOpenGLView *)view now:(nonnull const CVTimeStamp *)now outputTime:(nonnull const CVTimeStamp *)outputTime;
@end
/*! /*!
Provides an OpenGL canvas with a refresh-linked update timer that can forward a subset Provides an OpenGL canvas with a refresh-linked update timer that can forward a subset
of typical first-responder actions. of typical first-responder actions.
@ -109,6 +126,7 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
@property (atomic, weak, nullable) id <CSOpenGLViewDelegate> delegate; @property (atomic, weak, nullable) id <CSOpenGLViewDelegate> delegate;
@property (nonatomic, weak, nullable) id <CSOpenGLViewResponderDelegate> responderDelegate; @property (nonatomic, weak, nullable) id <CSOpenGLViewResponderDelegate> responderDelegate;
@property (atomic, weak, nullable) id <CSOpenGLViewDisplayLinkDelegate> displayLinkDelegate;
/// Determines whether the view offers mouse capturing — i.e. if the user clicks on the view then /// 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 /// then the system cursor is disabled and the mouse events defined by CSOpenGLViewResponderDelegate
@ -139,6 +157,7 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
Locks this view's OpenGL context and makes it current, performs @c action and then unlocks 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. 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)performWithGLContext:(nonnull dispatch_block_t)action;
/*! /*!

View File

@ -30,11 +30,6 @@
// Note the initial screen. // Note the initial screen.
_currentScreen = self.window.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 // set the clear colour
[self.openGLContext makeCurrentContext]; [self.openGLContext makeCurrentContext];
glClearColor(0.0, 0.0, 0.0, 1.0); glClearColor(0.0, 0.0, 0.0, 1.0);
@ -75,7 +70,8 @@
static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) { static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) {
CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext; CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext;
[view drawAtTime:now frequency:CVDisplayLinkGetActualOutputVideoRefreshPeriod(displayLink)]; [view checkDisplayLink];
[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. 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, Specifically: Apple provides CVDisplayLinkStop but a call to that merely prevents future calls to the callback,
@ -89,7 +85,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
return kCVReturnSuccess; 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. // 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, // 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 // but since this method is going to be called repeatedly anyway, and the test is cheap, polling
@ -105,7 +101,9 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
// the window is actually on, and at its rate. // the window is actually on, and at its rate.
[self setupDisplayLink]; [self setupDisplayLink];
} }
}
- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency {
[self redrawWithEvent:CSOpenGLViewRedrawEventTimer]; [self redrawWithEvent:CSOpenGLViewRedrawEventTimer];
} }
@ -113,11 +111,10 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
[self redrawWithEvent:CSOpenGLViewRedrawEventAppKit]; [self redrawWithEvent:CSOpenGLViewRedrawEventAppKit];
} }
- (void)redrawWithEvent:(CSOpenGLViewRedrawEvent)event { - (void)redrawWithEvent:(CSOpenGLViewRedrawEvent)event {
[self performWithGLContext:^{ [self performWithGLContext:^{
[self.delegate openGLViewRedraw:self event:event]; [self.delegate openGLViewRedraw:self event:event];
CGLFlushDrawable([[self openGLContext] CGLContextObj]); } flushDrawable:YES];
}];
} }
- (void)invalidate { - (void)invalidate {
@ -145,7 +142,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
[self performWithGLContext:^{ [self performWithGLContext:^{
CGSize viewSize = [self backingSize]; CGSize viewSize = [self backingSize];
glViewport(0, 0, (GLsizei)viewSize.width, (GLsizei)viewSize.height); glViewport(0, 0, (GLsizei)viewSize.width, (GLsizei)viewSize.height);
}]; } flushDrawable:NO];
} }
- (void)awakeFromNib { - (void)awakeFromNib {
@ -178,11 +175,17 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
[self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]]; [self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]];
} }
- (void)performWithGLContext:(dispatch_block_t)action { - (void)performWithGLContext:(dispatch_block_t)action flushDrawable:(BOOL)flushDrawable {
CGLLockContext([[self openGLContext] CGLContextObj]); CGLLockContext([[self openGLContext] CGLContextObj]);
[self.openGLContext makeCurrentContext]; [self.openGLContext makeCurrentContext];
action(); action();
CGLUnlockContext([[self openGLContext] CGLContextObj]); CGLUnlockContext([[self openGLContext] CGLContextObj]);
if(flushDrawable) CGLFlushDrawable([[self openGLContext] CGLContextObj]);
}
- (void)performWithGLContext:(nonnull dispatch_block_t)action {
[self performWithGLContext:action flushDrawable:NO];
} }
#pragma mark - NSResponder #pragma mark - NSResponder

View File

@ -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 = float(vertical_flywheel_->get_locked_period()) / float(time_multiplier_);
status.field_duration_gradient = float(vertical_flywheel_->get_last_period_adjustment()) / 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.retrace_duration = float(vertical_flywheel_->get_retrace_period()) / float(time_multiplier_);
status.current_position = status.current_position = float(vertical_flywheel_->get_current_phase()) / float(vertical_flywheel_->get_locked_scan_period());
std::max(0.0f,
float(vertical_flywheel_->get_current_output_position()) / (float(vertical_flywheel_->get_locked_period()) * float(time_multiplier_))
);
status.hsync_count = vertical_flywheel_->get_number_of_retraces(); status.hsync_count = vertical_flywheel_->get_number_of_retraces();
return status; return status;
} }

View File

@ -139,6 +139,16 @@ struct Flywheel {
return counter_ - retrace_time_; 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. @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_; 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. @returns the expected length of a complete scan and retrace cycle.
*/ */

View File

@ -251,21 +251,6 @@ void ScanTarget::will_change_owner() {
vended_scan_ = nullptr; 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) { 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. // Forward the event to the display metrics tracker.
display_metrics_.announce_event(event); 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) { if(event == ScanTarget::Event::EndVerticalRetrace) {
// The previous-frame-is-complete flag is subject to a two-slot queue because // 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 // 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 // result needs to be put somewhere — it'll be attached to the first successful
// only after it has been put somewhere also doesn't work, since if the first // line output.
// few lines of a frame are skipped for any reason, there'll be nowhere to
// put it.
is_first_in_frame_ = true; is_first_in_frame_ = true;
previous_frame_was_complete_ = frame_is_complete_; previous_frame_was_complete_ = frame_is_complete_;
frame_is_complete_ = true; 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. // Attempt to allocate a new line; note allocation failure if necessary.
const auto next_line = uint16_t((write_pointers_.line + 1) % LineBufferHeight); const auto next_line = uint16_t((write_pointers_.line + 1) % LineBufferHeight);
if(next_line == read_pointers.line) { if(next_line == read_pointers.line) {
line_allocation_has_failed_ = allocation_has_failed_ = true; allocation_has_failed_ = true;
active_line_ = nullptr; active_line_ = nullptr;
} else { } else {
line_allocation_has_failed_ = false;
write_pointers_.line = next_line; write_pointers_.line = next_line;
active_line_ = &line_buffer_[size_t(write_pointers_.line)]; active_line_ = &line_buffer_[size_t(write_pointers_.line)];
} }
provided_scans_ = 0; 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_) { if(active_line_) {
@ -329,6 +301,7 @@ void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display::
} }
} else { } else {
if(active_line_) { if(active_line_) {
// A successfully-allocated line is ending.
active_line_->end_points[1].x = location.x; active_line_->end_points[1].x = location.x;
active_line_->end_points[1].y = location.y; 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; 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 #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; output_is_visible_ = is_visible;
} }

View File

@ -77,7 +77,6 @@ class ScanTarget: public Outputs::Display::ScanTarget {
void end_scan() final; void end_scan() final;
uint8_t *begin_data(size_t required_length, size_t required_alignment) final; uint8_t *begin_data(size_t required_length, size_t required_alignment) final;
void end_data(size_t actual_length) 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 announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t colour_burst_amplitude) final;
void will_change_owner() final; void will_change_owner() final;
@ -109,7 +108,7 @@ class ScanTarget: public Outputs::Display::ScanTarget {
// The sizes below might be less hassle as something more natural like ints, // 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 // but squeezing this struct into 64 bits makes the std::atomics more likely
// to be lock free; they are under LLVM x86-64. // 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 scan_buffer = 0;
uint16_t line = 0; uint16_t line = 0;
}; };
@ -188,7 +187,6 @@ class ScanTarget: public Outputs::Display::ScanTarget {
// Track allocation failures. // Track allocation failures.
bool data_is_allocated_ = false; bool data_is_allocated_ = false;
bool allocation_has_failed_ = false; bool allocation_has_failed_ = false;
bool line_allocation_has_failed_ = false;
// Receives scan target modals. // Receives scan target modals.
Modals modals_; Modals modals_;

View File

@ -273,18 +273,22 @@ struct ScanTarget {
/// data and scan allocations should be invalidated. /// data and scan allocations should be invalidated.
virtual void will_change_owner() {} 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 /// (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 /// 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) /// begin_scan — then (ii) is a required response. But a scan target may also need to opt for (ii)
/// for any other reason. /// for any other reason.
/// ///
/// The ScanTarget isn't bound to take any drawing action immediately; it may sit on submitted data for /// 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; virtual void submit() {}
/* /*
@ -303,6 +307,12 @@ struct ScanTarget {
/*! /*!
Provides a hint that the named event has occurred. 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 event The event.
@param is_visible @c true if the output stream is visible immediately after this event; @c false otherwise. @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. @param location The location of the event.

View File

@ -60,6 +60,10 @@ template <typename T> class LowpassSpeaker: public Speaker {
// Implemented as per Speaker. // Implemented as per Speaker.
void set_computed_output_rate(float cycles_per_second, int buffer_size) final { void set_computed_output_rate(float cycles_per_second, int buffer_size) final {
std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_); std::lock_guard<std::mutex> 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_.output_cycles_per_second = cycles_per_second;
filter_parameters_.parameters_are_dirty = true; filter_parameters_.parameters_are_dirty = true;
output_buffer_.resize(std::size_t(buffer_size)); output_buffer_.resize(std::size_t(buffer_size));
@ -70,6 +74,9 @@ template <typename T> class LowpassSpeaker: public Speaker {
*/ */
void set_input_rate(float cycles_per_second) { void set_input_rate(float cycles_per_second) {
std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_); std::lock_guard<std::mutex> 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_.input_cycles_per_second = cycles_per_second;
filter_parameters_.parameters_are_dirty = true; filter_parameters_.parameters_are_dirty = true;
filter_parameters_.input_rate_changed = true; filter_parameters_.input_rate_changed = true;
@ -83,6 +90,9 @@ template <typename T> class LowpassSpeaker: public Speaker {
*/ */
void set_high_frequency_cutoff(float high_frequency) { void set_high_frequency_cutoff(float high_frequency) {
std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_); std::lock_guard<std::mutex> lock_guard(filter_parameters_mutex_);
if(filter_parameters_.high_frequency_cutoff == high_frequency) {
return;
}
filter_parameters_.high_frequency_cutoff = high_frequency; filter_parameters_.high_frequency_cutoff = high_frequency;
filter_parameters_.parameters_are_dirty = true; filter_parameters_.parameters_are_dirty = true;
} }
@ -99,6 +109,12 @@ template <typename T> class LowpassSpeaker: public Speaker {
} }
private: private:
enum class Conversion {
ResampleSmaller,
Copy,
ResampleLarger
} conversion_ = Conversion::Copy;
/*! /*!
Advances by the number of cycles specified, obtaining data from the sample source supplied 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. at construction, filtering it and passing it on to the speaker's delegate if there is one.
@ -121,69 +137,41 @@ template <typename T> class LowpassSpeaker: public Speaker {
delegate_->speaker_did_change_input_clock(this); delegate_->speaker_did_change_input_clock(this);
} }
// If input and output rates exactly match, and no additional cut-off has been specified, switch(conversion_) {
// just accumulate results and pass on. case Conversion::Copy:
if( filter_parameters.input_cycles_per_second == filter_parameters.output_cycles_per_second && while(cycles_remaining) {
filter_parameters.high_frequency_cutoff < 0.0) { const auto cycles_to_read = std::min(output_buffer_.size() - output_buffer_pointer_, cycles_remaining);
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_]); sample_source_.get_samples(cycles_to_read, &output_buffer_[output_buffer_pointer_]);
output_buffer_pointer_ += cycles_to_read; output_buffer_pointer_ += cycles_to_read;
// 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_);
}
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.
if(output_buffer_pointer_ == output_buffer_.size()) { if(output_buffer_pointer_ == output_buffer_.size()) {
output_buffer_pointer_ = 0; output_buffer_pointer_ = 0;
did_complete_samples(this, output_buffer_); 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 cycles_remaining -= cycles_to_read;
// 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. break;
const auto steps = stepper_->step();
if(steps < input_buffer_.size()) { case Conversion::ResampleSmaller:
auto *const input_buffer = input_buffer_.data(); while(cycles_remaining) {
std::memmove( input_buffer, const auto cycles_to_read = std::min(cycles_remaining, input_buffer_.size() - input_buffer_depth_);
&input_buffer[steps], sample_source_.get_samples(cycles_to_read, &input_buffer_[input_buffer_depth_]);
sizeof(int16_t) * (input_buffer_.size() - steps)); cycles_remaining -= cycles_to_read;
input_buffer_depth_ -= steps; input_buffer_depth_ += cycles_to_read;
} else {
if(steps > input_buffer_.size()) if(input_buffer_depth_ == input_buffer_.size()) {
sample_source_.skip_samples(steps - input_buffer_.size()); resample_input_buffer();
input_buffer_depth_ = 0;
} }
} }
} 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_; T &sample_source_;
@ -218,7 +206,6 @@ template <typename T> class LowpassSpeaker: public Speaker {
); );
number_of_taps = (number_of_taps * 2) | 1; number_of_taps = (number_of_taps * 2) | 1;
output_buffer_pointer_ = 0;
stepper_ = std::make_unique<SignalProcessing::Stepper>( stepper_ = std::make_unique<SignalProcessing::Stepper>(
uint64_t(filter_parameters.input_cycles_per_second), uint64_t(filter_parameters.input_cycles_per_second),
uint64_t(filter_parameters.output_cycles_per_second)); uint64_t(filter_parameters.output_cycles_per_second));
@ -230,8 +217,68 @@ template <typename T> class LowpassSpeaker: public Speaker {
high_pass_frequency, high_pass_frequency,
SignalProcessing::FIRFilter::DefaultAttenuation); 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;
}
} }
}; };