mirror of
https://github.com/TomHarte/CLK.git
synced 2024-12-30 19:30:17 +00:00
Merge pull request #746 from TomHarte/LatencyChop
Reduces latency in macOS, improves concurrency
This commit is contained in:
commit
f1cd35fa16
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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 = "<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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -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 = "<group>";
|
||||
};
|
||||
4BD5F1961D1352A000631CD1 /* Updater */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4BD5F1931D13528900631CD1 /* CSBestEffortUpdater.h */,
|
||||
4BD5F1941D13528900631CD1 /* CSBestEffortUpdater.mm */,
|
||||
);
|
||||
name = Updater;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
@ -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
|
||||
|
@ -13,7 +13,6 @@
|
||||
#import "CSOpenGLView.h"
|
||||
#import "CSROMReceiverView.h"
|
||||
|
||||
#import "CSBestEffortUpdater.h"
|
||||
#import "CSJoystickManager.h"
|
||||
|
||||
#import "NSData+CRC32.h"
|
||||
|
@ -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
|
||||
@ -187,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 {
|
||||
@ -200,7 +188,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 +207,7 @@ class MachineDocument:
|
||||
openGLView.window!.makeFirstResponder(openGLView)
|
||||
|
||||
// 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.
|
||||
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)
|
||||
|
@ -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
|
||||
|
@ -57,13 +57,14 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
|
||||
*/
|
||||
- (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;
|
||||
- (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize;
|
||||
|
||||
- (void)setView:(nullable CSOpenGLView *)view aspectRatio:(float)aspectRatio;
|
||||
|
||||
- (void)start;
|
||||
- (void)stop;
|
||||
|
||||
- (void)updateViewForPixelSize:(CGSize)pixelSize;
|
||||
- (void)drawViewForPixelSize:(CGSize)pixelSize;
|
||||
|
||||
|
@ -10,6 +10,7 @@
|
||||
#import "CSMachine+Target.h"
|
||||
|
||||
#include "CSROMFetcher.hpp"
|
||||
#import "CSHighPrecisionTimer.h"
|
||||
|
||||
#include "MediaTarget.hpp"
|
||||
#include "JoystickMachine.hpp"
|
||||
@ -24,6 +25,7 @@
|
||||
#import "NSBundle+DataResource.h"
|
||||
#import "NSData+StdVector.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <bitset>
|
||||
|
||||
#import <OpenGL/OpenGL.h>
|
||||
@ -32,7 +34,7 @@
|
||||
#include "../../../../Outputs/OpenGL/ScanTarget.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)speakerDidChangeInputClock:(Outputs::Speaker::Speaker *)speaker;
|
||||
- (void)addLED:(NSString *)led;
|
||||
@ -149,6 +151,17 @@ struct ActivityObserver: public Activity::Observer {
|
||||
std::bitset<65536> _depressedKeys;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -156,6 +169,7 @@ struct ActivityObserver: public Activity::Observer {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
_analyser = result;
|
||||
_speedMultiplier = 1.0;
|
||||
|
||||
Machine::Error error;
|
||||
std::vector<ROMMachine::ROM> missing_roms;
|
||||
@ -201,6 +215,8 @@ struct ActivityObserver: public Activity::Observer {
|
||||
_speakerDelegate.machineAccessLock = _delegateMachineAccessLock;
|
||||
|
||||
_joystickMachine = _machine->joystick_machine();
|
||||
[self updateJoystickTimer];
|
||||
_isUpdating.clear();
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@ -214,6 +230,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.
|
||||
@ -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) {
|
||||
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<bool> 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<bool> 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);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setView:(CSOpenGLView *)view aspectRatio:(float)aspectRatio {
|
||||
_view = view;
|
||||
_view.displayLinkDelegate = self;
|
||||
[view performWithGLContext:^{
|
||||
[self setupOutputWithAspectRatio:aspectRatio];
|
||||
}];
|
||||
} flushDrawable:NO];
|
||||
}
|
||||
|
||||
- (void)setupOutputWithAspectRatio:(float)aspectRatio {
|
||||
@ -326,7 +352,7 @@ struct ActivityObserver: public Activity::Observer {
|
||||
}
|
||||
|
||||
- (void)updateViewForPixelSize:(CGSize)pixelSize {
|
||||
_scanTarget->update((int)pixelSize.width, (int)pixelSize.height);
|
||||
// _pixelSize = pixelSize;
|
||||
|
||||
// @synchronized(self) {
|
||||
// const auto scan_status = _machine->crt_machine()->get_scan_status();
|
||||
@ -375,15 +401,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 {
|
||||
@ -694,4 +722,139 @@ struct ActivityObserver: public Activity::Observer {
|
||||
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
|
||||
|
@ -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
|
@ -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
|
@ -101,6 +101,23 @@ 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 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
|
||||
of typical first-responder actions.
|
||||
@ -109,6 +126,7 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
|
||||
|
||||
@property (atomic, weak, nullable) id <CSOpenGLViewDelegate> delegate;
|
||||
@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
|
||||
/// 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
|
||||
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;
|
||||
|
||||
/*!
|
||||
|
@ -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);
|
||||
@ -75,7 +70,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 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,
|
||||
@ -89,7 +85,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 +101,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 +111,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 +142,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 +175,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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
@ -109,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;
|
||||
};
|
||||
@ -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_;
|
||||
|
@ -273,18 +273,22 @@ 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.
|
||||
virtual void submit() = 0;
|
||||
/// as long as it feels is appropriate, subject to a @c flush.
|
||||
virtual void submit() {}
|
||||
|
||||
|
||||
/*
|
||||
@ -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.
|
||||
|
@ -60,6 +60,10 @@ template <typename T> class LowpassSpeaker: public Speaker {
|
||||
// Implemented as per Speaker.
|
||||
void set_computed_output_rate(float cycles_per_second, int buffer_size) final {
|
||||
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_.parameters_are_dirty = true;
|
||||
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) {
|
||||
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_.parameters_are_dirty = 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) {
|
||||
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_.parameters_are_dirty = true;
|
||||
}
|
||||
@ -99,6 +109,12 @@ template <typename T> 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.
|
||||
@ -121,69 +137,41 @@ template <typename T> 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_;
|
||||
@ -218,7 +206,6 @@ template <typename T> class LowpassSpeaker: public Speaker {
|
||||
);
|
||||
number_of_taps = (number_of_taps * 2) | 1;
|
||||
|
||||
output_buffer_pointer_ = 0;
|
||||
stepper_ = std::make_unique<SignalProcessing::Stepper>(
|
||||
uint64_t(filter_parameters.input_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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user