1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-04-10 22:37:30 +00:00

Merge pull request #1063 from TomHarte/EventDriven

Switch macOS to an event-driven emulation.
This commit is contained in:
Thomas Harte 2022-07-14 11:37:20 -04:00 committed by GitHub
commit 5aa129fbd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 440 additions and 340 deletions

View File

@ -20,6 +20,11 @@ inline Nanos nanos_now() {
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count();
}
inline Seconds seconds(Nanos nanos) {
return double(nanos) / 1e9;
}
}
#endif /* TimeTypes_h */

View File

@ -89,6 +89,7 @@ DeferringAsyncTaskQueue::~DeferringAsyncTaskQueue() {
void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) {
if(!deferred_tasks_) {
deferred_tasks_ = std::make_unique<TaskList>();
deferred_tasks_->reserve(16);
}
deferred_tasks_->push_back(function);
}

View File

@ -15,6 +15,7 @@
#include <list>
#include <memory>
#include <thread>
#include <vector>
#if defined(__APPLE__) && !defined(IGNORE_APPLE)
#include <dispatch/dispatch.h>
@ -23,7 +24,7 @@
namespace Concurrency {
using TaskList = std::list<std::function<void(void)>>;
using TaskList = std::vector<std::function<void(void)>>;
/*!
An async task queue allows a caller to enqueue void(void) functions. Those functions are guaranteed
@ -56,7 +57,7 @@ class AsyncTaskQueue {
std::atomic_bool should_destruct_;
std::condition_variable processing_condition_;
std::mutex queue_mutex_;
TaskList pending_tasks_;
std::list<std::function<void(void)>> pending_tasks_;
std::thread thread_;
#endif

View File

@ -0,0 +1,99 @@
//
// AsyncUpdater.h
// Clock Signal
//
// Created by Thomas Harte on 06/07/2022.
// Copyright © 2022 Thomas Harte. All rights reserved.
//
#ifndef AsyncUpdater_hpp
#define AsyncUpdater_hpp
#include <atomic>
#include <condition_variable>
#include <mutex>
#include "../ClockReceiver/TimeTypes.hpp"
namespace Concurrency {
template <typename Performer> class AsyncUpdater {
public:
template <typename... Args> AsyncUpdater(Args&&... args) :
performer(std::forward<Args>(args)...),
actions_(std::make_unique<ActionVector>()),
performer_thread_{
[this] {
Time::Nanos last_fired = Time::nanos_now();
auto actions = std::make_unique<ActionVector>();
while(!should_quit) {
// Wait for new actions to be signalled, and grab them.
std::unique_lock lock(condition_mutex_);
while(actions_->empty()) {
condition_.wait(lock);
}
std::swap(actions, actions_);
lock.unlock();
// Update to now.
auto time_now = Time::nanos_now();
performer.perform(time_now - last_fired);
last_fired = time_now;
// Perform the actions.
for(const auto& action: *actions) {
action();
}
actions->clear();
}
}
} {}
/// Run the performer up to 'now' and then perform @c post_action.
///
/// @c post_action will be performed asynchronously, on the same
/// thread as the performer.
///
/// Actions may be elided,
void update(const std::function<void(void)> &post_action) {
std::lock_guard guard(condition_mutex_);
actions_->push_back(post_action);
condition_.notify_all();
}
void stop() {
if(performer_thread_.joinable()) {
should_quit = true;
update([] {});
performer_thread_.join();
}
}
~AsyncUpdater() {
stop();
}
// The object that will actually receive time advances.
Performer performer;
private:
// The list of actions waiting be performed. These will be elided,
// increasing their latency, if the emulation thread falls behind.
using ActionVector = std::vector<std::function<void(void)>>;
std::unique_ptr<ActionVector> actions_;
// Necessary synchronisation parts.
std::atomic<bool> should_quit = false;
std::mutex condition_mutex_;
std::condition_variable condition_;
// Ensure the thread isn't constructed until after the mutex
// and condition variable.
std::thread performer_thread_;
};
}
#endif /* AsyncUpdater_hpp */

View File

@ -176,10 +176,6 @@ class ConcreteMachine:
return total_length - cycle.length;
}
void flush() {
chipset_.flush();
}
private:
CPU::MC68000Mk2::Processor<ConcreteMachine, true, true> mc68000_;
@ -209,15 +205,18 @@ class ConcreteMachine:
chipset_.set_scan_target(scan_target);
}
Outputs::Display::ScanStatus get_scaled_scan_status() const {
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return chipset_.get_scaled_scan_status();
}
// MARK: - MachineTypes::TimedMachine.
void run_for(const Cycles cycles) {
void run_for(const Cycles cycles) final {
mc68000_.run_for(cycles);
flush();
}
void flush_output(int) final {
chipset_.flush();
}
// MARK: - MachineTypes::MouseMachine.
@ -228,7 +227,7 @@ class ConcreteMachine:
// MARK: - MachineTypes::JoystickMachine.
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() {
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
return chipset_.get_joysticks();
}

View File

@ -1048,11 +1048,15 @@ template <bool has_fdc> class ConcreteMachine:
return HalfCycles(0);
}
/// Another Z80 entry point; indicates that a partcular run request has concluded.
void flush() {
/// Fields requests to pump all output.
void flush_output(int outputs) final {
// Just flush the AY.
ay_.update();
ay_.flush();
if(outputs & Output::Audio) {
ay_.update();
ay_.flush();
}
// Always flush the FDC.
flush_fdc();
}

View File

@ -810,11 +810,16 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
return Cycles(1);
}
void flush() {
update_video();
update_audio();
void flush_output(int outputs) final {
update_just_in_time_cards();
audio_queue_.perform();
if(outputs & Output::Video) {
update_video();
}
if(outputs & Output::Audio) {
update_audio();
audio_queue_.perform();
}
}
void run_for(const Cycles cycles) final {

View File

@ -326,13 +326,17 @@ class ConcreteMachine:
m65816_.run_for(cycles);
}
void flush() {
video_.flush();
void flush_output(int outputs) final {
iwm_.flush();
adb_glu_.flush();
AudioUpdater updater(this);
audio_queue_.perform();
if(outputs & Output::Video) {
video_.flush();
}
if(outputs & Output::Audio) {
AudioUpdater updater(this);
audio_queue_.perform();
}
}
void set_scan_target(Outputs::Display::ScanTarget *target) override {

View File

@ -190,7 +190,6 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
void run_for(const Cycles cycles) final {
mc68000_.run_for(cycles);
flush();
}
using Microcycle = CPU::MC68000Mk2::Microcycle;
@ -366,7 +365,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
return delay;
}
void flush() {
void flush_output(int) {
// Flush the video before the audio queue; in a Mac the
// video is responsible for providing part of the
// audio signal, so the two aren't as distinct as in

View File

@ -174,7 +174,7 @@ class ConcreteMachine:
bus_->apply_confidence(confidence_counter_);
}
void flush() {
void flush_output(int) final {
bus_->flush();
}

View File

@ -154,7 +154,6 @@ class ConcreteMachine:
}
mc68000_.run_for(cycles);
flush();
}
// MARK: MC68000::BusHandler
@ -414,14 +413,19 @@ class ConcreteMachine:
return HalfCycles(0);
}
void flush() {
void flush_output(int outputs) final {
dma_.flush();
mfp_.flush();
keyboard_acia_.flush();
midi_acia_.flush();
video_.flush();
update_audio();
audio_queue_.perform();
if(outputs & Output::Video) {
video_.flush();
}
if(outputs & Output::Audio) {
update_audio();
audio_queue_.perform();
}
}
private:

View File

@ -342,10 +342,14 @@ class ConcreteMachine:
return penalty;
}
void flush() {
vdp_.flush();
update_audio();
audio_queue_.perform();
void flush_output(int outputs) final {
if(outputs & Output::Video) {
vdp_.flush();
}
if(outputs & Output::Audio) {
update_audio();
audio_queue_.perform();
}
}
float get_confidence() final {

View File

@ -620,9 +620,13 @@ class ConcreteMachine:
return Cycles(1);
}
void flush() {
update_video();
mos6560_.flush();
void flush_output(int outputs) final {
if(outputs & Output::Video) {
update_video();
}
if(outputs & Output::Audio) {
mos6560_.flush();
}
}
void run_for(const Cycles cycles) final {

View File

@ -501,10 +501,14 @@ template <bool has_scsi_bus> class ConcreteMachine:
return Cycles(int(cycles));
}
forceinline void flush() {
video_.flush();
update_audio();
audio_queue_.perform();
void flush_output(int outputs) final {
if(outputs & Output::Video) {
video_.flush();
}
if(outputs & Output::Audio) {
update_audio();
audio_queue_.perform();
}
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {

View File

@ -539,10 +539,14 @@ template <bool has_disk_controller, bool is_6mhz> class ConcreteMachine:
return penalty;
}
void flush() {
nick_.flush();
update_audio();
audio_queue_.perform();
void flush_output(int outputs) final {
if(outputs & Output::Video) {
nick_.flush();
}
if(outputs & Output::Audio) {
update_audio();
audio_queue_.perform();
}
}
private:

View File

@ -615,10 +615,14 @@ class ConcreteMachine:
return addition;
}
void flush() {
vdp_.flush();
update_audio();
audio_queue_.perform();
void flush_output(int outputs) final {
if(outputs & Output::Video) {
vdp_.flush();
}
if(outputs & Output::Audio) {
update_audio();
audio_queue_.perform();
}
}
void set_keyboard_line(int line) {

View File

@ -210,6 +210,16 @@ class ConcreteMachine:
z80_.run_for(cycles);
}
void flush_output(int outputs) final {
if(outputs & Output::Video) {
vdp_.flush();
}
if(outputs & Output::Audio) {
update_audio();
audio_queue_.perform();
}
}
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
if(vdp_ += cycle.length) {
z80_.set_interrupt_line(vdp_->get_interrupt_line(), vdp_.last_sequence_point_overrun());
@ -382,12 +392,6 @@ class ConcreteMachine:
return HalfCycles(0);
}
void flush() {
vdp_.flush();
update_audio();
audio_queue_.perform();
}
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
return joysticks_;
}

View File

@ -578,9 +578,13 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
return Cycles(1);
}
forceinline void flush() {
video_.flush();
via_.flush();
void flush_output(int outputs) final {
if(outputs & Output::Video) {
video_.flush();
}
if(outputs & Output::Audio) {
via_.flush();
}
diskii_.flush();
}

View File

@ -292,11 +292,16 @@ template<bool is_zx81> class ConcreteMachine:
return HalfCycles(0);
}
forceinline void flush() {
video_.flush();
void flush_output(int outputs) final {
if(outputs & Output::Video) {
video_.flush();
}
if constexpr (is_zx81) {
update_audio();
audio_queue_.perform();
if(outputs & Output::Audio) {
update_audio();
audio_queue_.perform();
}
}
}

View File

@ -263,10 +263,15 @@ template<Model model> class ConcreteMachine:
}
}
void flush() {
video_.flush();
update_audio();
audio_queue_.perform();
void flush_output(int outputs) override {
if(outputs & Output::Video) {
video_.flush();
}
if(outputs & Output::Audio) {
update_audio();
audio_queue_.perform();
}
if constexpr (model == Model::Plus3) {
fdc_.flush();

View File

@ -39,6 +39,10 @@ class TimedMachine {
fiction: it will apply across the system, including to the CRT.
*/
virtual void set_speed_multiplier(double multiplier) {
if(speed_multiplier_ == multiplier) {
return;
}
speed_multiplier_ = multiplier;
auto audio_producer = dynamic_cast<AudioProducer *>(this);
@ -61,6 +65,16 @@ class TimedMachine {
virtual float get_confidence() { return 0.5f; }
virtual std::string debug_type() { return ""; }
struct Output {
static constexpr int Video = 1 << 0;
static constexpr int Audio = 1 << 1;
static constexpr int All = Video | Audio;
};
/// Ensures all locally-buffered output is posted onward for the types of output indicated
/// by the bitfield argument, which is comprised of flags from the namespace @c Output.
virtual void flush_output(int) {}
protected:
/// Runs the machine for @c cycles.
virtual void run_for(const Cycles cycles) = 0;

View File

@ -2138,6 +2138,7 @@
4BDCC5F81FB27A5E001220C5 /* ROMMachine.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ROMMachine.hpp; sourceTree = "<group>"; };
4BDDBA981EF3451200347E61 /* Z80MachineCycleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Z80MachineCycleTests.swift; sourceTree = "<group>"; };
4BE0151C286A8C8E00EA42E9 /* MemorySwitches.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MemorySwitches.hpp; sourceTree = "<group>"; };
4BE0151E28766ECF00EA42E9 /* AsyncUpdater.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = AsyncUpdater.hpp; sourceTree = "<group>"; };
4BE0A3EC237BB170002AB46F /* ST.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ST.cpp; sourceTree = "<group>"; };
4BE0A3ED237BB170002AB46F /* ST.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ST.hpp; sourceTree = "<group>"; };
4BE211DD253E4E4800435408 /* 65C02_no_Rockwell_test.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = 65C02_no_Rockwell_test.bin; path = "Klaus Dormann/65C02_no_Rockwell_test.bin"; sourceTree = "<group>"; };
@ -2754,6 +2755,7 @@
children = (
4B3940E51DA83C8300427841 /* AsyncTaskQueue.cpp */,
4B3940E61DA83C8300427841 /* AsyncTaskQueue.hpp */,
4BE0151E28766ECF00EA42E9 /* AsyncUpdater.hpp */,
);
name = Concurrency;
path = ../../Concurrency;

View File

@ -82,19 +82,19 @@
</CommandLineArgument>
<CommandLineArgument
argument = "--volume=0.001"
isEnabled = "YES">
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--new=enterprise"
isEnabled = "YES">
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--basic-version=any"
isEnabled = "YES">
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "&quot;/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/ColecoVision/Galaxian (1983)(Atari).col&quot;"
isEnabled = "NO">
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "&quot;/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/Master System/R-Type (NTSC).sms&quot;"

View File

@ -29,7 +29,7 @@
@returns An instance of CSAudioQueue if successful; @c nil otherwise.
*/
- (nonnull instancetype)initWithSamplingRate:(Float64)samplingRate isStereo:(BOOL)isStereo NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithSamplingRate:(Float64)samplingRate isStereo:(BOOL)isStereo NS_DESIGNATED_INITIALIZER;
- (nonnull instancetype)init __attribute((unavailable));
/*!
@ -58,4 +58,9 @@
*/
@property (nonatomic, readonly) NSUInteger preferredBufferSize;
/*!
@returns @C YES if this queue is running low or is completely exhausted of new audio buffers.
*/
@property (atomic, readonly) BOOL isRunningDry;
@end

View File

@ -8,98 +8,56 @@
#import "CSAudioQueue.h"
@import AudioToolbox;
#include <stdatomic.h>
#define AudioQueueBufferMaxLength 8192
#define NumberOfStoredAudioQueueBuffer 16
#define OSSGuard(x) { \
const OSStatus status = x; \
assert(!status); \
(void)status; \
}
static NSLock *CSAudioQueueDeallocLock;
/*!
Holds a weak reference to a CSAudioQueue. Used to work around an apparent AudioQueue bug.
See -[CSAudioQueue dealloc].
*/
@interface CSWeakAudioQueuePointer: NSObject
@property(nonatomic, weak) CSAudioQueue *queue;
@end
@implementation CSWeakAudioQueuePointer
@end
#define IsDry(x) (x) < 2
@implementation CSAudioQueue {
AudioQueueRef _audioQueue;
NSLock *_storedBuffersLock;
CSWeakAudioQueuePointer *_weakPointer;
int _enqueuedBuffers;
NSLock *_deallocLock;
NSLock *_queueLock;
atomic_int _enqueuedBuffers;
}
#pragma mark - AudioQueue callbacks
#pragma mark - Status
/*!
@returns @c YES if the queue is running dry; @c NO otherwise.
*/
- (BOOL)audioQueue:(AudioQueueRef)theAudioQueue didCallbackWithBuffer:(AudioQueueBufferRef)buffer {
[_storedBuffersLock lock];
--_enqueuedBuffers;
// If that leaves nothing in the queue, re-enqueue whatever just came back in order to keep the
// queue going. AudioQueues seem to stop playing and never restart no matter how much encouragement
// if exhausted.
if(!_enqueuedBuffers) {
AudioQueueEnqueueBuffer(theAudioQueue, buffer, 0, NULL);
++_enqueuedBuffers;
} else {
AudioQueueFreeBuffer(_audioQueue, buffer);
}
[_storedBuffersLock unlock];
return YES;
- (BOOL)isRunningDry {
return IsDry(atomic_load_explicit(&_enqueuedBuffers, memory_order_relaxed));
}
static void audioOutputCallback(
void *inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer) {
// Pull the delegate call for audio queue running dry outside of the locked region, to allow non-deadlocking
// lifecycle -dealloc events to result from it.
if([CSAudioQueueDeallocLock tryLock]) {
CSAudioQueue *queue = ((__bridge CSWeakAudioQueuePointer *)inUserData).queue;
BOOL isRunningDry = NO;
isRunningDry = [queue audioQueue:inAQ didCallbackWithBuffer:inBuffer];
id<CSAudioQueueDelegate> delegate = queue.delegate;
[CSAudioQueueDeallocLock unlock];
if(isRunningDry) [delegate audioQueueIsRunningDry:queue];
}
}
#pragma mark - Standard object lifecycle
#pragma mark - Object lifecycle
- (instancetype)initWithSamplingRate:(Float64)samplingRate isStereo:(BOOL)isStereo {
self = [super init];
if(self) {
if(!CSAudioQueueDeallocLock) {
CSAudioQueueDeallocLock = [[NSLock alloc] init];
}
_storedBuffersLock = [[NSLock alloc] init];
_deallocLock = [[NSLock alloc] init];
_queueLock = [[NSLock alloc] init];
atomic_store_explicit(&_enqueuedBuffers, 0, memory_order_relaxed);
_samplingRate = samplingRate;
// determine preferred buffer sizes
_preferredBufferSize = AudioQueueBufferMaxLength;
while((Float64)_preferredBufferSize*100.0 > samplingRate) _preferredBufferSize >>= 1;
// Determine preferred buffer size as being the first power of two
// not less than 1/100th of a second.
_preferredBufferSize = 1;
const NSUInteger oneHundredthOfRate = (NSUInteger)(samplingRate / 100.0);
while(_preferredBufferSize < oneHundredthOfRate) _preferredBufferSize <<= 1;
/*
Describe a mono 16bit stream of the requested sampling rate
*/
// Describe a 16bit stream of the requested sampling rate.
AudioStreamBasicDescription outputDescription;
outputDescription.mSampleRate = samplingRate;
outputDescription.mChannelsPerFrame = isStereo ? 2 : 1;
outputDescription.mFormatID = kAudioFormatLinearPCM;
outputDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
outputDescription.mChannelsPerFrame = isStereo ? 2 : 1;
outputDescription.mFramesPerPacket = 1;
outputDescription.mBytesPerFrame = 2 * outputDescription.mChannelsPerFrame;
outputDescription.mBytesPerPacket = outputDescription.mBytesPerFrame * outputDescription.mFramesPerPacket;
@ -107,17 +65,38 @@ static void audioOutputCallback(
outputDescription.mReserved = 0;
// create an audio output queue along those lines; see -dealloc re: the CSWeakAudioQueuePointer
_weakPointer = [[CSWeakAudioQueuePointer alloc] init];
_weakPointer.queue = self;
if(!AudioQueueNewOutput(
// Create an audio output queue along those lines.
__weak CSAudioQueue *weakSelf = self;
if(AudioQueueNewOutputWithDispatchQueue(
&_audioQueue,
&outputDescription,
audioOutputCallback,
(__bridge void *)(_weakPointer),
NULL,
kCFRunLoopCommonModes,
0,
&_audioQueue)) {
dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0),
^(AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {
CSAudioQueue *queue = weakSelf;
if(!queue) {
return;
}
if([queue->_deallocLock tryLock]) {
[queue->_queueLock lock];
OSSGuard(AudioQueueFreeBuffer(inAQ, inBuffer));
[queue->_queueLock unlock];
const int buffers = atomic_fetch_add(&queue->_enqueuedBuffers, -1) - 1;
if(!buffers) {
OSSGuard(AudioQueuePause(queue->_audioQueue));
}
id<CSAudioQueueDelegate> delegate = queue.delegate;
[queue->_deallocLock unlock];
if(IsDry(buffers)) [delegate audioQueueIsRunningDry:queue];
}
}
)
) {
return nil;
}
}
@ -125,30 +104,17 @@ static void audioOutputCallback(
}
- (void)dealloc {
[CSAudioQueueDeallocLock lock];
[_deallocLock lock];
if(_audioQueue) {
AudioQueueDispose(_audioQueue, true);
OSSGuard(AudioQueueDispose(_audioQueue, true));
_audioQueue = NULL;
}
[CSAudioQueueDeallocLock unlock];
// Yuck. Horrid hack happening here. At least under macOS v10.12, I am frequently seeing calls to
// my registered audio callback (audioOutputCallback in this case) that occur **after** the call
// to AudioQueueDispose above, even though the second parameter there asks for a synchronous shutdown.
// So this appears to be a bug on Apple's side.
//
// Since the audio callback receives a void * pointer that identifies the class it should branch into,
// it's therefore unsafe to pass 'self'. Instead I pass a CSWeakAudioQueuePointer which points to the actual
// queue. The lifetime of that class is the lifetime of this instance plus 1 second, as effected by the
// artificial dispatch_after below; it serves only to keep pointerSaviour alive for an extra second.
//
// Why a second? That's definitely quite a lot longer than any amount of audio that may be queued. So
// probably safe. As and where Apple's audio queue works properly, CSAudioQueueDeallocLock should provide
// absolute safety; elsewhere the CSWeakAudioQueuePointer provides probabilistic.
CSWeakAudioQueuePointer *pointerSaviour = _weakPointer;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[pointerSaviour hash];
});
// nil out the dealloc lock before entering the critical section such
// that it becomes impossible for anyone else to acquire.
NSLock *deallocLock = _deallocLock;
_deallocLock = nil;
[deallocLock unlock];
}
#pragma mark - Audio enqueuer
@ -156,25 +122,27 @@ static void audioOutputCallback(
- (void)enqueueAudioBuffer:(const int16_t *)buffer numberOfSamples:(size_t)lengthInSamples {
size_t bufferBytes = lengthInSamples * sizeof(int16_t);
[_storedBuffersLock lock];
// Don't enqueue more than 4 buffers ahead of now, to ensure not too much latency accrues.
if(_enqueuedBuffers > 4) {
[_storedBuffersLock unlock];
if(atomic_load_explicit(&_enqueuedBuffers, memory_order_relaxed) == 4) {
return;
}
++_enqueuedBuffers;
const int enqueuedBuffers = atomic_fetch_add(&_enqueuedBuffers, 1) + 1;
[_queueLock lock];
AudioQueueBufferRef newBuffer;
AudioQueueAllocateBuffer(_audioQueue, (UInt32)bufferBytes * 2, &newBuffer);
OSSGuard(AudioQueueAllocateBuffer(_audioQueue, (UInt32)bufferBytes * 2, &newBuffer));
memcpy(newBuffer->mAudioData, buffer, bufferBytes);
newBuffer->mAudioDataByteSize = (UInt32)bufferBytes;
AudioQueueEnqueueBuffer(_audioQueue, newBuffer, 0, NULL);
[_storedBuffersLock unlock];
OSSGuard(AudioQueueEnqueueBuffer(_audioQueue, newBuffer, 0, NULL));
// '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);
// Start the queue if it isn't started yet, and there are now some packets waiting.
if(enqueuedBuffers > 1) {
OSSGuard(AudioQueueStart(_audioQueue, NULL));
}
[_queueLock unlock];
}
#pragma mark - Sampling Rate getters

View File

@ -15,7 +15,6 @@ class MachineDocument:
NSWindowDelegate,
CSMachineDelegate,
CSScanTargetViewResponderDelegate,
CSAudioQueueDelegate,
CSROMReciverViewDelegate
{
// MARK: - Mutual Exclusion.
@ -274,17 +273,12 @@ class MachineDocument:
if self.audioQueue == nil || self.audioQueue.samplingRate != selectedSamplingRate || self.audioQueue != self.machine.audioQueue {
self.machine.audioQueue = nil
self.audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate), isStereo:isStereo)
self.audioQueue.delegate = self
self.machine.audioQueue = self.audioQueue
self.machine.setAudioSamplingRate(Float(selectedSamplingRate), bufferSize:audioQueue.preferredBufferSize, stereo:isStereo)
}
}
}
/// Responds to the CSAudioQueueDelegate dry-queue warning message by requesting a machine update.
final func audioQueueIsRunningDry(_ audioQueue: CSAudioQueue) {
}
// MARK: - Pasteboard Forwarding.
/// Forwards any text currently on the pasteboard into the active machine.

View File

@ -73,7 +73,7 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
- (void)setMouseButton:(int)button isPressed:(BOOL)isPressed;
- (void)addMouseMotionX:(CGFloat)deltaX y:(CGFloat)deltaY;
@property (atomic, strong, nullable) CSAudioQueue *audioQueue;
@property (nonatomic, strong, nullable) CSAudioQueue *audioQueue;
@property (nonatomic, readonly, nonnull) CSScanTargetView *view;
@property (nonatomic, weak, nullable) id<CSMachineDelegate> delegate;

View File

@ -24,6 +24,7 @@
#include "../../../../ClockReceiver/TimeTypes.hpp"
#include "../../../../ClockReceiver/ScanSynchroniser.hpp"
#include "../../../../Concurrency/AsyncUpdater.hpp"
#import "CSStaticAnalyser+TargetVector.h"
#import "NSBundle+DataResource.h"
@ -34,12 +35,31 @@
#include <codecvt>
#include <locale>
namespace {
struct MachineUpdater {
void perform(Time::Nanos duration) {
// Top out at 1/20th of a second; this is a safeguard against a negative
// feedback loop if emulation starts running slowly.
const auto seconds = std::min(Time::seconds(duration), 0.05);
machine->run_for(seconds);
}
MachineTypes::TimedMachine *machine = nullptr;
};
}
@interface CSMachine() <CSScanTargetViewDisplayLinkDelegate>
- (void)speaker:(Outputs::Speaker::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length;
- (void)speakerDidChangeInputClock:(Outputs::Speaker::Speaker *)speaker;
- (void)addLED:(NSString *)led isPersistent:(BOOL)isPersistent;
@end
@interface CSMachine() <CSAudioQueueDelegate>
- (void)audioQueueIsRunningDry:(nonnull CSAudioQueue *)audioQueue;
@end
struct LockProtectedDelegate {
// Contractual promise is: machine, the pointer **and** the object **, may be accessed only
// in sections protected by the machineAccessLock;
@ -101,13 +121,7 @@ struct ActivityObserver: public Activity::Observer {
CSJoystickManager *_joystickManager;
NSMutableArray<CSMachineLED *> *_leds;
CSHighPrecisionTimer *_timer;
std::atomic_flag _isUpdating;
Time::Nanos _syncTime;
Time::Nanos _timeDiff;
double _refreshPeriod;
BOOL _isSyncLocking;
Concurrency::AsyncUpdater<MachineUpdater> updater;
Time::ScanSynchroniser _scanSynchroniser;
NSTimer *_joystickTimer;
@ -133,6 +147,7 @@ struct ActivityObserver: public Activity::Observer {
[missingROMs appendString:[NSString stringWithUTF8String:wstring_converter.to_bytes(description).c_str()]];
return nil;
}
updater.performer.machine = _machine->timed_machine();
// Use the keyboard as a joystick if the machine has no keyboard, or if it has a 'non-exclusive' keyboard.
_inputMode =
@ -155,7 +170,6 @@ struct ActivityObserver: public Activity::Observer {
_joystickMachine = _machine->joystick_machine();
[self updateJoystickTimer];
_isUpdating.clear();
}
return self;
}
@ -210,6 +224,11 @@ struct ActivityObserver: public Activity::Observer {
}
}
- (void)setAudioQueue:(CSAudioQueue *)audioQueue {
_audioQueue = audioQueue;
audioQueue.delegate = self;
}
- (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize stereo:(BOOL)stereo {
@synchronized(self) {
[self setSpeakerDelegate:&_speakerDelegate sampleRate:samplingRate bufferSize:bufferSize stereo:stereo];
@ -433,9 +452,9 @@ struct ActivityObserver: public Activity::Observer {
}
- (void)applyInputEvent:(dispatch_block_t)event {
@synchronized(_inputEvents) {
[_inputEvents addObject:event];
}
updater.update([event] {
event();
});
}
- (void)clearAllKeys {
@ -646,121 +665,52 @@ struct ActivityObserver: public Activity::Observer {
#pragma mark - Timer
- (void)scanTargetViewDisplayLinkDidFire:(CSScanTargetView *)view now:(const CVTimeStamp *)now outputTime:(const CVTimeStamp *)outputTime {
// First order of business: grab a timestamp.
const auto timeNow = Time::nanos_now();
BOOL isSyncLocking;
@synchronized(self) {
// Store a means to map from CVTimeStamp.hostTime to Time::Nanos;
// there is an extremely dodgy assumption here that the former is in ns.
// If you can find a well-defined way to get the CVTimeStamp.hostTime units,
// whether at runtime or via preprocessor define, I'd love to know about it.
if(!_timeDiff) {
_timeDiff = int64_t(timeNow) - int64_t(now->hostTime);
}
// 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;
// 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 draw];
}
- (void)audioQueueIsRunningDry:(nonnull CSAudioQueue *)audioQueue {
updater.update([self] {
updater.performer.machine->flush_output(MachineTypes::TimedMachine::Output::Audio);
});
}
#define TICKS 1000
- (void)scanTargetViewDisplayLinkDidFire:(CSScanTargetView *)view now:(const CVTimeStamp *)now outputTime:(const CVTimeStamp *)outputTime {
updater.update([self] {
// Grab a pointer to the timed machine from somewhere where it has already
// been dynamically cast, to avoid that cost here.
MachineTypes::TimedMachine *const timed_machine = updater.performer.machine;
// Definitely update video; update audio too if that pipeline is looking a little dry.
auto outputs = MachineTypes::TimedMachine::Output::Video;
if(_audioQueue.isRunningDry) {
outputs |= MachineTypes::TimedMachine::Output::Audio;
}
timed_machine->flush_output(outputs);
// Attempt sync-matching if this machine is a fit.
//
// TODO: either cache scan_producer(), or possibly start caching these things
// inside the DynamicMachine?
const auto scanStatus = self->_machine->scan_producer()->get_scan_status();
const bool canSynchronise = self->_scanSynchroniser.can_synchronise(scanStatus, self.view.refreshPeriod);
if(canSynchronise) {
const double multiplier = self->_scanSynchroniser.next_speed_multiplier(self->_machine->scan_producer()->get_scan_status());
timed_machine->set_speed_multiplier(multiplier);
} else {
timed_machine->set_speed_multiplier(1.0);
}
// Ask Metal to rasterise all that just happened and present it.
[self.view updateBacking];
dispatch_async(dispatch_get_main_queue(), ^{
[self.view draw];
});
});
}
- (void)start {
__block auto lastTime = Time::nanos_now();
_timer = [[CSHighPrecisionTimer alloc] initWithTask:^{
// Grab the time now and, therefore, the amount of time since the timer last fired
// (subject to a cap to avoid potential perpetual regression).
const auto timeNow = Time::nanos_now();
lastTime = std::max(timeNow - Time::Nanos(10'000'000'000 / TICKS), lastTime);
const auto duration = timeNow - lastTime;
BOOL splitAndSync = NO;
@synchronized(self) {
// Post on input events.
@synchronized(self->_inputEvents) {
for(dispatch_block_t action: self->_inputEvents) {
action();
}
[self->_inputEvents removeAllObjects];
}
// If this tick includes vsync then inspect the machine.
//
// _syncTime = 0 is used here as a sentinel to mark that a sync time is known;
// this with the >= test ensures that no syncs are missed even if some sort of
// performance problem is afoot (e.g. I'm debugging).
if(self->_syncTime && timeNow >= self->_syncTime) {
splitAndSync = self->_isSyncLocking = self->_scanSynchroniser.can_synchronise(self->_machine->scan_producer()->get_scan_status(), self->_refreshPeriod);
// 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. Include a sanity check against an out-of-bounds
// _syncTime; that can happen when debugging (possibly inter alia?).
if(splitAndSync) {
if(self->_syncTime >= lastTime) {
self->_machine->timed_machine()->run_for((double)(self->_syncTime - lastTime) / 1e9);
self->_machine->timed_machine()->set_speed_multiplier(
self->_scanSynchroniser.next_speed_multiplier(self->_machine->scan_producer()->get_scan_status())
);
self->_machine->timed_machine()->run_for((double)(timeNow - self->_syncTime) / 1e9);
} else {
self->_machine->timed_machine()->run_for((double)(timeNow - lastTime) / 1e9);
}
}
self->_syncTime = 0;
}
// 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->timed_machine()->run_for((double)duration / 1e9);
}
}
// 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 updateBacking];
if(splitAndSync) {
[self.view draw];
}
self->_isUpdating.clear();
});
}
lastTime = timeNow;
} interval:uint64_t(1000000000) / uint64_t(TICKS)];
// A no-op; retained in case of future changes to the manner of scheduling.
}
#undef TICKS
- (void)stop {
[_timer invalidate];
_timer = nil;
updater.stop();
}
+ (BOOL)attemptInstallROM:(NSURL *)url {

View File

@ -939,12 +939,14 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
- (void)updateFrameBuffer {
// TODO: rethink BufferingScanTarget::perform. Is it now really just for guarding the modals?
_scanTarget.perform([=] {
const Outputs::Display::ScanTarget::Modals *const newModals = _scanTarget.new_modals();
if(newModals) {
[self setModals:*newModals];
}
});
if(_scanTarget.has_new_modals()) {
_scanTarget.perform([=] {
const Outputs::Display::ScanTarget::Modals *const newModals = _scanTarget.new_modals();
if(newModals) {
[self setModals:*newModals];
}
});
}
@synchronized(self) {
if(!_frameBufferRenderPass) return;

View File

@ -170,4 +170,9 @@
*/
- (void)willChangeScanTargetOwner;
/*!
@returns The period of a frame.
*/
@property(nonatomic, readonly) double refreshPeriod;
@end

View File

@ -103,8 +103,12 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
[self stopDisplayLink];
}
- (double)refreshPeriod {
return CVDisplayLinkGetActualOutputVideoRefreshPeriod(_displayLink);
}
- (void)stopDisplayLink {
const double duration = CVDisplayLinkGetActualOutputVideoRefreshPeriod(_displayLink);
const double duration = [self refreshPeriod];
CVDisplayLinkStop(_displayLink);
// This is a workaround; CVDisplayLinkStop does not wait for any existing call to the

View File

@ -176,8 +176,6 @@ struct BusHandler {
return HalfCycles(0);
}
void flush() {}
int transaction_delay;
int instructions;

View File

@ -31,6 +31,7 @@ void Timer::tick() {
std::lock_guard lock_guard(*machineMutex);
machine->run_for(double(duration) / 1e9);
machine->flush_output(MachineTypes::TimedMachine::Output::All);
}
Timer::~Timer() {

View File

@ -151,6 +151,7 @@ struct MachineRunner {
if(split_and_sync) {
timed_machine->run_for(double(vsync_time - last_time_) / 1e9);
timed_machine->flush_output(MachineTypes::TimedMachine::Output::All);
timed_machine->set_speed_multiplier(
scan_synchroniser_.next_speed_multiplier(scan_producer->get_scan_status())
);
@ -163,9 +164,11 @@ struct MachineRunner {
lock_guard.lock();
timed_machine->run_for(double(time_now - vsync_time) / 1e9);
timed_machine->flush_output(MachineTypes::TimedMachine::Output::All);
} else {
timed_machine->set_speed_multiplier(scan_synchroniser_.get_base_speed_multiplier());
timed_machine->run_for(double(time_now - last_time_) / 1e9);
timed_machine->flush_output(MachineTypes::TimedMachine::Output::All);
}
last_time_ = time_now;
}

View File

@ -302,7 +302,7 @@ size_t BufferingScanTarget::write_area_data_size() const {
void BufferingScanTarget::set_modals(Modals modals) {
perform([=] {
modals_ = modals;
modals_are_dirty_ = true;
modals_are_dirty_.store(true, std::memory_order::memory_order_relaxed);
});
}
@ -374,10 +374,12 @@ void BufferingScanTarget::set_line_buffer(Line *line_buffer, LineMetadata *metad
}
const Outputs::Display::ScanTarget::Modals *BufferingScanTarget::new_modals() {
if(!modals_are_dirty_) {
const auto modals_are_dirty = modals_are_dirty_.load(std::memory_order::memory_order_relaxed);
if(!modals_are_dirty) {
return nullptr;
}
modals_are_dirty_ = false;
modals_are_dirty_.store(false, std::memory_order::memory_order_relaxed);
// MAJOR SHARP EDGE HERE: assume that because the new_modals have been fetched then the caller will
// now ensure their texture buffer is appropriate. They might provide a new pointer and might now.
@ -392,3 +394,7 @@ const Outputs::Display::ScanTarget::Modals *BufferingScanTarget::new_modals() {
const Outputs::Display::ScanTarget::Modals &BufferingScanTarget::modals() const {
return modals_;
}
bool BufferingScanTarget::has_new_modals() const {
return modals_are_dirty_.load(std::memory_order::memory_order_relaxed);
}

View File

@ -161,6 +161,11 @@ class BufferingScanTarget: public Outputs::Display::ScanTarget {
/// @returns the current @c Modals.
const Modals &modals() const;
/// @returns @c true if new modals are available; @c false otherwise.
///
/// Safe to call from any thread.
bool has_new_modals() const;
private:
// ScanTarget overrides.
void set_modals(Modals) final;
@ -253,7 +258,7 @@ class BufferingScanTarget: public Outputs::Display::ScanTarget {
// Current modals and whether they've yet been returned
// from a call to @c get_new_modals.
Modals modals_;
bool modals_are_dirty_ = false;
std::atomic<bool> modals_are_dirty_ = false;
// Provides a per-data size implementation of end_data; a previous
// implementation used blind memcpy and that turned into something

View File

@ -170,6 +170,10 @@ template <typename ConcreteT, bool is_stereo> class LowpassBase: public Speaker
}
inline void resample_input_buffer(int scale) {
if(output_buffer_.empty()) {
return;
}
if constexpr (is_stereo) {
output_buffer_[output_buffer_pointer_ + 0] = filter_->apply(input_buffer_.data(), 2);
output_buffer_[output_buffer_pointer_ + 1] = filter_->apply(input_buffer_.data() + 1, 2);
@ -364,6 +368,10 @@ template <typename SampleSource> class PullLowpass: public LowpassBase<PullLowpa
at construction, filtering it and passing it on to the speaker's delegate if there is one.
*/
void run_for(Concurrency::DeferringAsyncTaskQueue &queue, const Cycles cycles) {
if(cycles == Cycles(0)) {
return;
}
queue.defer([this, cycles] {
run_for(cycles);
});
@ -379,10 +387,7 @@ template <typename SampleSource> class PullLowpass: public LowpassBase<PullLowpa
at construction, filtering it and passing it on to the speaker's delegate if there is one.
*/
void run_for(const Cycles cycles) {
std::size_t cycles_remaining = size_t(cycles.as_integral());
if(!cycles_remaining) return;
process(cycles_remaining);
process(size_t(cycles.as_integral()));
}
SampleSource &sample_source_;

View File

@ -650,7 +650,6 @@ template <Personality personality, typename T, bool uses_ready_line> void Proces
}
cycles_left_to_run_ = number_of_cycles;
bus_handler_.flush();
}
template <Personality personality, typename T, bool uses_ready_line> void Processor<personality, T, uses_ready_line>::set_ready_line(bool active) {

View File

@ -141,12 +141,6 @@ template <typename addr_t> class BusHandler {
Cycles perform_bus_operation([[maybe_unused]] BusOperation operation, [[maybe_unused]] addr_t address, [[maybe_unused]] uint8_t *value) {
return Cycles(1);
}
/*!
Announces completion of all the cycles supplied to a .run_for request on the 6502. Intended to allow
bus handlers to perform any deferred output work.
*/
void flush() {}
};
}

View File

@ -1022,7 +1022,6 @@ template <typename BusHandler, bool uses_ready_line> void Processor<BusHandler,
#undef stack_address
cycles_left_to_run_ = number_of_cycles;
bus_handler_.flush();
}
void ProcessorBase::set_power_on(bool active) {

View File

@ -357,8 +357,6 @@ class BusHandler {
return HalfCycles(0);
}
void flush() {}
/*!
Provides information about the path of execution if enabled via the template.
*/

View File

@ -2198,7 +2198,6 @@ template <class T, bool dtack_is_implicit, bool signal_will_perform> void Proces
#undef destination
#undef destination_address
bus_handler_.flush();
e_clock_phase_ = (e_clock_phase_ + cycles_run_for) % 20;
half_cycles_left_to_run_ = remaining_duration - cycles_run_for;
}

View File

@ -347,8 +347,6 @@ class BusHandler {
return HalfCycles(0);
}
void flush() {}
/*!
Provides information about the path of execution if enabled via the template.
*/

View File

@ -48,7 +48,6 @@ template < class T,
static PartialMachineCycle bus_acknowledge_cycle = {PartialMachineCycle::BusAcknowledge, HalfCycles(2), nullptr, nullptr, false};
number_of_cycles_ -= bus_handler_.perform_machine_cycle(bus_acknowledge_cycle) + HalfCycles(1);
if(!number_of_cycles_) {
bus_handler_.flush();
return;
}
}
@ -70,7 +69,6 @@ template < class T,
case MicroOp::BusOperation:
if(number_of_cycles_ < operation->machine_cycle.length) {
scheduled_program_counter_--;
bus_handler_.flush();
return;
}
if(uses_wait_line && operation->machine_cycle.was_requested) {

View File

@ -399,12 +399,6 @@ class BusHandler {
HalfCycles perform_machine_cycle([[maybe_unused]] const PartialMachineCycle &cycle) {
return HalfCycles(0);
}
/*!
Announces completion of all the cycles supplied to a .run_for request on the Z80. Intended to allow
bus handlers to perform any deferred output work.
*/
void flush() {}
};
#include "Implementation/Z80Storage.hpp"