diff --git a/Machines/Apple/AppleIIgs/Sound.cpp b/Machines/Apple/AppleIIgs/Sound.cpp index de2e0d204..1f2baa9eb 100644 --- a/Machines/Apple/AppleIIgs/Sound.cpp +++ b/Machines/Apple/AppleIIgs/Sound.cpp @@ -10,14 +10,25 @@ #include +// TODO: is it safe not to check for back-pressure in pending_stores_? + using namespace Apple::IIgs::Sound; GLU::GLU(Concurrency::DeferringAsyncTaskQueue &audio_queue) : audio_queue_(audio_queue) {} void GLU::set_data(uint8_t data) { - if(control_ & 0x40) { + if(local_.control & 0x40) { // RAM access. local_.ram_[address_] = data; + + MemoryWrite write; + write.enabled = true; + write.address = address_; + write.value = data; + write.time = pending_store_write_time_; + pending_stores_[pending_store_write_].store(write, std::memory_order::memory_order_release); + + pending_store_write_ = (pending_store_write_ + 1) % (StoreBufferSize - 1); } else { // Register access. switch(address_ & 0xe0) { @@ -31,7 +42,7 @@ void GLU::set_data(uint8_t data) { // printf("Register write %04x\n", address_); } - if(control_ & 0x20) { + if(local_.control & 0x20) { ++address_; } } @@ -44,17 +55,34 @@ uint8_t GLU::get_data() { void GLU::run_for(Cycles cycles) { // Update local state, without generating audio. - local_.skip_audio(cycles.as()); + skip_audio(local_, cycles.as()); + + // Update the timestamp for memory writes; + pending_store_write_time_ += cycles.as(); } void GLU::get_samples(std::size_t number_of_samples, std::int16_t *target) { // Update remote state, generating audio. - remote_.generate_audio(number_of_samples, target, output_range_); + generate_audio(number_of_samples, target, output_range_); } void GLU::skip_samples(const std::size_t number_of_samples) { // Update remote state, without generating audio. - remote_.skip_audio(number_of_samples); + skip_audio(remote_, number_of_samples); + + // Apply any pending stores. + std::atomic_thread_fence(std::memory_order::memory_order_acquire); + const uint32_t final_time = pending_store_read_time_ + uint32_t(number_of_samples); + while(true) { + auto next_store = pending_stores_[pending_store_read_].load(std::memory_order::memory_order_acquire); + if(!next_store.enabled) break; + if(next_store.time >= final_time) break; + remote_.ram_[next_store.address] = next_store.value; + next_store.enabled = false; + pending_stores_[pending_store_read_].store(next_store, std::memory_order::memory_order_relaxed); + + pending_store_read_ = (pending_store_read_ + 1) & (StoreBufferSize - 1); + } } void GLU::set_sample_volume_range(std::int16_t range) { @@ -92,16 +120,29 @@ uint8_t GLU::get_address_high() { // MARK: - Update logic. -void GLU::EnsoniqState::generate_audio(size_t number_of_samples, std::int16_t *target, int16_t range) { +void GLU::generate_audio(size_t number_of_samples, std::int16_t *target, int16_t range) { (void)number_of_samples; (void)target; (void)range; - memset(target, 0, number_of_samples * sizeof(int16_t)); + auto next_store = pending_stores_[pending_store_read_].load(std::memory_order::memory_order_acquire); + for(size_t c = 0; c < number_of_samples; c++) { + target[c] = 0; + + // Apply any RAM writes that interleave here. + ++pending_store_read_time_; + if(!next_store.enabled) continue; + if(next_store.time != pending_store_read_time_) continue; + remote_.ram_[next_store.address] = next_store.value; + next_store.enabled = false; + pending_stores_[pending_store_read_].store(next_store, std::memory_order::memory_order_relaxed); + pending_store_read_ = (pending_store_read_ + 1) & (StoreBufferSize - 1); + } } -void GLU::EnsoniqState::skip_audio(size_t number_of_samples) { +void GLU::skip_audio(EnsoniqState &state, size_t number_of_samples) { (void)number_of_samples; + (void)state; // Just advance all oscillator pointers and check for interrupts. // If a read occurs to the current-output level, generate it then. diff --git a/Machines/Apple/AppleIIgs/Sound.hpp b/Machines/Apple/AppleIIgs/Sound.hpp index 3732223a8..6048650c9 100644 --- a/Machines/Apple/AppleIIgs/Sound.hpp +++ b/Machines/Apple/AppleIIgs/Sound.hpp @@ -9,6 +9,8 @@ #ifndef Apple_IIgs_Sound_hpp #define Apple_IIgs_Sound_hpp +#include + #include "../../../ClockReceiver/ClockReceiver.hpp" #include "../../../Concurrency/AsyncTaskQueue.hpp" #include "../../../Outputs/Speaker/Implementation/SampleSource.hpp" @@ -42,6 +44,26 @@ class GLU: public Outputs::Speaker::SampleSource { uint16_t address_ = 0; + // Use a circular buffer for piping memory alterations onto the audio + // thread; it would be prohibitive to defer every write individually. + // + // Assumed: on most modern architectures, an atomic 64-bit read or + // write can be achieved locklessly. + struct MemoryWrite { + uint32_t time; + uint16_t address; + uint8_t value; + bool enabled = false; + }; + static_assert(sizeof(MemoryWrite) == 8); + constexpr static int StoreBufferSize = 16384; + + std::atomic pending_stores_[StoreBufferSize]; + uint32_t pending_store_read_ = 0, pending_store_read_time_ = 0; + uint32_t pending_store_write_ = 0, pending_store_write_time_ = 0; + + // Maintain state both 'locally' (i.e. on the emulation thread) and + // 'remotely' (i.e. on the audio thread). struct EnsoniqState { uint8_t ram_[65536]; struct Oscillator { @@ -55,16 +77,18 @@ class GLU: public Outputs::Speaker::SampleSource { uint8_t table_size; } oscillators_[32]; + // TODO: do all of these need to be on the audio thread? uint8_t control; - - void generate_audio(size_t number_of_samples, std::int16_t *target, int16_t range); - void skip_audio(size_t number_of_samples); + uint8_t interrupt_state; + uint8_t oscillator_enable; } local_, remote_; - uint8_t interrupt_state_; - uint8_t oscillator_enable_; - uint8_t control_ = 0x00; + // Functions to update an EnsoniqState; these don't belong to the state itself + // because they also access the pending stores (inter alia). + void generate_audio(size_t number_of_samples, std::int16_t *target, int16_t range); + void skip_audio(EnsoniqState &state, size_t number_of_samples); + // Audio-thread state. int16_t output_range_ = 0; };