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:
commit
5aa129fbd3
@ -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 */
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
99
Concurrency/AsyncUpdater.hpp
Normal file
99
Concurrency/AsyncUpdater.hpp
Normal 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 */
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -174,7 +174,7 @@ class ConcreteMachine:
|
||||
bus_->apply_confidence(confidence_counter_);
|
||||
}
|
||||
|
||||
void flush() {
|
||||
void flush_output(int) final {
|
||||
bus_->flush();
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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:
|
||||
|
@ -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) {
|
||||
|
@ -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_;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 = ""/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/ColecoVision/Galaxian (1983)(Atari).col""
|
||||
isEnabled = "NO">
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = ""/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/Master System/R-Type (NTSC).sms""
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -170,4 +170,9 @@
|
||||
*/
|
||||
- (void)willChangeScanTargetOwner;
|
||||
|
||||
/*!
|
||||
@returns The period of a frame.
|
||||
*/
|
||||
@property(nonatomic, readonly) double refreshPeriod;
|
||||
|
||||
@end
|
||||
|
@ -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
|
||||
|
@ -176,8 +176,6 @@ struct BusHandler {
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
void flush() {}
|
||||
|
||||
int transaction_delay;
|
||||
int instructions;
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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_;
|
||||
|
@ -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) {
|
||||
|
@ -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() {}
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -357,8 +357,6 @@ class BusHandler {
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
void flush() {}
|
||||
|
||||
/*!
|
||||
Provides information about the path of execution if enabled via the template.
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -347,8 +347,6 @@ class BusHandler {
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
void flush() {}
|
||||
|
||||
/*!
|
||||
Provides information about the path of execution if enabled via the template.
|
||||
*/
|
||||
|
@ -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) {
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user