1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-09-12 02:24:31 +00:00

Compare commits

..

6 Commits

Author SHA1 Message Date
Thomas Harte
4078baa424 Capture up to eight scans per submission group. 2022-07-03 21:54:28 -04:00
Thomas Harte
3179d0d963 Vend data pointers. 2022-07-02 21:58:23 -04:00
Thomas Harte
6a8c792c63 Add new scan target to SDL build. 2022-07-02 21:57:48 -04:00
Thomas Harte
678e1a38fa Ensure some basic traffic reaches the putative software scan target. 2022-07-02 21:29:59 -04:00
Thomas Harte
f4004baff8 Set out a stall. 2022-07-02 16:31:38 -04:00
Thomas Harte
f04e4faae2 Avoid potential ScanTarget include-guard collisions. 2022-07-02 13:54:16 -04:00
87 changed files with 916 additions and 853 deletions

View File

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

View File

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

View File

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

View File

@@ -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;
});
}

View File

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

View File

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

View File

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

View File

@@ -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;
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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();
}

View File

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

View File

@@ -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();
}

View File

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

View File

@@ -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;
/// b3b5: planes 1, 3 and 5;
/// b0b2: 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);

View File

@@ -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];
}

View File

@@ -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) {
// b8b15: low 8 bits of VSTART;
// b0b7: 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) {
// b8b15: low 8 bits of VSTOP;
// b7: attachment flag;
// b3b6: 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(

View File

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

View File

@@ -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();
}

View File

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

View File

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

View File

@@ -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;
});
}

View File

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

View File

@@ -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();
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
});

View File

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

View File

@@ -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);
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 */,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -72,6 +72,8 @@ SOURCES += \
$$SRC/Components/OPx/*.cpp \
$$SRC/Components/SN76489/*.cpp \
$$SRC/Components/Serial/*.cpp \
\
$$SRC/Concurrency/*.cpp \
\
$$SRC/Inputs/*.cpp \
\

View File

@@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

@@ -302,7 +302,7 @@ size_t BufferingScanTarget::write_area_data_size() const {
void BufferingScanTarget::set_modals(Modals modals) {
perform([=] {
modals_ = modals;
modals_are_dirty_.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);
}

View File

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

View 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;
}

View 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 */

View File

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

View File

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

View File

@@ -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() {}
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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_;
};
/*!

View File

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

View File

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