mirror of
https://github.com/TomHarte/CLK.git
synced 2025-09-12 02:24:31 +00:00
Compare commits
6 Commits
2022-07-23
...
SoftwareOu
Author | SHA1 | Date | |
---|---|---|---|
|
4078baa424 | ||
|
3179d0d963 | ||
|
6a8c792c63 | ||
|
678e1a38fa | ||
|
f4004baff8 | ||
|
f04e4faae2 |
@@ -47,7 +47,7 @@ template <typename MachineType> class MultiInterface {
|
||||
std::recursive_mutex &machines_mutex_;
|
||||
|
||||
private:
|
||||
std::vector<Concurrency::AsyncTaskQueue<true>> queues_;
|
||||
std::vector<Concurrency::AsyncTaskQueue> queues_;
|
||||
};
|
||||
|
||||
class MultiTimedMachine: public MultiInterface<MachineTypes::TimedMachine>, public MachineTypes::TimedMachine {
|
||||
|
@@ -315,7 +315,7 @@ template <class T, class LocalTimeScale = HalfCycles, class TargetTimeScale = Lo
|
||||
LocalTimeScale time_since_update_;
|
||||
TargetTimeScale threshold_;
|
||||
bool is_flushed_ = true;
|
||||
Concurrency::AsyncTaskQueue<true> task_queue_;
|
||||
Concurrency::AsyncTaskQueue task_queue_;
|
||||
};
|
||||
|
||||
#endif /* JustInTime_h */
|
||||
|
@@ -20,11 +20,6 @@ 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 */
|
||||
|
||||
|
@@ -12,18 +12,18 @@
|
||||
|
||||
using namespace MOS::MOS6560;
|
||||
|
||||
AudioGenerator::AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
AudioGenerator::AudioGenerator(Concurrency::DeferringAsyncTaskQueue &audio_queue) :
|
||||
audio_queue_(audio_queue) {}
|
||||
|
||||
|
||||
void AudioGenerator::set_volume(uint8_t volume) {
|
||||
audio_queue_.enqueue([this, volume]() {
|
||||
audio_queue_.defer([this, volume]() {
|
||||
volume_ = int16_t(volume) * range_multiplier_;
|
||||
});
|
||||
}
|
||||
|
||||
void AudioGenerator::set_control(int channel, uint8_t value) {
|
||||
audio_queue_.enqueue([this, channel, value]() {
|
||||
audio_queue_.defer([this, channel, value]() {
|
||||
control_registers_[channel] = value;
|
||||
});
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ namespace MOS6560 {
|
||||
// audio state
|
||||
class AudioGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
AudioGenerator(Concurrency::DeferringAsyncTaskQueue &audio_queue);
|
||||
|
||||
void set_volume(uint8_t volume);
|
||||
void set_control(int channel, uint8_t value);
|
||||
@@ -33,7 +33,7 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
|
||||
unsigned int counters_[4] = {2, 1, 0, 0}; // create a slight phase offset for the three channels
|
||||
unsigned int shift_registers_[4] = {0, 0, 0, 0};
|
||||
@@ -433,7 +433,7 @@ template <class BusHandler> class MOS6560 {
|
||||
BusHandler &bus_handler_;
|
||||
Outputs::CRT::CRT crt_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
AudioGenerator audio_generator_;
|
||||
Outputs::Speaker::PullLowpass<AudioGenerator> speaker_;
|
||||
|
||||
|
@@ -16,7 +16,7 @@
|
||||
using namespace GI::AY38910;
|
||||
|
||||
template <bool is_stereo>
|
||||
AY38910<is_stereo>::AY38910(Personality personality, Concurrency::AsyncTaskQueue<false> &task_queue) : task_queue_(task_queue) {
|
||||
AY38910<is_stereo>::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {
|
||||
// Don't use the low bit of the envelope position if this is an AY.
|
||||
envelope_position_mask_ |= personality == Personality::AY38910;
|
||||
|
||||
@@ -252,7 +252,7 @@ template <bool is_stereo> void AY38910<is_stereo>::set_register_value(uint8_t va
|
||||
// If this is a register that affects audio output, enqueue a mutation onto the
|
||||
// audio generation thread.
|
||||
if(selected_register_ < 14) {
|
||||
task_queue_.enqueue([this, selected_register = selected_register_, value] () {
|
||||
task_queue_.defer([this, selected_register = selected_register_, value] () {
|
||||
// Perform any register-specific mutation to output generation.
|
||||
uint8_t masked_value = value;
|
||||
switch(selected_register) {
|
||||
|
@@ -71,7 +71,7 @@ enum class Personality {
|
||||
template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
/// Creates a new AY38910.
|
||||
AY38910(Personality, Concurrency::AsyncTaskQueue<false> &);
|
||||
AY38910(Personality, Concurrency::DeferringAsyncTaskQueue &);
|
||||
|
||||
/// Sets the value the AY would read from its data lines if it were not outputting.
|
||||
void set_data_input(uint8_t r);
|
||||
@@ -114,7 +114,7 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
|
||||
static constexpr bool get_is_stereo() { return is_stereo; }
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
||||
|
||||
int selected_register_ = 0;
|
||||
uint8_t registers_[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
using namespace Audio;
|
||||
|
||||
Audio::Toggle::Toggle(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
Audio::Toggle::Toggle(Concurrency::DeferringAsyncTaskQueue &audio_queue) :
|
||||
audio_queue_(audio_queue) {}
|
||||
|
||||
void Toggle::get_samples(std::size_t number_of_samples, std::int16_t *target) {
|
||||
@@ -28,7 +28,7 @@ void Toggle::skip_samples(std::size_t) {}
|
||||
void Toggle::set_output(bool enabled) {
|
||||
if(is_enabled_ == enabled) return;
|
||||
is_enabled_ = enabled;
|
||||
audio_queue_.enqueue([this, enabled] {
|
||||
audio_queue_.defer([this, enabled] {
|
||||
level_ = enabled ? volume_ : 0;
|
||||
});
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ namespace Audio {
|
||||
*/
|
||||
class Toggle: public Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
Toggle(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
Toggle(Concurrency::DeferringAsyncTaskQueue &audio_queue);
|
||||
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||
void set_sample_volume_range(std::int16_t range);
|
||||
@@ -31,7 +31,7 @@ class Toggle: public Outputs::Speaker::SampleSource {
|
||||
private:
|
||||
// Accessed on the calling thread.
|
||||
bool is_enabled_ = false;
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
|
||||
// Accessed on the audio thread.
|
||||
int16_t level_ = 0, volume_ = 0;
|
||||
|
@@ -12,7 +12,7 @@
|
||||
|
||||
using namespace Konami;
|
||||
|
||||
SCC::SCC(Concurrency::AsyncTaskQueue<false> &task_queue) :
|
||||
SCC::SCC(Concurrency::DeferringAsyncTaskQueue &task_queue) :
|
||||
task_queue_(task_queue) {}
|
||||
|
||||
bool SCC::is_zero_level() const {
|
||||
@@ -55,7 +55,7 @@ void SCC::write(uint16_t address, uint8_t value) {
|
||||
address &= 0xff;
|
||||
if(address < 0x80) ram_[address] = value;
|
||||
|
||||
task_queue_.enqueue([this, address, value] {
|
||||
task_queue_.defer([this, address, value] {
|
||||
// Check for a write into waveform memory.
|
||||
if(address < 0x80) {
|
||||
waves_[address >> 5].samples[address & 0x1f] = value;
|
||||
|
@@ -24,7 +24,7 @@ namespace Konami {
|
||||
class SCC: public ::Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
/// Creates a new SCC.
|
||||
SCC(Concurrency::AsyncTaskQueue<false> &task_queue);
|
||||
SCC(Concurrency::DeferringAsyncTaskQueue &task_queue);
|
||||
|
||||
/// As per ::SampleSource; provides a broadphase test for silence.
|
||||
bool is_zero_level() const;
|
||||
@@ -41,7 +41,7 @@ class SCC: public ::Outputs::Speaker::SampleSource {
|
||||
uint8_t read(uint16_t address);
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
||||
|
||||
// State from here on down is accessed ony from the audio thread.
|
||||
int master_divider_ = 0;
|
||||
|
@@ -26,9 +26,9 @@ template <typename Child> class OPLBase: public ::Outputs::Speaker::SampleSource
|
||||
}
|
||||
|
||||
protected:
|
||||
OPLBase(Concurrency::AsyncTaskQueue<false> &task_queue) : task_queue_(task_queue) {}
|
||||
OPLBase(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {}
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
||||
|
||||
private:
|
||||
uint8_t selected_register_ = 0;
|
||||
|
@@ -12,7 +12,7 @@
|
||||
|
||||
using namespace Yamaha::OPL;
|
||||
|
||||
OPLL::OPLL(Concurrency::AsyncTaskQueue<false> &task_queue, int audio_divider, bool is_vrc7):
|
||||
OPLL::OPLL(Concurrency::DeferringAsyncTaskQueue &task_queue, int audio_divider, bool is_vrc7):
|
||||
OPLBase(task_queue), audio_divider_(audio_divider), is_vrc7_(is_vrc7) {
|
||||
// Due to the way that sound mixing works on the OPLL, the audio divider may not
|
||||
// be larger than 4.
|
||||
@@ -74,7 +74,7 @@ OPLL::OPLL(Concurrency::AsyncTaskQueue<false> &task_queue, int audio_divider, bo
|
||||
void OPLL::write_register(uint8_t address, uint8_t value) {
|
||||
// The OPLL doesn't have timers or other non-audio functions, so all writes
|
||||
// go to the audio queue.
|
||||
task_queue_.enqueue([this, address, value] {
|
||||
task_queue_.defer([this, address, value] {
|
||||
// The first 8 locations are used to define the custom instrument, and have
|
||||
// exactly the same format as the patch set arrays at the head of this file.
|
||||
if(address < 8) {
|
||||
|
@@ -24,7 +24,7 @@ namespace OPL {
|
||||
class OPLL: public OPLBase<OPLL> {
|
||||
public:
|
||||
/// Creates a new OPLL or VRC7.
|
||||
OPLL(Concurrency::AsyncTaskQueue<false> &task_queue, int audio_divider = 1, bool is_vrc7 = false);
|
||||
OPLL(Concurrency::DeferringAsyncTaskQueue &task_queue, int audio_divider = 1, bool is_vrc7 = false);
|
||||
|
||||
/// As per ::SampleSource; provides audio output.
|
||||
void get_samples(std::size_t number_of_samples, std::int16_t *target);
|
||||
|
@@ -13,7 +13,7 @@
|
||||
|
||||
using namespace TI;
|
||||
|
||||
SN76489::SN76489(Personality personality, Concurrency::AsyncTaskQueue<false> &task_queue, int additional_divider) : task_queue_(task_queue) {
|
||||
SN76489::SN76489(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue, int additional_divider) : task_queue_(task_queue) {
|
||||
set_sample_volume_range(0);
|
||||
|
||||
switch(personality) {
|
||||
@@ -49,7 +49,7 @@ void SN76489::set_sample_volume_range(std::int16_t range) {
|
||||
}
|
||||
|
||||
void SN76489::write(uint8_t value) {
|
||||
task_queue_.enqueue([value, this] () {
|
||||
task_queue_.defer([value, this] () {
|
||||
if(value & 0x80) {
|
||||
active_register_ = value;
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ class SN76489: public Outputs::Speaker::SampleSource {
|
||||
};
|
||||
|
||||
/// Creates a new SN76489.
|
||||
SN76489(Personality personality, Concurrency::AsyncTaskQueue<false> &task_queue, int additional_divider = 1);
|
||||
SN76489(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue, int additional_divider = 1);
|
||||
|
||||
/// Writes a new value to the SN76489.
|
||||
void write(uint8_t value);
|
||||
@@ -41,7 +41,7 @@ class SN76489: public Outputs::Speaker::SampleSource {
|
||||
void evaluate_output_volume();
|
||||
int volumes_[16];
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
||||
|
||||
struct ToneChannel {
|
||||
// Programmatically-set state; updated by the processor.
|
||||
|
109
Concurrency/AsyncTaskQueue.cpp
Normal file
109
Concurrency/AsyncTaskQueue.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// AsyncTaskQueue.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 07/10/2016.
|
||||
// Copyright 2016 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "AsyncTaskQueue.hpp"
|
||||
|
||||
using namespace Concurrency;
|
||||
|
||||
AsyncTaskQueue::AsyncTaskQueue()
|
||||
#ifndef USE_GCD
|
||||
:
|
||||
should_destruct_(false),
|
||||
thread_([this] () {
|
||||
while(!should_destruct_) {
|
||||
std::function<void(void)> next_function;
|
||||
|
||||
// Take lock, check for a new task.
|
||||
std::unique_lock lock(queue_mutex_);
|
||||
if(!pending_tasks_.empty()) {
|
||||
next_function = pending_tasks_.front();
|
||||
pending_tasks_.pop_front();
|
||||
}
|
||||
|
||||
if(next_function) {
|
||||
// If there is a task, release lock and perform it.
|
||||
lock.unlock();
|
||||
next_function();
|
||||
} else {
|
||||
// If there isn't a task, atomically block on the processing condition and release the lock
|
||||
// until there's something pending (and then release it again via scope).
|
||||
processing_condition_.wait(lock);
|
||||
}
|
||||
}
|
||||
})
|
||||
#else
|
||||
: serial_dispatch_queue_(dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL))
|
||||
#endif
|
||||
{}
|
||||
|
||||
AsyncTaskQueue::~AsyncTaskQueue() {
|
||||
#ifdef USE_GCD
|
||||
flush();
|
||||
dispatch_release(serial_dispatch_queue_);
|
||||
#else
|
||||
// Set should destruct, and then give the thread a bit of a nudge
|
||||
// via an empty enqueue.
|
||||
should_destruct_ = true;
|
||||
enqueue([](){});
|
||||
|
||||
// Wait for the thread safely to terminate.
|
||||
thread_.join();
|
||||
#endif
|
||||
}
|
||||
|
||||
void AsyncTaskQueue::enqueue(std::function<void(void)> function) {
|
||||
#ifdef USE_GCD
|
||||
dispatch_async(serial_dispatch_queue_, ^{function();});
|
||||
#else
|
||||
std::lock_guard lock(queue_mutex_);
|
||||
pending_tasks_.push_back(function);
|
||||
processing_condition_.notify_all();
|
||||
#endif
|
||||
}
|
||||
|
||||
void AsyncTaskQueue::flush() {
|
||||
#ifdef USE_GCD
|
||||
dispatch_sync(serial_dispatch_queue_, ^{});
|
||||
#else
|
||||
auto flush_mutex = std::make_shared<std::mutex>();
|
||||
auto flush_condition = std::make_shared<std::condition_variable>();
|
||||
std::unique_lock lock(*flush_mutex);
|
||||
enqueue([=] () {
|
||||
std::unique_lock inner_lock(*flush_mutex);
|
||||
flush_condition->notify_all();
|
||||
});
|
||||
flush_condition->wait(lock);
|
||||
#endif
|
||||
}
|
||||
|
||||
DeferringAsyncTaskQueue::~DeferringAsyncTaskQueue() {
|
||||
perform();
|
||||
flush();
|
||||
}
|
||||
|
||||
void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) {
|
||||
if(!deferred_tasks_) {
|
||||
deferred_tasks_ = std::make_unique<TaskList>();
|
||||
}
|
||||
deferred_tasks_->push_back(function);
|
||||
}
|
||||
|
||||
void DeferringAsyncTaskQueue::perform() {
|
||||
if(!deferred_tasks_) return;
|
||||
enqueue([deferred_tasks_raw = deferred_tasks_.release()] {
|
||||
std::unique_ptr<TaskList> deferred_tasks(deferred_tasks_raw);
|
||||
for(const auto &function : *deferred_tasks) {
|
||||
function();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DeferringAsyncTaskQueue::flush() {
|
||||
perform();
|
||||
AsyncTaskQueue::flush();
|
||||
}
|
@@ -12,171 +12,92 @@
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "../ClockReceiver/TimeTypes.hpp"
|
||||
#if defined(__APPLE__) && !defined(IGNORE_APPLE)
|
||||
#include <dispatch/dispatch.h>
|
||||
#define USE_GCD
|
||||
#endif
|
||||
|
||||
namespace Concurrency {
|
||||
|
||||
/// An implementation detail; provides the time-centric part of a TaskQueue with a real Performer.
|
||||
template <typename Performer> struct TaskQueueStorage {
|
||||
template <typename... Args> TaskQueueStorage(Args&&... args) :
|
||||
performer(std::forward<Args>(args)...),
|
||||
last_fired_(Time::nanos_now()) {}
|
||||
using TaskList = std::list<std::function<void(void)>>;
|
||||
|
||||
Performer performer;
|
||||
/*!
|
||||
An async task queue allows a caller to enqueue void(void) functions. Those functions are guaranteed
|
||||
to be performed serially and asynchronously from the caller. A caller may also request to flush,
|
||||
causing it to block until all previously-enqueued functions are complete.
|
||||
*/
|
||||
class AsyncTaskQueue {
|
||||
public:
|
||||
AsyncTaskQueue();
|
||||
virtual ~AsyncTaskQueue();
|
||||
|
||||
protected:
|
||||
void update() {
|
||||
auto time_now = Time::nanos_now();
|
||||
performer.perform(time_now - last_fired_);
|
||||
last_fired_ = time_now;
|
||||
}
|
||||
/*!
|
||||
Adds @c function to the queue.
|
||||
|
||||
@discussion Functions will be performed serially and asynchronously. This method is safe to
|
||||
call from multiple threads.
|
||||
@parameter function The function to enqueue.
|
||||
*/
|
||||
void enqueue(std::function<void(void)> function);
|
||||
|
||||
/*!
|
||||
Blocks the caller until all previously-enqueud functions have completed.
|
||||
*/
|
||||
void flush();
|
||||
|
||||
private:
|
||||
Time::Nanos last_fired_;
|
||||
};
|
||||
#ifdef USE_GCD
|
||||
dispatch_queue_t serial_dispatch_queue_;
|
||||
#else
|
||||
std::atomic_bool should_destruct_;
|
||||
std::condition_variable processing_condition_;
|
||||
std::mutex queue_mutex_;
|
||||
TaskList pending_tasks_;
|
||||
|
||||
/// An implementation detail; provides a no-op implementation of time advances for TaskQueues without a Performer.
|
||||
template <> struct TaskQueueStorage<void> {
|
||||
TaskQueueStorage() {}
|
||||
|
||||
protected:
|
||||
void update() {}
|
||||
std::thread thread_;
|
||||
#endif
|
||||
};
|
||||
|
||||
/*!
|
||||
A task queue allows a caller to enqueue @c void(void) functions. Those functions are guaranteed
|
||||
to be performed serially and asynchronously from the caller.
|
||||
A deferring async task queue is one that accepts a list of functions to be performed but defers
|
||||
any action until told to perform. It performs them by enquing a single asynchronous task that will
|
||||
perform the deferred tasks in order.
|
||||
|
||||
If @c perform_automatically is true, functions will be performed as soon as is possible,
|
||||
at the cost of thread synchronisation.
|
||||
|
||||
If @c perform_automatically is false, functions will be queued up but not dispatched
|
||||
until a call to perform().
|
||||
|
||||
If a @c Performer type is supplied then a public member, @c performer will be constructed
|
||||
with the arguments supplied to TaskQueue's constructor. That instance will receive calls of the
|
||||
form @c .perform(nanos) before every batch of new actions, indicating how much time has
|
||||
passed since the previous @c perform.
|
||||
|
||||
@note Even if @c perform_automatically is true, actions may be batched, when a long-running
|
||||
action occupies the asynchronous thread for long enough. So it is not true that @c perform will be
|
||||
called once per action.
|
||||
It therefore offers similar semantics to an asynchronous task queue, but allows for management of
|
||||
synchronisation costs, since neither defer nor perform make any effort to be thread safe.
|
||||
*/
|
||||
template <bool perform_automatically, typename Performer = void> class AsyncTaskQueue: public TaskQueueStorage<Performer> {
|
||||
class DeferringAsyncTaskQueue: public AsyncTaskQueue {
|
||||
public:
|
||||
template <typename... Args> AsyncTaskQueue(Args&&... args) :
|
||||
TaskQueueStorage<Performer>(std::forward<Args>(args)...),
|
||||
thread_{
|
||||
[this] {
|
||||
ActionVector actions;
|
||||
~DeferringAsyncTaskQueue();
|
||||
|
||||
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();
|
||||
/*!
|
||||
Adds a function to the deferral list.
|
||||
|
||||
// Update to now (which is possibly a no-op).
|
||||
TaskQueueStorage<Performer>::update();
|
||||
This is not thread safe; it should be serialised with other calls to itself and to perform.
|
||||
*/
|
||||
void defer(std::function<void(void)> function);
|
||||
|
||||
// Perform the actions and destroy them.
|
||||
for(const auto &action: actions) {
|
||||
action();
|
||||
}
|
||||
actions.clear();
|
||||
}
|
||||
}
|
||||
} {}
|
||||
/*!
|
||||
Enqueues a function that will perform all currently deferred functions, in the
|
||||
order that they were deferred.
|
||||
|
||||
/// Enqueus @c post_action to be performed asynchronously at some point
|
||||
/// in the future. If @c perform_automatically is @c true then the action
|
||||
/// will be performed as soon as possible. Otherwise it will sit unsheculed until
|
||||
/// a call to @c perform().
|
||||
///
|
||||
/// Actions may be elided.
|
||||
///
|
||||
/// If this TaskQueue has a @c Performer then the action will be performed
|
||||
/// on the same thread as the performer, after the performer has been updated
|
||||
/// to 'now'.
|
||||
void enqueue(const std::function<void(void)> &post_action) {
|
||||
std::lock_guard guard(condition_mutex_);
|
||||
actions_.push_back(post_action);
|
||||
This is not thread safe; it should be serialised with other calls to itself and to defer.
|
||||
*/
|
||||
void perform();
|
||||
|
||||
if constexpr (perform_automatically) {
|
||||
condition_.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
/// Causes any enqueued actions that are not yet scheduled to be scheduled.
|
||||
void perform() {
|
||||
if(actions_.empty()) {
|
||||
return;
|
||||
}
|
||||
condition_.notify_all();
|
||||
}
|
||||
|
||||
/// Permanently stops this task queue, blocking until that has happened.
|
||||
/// All pending actions will be performed first.
|
||||
///
|
||||
/// The queue cannot be restarted; this is a destructive action.
|
||||
void stop() {
|
||||
if(thread_.joinable()) {
|
||||
should_quit_ = true;
|
||||
enqueue([] {});
|
||||
if constexpr (!perform_automatically) {
|
||||
perform();
|
||||
}
|
||||
thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedules any remaining unscheduled work, then blocks synchronously
|
||||
/// until all scheduled work has been performed.
|
||||
void flush() {
|
||||
std::mutex flush_mutex;
|
||||
std::condition_variable flush_condition;
|
||||
bool has_run = false;
|
||||
std::unique_lock lock(flush_mutex);
|
||||
|
||||
enqueue([&flush_mutex, &flush_condition, &has_run] () {
|
||||
std::unique_lock inner_lock(flush_mutex);
|
||||
has_run = true;
|
||||
flush_condition.notify_all();
|
||||
});
|
||||
|
||||
if constexpr (!perform_automatically) {
|
||||
perform();
|
||||
}
|
||||
|
||||
flush_condition.wait(lock, [&has_run] { return has_run; });
|
||||
}
|
||||
|
||||
~AsyncTaskQueue() {
|
||||
stop();
|
||||
}
|
||||
/*!
|
||||
Blocks the caller until all previously-enqueud functions have completed.
|
||||
*/
|
||||
void flush();
|
||||
|
||||
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)>>;
|
||||
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 thread_;
|
||||
std::unique_ptr<TaskList> deferred_tasks_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* AsyncTaskQueue_hpp */
|
||||
#endif /* Concurrency_hpp */
|
||||
|
@@ -176,6 +176,10 @@ class ConcreteMachine:
|
||||
return total_length - cycle.length;
|
||||
}
|
||||
|
||||
void flush() {
|
||||
chipset_.flush();
|
||||
}
|
||||
|
||||
private:
|
||||
CPU::MC68000Mk2::Processor<ConcreteMachine, true, true> mc68000_;
|
||||
|
||||
@@ -205,18 +209,15 @@ class ConcreteMachine:
|
||||
chipset_.set_scan_target(scan_target);
|
||||
}
|
||||
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||||
Outputs::Display::ScanStatus get_scaled_scan_status() const {
|
||||
return chipset_.get_scaled_scan_status();
|
||||
}
|
||||
|
||||
// MARK: - MachineTypes::TimedMachine.
|
||||
|
||||
void run_for(const Cycles cycles) final {
|
||||
void run_for(const Cycles cycles) {
|
||||
mc68000_.run_for(cycles);
|
||||
}
|
||||
|
||||
void flush_output(int) final {
|
||||
chipset_.flush();
|
||||
flush();
|
||||
}
|
||||
|
||||
// MARK: - MachineTypes::MouseMachine.
|
||||
@@ -227,7 +228,7 @@ class ConcreteMachine:
|
||||
|
||||
// MARK: - MachineTypes::JoystickMachine.
|
||||
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
|
||||
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() {
|
||||
return chipset_.get_joysticks();
|
||||
}
|
||||
|
||||
|
@@ -149,7 +149,7 @@ class Audio: public DMADevice<4> {
|
||||
|
||||
// Transient output state, and its destination.
|
||||
Outputs::Speaker::PushLowpass<true> speaker_;
|
||||
Concurrency::AsyncTaskQueue<true> queue_;
|
||||
Concurrency::AsyncTaskQueue queue_;
|
||||
|
||||
using AudioBuffer = std::array<int16_t, 4096>;
|
||||
static constexpr int BufferCount = 3;
|
||||
|
@@ -64,7 +64,7 @@ class BitplaneShifter {
|
||||
int odd_delay,
|
||||
int even_delay);
|
||||
|
||||
/// Shifts either two pixels (in low-res mode) or four pixels (in high-res).
|
||||
/// Shifts either two pixels (in low-res mode) and four pixels (in high-res).
|
||||
void shift(bool high_res) {
|
||||
constexpr int shifts[] = {16, 32};
|
||||
|
||||
@@ -73,14 +73,8 @@ class BitplaneShifter {
|
||||
}
|
||||
|
||||
/// @returns The next four pixels to output; in low-resolution mode only two
|
||||
/// of them will be unique.
|
||||
///
|
||||
/// The value is arranges so that MSB = first pixel to output, LSB = last.
|
||||
///
|
||||
/// Each byte is swizzled to provide easier playfield separation, being in the form:
|
||||
/// b6, b7 = 0;
|
||||
/// b3–b5: planes 1, 3 and 5;
|
||||
/// b0–b2: planes 0, 2 and 4.
|
||||
/// of them will be unique. The value is arranges so that MSB = first pixel to output,
|
||||
/// LSB = last. Each byte is formed as 00[bitplane 5][bitplane 4]...[bitplane 0].
|
||||
uint32_t get(bool high_res) {
|
||||
if(high_res) {
|
||||
return uint32_t(data_[1] >> 32);
|
||||
|
@@ -21,8 +21,6 @@
|
||||
|
||||
using namespace Amiga;
|
||||
|
||||
// TODO: I don't think the nonsense below, which was intended to allow a typed enum but also
|
||||
// clean combination, really works. Rethink.
|
||||
namespace {
|
||||
|
||||
template <typename EnumT, EnumT... T> struct Mask {
|
||||
@@ -36,18 +34,6 @@ template <typename EnumT, EnumT F, EnumT... T> struct Mask<EnumT, F, T...> {
|
||||
template <InterruptFlag... Flags> struct InterruptMask: Mask<InterruptFlag, Flags...> {};
|
||||
template <DMAFlag... Flags> struct DMAMask: Mask<DMAFlag, Flags...> {};
|
||||
|
||||
constexpr uint16_t AudioFlags[] = {
|
||||
DMAMask<DMAFlag::AudioChannel0, DMAFlag::AllBelow>::value,
|
||||
DMAMask<DMAFlag::AudioChannel1, DMAFlag::AllBelow>::value,
|
||||
DMAMask<DMAFlag::AudioChannel2, DMAFlag::AllBelow>::value,
|
||||
DMAMask<DMAFlag::AudioChannel3, DMAFlag::AllBelow>::value,
|
||||
};
|
||||
constexpr auto BlitterFlag = DMAMask<DMAFlag::Blitter, DMAFlag::AllBelow>::value;
|
||||
constexpr auto BitplaneFlag = DMAMask<DMAFlag::Bitplane, DMAFlag::AllBelow>::value;
|
||||
constexpr auto CopperFlag = DMAMask<DMAFlag::Copper, DMAFlag::AllBelow>::value;
|
||||
constexpr auto DiskFlag = DMAMask<DMAFlag::Disk, DMAFlag::AllBelow>::value;
|
||||
constexpr auto SpritesFlag = DMAMask<DMAFlag::Sprites, DMAFlag::AllBelow>::value;
|
||||
|
||||
}
|
||||
|
||||
#define DMA_CONSTRUCT *this, reinterpret_cast<uint16_t *>(map.chip_ram.data()), map.chip_ram.size() >> 1
|
||||
@@ -116,17 +102,17 @@ void Chipset::apply_ham(uint8_t modification) {
|
||||
case 0x00: // Direct palette lookup.
|
||||
last_colour_ = swizzled_palette_[modification & 0x1b];
|
||||
break;
|
||||
case 0x04: // Replace blue.
|
||||
colour[1] = uint8_t(
|
||||
(colour[1] & 0xf0) |
|
||||
case 0x04: // Replace red.
|
||||
colour[0] = uint8_t(
|
||||
((modification & 0x10) >> 1) | // bit 3.
|
||||
((modification & 0x02) << 1) | // bit 2.
|
||||
((modification & 0x08) >> 2) | // bit 1.
|
||||
(modification & 0x01) // bit 0.
|
||||
);
|
||||
break;
|
||||
case 0x20: // Replace red.
|
||||
colour[0] = uint8_t(
|
||||
case 0x20: // Replace blue.
|
||||
colour[1] = uint8_t(
|
||||
(colour[1] & 0xf0) |
|
||||
((modification & 0x10) >> 1) | // bit 3.
|
||||
((modification & 0x02) << 1) | // bit 2.
|
||||
((modification & 0x08) >> 2) | // bit 1.
|
||||
@@ -199,114 +185,82 @@ void Chipset::output_pixels(int cycles_until_sync) {
|
||||
}
|
||||
}
|
||||
|
||||
// This will store flags to indicate presence or absence of sprite pixels for the four shifters.
|
||||
// Compute masks potentially to obscure sprites.
|
||||
int playfield_odd_pixel_mask =
|
||||
(((playfield >> 22) | (playfield >> 24) | (playfield >> 26)) & 8) |
|
||||
(((playfield >> 15) | (playfield >> 17) | (playfield >> 19)) & 4) |
|
||||
(((playfield >> 8) | (playfield >> 10) | (playfield >> 12)) & 2) |
|
||||
(((playfield >> 1) | (playfield >> 3) | (playfield >> 5)) & 1);
|
||||
int playfield_even_pixel_mask =
|
||||
(((playfield >> 21) | (playfield >> 23) | (playfield >> 25)) & 8) |
|
||||
(((playfield >> 14) | (playfield >> 16) | (playfield >> 18)) & 4) |
|
||||
(((playfield >> 7) | (playfield >> 9) | (playfield >> 11)) & 2) |
|
||||
(((playfield >> 0) | (playfield >> 2) | (playfield >> 4)) & 1);
|
||||
|
||||
// If only a single playfield is in use, treat the mask as playing
|
||||
// into the priority selected for the even bitfields.
|
||||
if(!dual_playfields_) {
|
||||
playfield_even_pixel_mask |= playfield_odd_pixel_mask;
|
||||
playfield_odd_pixel_mask = 0;
|
||||
}
|
||||
|
||||
// Process sprites.
|
||||
int collision_masks[4] = {0, 0, 0, 0};
|
||||
int index = int(sprite_shifters_.size());
|
||||
for(auto shifter = sprite_shifters_.rbegin(); shifter != sprite_shifters_.rend(); ++shifter) {
|
||||
// Update the index, and skip this shifter entirely if it's empty.
|
||||
--index;
|
||||
const uint8_t data = shifter->get();
|
||||
if(!data) continue;
|
||||
|
||||
// If there are sprites visible, bother to figure out the playfield masks here.
|
||||
if(sprite_shifters_[0].get() | sprite_shifters_[1].get() | sprite_shifters_[2].get() | sprite_shifters_[3].get()) {
|
||||
// The playfield value is arranged as:
|
||||
//
|
||||
// pixel = [0 0 b5 b3 b1 b4 b2 b0]
|
||||
// full value = [pixel] [pixel] [pixel] [pixel]
|
||||
//
|
||||
// i.e. the odd pixel mask is:
|
||||
// b0 = bits 3, 4, 5;
|
||||
// b1 = bits 11, 12, 13;
|
||||
// b2 = bits 19, 20, 21;
|
||||
// b3 = bits 27, 28, 29.
|
||||
//
|
||||
// ... and the even pixel mask is the other set.
|
||||
|
||||
// Ensure that b0, b8, b16, b24 are the complete mask state of the even playfields,
|
||||
// and b3, b11, b19, b27 are the complete mask state of the odd playfields.
|
||||
const uint32_t merged_playfield = playfield | (playfield >> 1) | (playfield >> 2);
|
||||
|
||||
// Collect b0, b8, b16 and b24 as b0, b1, b2, b3 (and give no regard to the other bits).
|
||||
uint32_t playfield_even_pixel_mask = merged_playfield & 0x01010101;
|
||||
playfield_even_pixel_mask |= playfield_even_pixel_mask >> 7;
|
||||
playfield_even_pixel_mask |= playfield_even_pixel_mask >> 14;
|
||||
|
||||
// Collect b3, b11, b19 and b27 as b0, b1, b2, b3 (and give no regard to the other bits).
|
||||
uint32_t playfield_odd_pixel_mask = (merged_playfield >> 3) & 0x01010101;
|
||||
playfield_odd_pixel_mask |= playfield_odd_pixel_mask >> 7;
|
||||
playfield_odd_pixel_mask |= playfield_odd_pixel_mask >> 14;
|
||||
|
||||
// If only a single playfield is in use, treat the mask as playing
|
||||
// into the priority selected for the even bitfields.
|
||||
if(!dual_playfields_) {
|
||||
playfield_even_pixel_mask |= playfield_odd_pixel_mask;
|
||||
playfield_odd_pixel_mask = 0;
|
||||
// Determine the collision mask.
|
||||
collision_masks[index] = data | (data >> 1);
|
||||
if(collisions_flags_ & (0x1000 << index)) {
|
||||
collision_masks[index] |= (data >> 2) | (data >> 3);
|
||||
}
|
||||
collision_masks[index] = (collision_masks[index] & 0x01) | ((collision_masks[index] & 0x10) >> 3);
|
||||
|
||||
// Draw sprites.
|
||||
int index = int(sprite_shifters_.size());
|
||||
for(auto shifter = sprite_shifters_.rbegin(); shifter != sprite_shifters_.rend(); ++shifter) {
|
||||
// Update the index, and skip this shifter entirely if it's empty.
|
||||
--index;
|
||||
const uint8_t data = shifter->get();
|
||||
if(!data) continue;
|
||||
// Get the specific pixel mask.
|
||||
const int pixel_mask =
|
||||
(
|
||||
((odd_priority_ <= index) ? playfield_odd_pixel_mask : 0) |
|
||||
((even_priority_ <= index) ? playfield_even_pixel_mask : 0)
|
||||
);
|
||||
|
||||
// Determine the collision mask.
|
||||
collision_masks[index] = data | (data >> 1);
|
||||
if(collisions_flags_ & (0x1000 << index)) {
|
||||
collision_masks[index] |= (data >> 2) | (data >> 3);
|
||||
}
|
||||
collision_masks[index] = (collision_masks[index] & 0x01) | ((collision_masks[index] & 0x10) >> 3);
|
||||
// Output pixels, if a buffer exists.
|
||||
const auto base = (index << 2) + 16;
|
||||
if(pixels_) {
|
||||
if(sprites_[size_t((index << 1) + 1)].attached) {
|
||||
// Left pixel.
|
||||
if(data >> 4) {
|
||||
if(!(pixel_mask & 0x8)) pixels_[0] = palette_[16 + (data >> 4)];
|
||||
if(!(pixel_mask & 0x4)) pixels_[1] = palette_[16 + (data >> 4)];
|
||||
}
|
||||
|
||||
// Get the specific pixel mask;
|
||||
//
|
||||
// Playfield priority meanings:
|
||||
//
|
||||
// 4: behind all sprites;
|
||||
// 3: in front of sprites 6 & 7, behind all others;
|
||||
// 2: in front of 4, 5, 6 & 7; behind all others;
|
||||
// 1: in front of 2, 3, 4, 5, 6, & 7; behind 0 & 1;
|
||||
// 0: in front of all sprites.
|
||||
//
|
||||
// i.e. the playfield is in front of the two sprites in shifter n
|
||||
// if and only if it has a priority of n or less.
|
||||
const auto pixel_mask =
|
||||
(
|
||||
((odd_priority_ <= index) ? playfield_odd_pixel_mask : 0) |
|
||||
((even_priority_ <= index) ? playfield_even_pixel_mask : 0)
|
||||
);
|
||||
// Right pixel.
|
||||
if(data & 15) {
|
||||
if(!(pixel_mask & 0x2)) pixels_[2] = palette_[16 + (data & 15)];
|
||||
if(!(pixel_mask & 0x1)) pixels_[3] = palette_[16 + (data & 15)];
|
||||
}
|
||||
} else {
|
||||
// Left pixel.
|
||||
if((data >> 4) & 3) {
|
||||
if(!(pixel_mask & 0x8)) pixels_[0] = palette_[base + ((data >> 4)&3)];
|
||||
if(!(pixel_mask & 0x4)) pixels_[1] = palette_[base + ((data >> 4)&3)];
|
||||
}
|
||||
if(data >> 6) {
|
||||
if(!(pixel_mask & 0x8)) pixels_[0] = palette_[base + (data >> 6)];
|
||||
if(!(pixel_mask & 0x4)) pixels_[1] = palette_[base + (data >> 6)];
|
||||
}
|
||||
|
||||
// Output pixels, if a buffer exists and only where the pixel
|
||||
// mask allows. TODO: try to find a less branchy version of the below.
|
||||
const auto base = (index << 2) + 16;
|
||||
if(pixels_) {
|
||||
if(sprites_[size_t((index << 1) + 1)].attached) {
|
||||
// Left pixel.
|
||||
if(data >> 4) {
|
||||
if(!(pixel_mask & 0x8)) pixels_[0] = palette_[16 + (data >> 4)];
|
||||
if(!(pixel_mask & 0x4)) pixels_[1] = palette_[16 + (data >> 4)];
|
||||
}
|
||||
|
||||
// Right pixel.
|
||||
if(data & 15) {
|
||||
if(!(pixel_mask & 0x2)) pixels_[2] = palette_[16 + (data & 15)];
|
||||
if(!(pixel_mask & 0x1)) pixels_[3] = palette_[16 + (data & 15)];
|
||||
}
|
||||
} else {
|
||||
// Left pixel.
|
||||
if((data >> 4) & 3) {
|
||||
if(!(pixel_mask & 0x8)) pixels_[0] = palette_[base + ((data >> 4)&3)];
|
||||
if(!(pixel_mask & 0x4)) pixels_[1] = palette_[base + ((data >> 4)&3)];
|
||||
}
|
||||
if(data >> 6) {
|
||||
if(!(pixel_mask & 0x8)) pixels_[0] = palette_[base + (data >> 6)];
|
||||
if(!(pixel_mask & 0x4)) pixels_[1] = palette_[base + (data >> 6)];
|
||||
}
|
||||
|
||||
// Right pixel.
|
||||
if(data & 3) {
|
||||
if(!(pixel_mask & 0x2)) pixels_[2] = palette_[base + (data & 3)];
|
||||
if(!(pixel_mask & 0x1)) pixels_[3] = palette_[base + (data & 3)];
|
||||
}
|
||||
if((data >> 2) & 3) {
|
||||
if(!(pixel_mask & 0x2)) pixels_[2] = palette_[base + ((data >> 2)&3)];
|
||||
if(!(pixel_mask & 0x1)) pixels_[3] = palette_[base + ((data >> 2)&3)];
|
||||
}
|
||||
// Right pixel.
|
||||
if(data & 3) {
|
||||
if(!(pixel_mask & 0x2)) pixels_[2] = palette_[base + (data & 3)];
|
||||
if(!(pixel_mask & 0x1)) pixels_[3] = palette_[base + (data & 3)];
|
||||
}
|
||||
if((data >> 2) & 3) {
|
||||
if(!(pixel_mask & 0x2)) pixels_[2] = palette_[base + ((data >> 2)&3)];
|
||||
if(!(pixel_mask & 0x1)) pixels_[3] = palette_[base + ((data >> 2)&3)];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -534,6 +488,17 @@ void Chipset::flush_output() {
|
||||
|
||||
/// @returns @c true if this was a CPU slot; @c false otherwise.
|
||||
template <int cycle, bool stop_if_cpu> bool Chipset::perform_cycle() {
|
||||
constexpr uint16_t AudioFlags[] = {
|
||||
DMAMask<DMAFlag::AudioChannel0, DMAFlag::AllBelow>::value,
|
||||
DMAMask<DMAFlag::AudioChannel1, DMAFlag::AllBelow>::value,
|
||||
DMAMask<DMAFlag::AudioChannel2, DMAFlag::AllBelow>::value,
|
||||
DMAMask<DMAFlag::AudioChannel3, DMAFlag::AllBelow>::value,
|
||||
};
|
||||
constexpr auto BlitterFlag = DMAMask<DMAFlag::Blitter, DMAFlag::AllBelow>::value;
|
||||
constexpr auto BitplaneFlag = DMAMask<DMAFlag::Bitplane, DMAFlag::AllBelow>::value;
|
||||
constexpr auto CopperFlag = DMAMask<DMAFlag::Copper, DMAFlag::AllBelow>::value;
|
||||
constexpr auto DiskFlag = DMAMask<DMAFlag::Disk, DMAFlag::AllBelow>::value;
|
||||
constexpr auto SpritesFlag = DMAMask<DMAFlag::Sprites, DMAFlag::AllBelow>::value;
|
||||
|
||||
// Update state as to whether bitplane fetching should happen now.
|
||||
//
|
||||
@@ -642,11 +607,11 @@ template <int cycle, bool stop_if_cpu> bool Chipset::perform_cycle() {
|
||||
}
|
||||
|
||||
if constexpr (cycle >= 0x16 && cycle < 0x36) {
|
||||
if(y_ >= vertical_blank_height_ && (dma_control_ & SpritesFlag) == SpritesFlag) {
|
||||
if((dma_control_ & SpritesFlag) == SpritesFlag && y_ >= vertical_blank_height_) {
|
||||
constexpr auto sprite_id = (cycle - 0x16) >> 2;
|
||||
static_assert(sprite_id >= 0 && sprite_id < std::tuple_size<decltype(sprites_)>::value);
|
||||
|
||||
if(sprites_[sprite_id].advance_dma((~cycle&2) >> 1, y_, y_ == vertical_blank_height_)) {
|
||||
if(sprites_[sprite_id].advance_dma(!(cycle&2))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -776,6 +741,10 @@ template <bool stop_on_cpu> Chipset::Changes Chipset::run(HalfCycles length) {
|
||||
is_long_field_ ^= interlace_;
|
||||
}
|
||||
|
||||
for(auto &sprite: sprites_) {
|
||||
sprite.advance_line(y_, y_ == vertical_blank_height_);
|
||||
}
|
||||
|
||||
fetch_vertical_ |= y_ == display_window_start_[1];
|
||||
fetch_vertical_ &= y_ != display_window_stop_[1];
|
||||
}
|
||||
|
@@ -8,8 +8,6 @@
|
||||
|
||||
#include "Sprites.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
using namespace Amiga;
|
||||
|
||||
namespace {
|
||||
@@ -35,69 +33,61 @@ static_assert(expand_sprite_word(0x0000) == 0x00'00'00'00'00'00'00'00);
|
||||
// MARK: - Sprites.
|
||||
|
||||
void Sprite::set_start_position(uint16_t value) {
|
||||
// b8–b15: low 8 bits of VSTART;
|
||||
// b0–b7: high 8 bits of HSTART.
|
||||
v_start_ = (v_start_ & 0xff00) | (value >> 8);
|
||||
h_start = uint16_t((h_start & 0x0001) | ((value & 0xff) << 1));
|
||||
}
|
||||
|
||||
void Sprite::set_stop_and_control(uint16_t value) {
|
||||
// b8–b15: low 8 bits of VSTOP;
|
||||
// b7: attachment flag;
|
||||
// b3–b6: unused;
|
||||
// b2: VSTART high bit;
|
||||
// b1: VSTOP high bit;
|
||||
// b0: HSTART low bit.
|
||||
h_start = uint16_t((h_start & 0x01fe) | (value & 0x01));
|
||||
v_stop_ = uint16_t((value >> 8) | ((value & 0x02) << 7));
|
||||
v_start_ = uint16_t((v_start_ & 0x00ff) | ((value & 0x04) << 6));
|
||||
attached = value & 0x80;
|
||||
|
||||
// Disarm the sprite.
|
||||
// Disarm the sprite, but expect graphics next from DMA.
|
||||
visible = false;
|
||||
dma_state_ = DMAState::FetchImage;
|
||||
}
|
||||
|
||||
void Sprite::set_image_data(int slot, uint16_t value) {
|
||||
// Store data; also mark sprite as visible (i.e. 'arm' it)
|
||||
// if data is being stored to slot 0.
|
||||
data[slot] = value;
|
||||
visible |= slot == 0;
|
||||
}
|
||||
|
||||
bool Sprite::advance_dma(int offset, int y, bool is_first_line) {
|
||||
assert(offset == 0 || offset == 1);
|
||||
|
||||
// Determine which word would be fetched, if DMA occurs.
|
||||
// A bit of a cheat.
|
||||
const uint16_t next_word = ram_[pointer_[0] & ram_mask_];
|
||||
|
||||
// "When the vertical position of the beam counter is equal to the VSTOP
|
||||
// value in the sprite control words, the next two words fetched from the
|
||||
// sprite data structure are written into the sprite control registers
|
||||
// instead of being sent to the color registers"
|
||||
//
|
||||
// Guesswork, primarily from observing Spindizzy Worlds: the first line after
|
||||
// vertical blank also triggers a control reload. Seek to verify.
|
||||
if(y == v_stop_ || is_first_line) {
|
||||
if(offset) {
|
||||
// Second control word: stop position (mostly).
|
||||
set_stop_and_control(next_word);
|
||||
} else {
|
||||
// First control word: start position.
|
||||
set_start_position(next_word);
|
||||
}
|
||||
} else {
|
||||
visible |= y == v_start_;
|
||||
if(!visible) return false; // Act as if there wasn't a fetch.
|
||||
|
||||
// Write colour word 1, then colour word 0; 0 is the word that 'arms'
|
||||
// the sprite (i.e. makes it visible).
|
||||
set_image_data(1 - offset, next_word);
|
||||
void Sprite::advance_line(int y, bool is_end_of_blank) {
|
||||
if(dma_state_ == DMAState::FetchImage && y == v_start_) {
|
||||
visible = true;
|
||||
}
|
||||
if(is_end_of_blank || y == v_stop_) {
|
||||
dma_state_ = DMAState::FetchControl;
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Acknowledge the fetch.
|
||||
bool Sprite::advance_dma(int offset) {
|
||||
if(!visible) return false;
|
||||
|
||||
// Fetch another word.
|
||||
const uint16_t next_word = ram_[pointer_[0] & ram_mask_];
|
||||
++pointer_[0];
|
||||
return true;
|
||||
|
||||
// Put the fetched word somewhere appropriate and update the DMA state.
|
||||
switch(dma_state_) {
|
||||
// i.e. stopped.
|
||||
default: return false;
|
||||
|
||||
case DMAState::FetchControl:
|
||||
if(offset) {
|
||||
set_stop_and_control(next_word);
|
||||
} else {
|
||||
set_start_position(next_word);
|
||||
}
|
||||
return true;
|
||||
|
||||
case DMAState::FetchImage:
|
||||
set_image_data(1 - bool(offset), next_word);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <int sprite> void TwoSpriteShifter::load(
|
||||
|
@@ -23,7 +23,8 @@ class Sprite: public DMADevice<1> {
|
||||
void set_stop_and_control(uint16_t value);
|
||||
void set_image_data(int slot, uint16_t value);
|
||||
|
||||
bool advance_dma(int offset, int y, bool is_first_line);
|
||||
void advance_line(int y, bool is_end_of_blank);
|
||||
bool advance_dma(int offset);
|
||||
|
||||
uint16_t data[2]{};
|
||||
bool attached = false;
|
||||
@@ -32,6 +33,11 @@ class Sprite: public DMADevice<1> {
|
||||
|
||||
private:
|
||||
uint16_t v_start_ = 0, v_stop_ = 0;
|
||||
|
||||
enum class DMAState {
|
||||
FetchControl,
|
||||
FetchImage
|
||||
} dma_state_ = DMAState::FetchControl;
|
||||
};
|
||||
|
||||
class TwoSpriteShifter {
|
||||
|
@@ -156,7 +156,7 @@ class AYDeferrer {
|
||||
}
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910<true> ay_;
|
||||
Outputs::Speaker::PullLowpass<GI::AY38910::AY38910<true>> speaker_;
|
||||
HalfCycles cycles_since_update_;
|
||||
@@ -1048,15 +1048,11 @@ template <bool has_fdc> class ConcreteMachine:
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
/// Fields requests to pump all output.
|
||||
void flush_output(int outputs) final {
|
||||
/// Another Z80 entry point; indicates that a partcular run request has concluded.
|
||||
void flush() {
|
||||
// Just flush the AY.
|
||||
if(outputs & Output::Audio) {
|
||||
ay_.update();
|
||||
ay_.flush();
|
||||
}
|
||||
|
||||
// Always flush the FDC.
|
||||
ay_.update();
|
||||
ay_.flush();
|
||||
flush_fdc();
|
||||
}
|
||||
|
||||
|
@@ -95,7 +95,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
uint8_t ram_[65536], aux_ram_[65536];
|
||||
std::vector<uint8_t> rom_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
Audio::Toggle audio_toggle_;
|
||||
Outputs::Speaker::PullLowpass<Audio::Toggle> speaker_;
|
||||
Cycles cycles_since_audio_update_;
|
||||
@@ -810,16 +810,11 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
|
||||
return Cycles(1);
|
||||
}
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
void flush() {
|
||||
update_video();
|
||||
update_audio();
|
||||
update_just_in_time_cards();
|
||||
|
||||
if(outputs & Output::Video) {
|
||||
update_video();
|
||||
}
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) final {
|
||||
|
@@ -326,17 +326,13 @@ class ConcreteMachine:
|
||||
m65816_.run_for(cycles);
|
||||
}
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
void flush() {
|
||||
video_.flush();
|
||||
iwm_.flush();
|
||||
adb_glu_.flush();
|
||||
|
||||
if(outputs & Output::Video) {
|
||||
video_.flush();
|
||||
}
|
||||
if(outputs & Output::Audio) {
|
||||
AudioUpdater updater(this);
|
||||
audio_queue_.perform();
|
||||
}
|
||||
AudioUpdater updater(this);
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *target) override {
|
||||
@@ -1150,7 +1146,7 @@ class ConcreteMachine:
|
||||
Apple::Disk::DiskIIDrive drives525_[2];
|
||||
|
||||
// The audio parts.
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
Apple::IIgs::Sound::GLU sound_glu_;
|
||||
Audio::Toggle audio_toggle_;
|
||||
using AudioSource = Outputs::Speaker::CompoundSource<Apple::IIgs::Sound::GLU, Audio::Toggle>;
|
||||
|
@@ -16,7 +16,7 @@
|
||||
|
||||
using namespace Apple::IIgs::Sound;
|
||||
|
||||
GLU::GLU(Concurrency::AsyncTaskQueue<false> &audio_queue) : audio_queue_(audio_queue) {
|
||||
GLU::GLU(Concurrency::DeferringAsyncTaskQueue &audio_queue) : audio_queue_(audio_queue) {
|
||||
// Reset all pending stores.
|
||||
MemoryWrite disabled_write;
|
||||
disabled_write.enabled = false;
|
||||
@@ -42,7 +42,7 @@ void GLU::set_data(uint8_t data) {
|
||||
// Register access.
|
||||
const auto address = address_; // To make sure I don't inadvertently 'capture' address_.
|
||||
local_.set_register(address, data);
|
||||
audio_queue_.enqueue([this, address, data] () {
|
||||
audio_queue_.defer([this, address, data] () {
|
||||
remote_.set_register(address, data);
|
||||
});
|
||||
}
|
||||
@@ -191,7 +191,7 @@ void GLU::set_sample_volume_range(std::int16_t range) {
|
||||
|
||||
void GLU::set_control(uint8_t control) {
|
||||
local_.control = control;
|
||||
audio_queue_.enqueue([this, control] () {
|
||||
audio_queue_.defer([this, control] () {
|
||||
remote_.control = control;
|
||||
});
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ namespace Sound {
|
||||
|
||||
class GLU: public Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
GLU(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
GLU(Concurrency::DeferringAsyncTaskQueue &audio_queue);
|
||||
|
||||
void set_control(uint8_t);
|
||||
uint8_t get_control();
|
||||
@@ -42,7 +42,7 @@ class GLU: public Outputs::Speaker::SampleSource {
|
||||
void skip_samples(const std::size_t number_of_samples);
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
|
||||
uint16_t address_ = 0;
|
||||
|
||||
|
@@ -18,7 +18,7 @@ const std::size_t sample_length = 352 / 2;
|
||||
|
||||
}
|
||||
|
||||
Audio::Audio(Concurrency::AsyncTaskQueue<false> &task_queue) : task_queue_(task_queue) {}
|
||||
Audio::Audio(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {}
|
||||
|
||||
// MARK: - Inputs
|
||||
|
||||
@@ -35,7 +35,7 @@ void Audio::set_volume(int volume) {
|
||||
posted_volume_ = volume;
|
||||
|
||||
// Post the volume change as a deferred event.
|
||||
task_queue_.enqueue([this, volume] () {
|
||||
task_queue_.defer([this, volume] () {
|
||||
volume_ = volume;
|
||||
set_volume_multiplier();
|
||||
});
|
||||
@@ -47,7 +47,7 @@ void Audio::set_enabled(bool on) {
|
||||
posted_enable_mask_ = int(on);
|
||||
|
||||
// Post the enabled mask change as a deferred event.
|
||||
task_queue_.enqueue([this, on] () {
|
||||
task_queue_.defer([this, on] () {
|
||||
enabled_mask_ = int(on);
|
||||
set_volume_multiplier();
|
||||
});
|
||||
|
@@ -27,7 +27,7 @@ namespace Macintosh {
|
||||
*/
|
||||
class Audio: public ::Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
Audio(Concurrency::AsyncTaskQueue<false> &task_queue);
|
||||
Audio(Concurrency::DeferringAsyncTaskQueue &task_queue);
|
||||
|
||||
/*!
|
||||
Macintosh audio is (partly) sourced by the same scanning
|
||||
@@ -58,7 +58,7 @@ class Audio: public ::Outputs::Speaker::SampleSource {
|
||||
constexpr static bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &task_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue &task_queue_;
|
||||
|
||||
// A queue of fetched samples; read from by one thread,
|
||||
// written to by another.
|
||||
|
@@ -16,7 +16,7 @@ namespace Apple {
|
||||
namespace Macintosh {
|
||||
|
||||
struct DeferredAudio {
|
||||
Concurrency::AsyncTaskQueue<false> queue;
|
||||
Concurrency::DeferringAsyncTaskQueue queue;
|
||||
Audio audio;
|
||||
Outputs::Speaker::PullLowpass<Audio> speaker;
|
||||
HalfCycles time_since_update;
|
||||
|
@@ -190,6 +190,7 @@ 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;
|
||||
@@ -365,7 +366,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
|
||||
return delay;
|
||||
}
|
||||
|
||||
void flush_output(int) {
|
||||
void flush() {
|
||||
// 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
|
||||
|
@@ -12,16 +12,6 @@
|
||||
|
||||
using namespace Apple::Macintosh;
|
||||
|
||||
namespace {
|
||||
|
||||
#if TARGET_RT_BIG_ENDIAN
|
||||
constexpr uint64_t PixelMask = 0x8040201008040201;
|
||||
#else
|
||||
constexpr uint64_t PixelMask = 0x0102040810204080;
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// Re: CRT timings, see the Apple Guide to the Macintosh Hardware Family,
|
||||
// bottom of page 400:
|
||||
//
|
||||
@@ -96,20 +86,34 @@ void Video::run_for(HalfCycles duration) {
|
||||
const int final_pixel_word = std::min(final_word, 32);
|
||||
|
||||
if(!first_word) {
|
||||
pixel_buffer_ = reinterpret_cast<uint64_t *>(crt_.begin_data(512, 8));
|
||||
pixel_buffer_ = crt_.begin_data(512);
|
||||
}
|
||||
|
||||
if(pixel_buffer_) {
|
||||
for(int c = first_word; c < final_pixel_word; ++c) {
|
||||
const uint16_t pixels = ram_[video_base + video_address_] ^ 0xffff;
|
||||
uint16_t pixels = ram_[video_base + video_address_] ^ 0xffff;
|
||||
++video_address_;
|
||||
|
||||
const uint64_t low_pixels = (pixels & 0xff) * 0x0101010101010101;
|
||||
const uint64_t high_pixels = (pixels >> 8) * 0x0101010101010101;
|
||||
pixel_buffer_[15] = pixels & 0x01;
|
||||
pixel_buffer_[14] = pixels & 0x02;
|
||||
pixel_buffer_[13] = pixels & 0x04;
|
||||
pixel_buffer_[12] = pixels & 0x08;
|
||||
pixel_buffer_[11] = pixels & 0x10;
|
||||
pixel_buffer_[10] = pixels & 0x20;
|
||||
pixel_buffer_[9] = pixels & 0x40;
|
||||
pixel_buffer_[8] = pixels & 0x80;
|
||||
|
||||
pixel_buffer_[0] = high_pixels & PixelMask;
|
||||
pixel_buffer_[1] = low_pixels & PixelMask;
|
||||
pixel_buffer_ += 2;
|
||||
pixels >>= 8;
|
||||
pixel_buffer_[7] = pixels & 0x01;
|
||||
pixel_buffer_[6] = pixels & 0x02;
|
||||
pixel_buffer_[5] = pixels & 0x04;
|
||||
pixel_buffer_[4] = pixels & 0x08;
|
||||
pixel_buffer_[3] = pixels & 0x10;
|
||||
pixel_buffer_[2] = pixels & 0x20;
|
||||
pixel_buffer_[1] = pixels & 0x40;
|
||||
pixel_buffer_[0] = pixels & 0x80;
|
||||
|
||||
pixel_buffer_ += 16;
|
||||
}
|
||||
} else {
|
||||
video_address_ += size_t(final_pixel_word - first_word);
|
||||
|
@@ -96,7 +96,7 @@ class Video {
|
||||
size_t video_address_ = 0;
|
||||
size_t audio_address_ = 0;
|
||||
|
||||
uint64_t *pixel_buffer_ = nullptr;
|
||||
uint8_t *pixel_buffer_ = nullptr;
|
||||
|
||||
bool use_alternate_screen_buffer_ = false;
|
||||
bool use_alternate_audio_buffer_ = false;
|
||||
|
@@ -174,7 +174,7 @@ class ConcreteMachine:
|
||||
bus_->apply_confidence(confidence_counter_);
|
||||
}
|
||||
|
||||
void flush_output(int) final {
|
||||
void flush() {
|
||||
bus_->flush();
|
||||
}
|
||||
|
||||
|
@@ -39,7 +39,7 @@ class Bus {
|
||||
PIA mos6532_;
|
||||
TIA tia_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
TIASound tia_sound_;
|
||||
Outputs::Speaker::PullLowpass<TIASound> speaker_;
|
||||
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
using namespace Atari2600;
|
||||
|
||||
Atari2600::TIASound::TIASound(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
Atari2600::TIASound::TIASound(Concurrency::DeferringAsyncTaskQueue &audio_queue) :
|
||||
audio_queue_(audio_queue),
|
||||
poly4_counter_{0x00f, 0x00f},
|
||||
poly5_counter_{0x01f, 0x01f},
|
||||
@@ -18,20 +18,20 @@ Atari2600::TIASound::TIASound(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
{}
|
||||
|
||||
void Atari2600::TIASound::set_volume(int channel, uint8_t volume) {
|
||||
audio_queue_.enqueue([target = &volume_[channel], volume]() {
|
||||
audio_queue_.defer([target = &volume_[channel], volume]() {
|
||||
*target = volume & 0xf;
|
||||
});
|
||||
}
|
||||
|
||||
void Atari2600::TIASound::set_divider(int channel, uint8_t divider) {
|
||||
audio_queue_.enqueue([this, channel, divider]() {
|
||||
audio_queue_.defer([this, channel, divider]() {
|
||||
divider_[channel] = divider & 0x1f;
|
||||
divider_counter_[channel] = 0;
|
||||
});
|
||||
}
|
||||
|
||||
void Atari2600::TIASound::set_control(int channel, uint8_t control) {
|
||||
audio_queue_.enqueue([target = &control_[channel], control]() {
|
||||
audio_queue_.defer([target = &control_[channel], control]() {
|
||||
*target = control & 0xf;
|
||||
});
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ constexpr int CPUTicksPerAudioTick = 2;
|
||||
|
||||
class TIASound: public Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
TIASound(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
TIASound(Concurrency::DeferringAsyncTaskQueue &audio_queue);
|
||||
|
||||
void set_volume(int channel, uint8_t volume);
|
||||
void set_divider(int channel, uint8_t divider);
|
||||
@@ -32,7 +32,7 @@ class TIASound: public Outputs::Speaker::SampleSource {
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
|
||||
uint8_t volume_[2];
|
||||
uint8_t divider_[2];
|
||||
|
@@ -154,6 +154,7 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
mc68000_.run_for(cycles);
|
||||
flush();
|
||||
}
|
||||
|
||||
// MARK: MC68000::BusHandler
|
||||
@@ -413,19 +414,14 @@ class ConcreteMachine:
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
void flush() {
|
||||
dma_.flush();
|
||||
mfp_.flush();
|
||||
keyboard_acia_.flush();
|
||||
midi_acia_.flush();
|
||||
|
||||
if(outputs & Output::Video) {
|
||||
video_.flush();
|
||||
}
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
video_.flush();
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -485,7 +481,7 @@ class ConcreteMachine:
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, HalfCycles, 16> keyboard_acia_;
|
||||
JustInTimeActor<Motorola::ACIA::ACIA, HalfCycles, 16> midi_acia_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910<false> ay_;
|
||||
Outputs::Speaker::PullLowpass<GI::AY38910::AY38910<false>> speaker_;
|
||||
HalfCycles cycles_since_audio_update_;
|
||||
|
@@ -342,14 +342,10 @@ class ConcreteMachine:
|
||||
return penalty;
|
||||
}
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
if(outputs & Output::Video) {
|
||||
vdp_.flush();
|
||||
}
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
void flush() {
|
||||
vdp_.flush();
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
float get_confidence() final {
|
||||
@@ -381,7 +377,7 @@ class ConcreteMachine:
|
||||
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
|
||||
JustInTimeActor<TI::TMS::TMS9918> vdp_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
TI::SN76489 sn76489_;
|
||||
GI::AY38910::AY38910<false> ay_;
|
||||
Outputs::Speaker::CompoundSource<TI::SN76489, GI::AY38910::AY38910<false>> mixer_;
|
||||
|
@@ -620,13 +620,9 @@ class ConcreteMachine:
|
||||
return Cycles(1);
|
||||
}
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
if(outputs & Output::Video) {
|
||||
update_video();
|
||||
}
|
||||
if(outputs & Output::Audio) {
|
||||
mos6560_.flush();
|
||||
}
|
||||
void flush() {
|
||||
update_video();
|
||||
mos6560_.flush();
|
||||
}
|
||||
|
||||
void run_for(const Cycles cycles) final {
|
||||
|
@@ -501,14 +501,10 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
return Cycles(int(cycles));
|
||||
}
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
if(outputs & Output::Video) {
|
||||
video_.flush();
|
||||
}
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
forceinline void flush() {
|
||||
video_.flush();
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||||
@@ -770,7 +766,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
|
||||
// Outputs
|
||||
JustInTimeActor<VideoOutput, Cycles> video_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
SoundGenerator sound_generator_;
|
||||
Outputs::Speaker::PullLowpass<SoundGenerator> speaker_;
|
||||
|
||||
|
@@ -12,7 +12,7 @@
|
||||
|
||||
using namespace Electron;
|
||||
|
||||
SoundGenerator::SoundGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
SoundGenerator::SoundGenerator(Concurrency::DeferringAsyncTaskQueue &audio_queue) :
|
||||
audio_queue_(audio_queue) {}
|
||||
|
||||
void SoundGenerator::set_sample_volume_range(std::int16_t range) {
|
||||
@@ -36,13 +36,13 @@ void SoundGenerator::skip_samples(std::size_t number_of_samples) {
|
||||
}
|
||||
|
||||
void SoundGenerator::set_divider(uint8_t divider) {
|
||||
audio_queue_.enqueue([this, divider]() {
|
||||
audio_queue_.defer([this, divider]() {
|
||||
divider_ = divider * 32 / clock_rate_divider;
|
||||
});
|
||||
}
|
||||
|
||||
void SoundGenerator::set_is_enabled(bool is_enabled) {
|
||||
audio_queue_.enqueue([this, is_enabled]() {
|
||||
audio_queue_.defer([this, is_enabled]() {
|
||||
is_enabled_ = is_enabled;
|
||||
counter_ = 0;
|
||||
});
|
||||
|
@@ -16,7 +16,7 @@ namespace Electron {
|
||||
|
||||
class SoundGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
SoundGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
SoundGenerator(Concurrency::DeferringAsyncTaskQueue &audio_queue);
|
||||
|
||||
void set_divider(uint8_t divider);
|
||||
|
||||
@@ -31,7 +31,7 @@ class SoundGenerator: public ::Outputs::Speaker::SampleSource {
|
||||
static constexpr bool get_is_stereo() { return false; }
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
unsigned int counter_ = 0;
|
||||
unsigned int divider_ = 0;
|
||||
bool is_enabled_ = false;
|
||||
|
@@ -12,12 +12,12 @@ using namespace Enterprise::Dave;
|
||||
|
||||
// MARK: - Audio generator
|
||||
|
||||
Audio::Audio(Concurrency::AsyncTaskQueue<false> &audio_queue) :
|
||||
Audio::Audio(Concurrency::DeferringAsyncTaskQueue &audio_queue) :
|
||||
audio_queue_(audio_queue) {}
|
||||
|
||||
void Audio::write(uint16_t address, uint8_t value) {
|
||||
address &= 0x1f;
|
||||
audio_queue_.enqueue([address, value, this] {
|
||||
audio_queue_.defer([address, value, this] {
|
||||
switch(address) {
|
||||
case 0: case 2: case 4:
|
||||
channels_[address >> 1].reload = (channels_[address >> 1].reload & 0xff00) | value;
|
||||
@@ -63,7 +63,7 @@ void Audio::write(uint16_t address, uint8_t value) {
|
||||
}
|
||||
|
||||
void Audio::set_sample_volume_range(int16_t range) {
|
||||
audio_queue_.enqueue([range, this] {
|
||||
audio_queue_.defer([range, this] {
|
||||
volume_ = range / (63*4);
|
||||
});
|
||||
}
|
||||
|
@@ -30,7 +30,7 @@ enum class Interrupt: uint8_t {
|
||||
*/
|
||||
class Audio: public Outputs::Speaker::SampleSource {
|
||||
public:
|
||||
Audio(Concurrency::AsyncTaskQueue<false> &audio_queue);
|
||||
Audio(Concurrency::DeferringAsyncTaskQueue &audio_queue);
|
||||
|
||||
/// Modifies an register in the audio range; only the low 4 bits are
|
||||
/// used for register decoding so it's assumed that the caller has
|
||||
@@ -43,7 +43,7 @@ class Audio: public Outputs::Speaker::SampleSource {
|
||||
void get_samples(std::size_t number_of_samples, int16_t *target);
|
||||
|
||||
private:
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
|
||||
// Global divider (i.e. 8MHz/12Mhz switch).
|
||||
uint8_t global_divider_;
|
||||
|
@@ -539,14 +539,10 @@ template <bool has_disk_controller, bool is_6mhz> class ConcreteMachine:
|
||||
return penalty;
|
||||
}
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
if(outputs & Output::Video) {
|
||||
nick_.flush();
|
||||
}
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
void flush() {
|
||||
nick_.flush();
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -705,7 +701,7 @@ template <bool has_disk_controller, bool is_6mhz> class ConcreteMachine:
|
||||
bool previous_nick_interrupt_line_ = false;
|
||||
// Cf. timing guesses above.
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
Dave::Audio dave_audio_;
|
||||
Outputs::Speaker::PullLowpass<Dave::Audio> speaker_;
|
||||
HalfCycles time_since_audio_update_;
|
||||
|
@@ -615,14 +615,10 @@ class ConcreteMachine:
|
||||
return addition;
|
||||
}
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
if(outputs & Output::Video) {
|
||||
vdp_.flush();
|
||||
}
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
void flush() {
|
||||
vdp_.flush();
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
|
||||
void set_keyboard_line(int line) {
|
||||
@@ -747,7 +743,7 @@ class ConcreteMachine:
|
||||
JustInTimeActor<TI::TMS::TMS9918> vdp_;
|
||||
Intel::i8255::i8255<i8255PortHandler> i8255_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910<false> ay_;
|
||||
Audio::Toggle audio_toggle_;
|
||||
Konami::SCC scc_;
|
||||
|
@@ -210,16 +210,6 @@ 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());
|
||||
@@ -392,6 +382,12 @@ 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_;
|
||||
}
|
||||
@@ -459,7 +455,7 @@ class ConcreteMachine:
|
||||
// This is as per the audio control register;
|
||||
// see https://www.smspower.org/Development/AudioControlPort
|
||||
update_audio();
|
||||
audio_queue_.enqueue([this, mode] {
|
||||
audio_queue_.defer([this, mode] {
|
||||
switch(mode & 3) {
|
||||
case 0: // SN76489 only; the default.
|
||||
mixer_.set_relative_volumes({1.0f, 0.0f});
|
||||
@@ -487,7 +483,7 @@ class ConcreteMachine:
|
||||
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
|
||||
JustInTimeActor<TI::TMS::TMS9918> vdp_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
TI::SN76489 sn76489_;
|
||||
Yamaha::OPL::OPLL opll_;
|
||||
Outputs::Speaker::CompoundSource<decltype(sn76489_), decltype(opll_)> mixer_;
|
||||
|
@@ -178,7 +178,7 @@ class TapePlayer: public Storage::Tape::BinaryTapePlayer {
|
||||
*/
|
||||
class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
public:
|
||||
VIAPortHandler(Concurrency::AsyncTaskQueue<false> &audio_queue, AY &ay8910, Speaker &speaker, TapePlayer &tape_player, Keyboard &keyboard) :
|
||||
VIAPortHandler(Concurrency::DeferringAsyncTaskQueue &audio_queue, AY &ay8910, Speaker &speaker, TapePlayer &tape_player, Keyboard &keyboard) :
|
||||
audio_queue_(audio_queue), ay8910_(ay8910), speaker_(speaker), tape_player_(tape_player), keyboard_(keyboard)
|
||||
{
|
||||
// Attach a couple of joysticks.
|
||||
@@ -254,7 +254,7 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
||||
uint8_t porta_output_ = 0xff;
|
||||
HalfCycles cycles_since_ay_update_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> &audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
|
||||
AY &ay8910_;
|
||||
Speaker &speaker_;
|
||||
TapePlayer &tape_player_;
|
||||
@@ -578,13 +578,9 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
return Cycles(1);
|
||||
}
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
if(outputs & Output::Video) {
|
||||
video_.flush();
|
||||
}
|
||||
if(outputs & Output::Audio) {
|
||||
via_.flush();
|
||||
}
|
||||
forceinline void flush() {
|
||||
video_.flush();
|
||||
via_.flush();
|
||||
diskii_.flush();
|
||||
}
|
||||
|
||||
@@ -711,7 +707,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
|
||||
// Outputs
|
||||
JustInTimeActor<VideoOutput, Cycles> video_;
|
||||
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910<false> ay8910_;
|
||||
Speaker speaker_;
|
||||
|
||||
|
@@ -292,16 +292,11 @@ template<bool is_zx81> class ConcreteMachine:
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
void flush_output(int outputs) final {
|
||||
if(outputs & Output::Video) {
|
||||
video_.flush();
|
||||
}
|
||||
|
||||
forceinline void flush() {
|
||||
video_.flush();
|
||||
if constexpr (is_zx81) {
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,7 +463,7 @@ template<bool is_zx81> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// MARK: - Audio
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
using AY = GI::AY38910::AY38910<false>;
|
||||
AY ay_;
|
||||
Outputs::Speaker::PullLowpass<AY> speaker_;
|
||||
|
@@ -263,15 +263,10 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
void flush_output(int outputs) override {
|
||||
if(outputs & Output::Video) {
|
||||
video_.flush();
|
||||
}
|
||||
|
||||
if(outputs & Output::Audio) {
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
}
|
||||
void flush() {
|
||||
video_.flush();
|
||||
update_audio();
|
||||
audio_queue_.perform();
|
||||
|
||||
if constexpr (model == Model::Plus3) {
|
||||
fdc_.flush();
|
||||
@@ -849,7 +844,7 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// MARK: - Audio.
|
||||
Concurrency::AsyncTaskQueue<false> audio_queue_;
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
GI::AY38910::AY38910<false> ay_;
|
||||
Audio::Toggle audio_toggle_;
|
||||
Outputs::Speaker::CompoundSource<GI::AY38910::AY38910<false>, Audio::Toggle> mixer_;
|
||||
|
@@ -39,10 +39,6 @@ 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);
|
||||
@@ -65,16 +61,6 @@ 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;
|
||||
|
@@ -13,6 +13,7 @@
|
||||
4B0333B02094081A0050B93D /* AppleDSK.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0333AD2094081A0050B93D /* AppleDSK.cpp */; };
|
||||
4B049CDD1DA3C82F00322067 /* BCDTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B049CDC1DA3C82F00322067 /* BCDTest.swift */; };
|
||||
4B04C899285E3DC800AA8FD6 /* 65816ComparativeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B04C898285E3DC800AA8FD6 /* 65816ComparativeTests.mm */; };
|
||||
4B04C89F2870BDEC00AA8FD6 /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B04C89E2870BDDD00AA8FD6 /* ScanTarget.cpp */; };
|
||||
4B051C912669C90B00CA44E8 /* ROMCatalogue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B051C5826670A9300CA44E8 /* ROMCatalogue.cpp */; };
|
||||
4B051C922669C90B00CA44E8 /* ROMCatalogue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B051C5826670A9300CA44E8 /* ROMCatalogue.cpp */; };
|
||||
4B051C93266D9D6900CA44E8 /* ROMImages in Resources */ = {isa = PBXBuildFile; fileRef = 4BC9DF441D044FCA00F44158 /* ROMImages */; };
|
||||
@@ -33,6 +34,7 @@
|
||||
4B05401F219D1618001BF69C /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B05401D219D1618001BF69C /* ScanTarget.cpp */; };
|
||||
4B055A7A1FAE78A00060FFFF /* SDL2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B055A771FAE78210060FFFF /* SDL2.framework */; };
|
||||
4B055A7E1FAE84AA0060FFFF /* main.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B055A7C1FAE84A50060FFFF /* main.cpp */; };
|
||||
4B055A8D1FAE85920060FFFF /* AsyncTaskQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B3940E51DA83C8300427841 /* AsyncTaskQueue.cpp */; };
|
||||
4B055A8F1FAE85A90060FFFF /* FileHolder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5FADB81DE3151600AEC565 /* FileHolder.cpp */; };
|
||||
4B055A901FAE85A90060FFFF /* TimedEventLoop.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB697C91D4B6D3E00248BDF /* TimedEventLoop.cpp */; };
|
||||
4B055A911FAE85B50060FFFF /* Cartridge.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BEE0A6A1D72496600532C7B /* Cartridge.cpp */; };
|
||||
@@ -220,6 +222,7 @@
|
||||
4B322E041F5A2E3C004EB04C /* Z80Base.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B322E031F5A2E3C004EB04C /* Z80Base.cpp */; };
|
||||
4B37EE821D7345A6006A09A4 /* BinaryDump.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B37EE801D7345A6006A09A4 /* BinaryDump.cpp */; };
|
||||
4B38F3481F2EC11D00D9235D /* AmstradCPC.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B38F3461F2EC11D00D9235D /* AmstradCPC.cpp */; };
|
||||
4B3940E71DA83C8300427841 /* AsyncTaskQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B3940E51DA83C8300427841 /* AsyncTaskQueue.cpp */; };
|
||||
4B3BA0C31D318AEC005DD7A7 /* C1540Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3BA0C21D318AEB005DD7A7 /* C1540Tests.swift */; };
|
||||
4B3BA0CE1D318B44005DD7A7 /* C1540Bridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B3BA0C61D318B44005DD7A7 /* C1540Bridge.mm */; };
|
||||
4B3BA0CF1D318B44005DD7A7 /* MOS6522Bridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B3BA0C91D318B44005DD7A7 /* MOS6522Bridge.mm */; };
|
||||
@@ -346,6 +349,7 @@
|
||||
4B7752C128217F490073E2C5 /* FAT.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B477709268FBE4D005C2340 /* FAT.cpp */; };
|
||||
4B7752C228217F5C0073E2C5 /* Spectrum.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5D5C9525F56FC7001B4623 /* Spectrum.cpp */; };
|
||||
4B7752C328217F720073E2C5 /* Z80.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8DD39526360DDF00B3C866 /* Z80.cpp */; };
|
||||
4B778EEF23A5D6680000D260 /* AsyncTaskQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B3940E51DA83C8300427841 /* AsyncTaskQueue.cpp */; };
|
||||
4B778EF023A5D68C0000D260 /* 68000Storage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BFF1D3822337B0300838EA1 /* 68000Storage.cpp */; };
|
||||
4B778EF123A5D6B50000D260 /* 9918.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0E04F91FC9FA3100F43484 /* 9918.cpp */; };
|
||||
4B778EF323A5DB230000D260 /* PCMSegment.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4518731F75E91800926311 /* PCMSegment.cpp */; };
|
||||
@@ -1110,6 +1114,8 @@
|
||||
4B049CDC1DA3C82F00322067 /* BCDTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BCDTest.swift; sourceTree = "<group>"; };
|
||||
4B04B65622A58CB40006AB58 /* Target.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Target.hpp; sourceTree = "<group>"; };
|
||||
4B04C898285E3DC800AA8FD6 /* 65816ComparativeTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = 65816ComparativeTests.mm; sourceTree = "<group>"; };
|
||||
4B04C89D2870BDDD00AA8FD6 /* ScanTarget.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ScanTarget.hpp; sourceTree = "<group>"; };
|
||||
4B04C89E2870BDDD00AA8FD6 /* ScanTarget.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ScanTarget.cpp; sourceTree = "<group>"; };
|
||||
4B051C5826670A9300CA44E8 /* ROMCatalogue.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ROMCatalogue.cpp; sourceTree = "<group>"; };
|
||||
4B051C5926670A9300CA44E8 /* ROMCatalogue.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ROMCatalogue.hpp; sourceTree = "<group>"; };
|
||||
4B051C94266EF50200CA44E8 /* AppleIIController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppleIIController.swift; sourceTree = "<group>"; };
|
||||
@@ -1295,6 +1301,7 @@
|
||||
4B37EE811D7345A6006A09A4 /* BinaryDump.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = BinaryDump.hpp; sourceTree = "<group>"; };
|
||||
4B38F3461F2EC11D00D9235D /* AmstradCPC.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AmstradCPC.cpp; sourceTree = "<group>"; };
|
||||
4B38F3471F2EC11D00D9235D /* AmstradCPC.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = AmstradCPC.hpp; sourceTree = "<group>"; };
|
||||
4B3940E51DA83C8300427841 /* AsyncTaskQueue.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AsyncTaskQueue.cpp; sourceTree = "<group>"; };
|
||||
4B3940E61DA83C8300427841 /* AsyncTaskQueue.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = AsyncTaskQueue.hpp; sourceTree = "<group>"; };
|
||||
4B3AF7D02413470E00873C0B /* Enum.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Enum.hpp; sourceTree = "<group>"; };
|
||||
4B3AF7D12413472200873C0B /* Struct.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Struct.hpp; sourceTree = "<group>"; };
|
||||
@@ -2259,6 +2266,15 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
4B04C89C2870BDDD00AA8FD6 /* SoftwareRendering */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4B04C89D2870BDDD00AA8FD6 /* ScanTarget.hpp */,
|
||||
4B04C89E2870BDDD00AA8FD6 /* ScanTarget.cpp */,
|
||||
);
|
||||
path = SoftwareRendering;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4B051C9F2676F52200CA44E8 /* Enterprise */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -2727,6 +2743,7 @@
|
||||
4B0CCC411C62D0B3001CAC5F /* CRT */,
|
||||
4BD191D5219113B80042E144 /* OpenGL */,
|
||||
4BB8616B24E22DC500A00E03 /* ScanTargets */,
|
||||
4B04C89C2870BDDD00AA8FD6 /* SoftwareRendering */,
|
||||
4BD060A41FE49D3C006E14BE /* Speaker */,
|
||||
);
|
||||
name = Outputs;
|
||||
@@ -2748,6 +2765,7 @@
|
||||
4B3940E81DA83C8700427841 /* Concurrency */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4B3940E51DA83C8300427841 /* AsyncTaskQueue.cpp */,
|
||||
4B3940E61DA83C8300427841 /* AsyncTaskQueue.hpp */,
|
||||
);
|
||||
name = Concurrency;
|
||||
@@ -5630,6 +5648,7 @@
|
||||
4B0ACC2923775819008902D0 /* DMAController.cpp in Sources */,
|
||||
4B055A951FAE85BB0060FFFF /* BitReverse.cpp in Sources */,
|
||||
4B055ACE1FAE9B030060FFFF /* Plus3.cpp in Sources */,
|
||||
4B055A8D1FAE85920060FFFF /* AsyncTaskQueue.cpp in Sources */,
|
||||
4BAD13441FF709C700FD114A /* MSX.cpp in Sources */,
|
||||
4B055AC41FAE9AE80060FFFF /* Keyboard.cpp in Sources */,
|
||||
4B8DF506254E3C9D00F3433C /* ADB.cpp in Sources */,
|
||||
@@ -5637,6 +5656,7 @@
|
||||
4BBB70A5202011C2002FE009 /* MultiMediaTarget.cpp in Sources */,
|
||||
4B8318BC22D3E588006DB630 /* DisplayMetrics.cpp in Sources */,
|
||||
4BEDA40E25B2844B000C2DBD /* Decoder.cpp in Sources */,
|
||||
4B04C89F2870BDEC00AA8FD6 /* ScanTarget.cpp in Sources */,
|
||||
4B1B88BD202E3D3D00B67DFF /* MultiMachine.cpp in Sources */,
|
||||
4BE0A3EF237BB170002AB46F /* ST.cpp in Sources */,
|
||||
4B055A971FAE85BB0060FFFF /* ZX8081.cpp in Sources */,
|
||||
@@ -5879,6 +5899,7 @@
|
||||
4B17B58B20A8A9D9007CCA8F /* StringSerialiser.cpp in Sources */,
|
||||
4B2E2D9D1C3A070400138695 /* Electron.cpp in Sources */,
|
||||
4B051CA826781D6500CA44E8 /* StaticAnalyser.cpp in Sources */,
|
||||
4B3940E71DA83C8300427841 /* AsyncTaskQueue.cpp in Sources */,
|
||||
4B0E04FA1FC9FA3100F43484 /* 9918.cpp in Sources */,
|
||||
4B69FB3D1C4D908A00B5F0AA /* Tape.cpp in Sources */,
|
||||
4B4518841F75E91A00926311 /* UnformattedTrack.cpp in Sources */,
|
||||
@@ -6109,6 +6130,7 @@
|
||||
4B7752A728217E060073E2C5 /* Blitter.cpp in Sources */,
|
||||
4B778F4F23A5F21C0000D260 /* StaticAnalyser.cpp in Sources */,
|
||||
4B8DD3682633B2D400B3C866 /* SpectrumVideoContentionTests.mm in Sources */,
|
||||
4B778EEF23A5D6680000D260 /* AsyncTaskQueue.cpp in Sources */,
|
||||
4B7752A928217E200073E2C5 /* 65816Storage.cpp in Sources */,
|
||||
4B7752AC28217E6E0073E2C5 /* StaticAnalyser.cpp in Sources */,
|
||||
4B778F1223A5EC720000D260 /* CRT.cpp in Sources */,
|
||||
|
@@ -11,11 +11,16 @@ import Cocoa
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
private var failedMetalCheck = false
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
// Insert code here to initialize your application.
|
||||
|
||||
// Check for at least one Metal-capable GPU; this check
|
||||
// will become unnecessary if/when the minimum OS version
|
||||
// that this project supports reascends to 10.14.
|
||||
if (MTLCopyAllDevices().count == 0) {
|
||||
self.failedMetalCheck = true
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "This application requires a Metal-capable GPU"
|
||||
alert.addButton(withTitle: "Exit")
|
||||
|
@@ -29,15 +29,16 @@
|
||||
|
||||
@returns An instance of CSAudioQueue if successful; @c nil otherwise.
|
||||
*/
|
||||
- (nullable instancetype)initWithSamplingRate:(Float64)samplingRate isStereo:(BOOL)isStereo NS_DESIGNATED_INITIALIZER;
|
||||
- (nonnull instancetype)initWithSamplingRate:(Float64)samplingRate isStereo:(BOOL)isStereo NS_DESIGNATED_INITIALIZER;
|
||||
- (nonnull instancetype)init __attribute((unavailable));
|
||||
|
||||
/*!
|
||||
Enqueues a buffer for playback.
|
||||
|
||||
@param buffer A pointer to the data that comprises the buffer.
|
||||
@param lengthInSamples The length of the buffer, in samples.
|
||||
*/
|
||||
- (void)enqueueAudioBuffer:(nonnull const int16_t *)buffer;
|
||||
- (void)enqueueAudioBuffer:(nonnull const int16_t *)buffer numberOfSamples:(size_t)lengthInSamples;
|
||||
|
||||
/// @returns The sampling rate at which this queue is playing audio.
|
||||
@property (nonatomic, readonly) Float64 samplingRate;
|
||||
@@ -57,14 +58,4 @@
|
||||
*/
|
||||
@property (nonatomic, readonly) NSUInteger preferredBufferSize;
|
||||
|
||||
/*!
|
||||
Sets the size of buffers to be posted, in samplrs.
|
||||
*/
|
||||
@property (nonatomic) NSUInteger bufferSize;
|
||||
|
||||
/*!
|
||||
@returns @C YES if this queue is running low or is completely exhausted of new audio buffers.
|
||||
*/
|
||||
@property (atomic, readonly) BOOL isRunningDry;
|
||||
|
||||
@end
|
||||
|
@@ -8,70 +8,98 @@
|
||||
|
||||
#import "CSAudioQueue.h"
|
||||
@import AudioToolbox;
|
||||
#include <stdatomic.h>
|
||||
|
||||
#define OSSGuard(x) { \
|
||||
const OSStatus status = x; \
|
||||
assert(!status); \
|
||||
(void)status; \
|
||||
}
|
||||
#define AudioQueueBufferMaxLength 8192
|
||||
#define NumberOfStoredAudioQueueBuffer 16
|
||||
|
||||
#define IsDry(x) (x) < 2
|
||||
static NSLock *CSAudioQueueDeallocLock;
|
||||
|
||||
#define MaximumBacklog 4
|
||||
#define NumBuffers (MaximumBacklog + 1)
|
||||
/*!
|
||||
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
|
||||
|
||||
@implementation CSAudioQueue {
|
||||
AudioQueueRef _audioQueue;
|
||||
|
||||
NSLock *_deallocLock;
|
||||
NSLock *_queueLock;
|
||||
|
||||
atomic_int _enqueuedBuffers;
|
||||
AudioQueueBufferRef _buffers[NumBuffers];
|
||||
int _bufferWritePointer;
|
||||
|
||||
unsigned int _numChannels;
|
||||
NSLock *_storedBuffersLock;
|
||||
CSWeakAudioQueuePointer *_weakPointer;
|
||||
int _enqueuedBuffers;
|
||||
}
|
||||
|
||||
#pragma mark - Status
|
||||
#pragma mark - AudioQueue callbacks
|
||||
|
||||
- (BOOL)isRunningDry {
|
||||
return IsDry(atomic_load_explicit(&_enqueuedBuffers, memory_order_relaxed));
|
||||
/*!
|
||||
@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;
|
||||
}
|
||||
|
||||
#pragma mark - Object lifecycle
|
||||
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
|
||||
|
||||
- (instancetype)initWithSamplingRate:(Float64)samplingRate isStereo:(BOOL)isStereo {
|
||||
self = [super init];
|
||||
|
||||
if(self) {
|
||||
_deallocLock = [[NSLock alloc] init];
|
||||
_deallocLock.name = @"Dealloc lock";
|
||||
|
||||
_queueLock = [[NSLock alloc] init];
|
||||
_queueLock.name = @"Audio queue access lock";
|
||||
|
||||
atomic_store_explicit(&_enqueuedBuffers, 0, memory_order_relaxed);
|
||||
if(!CSAudioQueueDeallocLock) {
|
||||
CSAudioQueueDeallocLock = [[NSLock alloc] init];
|
||||
}
|
||||
_storedBuffersLock = [[NSLock alloc] init];
|
||||
|
||||
_samplingRate = samplingRate;
|
||||
_numChannels = isStereo ? 2 : 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;
|
||||
// determine preferred buffer sizes
|
||||
_preferredBufferSize = AudioQueueBufferMaxLength;
|
||||
while((Float64)_preferredBufferSize*100.0 > samplingRate) _preferredBufferSize >>= 1;
|
||||
|
||||
// Describe a 16bit stream of the requested sampling rate.
|
||||
/*
|
||||
Describe a mono 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;
|
||||
@@ -79,38 +107,17 @@
|
||||
|
||||
outputDescription.mReserved = 0;
|
||||
|
||||
// Create an audio output queue along those lines.
|
||||
__weak CSAudioQueue *weakSelf = self;
|
||||
if(AudioQueueNewOutputWithDispatchQueue(
|
||||
&_audioQueue,
|
||||
// create an audio output queue along those lines; see -dealloc re: the CSWeakAudioQueuePointer
|
||||
_weakPointer = [[CSWeakAudioQueuePointer alloc] init];
|
||||
_weakPointer.queue = self;
|
||||
if(!AudioQueueNewOutput(
|
||||
&outputDescription,
|
||||
audioOutputCallback,
|
||||
(__bridge void *)(_weakPointer),
|
||||
NULL,
|
||||
kCFRunLoopCommonModes,
|
||||
0,
|
||||
dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0),
|
||||
^(AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {
|
||||
(void)inBuffer;
|
||||
|
||||
CSAudioQueue *queue = weakSelf;
|
||||
if(!queue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if([queue->_deallocLock tryLock]) {
|
||||
const int buffers = atomic_fetch_add(&queue->_enqueuedBuffers, -1) - 1;
|
||||
if(!buffers) {
|
||||
[queue->_queueLock lock];
|
||||
OSSGuard(AudioQueuePause(inAQ));
|
||||
[queue->_queueLock unlock];
|
||||
}
|
||||
|
||||
id<CSAudioQueueDelegate> delegate = queue.delegate;
|
||||
[queue->_deallocLock unlock];
|
||||
|
||||
if(IsDry(buffers)) [delegate audioQueueIsRunningDry:queue];
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
return nil;
|
||||
&_audioQueue)) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,75 +125,56 @@
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[_deallocLock lock];
|
||||
// Ensure no buffers remain enqueued by stopping the queue.
|
||||
if(_audioQueue) {
|
||||
OSSGuard(AudioQueueStop(_audioQueue, true));
|
||||
}
|
||||
[CSAudioQueueDeallocLock lock];
|
||||
if(_audioQueue) {
|
||||
AudioQueueDispose(_audioQueue, true);
|
||||
_audioQueue = NULL;
|
||||
}
|
||||
[CSAudioQueueDeallocLock unlock];
|
||||
|
||||
// Free all buffers.
|
||||
for(size_t c = 0; c < NumBuffers; c++) {
|
||||
if(_buffers[c]) {
|
||||
OSSGuard(AudioQueueFreeBuffer(_audioQueue, _buffers[c]));
|
||||
_buffers[c] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Dispose of the queue.
|
||||
if(_audioQueue) {
|
||||
OSSGuard(AudioQueueDispose(_audioQueue, true));
|
||||
_audioQueue = NULL;
|
||||
}
|
||||
|
||||
// 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];
|
||||
// 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];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Audio enqueuer
|
||||
|
||||
- (void)setBufferSize:(NSUInteger)bufferSize {
|
||||
_bufferSize = bufferSize;
|
||||
- (void)enqueueAudioBuffer:(const int16_t *)buffer numberOfSamples:(size_t)lengthInSamples {
|
||||
size_t bufferBytes = lengthInSamples * sizeof(int16_t);
|
||||
|
||||
// Allocate future audio buffers.
|
||||
[_queueLock lock];
|
||||
const size_t bufferBytes = self.bufferSize * sizeof(int16_t) * _numChannels;
|
||||
for(size_t c = 0; c < NumBuffers; c++) {
|
||||
if(_buffers[c]) {
|
||||
OSSGuard(AudioQueueFreeBuffer(_audioQueue, _buffers[c]));
|
||||
}
|
||||
|
||||
OSSGuard(AudioQueueAllocateBuffer(_audioQueue, (UInt32)bufferBytes, &_buffers[c]));
|
||||
_buffers[c]->mAudioDataByteSize = (UInt32)bufferBytes;
|
||||
}
|
||||
[_queueLock unlock];
|
||||
}
|
||||
|
||||
- (void)enqueueAudioBuffer:(const int16_t *)buffer {
|
||||
const size_t bufferBytes = self.bufferSize * sizeof(int16_t) * _numChannels;
|
||||
|
||||
// Don't enqueue more than the allowed number of future buffers,
|
||||
// to ensure not too much latency accrues.
|
||||
if(atomic_load_explicit(&_enqueuedBuffers, memory_order_relaxed) == MaximumBacklog) {
|
||||
[_storedBuffersLock lock];
|
||||
// Don't enqueue more than 4 buffers ahead of now, to ensure not too much latency accrues.
|
||||
if(_enqueuedBuffers > 4) {
|
||||
[_storedBuffersLock unlock];
|
||||
return;
|
||||
}
|
||||
const int enqueuedBuffers = atomic_fetch_add(&_enqueuedBuffers, 1) + 1;
|
||||
++_enqueuedBuffers;
|
||||
|
||||
const int targetBuffer = _bufferWritePointer;
|
||||
_bufferWritePointer = (_bufferWritePointer + 1) % NumBuffers;
|
||||
memcpy(_buffers[targetBuffer]->mAudioData, buffer, bufferBytes);
|
||||
AudioQueueBufferRef newBuffer;
|
||||
AudioQueueAllocateBuffer(_audioQueue, (UInt32)bufferBytes * 2, &newBuffer);
|
||||
memcpy(newBuffer->mAudioData, buffer, bufferBytes);
|
||||
newBuffer->mAudioDataByteSize = (UInt32)bufferBytes;
|
||||
|
||||
[_queueLock lock];
|
||||
OSSGuard(AudioQueueEnqueueBuffer(_audioQueue, _buffers[targetBuffer], 0, NULL));
|
||||
AudioQueueEnqueueBuffer(_audioQueue, newBuffer, 0, NULL);
|
||||
[_storedBuffersLock unlock];
|
||||
|
||||
// Starting is a no-op if the queue is already playing, but it may not have been started
|
||||
// yet, or may have been paused due to a pipeline failure if the producer is running slowly.
|
||||
if(enqueuedBuffers > 1) {
|
||||
OSSGuard(AudioQueueStart(_audioQueue, NULL));
|
||||
}
|
||||
[_queueLock 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
|
||||
|
@@ -15,6 +15,7 @@ class MachineDocument:
|
||||
NSWindowDelegate,
|
||||
CSMachineDelegate,
|
||||
CSScanTargetViewResponderDelegate,
|
||||
CSAudioQueueDelegate,
|
||||
CSROMReciverViewDelegate
|
||||
{
|
||||
// MARK: - Mutual Exclusion.
|
||||
@@ -273,12 +274,17 @@ 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 (nonatomic, strong, nullable) CSAudioQueue *audioQueue;
|
||||
@property (atomic, strong, nullable) CSAudioQueue *audioQueue;
|
||||
@property (nonatomic, readonly, nonnull) CSScanTargetView *view;
|
||||
@property (nonatomic, weak, nullable) id<CSMachineDelegate> delegate;
|
||||
|
||||
|
@@ -24,43 +24,22 @@
|
||||
|
||||
#include "../../../../ClockReceiver/TimeTypes.hpp"
|
||||
#include "../../../../ClockReceiver/ScanSynchroniser.hpp"
|
||||
#include "../../../../Concurrency/AsyncTaskQueue.hpp"
|
||||
|
||||
#import "CSStaticAnalyser+TargetVector.h"
|
||||
#import "NSBundle+DataResource.h"
|
||||
#import "NSData+StdVector.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <atomic>
|
||||
#include <bitset>
|
||||
#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;
|
||||
@@ -122,7 +101,13 @@ struct ActivityObserver: public Activity::Observer {
|
||||
CSJoystickManager *_joystickManager;
|
||||
NSMutableArray<CSMachineLED *> *_leds;
|
||||
|
||||
Concurrency::AsyncTaskQueue<true, MachineUpdater> updater;
|
||||
CSHighPrecisionTimer *_timer;
|
||||
std::atomic_flag _isUpdating;
|
||||
Time::Nanos _syncTime;
|
||||
Time::Nanos _timeDiff;
|
||||
double _refreshPeriod;
|
||||
BOOL _isSyncLocking;
|
||||
|
||||
Time::ScanSynchroniser _scanSynchroniser;
|
||||
|
||||
NSTimer *_joystickTimer;
|
||||
@@ -148,7 +133,6 @@ 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 =
|
||||
@@ -171,13 +155,13 @@ struct ActivityObserver: public Activity::Observer {
|
||||
|
||||
_joystickMachine = _machine->joystick_machine();
|
||||
[self updateJoystickTimer];
|
||||
_isUpdating.clear();
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)speaker:(Outputs::Speaker::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length {
|
||||
assert(NSUInteger(length) == self.audioQueue.bufferSize*(speaker->get_is_stereo() ? 2 : 1));
|
||||
[self.audioQueue enqueueAudioBuffer:samples];
|
||||
[self.audioQueue enqueueAudioBuffer:samples numberOfSamples:(unsigned int)length];
|
||||
}
|
||||
|
||||
- (void)speakerDidChangeInputClock:(Outputs::Speaker::Speaker *)speaker {
|
||||
@@ -226,14 +210,8 @@ 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.audioQueue.bufferSize = bufferSize;
|
||||
[self setSpeakerDelegate:&_speakerDelegate sampleRate:samplingRate bufferSize:bufferSize stereo:stereo];
|
||||
}
|
||||
}
|
||||
@@ -455,9 +433,9 @@ struct ActivityObserver: public Activity::Observer {
|
||||
}
|
||||
|
||||
- (void)applyInputEvent:(dispatch_block_t)event {
|
||||
updater.enqueue([event] {
|
||||
event();
|
||||
});
|
||||
@synchronized(_inputEvents) {
|
||||
[_inputEvents addObject:event];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)clearAllKeys {
|
||||
@@ -668,68 +646,121 @@ struct ActivityObserver: public Activity::Observer {
|
||||
|
||||
#pragma mark - Timer
|
||||
|
||||
- (void)audioQueueIsRunningDry:(nonnull CSAudioQueue *)audioQueue {
|
||||
__weak CSMachine *weakSelf = self;
|
||||
|
||||
updater.enqueue([weakSelf] {
|
||||
CSMachine *const strongSelf = weakSelf;
|
||||
if(strongSelf) {
|
||||
strongSelf->updater.performer.machine->flush_output(MachineTypes::TimedMachine::Output::Audio);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)scanTargetViewDisplayLinkDidFire:(CSScanTargetView *)view now:(const CVTimeStamp *)now outputTime:(const CVTimeStamp *)outputTime {
|
||||
__weak CSMachine *weakSelf = self;
|
||||
// First order of business: grab a timestamp.
|
||||
const auto timeNow = Time::nanos_now();
|
||||
|
||||
updater.enqueue([weakSelf] {
|
||||
CSMachine *const strongSelf = weakSelf;
|
||||
if(!strongSelf) {
|
||||
return;
|
||||
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);
|
||||
}
|
||||
|
||||
// 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 = strongSelf->updater.performer.machine;
|
||||
// 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;
|
||||
|
||||
// Definitely update video; update audio too if that pipeline is looking a little dry.
|
||||
auto outputs = MachineTypes::TimedMachine::Output::Video;
|
||||
if(strongSelf->_audioQueue.isRunningDry) {
|
||||
outputs |= MachineTypes::TimedMachine::Output::Audio;
|
||||
}
|
||||
timed_machine->flush_output(outputs);
|
||||
// Set the current refresh period.
|
||||
_refreshPeriod = double(now->videoRefreshPeriod) / double(now->videoTimeScale);
|
||||
|
||||
// Attempt sync-matching if this machine is a fit.
|
||||
const auto scanStatus = strongSelf->_machine->scan_producer()->get_scan_status();
|
||||
const bool canSynchronise = strongSelf->_scanSynchroniser.can_synchronise(
|
||||
scanStatus,
|
||||
strongSelf.view.refreshPeriod
|
||||
);
|
||||
// Determine where responsibility lies for drawing.
|
||||
isSyncLocking = _isSyncLocking;
|
||||
}
|
||||
|
||||
if(canSynchronise) {
|
||||
const double multiplier = strongSelf->_scanSynchroniser.next_speed_multiplier(
|
||||
strongSelf->_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.
|
||||
[strongSelf.view updateBacking];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// This is safe even if weakSelf has been nulled out in the interim.
|
||||
[weakSelf.view draw];
|
||||
});
|
||||
});
|
||||
// Draw the current output. (TODO: do this within the timer if either raster racing or, at least, sync matching).
|
||||
if(!isSyncLocking) {
|
||||
[self.view draw];
|
||||
}
|
||||
}
|
||||
|
||||
#define TICKS 1000
|
||||
|
||||
- (void)start {
|
||||
// A no-op; retained in case of future changes to the manner of scheduling.
|
||||
__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)];
|
||||
}
|
||||
|
||||
#undef TICKS
|
||||
|
||||
- (void)stop {
|
||||
updater.stop();
|
||||
[_timer invalidate];
|
||||
_timer = nil;
|
||||
}
|
||||
|
||||
+ (BOOL)attemptInstallROM:(NSURL *)url {
|
||||
|
@@ -747,9 +747,7 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
|
||||
const auto chromaCoefficients = boxCoefficients(radiansPerPixel, 3.141592654f);
|
||||
_chromaKernelSize = 15;
|
||||
for(size_t c = 0; c < 8; ++c) {
|
||||
// Bit of a fix here: if the pipeline is for composite then assume that chroma separation wasn't
|
||||
// perfect and deemphasise the colour.
|
||||
firCoefficients[c].y = firCoefficients[c].z = (isSVideoOutput ? 2.0f : 1.25f) * chromaCoefficients[c];
|
||||
firCoefficients[c].y = firCoefficients[c].z = (isSVideoOutput ? 2.0f : 1.0f) * chromaCoefficients[c];
|
||||
firCoefficients[c].x = 0.0f;
|
||||
if(fabsf(chromaCoefficients[c]) < 0.01f) {
|
||||
_chromaKernelSize -= 2;
|
||||
@@ -758,20 +756,13 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
|
||||
firCoefficients[7].x = 1.0f;
|
||||
|
||||
// Luminance will be very soft as a result of the separation phase; apply a sharpen filter to try to undo that.
|
||||
// This is applied separately because the first composite processing step is going to select between the nominal
|
||||
// chroma and luma parts to take the place of luminance depending on whether a colour burst was found, and high-pass
|
||||
// filtering the chrominance channel would be visually detrimental.
|
||||
//
|
||||
// This is applied separately in order to partition three parts of the signal rather than two:
|
||||
//
|
||||
// 1) the luminance;
|
||||
// 2) not the luminance:
|
||||
// 2a) the chrominance; and
|
||||
// 2b) some noise.
|
||||
//
|
||||
// There are real numerical hazards here given the low number of taps I am permitting to be used, so the sharpen
|
||||
// filter below is just one that I found worked well. Since all numbers are fixed, the actual cutoff frequency is
|
||||
// going to be a function of the input clock, which is a bit phoney but the best way to stay safe within the
|
||||
// PCM sampling limits.
|
||||
// The low cut off ['Hz' but per line, not per second] is somewhat arbitrary.
|
||||
if(!isSVideoOutput) {
|
||||
SignalProcessing::FIRFilter sharpenFilter(15, 1368, 60.0f, 227.5f);
|
||||
SignalProcessing::FIRFilter sharpenFilter(15, float(_lineBufferPixelsPerLine), 40.0f, colourCyclesPerLine);
|
||||
const auto sharpen = sharpenFilter.get_coefficients();
|
||||
size_t sharpenFilterSize = 15;
|
||||
bool isStart = true;
|
||||
@@ -948,14 +939,12 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
|
||||
|
||||
- (void)updateFrameBuffer {
|
||||
// TODO: rethink BufferingScanTarget::perform. Is it now really just for guarding the modals?
|
||||
if(_scanTarget.has_new_modals()) {
|
||||
_scanTarget.perform([=] {
|
||||
const Outputs::Display::ScanTarget::Modals *const newModals = _scanTarget.new_modals();
|
||||
if(newModals) {
|
||||
[self setModals:*newModals];
|
||||
}
|
||||
});
|
||||
}
|
||||
_scanTarget.perform([=] {
|
||||
const Outputs::Display::ScanTarget::Modals *const newModals = _scanTarget.new_modals();
|
||||
if(newModals) {
|
||||
[self setModals:*newModals];
|
||||
}
|
||||
});
|
||||
|
||||
@synchronized(self) {
|
||||
if(!_frameBufferRenderPass) return;
|
||||
|
@@ -170,9 +170,4 @@
|
||||
*/
|
||||
- (void)willChangeScanTargetOwner;
|
||||
|
||||
/*!
|
||||
@returns The period of a frame.
|
||||
*/
|
||||
@property(nonatomic, readonly) double refreshPeriod;
|
||||
|
||||
@end
|
||||
|
@@ -103,12 +103,8 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
|
||||
[self stopDisplayLink];
|
||||
}
|
||||
|
||||
- (double)refreshPeriod {
|
||||
return CVDisplayLinkGetActualOutputVideoRefreshPeriod(_displayLink);
|
||||
}
|
||||
|
||||
- (void)stopDisplayLink {
|
||||
const double duration = [self refreshPeriod];
|
||||
const double duration = CVDisplayLinkGetActualOutputVideoRefreshPeriod(_displayLink);
|
||||
CVDisplayLinkStop(_displayLink);
|
||||
|
||||
// This is a workaround; CVDisplayLinkStop does not wait for any existing call to the
|
||||
|
@@ -176,6 +176,8 @@ struct BusHandler {
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
void flush() {}
|
||||
|
||||
int transaction_delay;
|
||||
int instructions;
|
||||
|
||||
|
@@ -72,6 +72,8 @@ SOURCES += \
|
||||
$$SRC/Components/OPx/*.cpp \
|
||||
$$SRC/Components/SN76489/*.cpp \
|
||||
$$SRC/Components/Serial/*.cpp \
|
||||
\
|
||||
$$SRC/Concurrency/*.cpp \
|
||||
\
|
||||
$$SRC/Inputs/*.cpp \
|
||||
\
|
||||
|
@@ -31,7 +31,6 @@ void Timer::tick() {
|
||||
|
||||
std::lock_guard lock_guard(*machineMutex);
|
||||
machine->run_for(double(duration) / 1e9);
|
||||
machine->flush_output(MachineTypes::TimedMachine::Output::All);
|
||||
}
|
||||
|
||||
Timer::~Timer() {
|
||||
|
@@ -58,6 +58,8 @@ SOURCES += glob.glob('../../Components/OPx/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/SN76489/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/Serial/*.cpp')
|
||||
|
||||
SOURCES += glob.glob('../../Concurrency/*.cpp')
|
||||
|
||||
SOURCES += glob.glob('../../Configurable/*.cpp')
|
||||
|
||||
SOURCES += glob.glob('../../Inputs/*.cpp')
|
||||
@@ -93,6 +95,9 @@ SOURCES += glob.glob('../../Machines/Sinclair/ZXSpectrum/*.cpp')
|
||||
SOURCES += glob.glob('../../Outputs/*.cpp')
|
||||
SOURCES += glob.glob('../../Outputs/CRT/*.cpp')
|
||||
SOURCES += glob.glob('../../Outputs/ScanTargets/*.cpp')
|
||||
|
||||
SOURCES += glob.glob('../../Outputs/SoftwareRendering/*.cpp')
|
||||
|
||||
SOURCES += glob.glob('../../Outputs/OpenGL/*.cpp')
|
||||
SOURCES += glob.glob('../../Outputs/OpenGL/Primitives/*.cpp')
|
||||
|
||||
|
@@ -30,6 +30,9 @@
|
||||
#include "../../Machines/MachineTypes.hpp"
|
||||
|
||||
#include "../../Activity/Observer.hpp"
|
||||
|
||||
#include "../../Outputs/SoftwareRendering/ScanTarget.hpp"
|
||||
|
||||
#include "../../Outputs/OpenGL/Primitives/Rectangle.hpp"
|
||||
#include "../../Outputs/OpenGL/ScanTarget.hpp"
|
||||
#include "../../Outputs/OpenGL/Screenshot.hpp"
|
||||
@@ -151,7 +154,6 @@ 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())
|
||||
);
|
||||
@@ -164,11 +166,9 @@ 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;
|
||||
}
|
||||
@@ -900,17 +900,18 @@ int main(int argc, char *argv[]) {
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &target_framebuffer);
|
||||
|
||||
// Setup output, assuming a CRT machine for now, and prepare a best-effort updater.
|
||||
Outputs::Display::OpenGL::ScanTarget scan_target(target_framebuffer);
|
||||
Outputs::Display::OpenGL::ScanTarget opengl_scan_target(target_framebuffer);
|
||||
Outputs::Display::Software::ScanTarget software_scan_target;
|
||||
std::unique_ptr<ActivityObserver> activity_observer;
|
||||
bool uses_mouse;
|
||||
std::vector<SDLJoystick> joysticks;
|
||||
|
||||
machine_runner.machine_mutex = &machine_mutex;
|
||||
const auto setup_machine_input_output = [&scan_target, &machine, &speaker_delegate, &activity_observer, &joysticks, &uses_mouse, &machine_runner] {
|
||||
const auto setup_machine_input_output = [&software_scan_target, &machine, &speaker_delegate, &activity_observer, &joysticks, &uses_mouse, &machine_runner] {
|
||||
// Wire up the best-effort updater, its delegate, and the speaker delegate.
|
||||
machine_runner.machine = machine.get();
|
||||
|
||||
machine->scan_producer()->set_scan_target(&scan_target);
|
||||
machine->scan_producer()->set_scan_target(&software_scan_target);
|
||||
|
||||
// For now, lie about audio output intentions.
|
||||
const auto audio_producer = machine->audio_producer();
|
||||
@@ -989,8 +990,8 @@ int main(int argc, char *argv[]) {
|
||||
machine_runner.start();
|
||||
while(!should_quit) {
|
||||
// Draw a new frame, indicating completion of the draw to the machine runner.
|
||||
scan_target.update(int(window_width), int(window_height));
|
||||
scan_target.draw(int(window_width), int(window_height));
|
||||
// opengl_scan_target.update(int(window_width), int(window_height));
|
||||
// opengl_scan_target.draw(int(window_width), int(window_height));
|
||||
if(activity_observer) activity_observer->draw();
|
||||
machine_runner.signal_did_draw();
|
||||
|
||||
@@ -1015,7 +1016,7 @@ int main(int argc, char *argv[]) {
|
||||
case SDL_WINDOWEVENT_RESIZED: {
|
||||
GLint target_framebuffer = 0;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &target_framebuffer);
|
||||
scan_target.set_target_framebuffer(target_framebuffer);
|
||||
opengl_scan_target.set_target_framebuffer(target_framebuffer);
|
||||
SDL_GetWindowSize(window, &window_width, &window_height);
|
||||
if(activity_observer) activity_observer->set_aspect_ratio(float(window_width) / float(window_height));
|
||||
} break;
|
||||
@@ -1042,7 +1043,7 @@ int main(int argc, char *argv[]) {
|
||||
if(error != Machine::Error::None) break;
|
||||
|
||||
machine = std::move(new_machine);
|
||||
static_cast<Outputs::Display::ScanTarget *>(&scan_target)->will_change_owner();
|
||||
static_cast<Outputs::Display::ScanTarget *>(&opengl_scan_target)->will_change_owner();
|
||||
setup_machine_input_output();
|
||||
window_titler.set_file_name(final_path_component(event.drop.file));
|
||||
} break;
|
||||
|
@@ -6,8 +6,8 @@
|
||||
// Copyright © 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef ScanTarget_hpp
|
||||
#define ScanTarget_hpp
|
||||
#ifndef Outputs_OpenGL_ScanTarget_hpp
|
||||
#define Outputs_OpenGL_ScanTarget_hpp
|
||||
|
||||
#include "../Log.hpp"
|
||||
#include "../DisplayMetrics.hpp"
|
||||
@@ -160,4 +160,4 @@ class ScanTarget: public Outputs::Display::BufferingScanTarget { // TODO: use pr
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* ScanTarget_hpp */
|
||||
#endif /* Outputs_OpenGL_ScanTarget_hpp */
|
||||
|
@@ -312,7 +312,7 @@ struct ScanTarget {
|
||||
/// @return A valid pointer, or @c nullptr if insufficient further storage is available.
|
||||
virtual Scan *begin_scan() = 0;
|
||||
|
||||
/// Requests a new scan to populate.
|
||||
/// Confirms that the scan returned by @c begin_scan (if any) is now fully populated.
|
||||
virtual void end_scan() {}
|
||||
|
||||
/// Finds the first available storage of at least @c required_length pixels in size which is
|
||||
|
@@ -302,7 +302,7 @@ size_t BufferingScanTarget::write_area_data_size() const {
|
||||
void BufferingScanTarget::set_modals(Modals modals) {
|
||||
perform([=] {
|
||||
modals_ = modals;
|
||||
modals_are_dirty_.store(true, std::memory_order::memory_order_relaxed);
|
||||
modals_are_dirty_ = true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -374,12 +374,10 @@ void BufferingScanTarget::set_line_buffer(Line *line_buffer, LineMetadata *metad
|
||||
}
|
||||
|
||||
const Outputs::Display::ScanTarget::Modals *BufferingScanTarget::new_modals() {
|
||||
const auto modals_are_dirty = modals_are_dirty_.load(std::memory_order::memory_order_relaxed);
|
||||
if(!modals_are_dirty) {
|
||||
if(!modals_are_dirty_) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
modals_are_dirty_.store(false, std::memory_order::memory_order_relaxed);
|
||||
modals_are_dirty_ = false;
|
||||
|
||||
// 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.
|
||||
@@ -394,7 +392,3 @@ 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,11 +161,6 @@ 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;
|
||||
@@ -258,7 +253,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_;
|
||||
std::atomic<bool> modals_are_dirty_ = false;
|
||||
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
|
||||
|
28
Outputs/SoftwareRendering/ScanTarget.cpp
Normal file
28
Outputs/SoftwareRendering/ScanTarget.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// SoftwareScanTarget.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 02/07/2022.
|
||||
// Copyright © 2022 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "ScanTarget.hpp"
|
||||
|
||||
using namespace Outputs::Display::Software;
|
||||
|
||||
template <
|
||||
Outputs::Display::InputDataType input_type,
|
||||
Outputs::Display::DisplayType display_type,
|
||||
Outputs::Display::ColourSpace colour_space
|
||||
> void ScanTarget::process() {
|
||||
// TODO.
|
||||
}
|
||||
|
||||
void ScanTarget::set_modals(Modals m) {
|
||||
printf("");
|
||||
}
|
||||
|
||||
void ScanTarget::submit() {
|
||||
scan_buffer_pointer_ = sample_buffer_pointer_ = 0;
|
||||
has_failed_ = false;
|
||||
}
|
82
Outputs/SoftwareRendering/ScanTarget.hpp
Normal file
82
Outputs/SoftwareRendering/ScanTarget.hpp
Normal file
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// SoftwareScanTarget.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 02/07/2022.
|
||||
// Copyright © 2022 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef SoftwareScanTarget_hpp
|
||||
#define SoftwareScanTarget_hpp
|
||||
|
||||
#include "../ScanTarget.hpp"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace Outputs {
|
||||
namespace Display {
|
||||
namespace Software {
|
||||
|
||||
/*!
|
||||
Provides a ScanTarget that does all intermediate processing on the CPU,
|
||||
and uses the FrameProducer interface to output results.
|
||||
*/
|
||||
class ScanTarget: public Outputs::Display::ScanTarget {
|
||||
|
||||
private:
|
||||
// The following are all overridden from Outputs::Display::ScanTarget;
|
||||
// some notes on their meaning to this specific scan target are given below.
|
||||
|
||||
void set_modals(Modals) override;
|
||||
|
||||
Scan *begin_scan() override {
|
||||
if(has_failed_ || scan_buffer_pointer_ == 8) {
|
||||
has_failed_ = true;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
vended_buffer_ = &scan_buffer_[scan_buffer_pointer_];
|
||||
++scan_buffer_pointer_;
|
||||
return vended_buffer_;
|
||||
}
|
||||
void end_scan() override {
|
||||
// TODO: adjust sample buffer locations.
|
||||
}
|
||||
|
||||
uint8_t *begin_data(size_t, size_t required_alignment) override {
|
||||
// Achieve required alignment.
|
||||
sample_buffer_pointer_ += (required_alignment - sample_buffer_pointer_) & (required_alignment - 1);
|
||||
|
||||
// Return target.
|
||||
return &sample_buffer_[sample_buffer_pointer_];
|
||||
|
||||
// TODO: nullptr case.
|
||||
}
|
||||
void end_data(size_t actual_length) override {
|
||||
sample_buffer_pointer_ += actual_length;
|
||||
}
|
||||
|
||||
//
|
||||
void submit() final;
|
||||
|
||||
template <InputDataType, DisplayType, ColourSpace> void process();
|
||||
|
||||
// Temporaries; each set of scans is rasterised synchronously upon
|
||||
// its submit, so the storage here is a lot simpler than for
|
||||
// the GPU-powered scan targets.
|
||||
std::array<Scan, 8> scan_buffer_;
|
||||
Scan *vended_buffer_ = nullptr;
|
||||
size_t scan_buffer_pointer_ = 0;
|
||||
|
||||
std::array<uint8_t, 2048> sample_buffer_;
|
||||
size_t sample_buffer_pointer_ = 0;
|
||||
|
||||
bool has_failed_ = false;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* SoftwareScanTarget_hpp */
|
@@ -170,10 +170,6 @@ 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);
|
||||
@@ -239,9 +235,9 @@ template <typename ConcreteT, bool is_stereo> class LowpassBase: public Speaker
|
||||
}
|
||||
|
||||
protected:
|
||||
bool process(size_t length) {
|
||||
void process(size_t length) {
|
||||
const auto delegate = delegate_.load(std::memory_order::memory_order_relaxed);
|
||||
if(!delegate) return false;
|
||||
if(!delegate) return;
|
||||
|
||||
const int scale = static_cast<ConcreteT *>(this)->get_scale();
|
||||
|
||||
@@ -286,8 +282,6 @@ template <typename ConcreteT, bool is_stereo> class LowpassBase: public Speaker
|
||||
// TODO: input rate is less than output rate.
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -336,11 +330,8 @@ template <bool is_stereo> class PushLowpass: public LowpassBase<PushLowpass<is_s
|
||||
*/
|
||||
void push(const int16_t *buffer, size_t length) {
|
||||
buffer_ = buffer;
|
||||
#ifndef NDEBUG
|
||||
const bool did_process =
|
||||
#endif
|
||||
process(length);
|
||||
assert(!did_process || buffer_ == buffer + (length * (1 + is_stereo)));
|
||||
process(length);
|
||||
assert(buffer_ == buffer + (length * (1 + is_stereo)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -372,12 +363,8 @@ template <typename SampleSource> class PullLowpass: public LowpassBase<PullLowpa
|
||||
The speaker will advance by obtaining data from the sample source supplied
|
||||
at construction, filtering it and passing it on to the speaker's delegate if there is one.
|
||||
*/
|
||||
void run_for(Concurrency::AsyncTaskQueue<false> &queue, const Cycles cycles) {
|
||||
if(cycles == Cycles(0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
queue.enqueue([this, cycles] {
|
||||
void run_for(Concurrency::DeferringAsyncTaskQueue &queue, const Cycles cycles) {
|
||||
queue.defer([this, cycles] {
|
||||
run_for(cycles);
|
||||
});
|
||||
}
|
||||
@@ -392,7 +379,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(const Cycles cycles) {
|
||||
process(size_t(cycles.as_integral()));
|
||||
std::size_t cycles_remaining = size_t(cycles.as_integral());
|
||||
if(!cycles_remaining) return;
|
||||
|
||||
process(cycles_remaining);
|
||||
}
|
||||
|
||||
SampleSource &sample_source_;
|
||||
|
@@ -171,8 +171,8 @@ template <Personality personality, typename T, bool uses_ready_line> void Proces
|
||||
case CyclePullY: s_++; read_mem(y_, s_ | 0x100); break;
|
||||
case CyclePullOperand: s_++; read_mem(operand_, s_ | 0x100); break;
|
||||
case OperationSetFlagsFromOperand: set_flags(operand_); continue;
|
||||
case OperationSetOperandFromFlagsWithBRKSet: operand_ = flags_.get(); continue;
|
||||
case OperationSetOperandFromFlags: operand_ = flags_.get() & ~Flag::Break; continue;
|
||||
case OperationSetOperandFromFlagsWithBRKSet: operand_ = flags_.get() | Flag::Break; continue;
|
||||
case OperationSetOperandFromFlags: operand_ = flags_.get(); continue;
|
||||
case OperationSetFlagsFromA: flags_.set_nz(a_); continue;
|
||||
case OperationSetFlagsFromX: flags_.set_nz(x_); continue;
|
||||
case OperationSetFlagsFromY: flags_.set_nz(y_); continue;
|
||||
@@ -650,6 +650,7 @@ 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,6 +141,12 @@ 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,6 +1022,7 @@ 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,6 +357,8 @@ class BusHandler {
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
void flush() {}
|
||||
|
||||
/*!
|
||||
Provides information about the path of execution if enabled via the template.
|
||||
*/
|
||||
|
@@ -2198,6 +2198,7 @@ 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,6 +347,8 @@ class BusHandler {
|
||||
return HalfCycles(0);
|
||||
}
|
||||
|
||||
void flush() {}
|
||||
|
||||
/*!
|
||||
Provides information about the path of execution if enabled via the template.
|
||||
*/
|
||||
|
@@ -48,6 +48,7 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -69,6 +70,7 @@ 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,6 +399,12 @@ 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"
|
||||
|
@@ -80,7 +80,7 @@ class DiskImageHolderBase: public Disk {
|
||||
protected:
|
||||
std::set<Track::Address> unwritten_tracks_;
|
||||
std::map<Track::Address, std::shared_ptr<Track>> cached_tracks_;
|
||||
std::unique_ptr<Concurrency::AsyncTaskQueue<true>> update_queue_;
|
||||
std::unique_ptr<Concurrency::AsyncTaskQueue> update_queue_;
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@@ -20,7 +20,7 @@ template <typename T> bool DiskImageHolder<T>::get_is_read_only() {
|
||||
|
||||
template <typename T> void DiskImageHolder<T>::flush_tracks() {
|
||||
if(!unwritten_tracks_.empty()) {
|
||||
if(!update_queue_) update_queue_ = std::make_unique<Concurrency::AsyncTaskQueue<true>>();
|
||||
if(!update_queue_) update_queue_ = std::make_unique<Concurrency::AsyncTaskQueue>();
|
||||
|
||||
using TrackMap = std::map<Track::Address, std::shared_ptr<Track>>;
|
||||
std::shared_ptr<TrackMap> track_copies(new TrackMap);
|
||||
|
@@ -25,7 +25,7 @@ constexpr uint32_t block(const char (& src)[5]) {
|
||||
);
|
||||
}
|
||||
|
||||
size_t block_size(Storage::FileHolder &file, uint8_t header) {
|
||||
constexpr size_t block_size(Storage::FileHolder &file, uint8_t header) {
|
||||
uint8_t size_width = header >> 5;
|
||||
size_t length = 0;
|
||||
while(size_width--) {
|
||||
|
Reference in New Issue
Block a user