diff --git a/src/engine/platform/sound/pokey/at/ataudio/pokey.h b/src/engine/platform/sound/pokey/at/ataudio/pokey.h new file mode 100644 index 00000000..66742288 --- /dev/null +++ b/src/engine/platform/sound/pokey/at/ataudio/pokey.h @@ -0,0 +1,406 @@ +// Altirra - Atari 800/800XL emulator +// Copyright (C) 2008 Avery Lee +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +#ifndef AT_POKEY_H +#define AT_POKEY_H + +#include +#include +#include + +#ifdef _MSC_VER + #pragma once +#endif + +class IATAudioOutput; +class ATPokeyEmulator; +class ATSaveStateReader; +class ATAudioFilter; +struct ATPokeyTables; +class ATPokeyRenderer; +class IATObjectState; + +class IATPokeyEmulatorConnections { +public: + virtual void PokeyAssertIRQ(bool cpuBased) = 0; + virtual void PokeyNegateIRQ(bool cpuBased) = 0; + virtual void PokeyBreak() = 0; + virtual bool PokeyIsInInterrupt() const = 0; + virtual bool PokeyIsKeyPushOK(uint8_t scanCode, bool cooldownExpired) const = 0; +}; + +class IATPokeySIODevice { +public: + virtual void PokeyAttachDevice(ATPokeyEmulator *pokey) = 0; + + // Returns true if burst I/O is allowed. + virtual bool PokeyWriteSIO(uint8_t c, bool command, uint32_t cyclesPerBit, uint64_t startTime, bool framingError) = 0; + + virtual void PokeyBeginCommand() = 0; + virtual void PokeyEndCommand() = 0; + virtual void PokeySerInReady() = 0; +}; + +class IATPokeyCassetteDevice { +public: + virtual void PokeyChangeSerialRate(uint32_t divisor) = 0; + virtual void PokeyResetSerialInput() = 0; + virtual void PokeyBeginCassetteData(uint8_t skctl) = 0; + virtual bool PokeyWriteCassetteData(uint8_t c, uint32_t cyclesPerBit) = 0; +}; + +class IATPokeyTraceOutput { +public: + virtual void AddIRQ(uint64_t start, uint64_t end) = 0; +}; + +struct ATPokeyRegisterState { + uint8_t mReg[0x20]; +}; + +struct ATPokeyAudioState { + int mChannelOutputs[4]; +}; + +struct ATPokeyAudioLog { + // Sampling buffer -- receives per-channel output state every N ticks, up to the given max + // number of samples per frame. Automatically cleared per frame. + ATPokeyAudioState *mpStates; + uint32_t mMaxSamples; + uint32_t mCyclesPerSample; + + // Mixed sampling buffer -- receives combined output state every sample, up to the given max + // samples per frame. This buffer is NOT automatically cleared and must be manually retriggered. + float *mpMixedSamples; + uint32_t mMaxMixedSamples; + + // === filled in by audio engine === + uint32_t mFullScaleValue; + uint32_t mTicksPerSample; + uint32_t mLastFrameSampleCount; + uint32_t mNumMixedSamples; + + // === for continuous use by audio engine === + uint32_t mStartingAudioTick; + uint32_t mLastAudioTick; + uint32_t mAccumulatedAudioTicks; + uint32_t mSampleIndex; + uint32_t mLastOutputMask; +}; + +class ATPokeyEmulator { +public: + ATPokeyEmulator(bool isSlave); + ~ATPokeyEmulator(); + + void Init(IATPokeyEmulatorConnections *mem, IATAudioOutput *output, ATPokeyTables *tables); + void ColdReset(); + + void SetSlave(ATPokeyEmulator *slave); + void SetCassette(IATPokeyCassetteDevice *dev); + void SetAudioLog(ATPokeyAudioLog *log); + void SetConsoleOutput(ATConsoleOutput *output); + + void Set5200Mode(bool enable); + + bool IsTraceSIOEnabled() const { return mbTraceSIO; } + void SetTraceSIOEnabled(bool enable) { mbTraceSIO = enable; } + + void AddSIODevice(IATPokeySIODevice *device); + void RemoveSIODevice(IATPokeySIODevice *device); + + void ReceiveSIOByte(uint8_t byte, uint32_t cyclesPerBit, bool simulateInputPort, bool allowBurst, bool synchronous, bool forceFramingError); + void SetSERIN(uint8_t v) { mSERIN = v; } + + void SetAudioLine2(int v); // used for audio from motor control line + void SetDataLine(bool newState, uint64_t flipTime = ~uint64_t(0)); + void SetCommandLine(bool newState); + void SetSpeaker(bool newState); + void SetStereoSoftEnable(bool enable); + + void SetExternalSerialClock(uint32_t basetime, uint32_t period); + uint32_t GetSerialCyclesPerBitRecv() const; + uint32_t GetSerialInputResetCounter() const { return mSerialInputResetCounter; } + + bool IsChannelEnabled(uint32_t channel) const; + void SetChannelEnabled(uint32_t channel, bool enabled); + + bool IsSecondaryChannelEnabled(uint32_t channel) const; + void SetSecondaryChannelEnabled(uint32_t channel, bool enabled); + + bool IsNonlinearMixingEnabled() const { return mbNonlinearMixingEnabled; } + void SetNonlinearMixingEnabled(bool enable); + + bool IsSerialNoiseEnabled() const { return mbSerialNoiseEnabled; } + void SetSerialNoiseEnabled(bool enable) { mbSerialNoiseEnabled = enable; } + + bool GetShiftKeyState() const { return mbShiftKeyState; } + void SetShiftKeyState(bool down, bool immediate); + bool GetControlKeyState() const { return mbControlKeyState; } + void SetControlKeyState(bool down); + void ClearKeyQueue(); + void PushKey(uint8_t c, bool repeat, bool allowQueue = false, bool flushQueue = true, bool useCooldown = true); + uint64_t GetRawKeyMask() const; + void PushRawKey(uint8_t c, bool immediate); + void ReleaseRawKey(uint8_t c, bool immediate); + void ReleaseAllRawKeys(bool immediate); + void SetBreakKeyState(bool down, bool immediate); + void PushBreak(); + + void SetKeyMatrix(const bool matrix[64]); + + void SetPotPos(unsigned idx, int pos); + void SetPotPosHires(unsigned idx, int pos, bool grounded); + + // Get/set immediate pot mode. Immediate pot mode allows the POT0-7 registers + // to update within a frame of the last pot scan triggered via POTGO. This + // fibs accuracy slightly for reduction in latency. + bool IsImmediatePotUpdateEnabled() const { return mbAllowImmediatePotUpdate; } + void SetImmediatePotUpdateEnabled(bool enabled) { mbAllowImmediatePotUpdate = enabled; } + + void AdvanceScanLine(); + void AdvanceFrame(bool pushAudio, uint64_t timestamp); + + uint8_t DebugReadByte(uint8_t reg) const; + uint8_t ReadByte(uint8_t reg); + void WriteByte(uint8_t reg, uint8_t value); + + void DumpStatus(ATConsoleOutput& out); + + void SaveState(IATObjectState **pp); + void LoadState(const IATObjectState& state); + void PostLoadState(); + + void GetRegisterState(ATPokeyRegisterState& state) const; + + void FlushAudio(bool pushAudio, uint64_t timestamp); + + void SetTraceOutput(IATPokeyTraceOutput *output); + + uint32_t GetCyclesToTimerFire(uint32_t ch) const; + +protected: + // override + void OnScheduledEvent(uint32_t id); + + void PostFrameUpdate(uint32_t t); + + template + void FireTimer(); + + uint32_t UpdateLast15KHzTime(); + uint32_t UpdateLast15KHzTime(uint32_t t); + uint32_t UpdateLast64KHzTime(); + uint32_t UpdateLast64KHzTime(uint32_t t); + + void UpdatePolyTime(); + + void OnSerialInputTick(); + void OnSerialOutputTick(); + bool IsSerialOutputClockRunning() const; + void FlushSerialOutput(); + + void RecomputeAllowedDeferredTimers(); + + template + void RecomputeTimerPeriod(); + + template + void UpdateTimerCounter(); + + void SetupTimers(uint8_t channels); + void FlushDeferredTimerEvents(int channel); + void SetupDeferredTimerEvents(int channel, uint32_t t0, uint32_t period); + void SetupDeferredTimerEventsLinked(int channel, uint32_t t0, uint32_t period, uint32_t hit0, uint32_t hiperiod, uint32_t hilooffset); + + void DumpStatus(ATConsoleOutput& out, bool isSlave); + + void UpdateMixTable(); + + void UpdateKeyMatrix(int index, uint16_t mask, uint16_t state); + void UpdateEffectiveKeyMatrix(); + bool CanPushKey(uint8_t scanCode) const; + void TryPushNextKey(); + + void SetKeyboardModes(bool cooked, bool scanEnabled); + void UpdateKeyboardScanEvent(); + void QueueKeyboardIRQ(); + void AssertKeyboardIRQ(); + void AssertBreakIRQ(); + void AssertIrq(bool cpuBased); + void NegateIrq(bool cpuBased); + + void ProcessReceivedSerialByte(); + void SyncRenderers(ATPokeyRenderer *r); + + void StartPotScan(); + void UpdatePots(uint32_t timeSkew); + + void UpdateAddressDecoding(); + +private: + ATPokeyRenderer *mpRenderer; + + int mTimerCounters[4]; + + bool mbCommandLineState; + bool mbPal; + bool mb5200Mode; + bool mbTraceSIO; + bool mbNonlinearMixingEnabled; + bool mbSerialNoiseEnabled = true; + + uint8_t mKBCODE; + uint32_t mKeyCodeTimer; + uint32_t mKeyCooldownTimer; + bool mbKeyboardIRQPending; + bool mbUseKeyCooldownTimer; + bool mbCookedKeyMode; + bool mbKeyboardScanEnabled; + bool mbShiftKeyState; + bool mbShiftKeyLatchedState; + bool mbControlKeyState; + bool mbControlKeyLatchedState; + bool mbBreakKeyState; + bool mbBreakKeyLatchedState; + + uint8_t mAddressMask; + uint8_t mIRQEN; + uint8_t mIRQST; + uint8_t mAUDF[4]; // $D200/2/4/6: audio frequency, channel 1/2/3/4 + uint8_t mAUDC[4]; // $D201/3/5/7: audio control, channel 1/2/3/4 + uint8_t mAUDCTL; // $D208 + // bit 7: use 9-bit poly instead of 17-bit poly + // bit 6: clock channel 1 with 1.79MHz instead of 64KHz + // bit 5: clock channel 3 with 1.79MHz instead of 64KHz + // bit 4: clock channel 2 with channel 1 instead of 64KHz + // bit 3: clock channel 4 with channel 3 instead of 64KHz + // bit 2: apply high pass filter to channel 1 using channel 3 + // bit 1: apply high pass filter to channel 2 using channel 4 + // bit 0: change 64KHz frequency to 15KHz + uint8_t mSERIN; // $D20D: SERIN + uint8_t mSEROUT; // $D20D: SEROUT + uint8_t mSKSTAT; // $D20F: SKSTAT + // bit 3: shift key depressed + // bit 2: key depressed + uint8_t mSKCTL; // $D20F: SKCTL + // bit 3: shift key depressed + // bit 2: key depressed + + ATPokeyRegisterState mState; + + // countdown timer values + int mAUDFP1[4]; // AUDF values, plus 1 (we use these everywhere) + int mCounter[4]; + int mCounterBorrow[4]; + uint32_t mTimerPeriod[4]; + uint32_t mTimerFullPeriod[2]; // time for timer to count off 256 in linked mode (#1 and #3 only) + + mutable uint32_t mLastPolyTime; + mutable uint32_t mPoly17Counter; + mutable uint32_t mPoly9Counter; + uint64_t mPolyShutOffTime; + + uint64_t mSerialOutputStartTime; + uint8_t mSerialInputShiftRegister; + uint8_t mSerialOutputShiftRegister; + uint8_t mSerialInputCounter; + uint8_t mSerialOutputCounter; + uint8_t mSerialInputPendingStatus; + bool mbSerOutValid; + bool mbSerShiftValid; + bool mbSerialOutputState; + bool mbSpeakerActive; + bool mbSerialRateChanged; + bool mbSerialWaitingForStartBit; + bool mbSerInBurstPendingIRQ1; + bool mbSerInBurstPendingIRQ2; + bool mbSerInBurstPendingData; + bool mbSerInDeferredLoad; + uint32_t mSerOutBurstDeadline; + uint32_t mSerialInputResetCounter = 0; + + uint32_t mSerialSimulateInputBaseTime; + uint32_t mSerialSimulateInputCyclesPerBit; + uint32_t mSerialSimulateInputData; + bool mbSerialSimulateInputPort; + + uint32_t mSerialExtBaseTime; + uint32_t mSerialExtPeriod; + + uint64_t mSerialDataInFlipTime = ~(uint64_t)0; + + ATPokeyTables *mpTables = nullptr; + + // AUDCTL breakout + bool mbFastTimer1; + bool mbFastTimer3; + bool mbLinkedTimers12; + bool mbLinkedTimers34; + bool mbUse15KHzClock; + + bool mbAllowDeferredTimer[4]; + + uint32_t mLast15KHzTime; + uint32_t mLast64KHzTime; + + bool mbDeferredTimerEvents[4]; + uint32_t mDeferredTimerStarts[4]; + uint32_t mDeferredTimerPeriods[4]; + + uint16_t mKeyMatrix[8] = {}; + uint16_t mEffectiveKeyMatrix[8] = {}; + + IATPokeyEmulatorConnections *mpConn; + IATPokeyTraceOutput *mpTraceOutput = nullptr; + ATPokeyEmulator *mpSlave; + const bool mbIsSlave; + bool mbIrqAsserted; + + IATAudioOutput *mpAudioOut = nullptr; + + typedef std::vector Devices; + Devices mDevices; + + IATPokeyCassetteDevice *mpCassette = nullptr; + + std::deque mKeyQueue; + + uint8_t mKeyScanState = 0; + uint8_t mKeyScanCode = 0; + uint8_t mKeyScanLatch = 0; + + uint8_t mPotPositions[8] = {}; + uint8_t mPotHiPositions[8] = {}; + uint8_t mPotLatches[8] = {}; + uint8_t mALLPOT = 0; + uint8_t mPotMasterCounter = 0; + uint64_t mPotLastScanTime = 0; // cycle time of last write to POTGO + uint32_t mPotLastTimeFast = 0; + uint32_t mPotLastTimeSlow = 0; + + bool mbAllowImmediatePotUpdate = false; + bool mbStereoSoftEnable = true; + + bool mTraceDirectionSend = false; + uint32_t mTraceByteIndex = 0; + + bool mbTraceIrqPending = false; + uint64_t mTraceIrqStart = 0; +}; + +#endif diff --git a/src/engine/platform/sound/pokey/at/ataudio/pokeyrenderer.h b/src/engine/platform/sound/pokey/at/ataudio/pokeyrenderer.h new file mode 100644 index 00000000..fb304d84 --- /dev/null +++ b/src/engine/platform/sound/pokey/at/ataudio/pokeyrenderer.h @@ -0,0 +1,266 @@ +// Altirra - Atari 800/800XL/5200 emulator +// Copyright (C) 2008-2019 Avery Lee +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +#ifndef f_AT_POKEYRENDERER_H +#define f_AT_POKEYRENDERER_H + +#include +#include +#include + +class ATScheduler; +struct ATPokeyTables; +struct ATPokeyAudioState; +class IATSyncAudioEdgePlayer; +struct ATPokeyAudioLog; +struct ATSaveStatePokeyRenderer; + +class ATPokeyRenderer { + ATPokeyRenderer(const ATPokeyRenderer&) = delete; + ATPokeyRenderer& operator=(const ATPokeyRenderer&) = delete; +public: + ATPokeyRenderer(); + ~ATPokeyRenderer(); + + void Init(ATPokeyTables *tables); + void ColdReset(); + + void SyncTo(const ATPokeyRenderer& src); + + bool GetChannelOutput(int index) const { return (mChannelOutputMask & (1 << index)) != 0; } + const float *GetOutputBuffer() const { return mRawOutputBuffer; } + + bool IsChannelEnabled(int channel) const { return mbChannelEnabled[channel]; } + void SetChannelEnabled(int channel, bool enable); + + void SetAudioLog(ATPokeyAudioLog *log); + void RestartAudioLog(bool initial = false); + + void SetFiltersEnabled(bool enable); + void SetInitMode(bool init); + bool SetSpeaker(bool state); + void SetAudioLine2(int v); + + void ResetTimers(); + void SetAUDCx(int index, uint8_t value); + void SetAUDCTL(uint8_t value); + + void AddChannelEvent(int channel); + void SetChannelDeferredEvents(int channel, uint32_t start, uint32_t period); + void SetChannelDeferredEventsLinked(int channel, uint32_t loStart, uint32_t loPeriod, uint32_t hiStart, uint32_t hiPeriod, uint32_t loOffset); + void ClearChannelDeferredEvents(int channel, uint32_t t); + + void AddSerialNoisePulse(uint32_t t); + + void StartBlock(); + + struct EndBlockInfo { + uint32_t mTimestamp; + uint32_t mSamples; + }; + + EndBlockInfo EndBlock(IATSyncAudioEdgePlayer *edgePlayer); + + void LoadState(const ATSaveStatePokeyRenderer& state); + ATSaveStatePokeyRenderer SaveState() const; + +protected: + enum class ChangeType : uint8_t { + Audc0, + Audc1, + Audc2, + Audc3, + Audctl, + Init, + ResetOutputs, + Flush + }; + + void QueueChangeEvent(ChangeType type, uint8_t value); + void ProcessChangeEvents(uint32_t t); + + void FlushDeferredEvents(int channel, uint32_t t); + void Flush(const uint32_t t); + void Flush2(const uint32_t t); + static void MergeOutputEvents(const uint32_t* src1, const uint32_t* src2, uint32_t* dst); + + typedef std::pair (ATPokeyRenderer::*FireTimerRoutine)(uint32_t* dst, const uint32_t* src, uint32_t timeBase, uint32_t timeLimit); + FireTimerRoutine GetFireTimerRoutine(int ch) const; + template + FireTimerRoutine GetFireTimerRoutine() const; + + template + std::pair FireTimer(uint32_t* dst, const uint32_t* src, uint32_t timeBase, uint32_t timeLimit); + + void ProcessOutputEdges(uint32_t timeBase, const uint32_t *edges, uint32_t n); + void UpdateVolume(int channel); + void UpdateOutput(uint32_t t); + void UpdateOutput2(uint32_t t2); + void UpdateOutput2(uint32_t t2, uint32_t vpok); + void GenerateSamples(uint32_t t2); + + void PostFilter(); + + void LogOutputChange(uint32_t t2) const; + void LogOutputEdges(uint32_t timeBase2, const uint32_t *edges, uint32_t n) const; + + ATScheduler *mpScheduler; + ATPokeyTables *mpTables; + bool mbInitMode; + + float mHighPassAccum; + float mOutputLevel; + uint32_t mLastFlushTime; + int mExternalInput; + + bool mbSpeakerState; + + // Noise/tone flip-flop state for all four channels. This is the version updated by the + // FireTimer() routines; change events are then produced to update the analogous bits 0-3 + // in the channel output mask. + uint8_t mNoiseFlipFlops = 0; + + // Noise/tone and high-pass flip-flop states. Bits 0-3 contain the noise flip-flop states, + // updated by the output code from change events generated from mNoiseFlipFlops; bits 4-5 + // contain the high-pass flip-flops for ch1-2. + uint8_t mChannelOutputMask = 0; + + // Bits 0-3 set if ch1-4 is in volume-only mode (AUDCx bit 4 = 1). + uint8_t mVolumeOnlyMask = 0; + + // Bits 0-3 set if AUDCx bit 0-3 > 0. Note that this includes muting, so we cannot use + // this for architectural state. + uint8_t mNonZeroVolumeMask = 0; + + uint8_t mChannelVolume[4] {}; + uint32_t mChannelVolMixIndex[4] {}; + + struct BufferedState { + uint8_t mAUDC[4]; + uint8_t mAUDCTL; + }; + + BufferedState mArchState {}; + BufferedState mRenderState {}; + + // True if the channel is enabled for update or muted. This does NOT affect architectural + // state; it must not affect whether flip-flops are updated. + bool mbChannelEnabled[4]; + + struct DeferredEvent { + bool mbEnabled; + + /// Set if 16-bit linked mode is enabled; this requires tracking the + /// high timer to know when to reset the low timer. + bool mbLinked; + + /// Timestamp of next lo event. + uint32_t mNextTime; + + /// Period of lo event in clocks. + uint32_t mPeriod; + + /// Timestamp of next hi event. + uint32_t mNextHiTime; + + /// Hi (16-bit) period in clocks. + uint32_t mHiPeriod; + + /// Offset from hi event to next lo event. + uint32_t mHiLoOffset; + }; + + DeferredEvent mDeferredEvents[4] {}; + + struct ChangeEvent { + uint32_t mTime; + ChangeType mType; + uint8_t mValue; + }; + + std::deque mChangeQueue; + + struct PolyState { + uint32_t mInitMask = 0; + + uintptr_t mPoly4Offset = 0; + uintptr_t mPoly5Offset = 0; + uintptr_t mPoly9Offset = 0; + uintptr_t mPoly17Offset = 0; + + uint32_t mLastPoly17Time = 0; + uint32_t mPoly17Counter = 0; + uint32_t mLastPoly9Time = 0; + uint32_t mPoly9Counter = 0; + uint32_t mLastPoly5Time = 0; + uint32_t mPoly5Counter = 0; + uint32_t mLastPoly4Time = 0; + uint32_t mPoly4Counter = 0; + + void UpdatePoly17Counter(uint32_t t); + void UpdatePoly9Counter(uint32_t t); + void UpdatePoly5Counter(uint32_t t); + void UpdatePoly4Counter(uint32_t t); + } mPolyState; + + uint32_t mBlockStartTime = 0; + uint32_t mBlockStartTime2 = 0; + uint32_t mOutputSampleCount = 0; + + ATPokeyAudioLog *mpAudioLog = nullptr; + + // The sorted edge lists hold ordered output change events. The events are stored as + // packed bitfields for fast merging: + // + // bits 14-31 (18): half-cycle offset from beginning of flush operation + // bits 8-13 (6): AND mask to apply to flip/flops + // bits 0-5 (6): OR mask to apply to flip/flops + // + // Bits 4-5 of the masks are special as they apply to the high-pass flip/flops. The OR + // mask for these bits is ANDed with the ch1/2 outputs, so they update the HP F/Fs instead + // of setting them. + typedef std::vector SortedEdges; + SortedEdges mSortedEdgesTemp[4]; + SortedEdges mSortedEdgesHpTemp1; + SortedEdges mSortedEdgesHpTemp2; + SortedEdges mSortedEdgesTemp2[2]; + SortedEdges mSortedEdges; + + // The channel edge lists hold an ordered list of timer underflow events. The ticks are + // in system time (from ATScheduler). + typedef std::vector ChannelEdges; + ChannelEdges mChannelEdges[4]; + uint32_t mChannelEdgeBases[4] {}; + + std::vector mSerialPulseTimes; + float mSerialPulse = 0; + + enum : uint32_t { + // 1271 samples is the max (35568 cycles/frame / 28 cycles/sample + 1). We add a little bit here + // to round it out. We need a 16 sample holdover in order to run the FIR filter. + kBufferSize = 1536, + + kMaxWriteIndex = kBufferSize - 16 + }; + + alignas(16) float mRawOutputBuffer[kBufferSize]; + + template + static const FireTimerRoutine kFireRoutines[2][16]; +}; + +#endif // f_AT_POKEYRENDERER_H diff --git a/src/engine/platform/sound/pokey/at/ataudio/pokeytables.h b/src/engine/platform/sound/pokey/at/ataudio/pokeytables.h new file mode 100644 index 00000000..c2ee59c3 --- /dev/null +++ b/src/engine/platform/sound/pokey/at/ataudio/pokeytables.h @@ -0,0 +1,84 @@ +// Altirra - Atari 800/800XL/5200 emulator +// Copyright (C) 2008-2018 Avery Lee +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +#ifndef f_AT_POKEYTABLES_H +#define f_AT_POKEYTABLES_H + +#include + +struct ATPokeyTables { + // Rate of decay per sample (28 cycles) for the first stage amplifier output. This + // affects POKEY output but not GTIA (CONSOL) output. + float mReferenceDecayPerSample; + + // Min/max clamps for first stage amplifier output. These are in terms of final output + // polarity, so negated from volume values. + float mReferenceClampLo; + float mReferenceClampHi; + + float mSpeakerLevel; + + // Volume table for sums of all four audio channels. + // + // The index for this table is constructed as follows: + // + // index = sum(v[i] & 1) + 5*sum((v[i] & 2)/2) + 25*sum((v[i] & 12) / 4) + // + // The packed fields are thus: the number of channels with volume bit 0 set, the + // number of channels with volume bit 1 set, and the number of volume bit 2 equivalent + // steps for all channels. The last part takes advantage of the measurement for bit 3 + // being close enough to twice bit 2 that we can combine the two to reduce table size. + // With this formulation, the POKEY channel volume levels can be translated to index + // deltas that are just added together. + // + // Also in this table is the saturation curve, which kicks in roughly at volume level + // 12 of a single channel, and negation, so the output is the same polarity as on + // the actual hardware (POKEY pulls down from ~4.80V). + // + // Finally, the result is divided by 56 to account for output being integrated across + // 56 half-cycles for each sample. + // + float mMixTable[325]; + + // Bit 0 = 17-bit polynomial + // Bit 1 = 9-bit polynomial + // Bit 2 = 5-bit polynomial + // Bit 3 = 4-bit polynomial + uint8_t mPolyBuffer[131071 * 2]; + uint8_t mInitModeBuffer[131071 * 2]; + + ATPokeyTables(); +}; + +// High-resolution filter table. +// +// This is used to resample from the 2x rate that audio events are generated +// at (3.54/3.58MHz) to the 1/28 mixing rate of 64KHz. It is a filter bank of +// 8-tap filter kernels at 56 sub-sample offsets, giving the response of the +// filter for a single half-tick impulse. Each transition in POKEY's output +// is converted to a half-tick edge pulse and the resampled through one of +// the 8-tap filters into the edge buffer. + +struct ATPokeyHiFilterTable { + // This table is anti-symmetric, so we only need half of the kernel -- the + // other half is generated on the fly via reflection. Size: 928 bytes. + alignas(64) float mFilter[29][8]; +}; + +extern const ATPokeyHiFilterTable g_ATPokeyHiFilterTable; + +#endif // f_AT_POKEYTABLES_H diff --git a/src/engine/platform/sound/pokey/pokey.cpp b/src/engine/platform/sound/pokey/pokey.cpp new file mode 100644 index 00000000..1662085f --- /dev/null +++ b/src/engine/platform/sound/pokey/pokey.cpp @@ -0,0 +1,3424 @@ +// Altirra - Atari 800/800XL emulator +// Copyright (C) 2008 Avery Lee +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +#include "at/ataudio/pokey.h" +#include "at/ataudio/pokeyrenderer.h" +#include "at/ataudio/pokeytables.h" +#include + +#include + +#ifdef _DEBUG + ATLogChannel g_ATLCPOKEYTI(false, false, "POKEYTI", "POKEY timer internal events"); + +// #define ATLOGPOKEYTI(msg, ...) printf("%08X | " msg, mpScheduler->GetTick(), __VA_ARGS__) + #define ATLOGPOKEYTI(msg, ...) if (!g_ATLCPOKEYTI.IsEnabled()) {} else (g_ATLCPOKEYTI(msg, __VA_ARGS__)) +#else + #define ATLOGPOKEYTI(msg, ...) ((void)0) +#endif + +namespace { + enum : uint32_t { + kATPokeyEventKeyboardIRQ = 1, + kATPokeyEventKeyboardScan, + kATPokeyEventTimer1Borrow, + kATPokeyEventTimer2Borrow, + kATPokeyEventTimer3Borrow, + kATPokeyEventTimer4Borrow, + + // There are two separate events for this because both timers 1 and 2 can initiate this + // reset in close proximity, and we need both events to effectively take place. + kATPokeyEventResetTwoTones1, + kATPokeyEventResetTwoTones2, + + kATPokeyEventResetTimers, + kATPokeyEventSerialOutput, + kATPokeyEventSerialInput, + }; + + const bool kForceActiveTimers = false; +} + +ATPokeyEmulator::ATPokeyEmulator(bool isSlave) + : mpRenderer(new ATPokeyRenderer) + , mbCommandLineState(false) + , mb5200Mode(false) + , mbTraceSIO(false) + , mbNonlinearMixingEnabled(true) + , mKBCODE(0) + , mKeyCodeTimer(0) + , mKeyCooldownTimer(0) + , mbKeyboardIRQPending(false) + , mbUseKeyCooldownTimer(true) + , mbCookedKeyMode(false) + , mbKeyboardScanEnabled(false) + , mbShiftKeyState(false) + , mbShiftKeyLatchedState(false) + , mbControlKeyState(false) + , mbControlKeyLatchedState(false) + , mbBreakKeyState(false) + , mbBreakKeyLatchedState(false) + , mAddressMask(0x0f) + , mIRQEN(0) + , mIRQST(0) + , mAUDCTL(0) + , mSERIN(0) + , mSEROUT(0) + , mSKSTAT(0) + , mSKCTL(0) + , mLastPolyTime(0) + , mPoly17Counter(0) + , mPoly9Counter(0) + , mPolyShutOffTime(0) + , mSerialOutputStartTime(0) + , mSerialInputShiftRegister(0) + , mSerialOutputShiftRegister(0) + , mSerialInputCounter(0) + , mSerialOutputCounter(0) + , mbSerOutValid(false) + , mbSerShiftValid(false) + , mbSerialOutputState(false) + , mbSpeakerActive(false) + , mbSerialRateChanged(false) + , mbSerialWaitingForStartBit(true) + , mbSerInBurstPendingIRQ1(false) + , mbSerInBurstPendingIRQ2(false) + , mbSerInBurstPendingData(false) + , mbSerInDeferredLoad(false) + , mSerialSimulateInputBaseTime(0) + , mSerialSimulateInputCyclesPerBit(0) + , mSerialSimulateInputData(0) + , mbSerialSimulateInputPort(false) + , mSerialExtBaseTime(0) + , mSerialExtPeriod(0) + , mbFastTimer1(false) + , mbFastTimer3(false) + , mbLinkedTimers12(false) + , mbLinkedTimers34(false) + , mbUse15KHzClock(false) + , mLast15KHzTime(0) + , mLast64KHzTime(0) + , mpConn(NULL) + , mpSlave(NULL) + , mbIsSlave(isSlave) + , mbIrqAsserted(false) +{ +} + +ATPokeyEmulator::~ATPokeyEmulator() { + delete mpRenderer; +} + +void ATPokeyEmulator::Init(IATPokeyEmulatorConnections *mem, IATAudioOutput *output, ATPokeyTables *tables) { + mpConn = mem; + mpAudioOut = output; + mpTables = tables; + UpdateMixTable(); + + mpRenderer->Init(tables); + mpRenderer->StartBlock(); + + VDASSERT(!mpKeyboardScanEvent); + VDASSERT(!mpKeyboardIRQEvent); + + for(int i=0; i<8; ++i) { + mPotPositions[i] = 228; + mPotHiPositions[i] = 228; + } + + ColdReset(); +} + +void ATPokeyEmulator::ColdReset() { + mKeyScanCode = 0; + mKeyScanState = 0; + mbKeyboardIRQPending = false; + mbControlKeyLatchedState = false; + mbShiftKeyLatchedState = false; + mbBreakKeyLatchedState = false; + + memset(&mState, 0, sizeof mState); + + mKBCODE = 0; + mSKSTAT = mbShiftKeyState ? 0x77 : 0x7F; + mSKCTL = 0; + mKeyCodeTimer = 0; + mKeyCooldownTimer = 0; + mIRQEN = 0; + mIRQST = 0xF7; + + mAUDCTL = 0; + mbFastTimer1 = false; + mbFastTimer3 = false; + mbLinkedTimers12 = false; + mbLinkedTimers34 = false; + mbUse15KHzClock = false; + + for(int i=0; i<4; ++i) { + mCounter[i] = 1; + mCounterBorrow[i] = 0; + mAUDFP1[i] = 1; + mbDeferredTimerEvents[i] = false; + + mpScheduler->UnsetEvent(mpTimerBorrowEvents[i]); + } + + RecomputeTimerPeriod<0>(); + RecomputeTimerPeriod<1>(); + RecomputeTimerPeriod<2>(); + RecomputeTimerPeriod<3>(); + RecomputeAllowedDeferredTimers(); + + mpScheduler->UnsetEvent(mpKeyboardScanEvent); + mpScheduler->UnsetEvent(mpKeyboardIRQEvent); + mpScheduler->UnsetEvent(mpResetTimersEvent); + mpScheduler->UnsetEvent(mpEventSerialOutput); + mpScheduler->UnsetEvent(mpEventSerialInput); + mpScheduler->UnsetEvent(mpEventResetTwoTones1); + mpScheduler->UnsetEvent(mpEventResetTwoTones2); + + mpRenderer->ColdReset(); + + mLastPolyTime = ATSCHEDULER_GETTIME(mpScheduler); + mPoly17Counter = 0; + mPoly9Counter = 0; + mSerialOutputStartTime = 0; + mSerialInputShiftRegister = 0; + mSerialOutputShiftRegister = 0; + mSerialOutputCounter = 0; + mSerialInputCounter = 0; + mbSerOutValid = false; + mbSerShiftValid = false; + mbSerialOutputState = false; + mbSerialWaitingForStartBit = true; + mbSerInBurstPendingIRQ1 = false; + mbSerInBurstPendingIRQ2 = false; + mbSerInBurstPendingData = false; + mSerOutBurstDeadline = 0; + mbSerialSimulateInputPort = false; + + memset(mAUDF, 0, sizeof mAUDF); + memset(mAUDC, 0, sizeof mAUDC); + + mLast15KHzTime = ATSCHEDULER_GETTIME(mpScheduler); + mLast64KHzTime = ATSCHEDULER_GETTIME(mpScheduler); + + mALLPOT = 0; + + // On power-up, this can either be $00 or $E4 on XL/XE machines. + // $00 is much more common. + for(auto& v : mPotLatches) + v = 0; + + mPotLastTimeFast = ATSCHEDULER_GETTIME(mpScheduler); + mPotLastTimeSlow = mPotLastTimeFast; + + mbCommandLineState = false; + + if (mpSlave) + mpSlave->ColdReset(); + + NegateIrq(false); +} + +void ATPokeyEmulator::SetSlave(ATPokeyEmulator *slave) { + if (mpSlave) + mpSlave->ColdReset(); + + mpSlave = slave; + + UpdateAddressDecoding(); + + if (mpSlave) { + mpSlave->ColdReset(); + mpSlave->SyncRenderers(mpRenderer); + + // If we're hot-starting a slave, let's try to get it into + // a somewhat reasonable state. + + static const uint8_t kInitRegs[][2]={ + { 0x00, 0xFF }, // AUDF1 = $00 + { 0x01, 0xB0 }, // AUDC1 = $B0 + { 0x02, 0xFF }, // AUDF2 = $00 + { 0x03, 0xB0 }, // AUDC2 = $B0 + { 0x04, 0xFF }, // AUDF3 = $00 + { 0x05, 0xB0 }, // AUDC3 = $B0 + { 0x06, 0xFF }, // AUDF4 = $00 + { 0x07, 0xB0 }, // AUDC4 = $B0 + { 0x08, 0x00 }, // AUDCTL = $00 + { 0x0F, 0x03 }, // SKCTL = $03 + }; + + for(const auto& data : kInitRegs) + mpSlave->WriteByte(data[0], data[1]); + } + + UpdateMixTable(); +} + +void ATPokeyEmulator::SetCassette(IATPokeyCassetteDevice *dev) { + mpCassette = dev; + mbSerialRateChanged = true; +} + +void ATPokeyEmulator::SetAudioLog(ATPokeyAudioLog *log) { + mpRenderer->SetAudioLog(log); +} + +void ATPokeyEmulator::SetConsoleOutput(ATConsoleOutput *output) { + mpConsoleOut = output ? output : &g_ATConsoleOutputNull; +} + +void ATPokeyEmulator::Set5200Mode(bool enable) { + if (mb5200Mode == enable) + return; + + mb5200Mode = enable; + UpdateKeyboardScanEvent(); +} + +void ATPokeyEmulator::AddSIODevice(IATPokeySIODevice *device) { + mDevices.push_back(device); + device->PokeyAttachDevice(this); +} + +void ATPokeyEmulator::RemoveSIODevice(IATPokeySIODevice *device) { + Devices::iterator it(std::find(mDevices.begin(), mDevices.end(), device)); + + if (it != mDevices.end()) + mDevices.erase(it); +} + +void ATPokeyEmulator::ReceiveSIOByte(uint8_t c, uint32_t cyclesPerBit, bool simulateInputPort, bool allowBurst, bool synchronous, bool forceFramingError) { + if (cyclesPerBit && mbSerialNoiseEnabled) { + const uint32_t t = ATSCHEDULER_GETTIME(mpScheduler); + + uint32_t pat = c + c + 0x200; + pat ^= (pat + pat + 1); + + for (uint32_t i = 0; i < 10; ++i) { + if (pat & (1 << i)) + mpRenderer->AddSerialNoisePulse(t + cyclesPerBit * i); + } + } + + if (mbTraceSIO) + (*mpConsoleOut)("POKEY: Receiving byte (c=%02X; %02X %02X) at %u cycles/bit (%.1f baud)", c, mSERIN, mSerialInputShiftRegister, cyclesPerBit, 7159090.0f / 4.0f / (float)cyclesPerBit); + + VDStringA sioDataInfo; + + + // check for attempted read in init mode (partial fix -- audio not emulated) + if (!(mSKCTL & 3)) { + if (mbTraceSIO) + mpConsoleOut->WriteLine("POKEY: Dropping byte due to initialization mode."); + + return; + } + + mbSerialSimulateInputPort = simulateInputPort; + + if (simulateInputPort) { + VDASSERT(cyclesPerBit); + mSerialSimulateInputBaseTime = ATSCHEDULER_GETTIME(mpScheduler); + mSerialSimulateInputCyclesPerBit = cyclesPerBit; + mSerialSimulateInputData = ((uint32_t)c << 1) + 0x200; + } + + if (!(mSKCTL & 0x30) && !mSerialExtPeriod) { + if (mbTraceSIO) + (*mpConsoleOut)("POKEY: Dropping byte $%02X due to external receive mode being used with no external clock (SKCTL=$%02X).", c, mSKCTL); + + return; + } + + mSerialInputPendingStatus = 0xff; + + if (forceFramingError) { + mSerialInputPendingStatus &= 0x7f; + sioDataInfo += " [framing error]\n"; + } + + // check for attempted read in synchronous mode; note that external clock mode is OK as presumably that + // is synchronized + if ((mSKCTL & 0x30) == 0x20 && !synchronous) { + // set the framing error bit + mSerialInputPendingStatus &= 0x7F; + + if (mbTraceSIO) + (*mpConsoleOut)("POKEY: Trashing byte $%02x and signaling framing error due to asynchronous input mode not being enabled (SKCTL=$%02X).", c, mSKCTL); + + // blown read -- trash the byte by faking a dropped bit + c = (c & 0x0f) + ((c & 0xe0) >> 1) + 0x80; + } + + // check for mismatched baud rate + if (cyclesPerBit) { + uint32_t expectedCPB = GetSerialCyclesPerBitRecv(); + uint32_t margin = (expectedCPB + 7) >> 3; + + if (cyclesPerBit < expectedCPB - margin || cyclesPerBit > expectedCPB + margin) { + // blown read -- trash the byte and assert the framing error bit + c = 0xFF; + mSerialInputPendingStatus &= 0x7F; + + if (mbTraceSIO) + (*mpConsoleOut)("POKEY: Signaling framing error due to receive rate mismatch (expected %d cycles/bit, got %d)", expectedCPB, cyclesPerBit); + + } + } + + if (!(mSKCTL & 0x30)) { + if (mpEventSerialInput) { + if (mbTraceSIO) + (*mpConsoleOut)("POKEY: Interrupting send already in progress (%u cycles, %u bits left).", c, mpScheduler->GetTicksToEvent(mpEventSerialInput), mSerialInputCounter); + } + + mpScheduler->SetEvent(mSerialExtPeriod, this, kATPokeyEventSerialInput, mpEventSerialInput); + } else if (mSKCTL & 0x10) { + // Restart timers 3 and 4 immediately. + UpdateTimerCounter<2>(); + UpdateTimerCounter<3>(); + + mCounter[2] = mAUDFP1[2]; + mCounter[3] = mAUDFP1[3]; + + mbSerialWaitingForStartBit = false; + SetupTimers(0x0c); + } + + mSerialInputShiftRegister = c; + + if (!(mSKCTL & 0x30)) + mSerialInputCounter = 9; + else + mSerialInputCounter = 19; + + // assert serial input busy + mSKSTAT &= 0xfd; + + mbSerInDeferredLoad = simulateInputPort; + + if (allowBurst) { + mbSerInBurstPendingData = true; + mbSerInBurstPendingIRQ1 = true; + mbSerInDeferredLoad = false; + } else { + mbSerInBurstPendingData = false; + mbSerInBurstPendingIRQ1 = false; + } + + mbSerInBurstPendingIRQ2 = false; + mSerOutBurstDeadline = 0; + + if (!mbSerInDeferredLoad) + ProcessReceivedSerialByte(); +} + +void ATPokeyEmulator::SetAudioLine2(int v) { + mpRenderer->SetAudioLine2(v); +} + +void ATPokeyEmulator::SetDataLine(bool newState, uint64 flipTime) { + if (newState) + mSKSTAT |= 0x10; + else + mSKSTAT &= ~0x10; + + mSerialDataInFlipTime = flipTime; +} + +void ATPokeyEmulator::SetCommandLine(bool newState) { + if (newState == mbCommandLineState) + return; + + if (mbTraceSIO) + (*mpConsoleOut)("POKEY: %s command line.", newState ? "asserting" : "negating"); + + mbCommandLineState = newState; + if (newState) { + for(Devices::const_iterator it(mDevices.begin()), itEnd(mDevices.end()); it!=itEnd; ++it) + (*it)->PokeyBeginCommand(); + } else { + for(Devices::const_iterator it(mDevices.begin()), itEnd(mDevices.end()); it!=itEnd; ++it) + (*it)->PokeyEndCommand(); + } +} + +void ATPokeyEmulator::SetSpeaker(bool newState) { + if (mpRenderer->SetSpeaker(newState)) + mbSpeakerActive = true; +} + +void ATPokeyEmulator::SetStereoSoftEnable(bool enable) { + if (mbStereoSoftEnable != enable) { + mbStereoSoftEnable = enable; + + if (mpSlave) + UpdateAddressDecoding(); + } +} + +void ATPokeyEmulator::SetExternalSerialClock(uint32_t basetime, uint32_t period) { + mSerialExtBaseTime = basetime; + mSerialExtPeriod = period; + + if (!period) { + mpScheduler->UnsetEvent(mpEventSerialInput); + mpScheduler->UnsetEvent(mpEventSerialOutput); + } +} + +bool ATPokeyEmulator::IsChannelEnabled(uint32_t channel) const { + return mpRenderer->IsChannelEnabled(channel); +} + +void ATPokeyEmulator::SetChannelEnabled(uint32_t channel, bool enabled) { + mpRenderer->SetChannelEnabled(channel, enabled); +} + +bool ATPokeyEmulator::IsSecondaryChannelEnabled(uint32_t channel) const { + return !mpSlave || mpSlave->mpRenderer->IsChannelEnabled(channel); +} + +void ATPokeyEmulator::SetSecondaryChannelEnabled(uint32_t channel, bool enabled) { + if (mpSlave) + mpSlave->mpRenderer->SetChannelEnabled(channel, enabled); +} + +void ATPokeyEmulator::SetNonlinearMixingEnabled(bool enable) { + if (mbNonlinearMixingEnabled != enable) { + mbNonlinearMixingEnabled = enable; + + UpdateMixTable(); + + if (mpRenderer) + mpRenderer->SetFiltersEnabled(enable); + + if (mpAudioOut) + mpAudioOut->SetFiltersEnabled(enable); + } +} + +void ATPokeyEmulator::SetShiftKeyState(bool newState, bool immediate) { + mbShiftKeyState = newState; + + // Shift key state can only change if keyboard scan is enabled. Debounce doesn't matter. + if (immediate && (mSKCTL & 0x02)) { + mbShiftKeyLatchedState = newState; + + if (newState) + mSKSTAT &= ~0x08; + else + mSKSTAT |= 0x08; + } + + // Shift is on the $10-17 row with the KR2 column + UpdateKeyMatrix(2, 0x100, newState ? 0x100 : 0); +} + +void ATPokeyEmulator::SetControlKeyState(bool newState) { + mbControlKeyState = newState; + + // Control is on the $00-07 row with the KR2 column + UpdateKeyMatrix(0, 0x100, newState ? 0x100 : 0); +} + +void ATPokeyEmulator::ClearKeyQueue() { + mKeyQueue.clear(); +} + +void ATPokeyEmulator::PushKey(uint8_t c, bool repeat, bool allowQueue, bool flushQueue, bool useCooldown) { + SetKeyboardModes(true, false); + + // Discard keys that are impossible due to key matrix conflicts. + // Codes 0xC0-C7 and 0xD0-D7 cannot be produced due to a keyboard + // matrix conflict when Ctrl+Shift is pressed and keys on 0x00-07/10-17. + // They ARE possible with debounce disabled, but we don't support that + // in the cooked key path. + if ((c & 0xE8) == 0xC0) + return; + + mbUseKeyCooldownTimer = useCooldown; + + if (allowQueue) { + // Queue a key if we already have keys queued, or the OS can't accept one yet. + if (!mKeyQueue.empty() || !CanPushKey(c)) { + mKeyQueue.push_back(c); + return; + } + } else if (flushQueue) { + mKeyQueue.clear(); + } + + // If debounce or scan is disabled, drop the key. + if ((mSKCTL & 3) != 3) + return; + + mKBCODE = c; + mSKSTAT &= ~0x04; + + if (!mKeyCodeTimer || !repeat) + QueueKeyboardIRQ(); + + mKeyCodeTimer = 1; + mKeyCooldownTimer = 0; +} + +uint64 ATPokeyEmulator::GetRawKeyMask() const { + uint64 v = 0; + + for(int i=0; i<8; ++i) + v += (uint64)(mKeyMatrix[i] & 0xFF) << (i*8); + + return v; +} + +void ATPokeyEmulator::PushRawKey(uint8_t c, bool immediate) { + if (mb5200Mode) + return; + + SetKeyboardModes(false, !immediate); + mKeyQueue.clear(); + + int row = (c >> 3) & 7; + uint16_t colbit = 1 << (c & 7); + + if (!immediate) { + UpdateKeyMatrix(row, colbit, 0xFF); + } else { + // Stomp the entire main keyboard matrix with the newly pressed key (NOT the + // extended matrix!). We need to update the effective matrix, too. Since we + // only have one key active in the main matrix, it is not possible to introduce + // phantoms in Ctrl/Shift/Break. It IS possible for Ctrl/Shift/Break to interfere + // with the main matrix if two are held down; we handle Ctrl+Shift by a check on + // the key code and ignore the Break conflict. + for(int i=0; i<8; ++i) + mKeyMatrix[i] &= 0xFF00; + + mKeyMatrix[row] |= colbit; + + memcpy(mEffectiveKeyMatrix, mKeyMatrix, sizeof mEffectiveKeyMatrix); + + // If we're in immediate mode, scan and debounce are enabled, and it's + // not a key blocked by an inherent matrix conflict, push it now. + if ((mSKCTL & 3) == 3 && (c & 0xE8) != 0xC0) { + mSKSTAT &= ~0x04; + mKBCODE = c; + + QueueKeyboardIRQ(); + } + } +} + +void ATPokeyEmulator::ReleaseRawKey(uint8_t c, bool immediate) { + if (mb5200Mode) + return; + + SetKeyboardModes(false, !immediate); + mKeyQueue.clear(); + + const int row = (c >> 3) & 7; + const uint16_t colbit = 1 << (c & 7); + if (!immediate) { + UpdateKeyMatrix(row, colbit, 0); + } else { + if (mKeyMatrix[row] & colbit) { + for(int i=0; i<8; ++i) + mKeyMatrix[i] &= 0xFF00; + + memcpy(mEffectiveKeyMatrix, mKeyMatrix, sizeof mEffectiveKeyMatrix); + + mSKSTAT |= 0x04; + } + } +} + +void ATPokeyEmulator::ReleaseAllRawKeys(bool immediate) { + if (mb5200Mode) + return; + + memset(mKeyMatrix, 0, sizeof mKeyMatrix); + memset(mEffectiveKeyMatrix, 0, sizeof mEffectiveKeyMatrix); + + if (immediate) { + mbKeyboardIRQPending = false; + mSKSTAT |= 0x04; + } + + SetKeyboardModes(mbCookedKeyMode, !immediate); +} + +void ATPokeyEmulator::SetBreakKeyState(bool state, bool immediate) { + if (mbBreakKeyState == state) + return; + + mbBreakKeyState = state; + + if (immediate) { + if (state) + PushBreak(); + } + + // Break is on the $30-37 row with the KR2 column + UpdateKeyMatrix(6, 0x100, state ? 0x100 : 0); +} + +void ATPokeyEmulator::PushBreak() { + mKeyQueue.clear(); + + // The keyboard scan must be enabled for Break to be detected. However, debounce + // doesn't matter. + if (mSKCTL & 2) + AssertBreakIRQ(); +} + +void ATPokeyEmulator::SetKeyMatrix(const bool matrix[64]) { + if (matrix) { + uint16_t *dst = mKeyMatrix; + const bool *src = matrix; + + for(int i=0; i<8; ++i) { + uint16_t v = (src[0] ? 0x01 : 0x00) + + (src[1] ? 0x02 : 0x00) + + (src[2] ? 0x04 : 0x00) + + (src[3] ? 0x08 : 0x00) + + (src[4] ? 0x10 : 0x00) + + (src[5] ? 0x20 : 0x00) + + (src[6] ? 0x40 : 0x00) + + (src[7] ? 0x80 : 0x00); + + dst[i] = (dst[i] & 0xFF00) + v; + src += 8; + } + } else + memset(mKeyMatrix, 0, sizeof mKeyMatrix); + + UpdateEffectiveKeyMatrix(); +} + +template +void ATPokeyEmulator::FireTimer() { + mpRenderer->AddChannelEvent(activeChannel); + + if constexpr (activeChannel == 0) { + if (mIRQEN & 0x01) { + mIRQST &= ~0x01; + AssertIrq(false); + } + + // two tone + if ((mSKCTL & 8) && mbSerialOutputState && !(mSKCTL & 0x80)) { + mpScheduler->SetEvent(2, this, kATPokeyEventResetTwoTones1, mpEventResetTwoTones1); + } + } + + // count timer 2 + if constexpr (activeChannel == 1) { + if (mIRQEN & 0x02) { + mIRQST &= ~0x02; + AssertIrq(false); + } + + // two tone + if (mSKCTL & 8) { + mpScheduler->SetEvent(2, this, kATPokeyEventResetTwoTones2, mpEventResetTwoTones2); + } + + if (mSerialOutputCounter) { + if ((mSKCTL & 0x60) == 0x60) + mpScheduler->SetEvent(2, this, kATPokeyEventSerialOutput, mpEventSerialOutput); + } + } + + // count timer 4 + if constexpr (activeChannel == 3) { + if (mIRQEN & 0x04) { + mIRQST &= ~0x04; + AssertIrq(false); + } + + if (mSKCTL & 0x30) + OnSerialInputTick(); + + if (mSerialOutputCounter) { + switch(mSKCTL & 0x60) { + case 0x20: + case 0x40: + mpScheduler->SetEvent(2, this, kATPokeyEventSerialOutput, mpEventSerialOutput); + break; + } + } + } +} + +uint32_t ATPokeyEmulator::UpdateLast15KHzTime() { + return UpdateLast15KHzTime(ATSCHEDULER_GETTIME(mpScheduler)); +} + +uint32_t ATPokeyEmulator::UpdateLast15KHzTime(uint32_t t) { + uint32_t offset = t - mLast15KHzTime; + + if (offset >= 114) + mLast15KHzTime += offset - offset % 114; + + return mLast15KHzTime; +} + +uint32_t ATPokeyEmulator::UpdateLast64KHzTime() { + return UpdateLast64KHzTime(ATSCHEDULER_GETTIME(mpScheduler)); +} + +uint32_t ATPokeyEmulator::UpdateLast64KHzTime(uint32_t t) { + uint32_t offset = t - mLast64KHzTime; + + if (offset >= 28) { + mLast64KHzTime += 28; + offset -= 28; + + if (offset >= 28) + mLast64KHzTime += offset - offset % 28; + } + + return mLast64KHzTime; +} + +void ATPokeyEmulator::UpdatePolyTime() { + uint32_t t = ATSCHEDULER_GETTIME(mpScheduler); + int polyDelta = (int)(t - mLastPolyTime); + mPoly9Counter += polyDelta; + mPoly17Counter += polyDelta; + mLastPolyTime = t; + + if (mPoly9Counter >= 511) + mPoly9Counter %= 511; + + if (mPoly17Counter >= 131071) + mPoly17Counter %= 131071; +} + +void ATPokeyEmulator::OnSerialInputTick() { + if (!mSerialInputCounter) + return; + + --mSerialInputCounter; + + if (!mSerialInputCounter) { + // deassert serial input active + mSKSTAT |= 0x02; + + mbSerialWaitingForStartBit = true; + + if ((mSKCTL & 0x10) && !mbLinkedTimers34) { + mCounter[2] = mAUDFP1[2]; + SetupTimers(0x04); + } + + if (mbSerInDeferredLoad) + ProcessReceivedSerialByte(); + } else { + if (!(mSKCTL & 0x30) && mSerialExtPeriod) + mpScheduler->SetEvent(mSerialExtPeriod, this, kATPokeyEventSerialInput, mpEventSerialInput); + } +} + +void ATPokeyEmulator::OnSerialOutputTick() { + --mSerialOutputCounter; + + // We've already transmitted the start bit (low), so we need to do data bits and then + // stop bit (high). + mbSerialOutputState = mSerialOutputCounter ? (mSerialOutputShiftRegister & (1 << (9 - (mSerialOutputCounter >> 1)))) != 0 : true; + + if (!mSerialOutputCounter) { + FlushSerialOutput(); + + if (mbSerOutValid) { + mSerialOutputCounter = 20; + mSerialOutputStartTime = mpScheduler->GetTick64(); + + if (mSerOutBurstDeadline && ATSCHEDULER_GETTIME(mpScheduler) - mSerOutBurstDeadline >= uint32_t(0x80000000)) + mSerialOutputCounter = 1; + + mbSerialOutputState = true; + mSerialOutputShiftRegister = mSEROUT; + mbSerOutValid = false; + mbSerShiftValid = true; + + if (mpCassette) + mpCassette->PokeyBeginCassetteData(mSKCTL); + + // bit 3 is special and doesn't get cleared by IRQEN + mIRQST |= 0x08; + + if (mIRQEN & 0x10) + mIRQST &= ~0x10; + } else + mIRQST &= ~0x08; + + if (mIRQEN & ~mIRQST) + AssertIrq(false); + else + NegateIrq(false); + } + + // check if we must reset the tick for external clock + if (mSerialOutputCounter && !(mSKCTL & 0x60)) { + if (mSerialExtPeriod) + mpScheduler->SetEvent(mSerialExtPeriod, this, kATPokeyEventSerialOutput, mpEventSerialOutput); + else + mpScheduler->UnsetEvent(mpEventSerialOutput); + } +} + +bool ATPokeyEmulator::IsSerialOutputClockRunning() const { + switch(mSKCTL & 0x60) { + default: + VDNEVERHERE; + case 0x00: // external clock + return mSerialExtPeriod != 0; + + case 0x20: // timer 4 as transmit clock + case 0x40: + if (mSKCTL & 0x10) { + // Asynchronous receive mode enabled, so clock only runs if receiving. For now, pretend + // it's always halted for output purposes. + return false; + } + + // check if initialization mode is active + if (mSKCTL & 3) { + // nope, clocks are running + return true; + } + + // init mode is active -- check if 3+4 are linked + if (mAUDCTL & 0x08) { + // linked -- running if timer 3 is 1.79MHz + return (mAUDCTL & 0x20) != 0; + } else { + // not linked -- timer 4 is halted + return false; + } + break; + + case 0x60: // timer 2 as transmit clock + // check if initialization mode is active + if (mSKCTL & 3) { + // nope, clocks are running + return true; + } + + // init mode is active -- check if 1+2 are linked + if (mAUDCTL & 0x10) { + // linked -- running if timer 1 is 1.79MHz + return (mAUDCTL & 0x40) != 0; + } else { + // not linked -- timer 2 is halted + return false; + } + break; + } +} + +void ATPokeyEmulator::FlushSerialOutput() { + if (!mbSerShiftValid) + return; + + const uint8_t originalCounter = mSerialOutputCounter; + + mbSerShiftValid = false; + mSerialOutputCounter = 0; + + // check if we got out the start bit; if not, no byte would be noticed by + // receivers + if (mSerialOutputCounter >= 18) + return; + + uint32_t cyclesPerBit; + + switch(mSKCTL & 0x60) { + default: + VDNEVERHERE; + case 0x00: // external clock + cyclesPerBit = mSerialExtPeriod; + break; + + case 0x20: // timer 4 as transmit clock + case 0x40: + cyclesPerBit = mTimerPeriod[3]; + break; + + case 0x60: // timer 2 as transmit clock + cyclesPerBit = mTimerPeriod[1]; + break; + } + + cyclesPerBit += cyclesPerBit; + + if (mbTraceSIO) { + (*mpConsoleOut)("POKEY: Transmitted serial byte %02x to SIO bus at %u cycles/bit (%.1f baud)" + , mSerialOutputShiftRegister + , cyclesPerBit + , (7159090.0f / 4.0f) / (float)cyclesPerBit + ); + } + + bool burstOK = false; + bool framingError = false; + uint8_t c = mSerialOutputShiftRegister; + + if (mSerialOutputCounter) { + // Byte may have been truncated -- adjust it and the stop bit. Transmission + // is LSB first, so stomp from MSBs down. + uint8_t truncationMask = 0xFF << ((18 - originalCounter) >> 1); + + c &= truncationMask; + + if (mbSerialNoiseEnabled) + c |= truncationMask; + + // signal framing error if output is still low by stop bit time + framingError = !mbSerialOutputState; + } + + for(IATPokeySIODevice *dev : mDevices) { + if (dev->PokeyWriteSIO(c, mbCommandLineState, cyclesPerBit, mSerialOutputStartTime, framingError)) + burstOK = true; + } + + if (mpCassette) + mpCassette->PokeyWriteCassetteData(mSerialOutputShiftRegister, cyclesPerBit); + + if (burstOK) + mSerOutBurstDeadline = (ATSCHEDULER_GETTIME(mpScheduler) + cyclesPerBit*10) | 1; + else + mSerOutBurstDeadline = 0; +} + +uint32_t ATPokeyEmulator::GetSerialCyclesPerBitRecv() const { + if (!(mSKCTL & 0x30)) + return mSerialExtPeriod; + + const uint32_t divisor = mTimerPeriod[3]; + + return divisor + divisor; +} + +void ATPokeyEmulator::SetPotPos(unsigned idx, int pos) { + SetPotPosHires(idx, pos << 16, false); +} + +void ATPokeyEmulator::SetPotPosHires(unsigned idx, int pos, bool grounded) { + uint8_t lopos = (uint8_t)std::clamp(pos >> 16, 1, 228); + uint8_t hipos = grounded ? 255 : (uint8_t)std::clamp((pos * 114) >> 16, 1, 229); + + if (mPotPositions[idx] == lopos && mPotHiPositions[idx] == hipos) + return; + + UpdatePots(0); + + mPotPositions[idx] = lopos; + mPotHiPositions[idx] = hipos; + + if (mbAllowImmediatePotUpdate) { + // If this pot line has already latched and it's been less than one full frame since we + // kicked off the pot scan, update the latch immediately. Since we are NTSC/PAL agnostic + // here, use a conservative timeout that exceeds a PAL frame. + + if (mpScheduler->GetTick64() - mPotLastScanTime < 37000 + && !(mALLPOT & (1 << idx))) + { + mPotLatches[idx] = std::min(229, mSKCTL & 4 ? hipos : lopos); + } + } +} + +void ATPokeyEmulator::AdvanceScanLine() { + if (mbSerialRateChanged) { + mbSerialRateChanged = false; + + if (mpCassette) { + uint32_t divisor = GetSerialCyclesPerBitRecv() >> 1; + + mpCassette->PokeyChangeSerialRate(divisor); + } + } + + if (mpSlave) + mpSlave->AdvanceScanLine(); +} + +void ATPokeyEmulator::AdvanceFrame(bool pushAudio, uint64 timestamp) { + UpdatePots(0); + + if (mpSlave) + mpSlave->UpdatePots(0); + + if (mKeyCodeTimer) { + if (!--mKeyCodeTimer) { + mSKSTAT |= 0x04; + + mKeyCooldownTimer = 60; + } + } else if (mbSpeakerActive) { + mKeyCooldownTimer = 60; + } else { + if (mKeyCooldownTimer) + --mKeyCooldownTimer; + + if (!mKeyQueue.empty() && CanPushKey(mKeyQueue.front())) + TryPushNextKey(); + } + + mbSpeakerActive = false; + + FlushAudio(pushAudio, timestamp); + + const uint32_t t = ATSCHEDULER_GETTIME(mpScheduler); + + PostFrameUpdate(t); + + if (mpSlave) + mpSlave->PostFrameUpdate(t); +} + +void ATPokeyEmulator::PostFrameUpdate(uint32_t t) { + mpRenderer->RestartAudioLog(); + + // Scan all of the deferred timers and push any that are too far behind. We need to do this to + // prevent any of the timers from getting too far behind the clock (>30 bits). This calculation + // is wrong for the low timer of a linked timer pair, but it turns out we don't use the start + // value in that case; this module uses the high timer delay, and only the renderer does the + // funky linked step. + + for(int i=0; i<4; ++i) { + if (!mbDeferredTimerEvents[i]) + continue; + + // Determine what kind of lag we want to check for on the deferred timer start. This must be + // at least 8893, which is the number of times that a 1.79MHz frequency 0 timer can count in + // a PAL frame. However, a slow 15KHz linked timer can take as long as 7.4M clocks to cycle. + // We keep shifting up the value until it's a bit more than a second. We do need to keep this + // value a multiple of the original, though. + uint32_t bigPeriod = mDeferredTimerPeriods[i]; + + while(bigPeriod < 2097152) { + bigPeriod <<= 4; + } + + // The deferred timers may start in the future for one that hasn't hit the first tick, + // so we must check for that case. + if ((sint32)(t - mDeferredTimerStarts[i]) <= (sint32)bigPeriod) + continue; + + // Bump the timer up by a number of periods so it catches up. + mDeferredTimerStarts[i] += bigPeriod; + } + + // Catch up 15KHz and 64Khz clocks. + UpdateLast15KHzTime(); + UpdateLast64KHzTime(); + + // Catch up poly counters. + UpdatePolyTime(); + + // Catch up the external clock, to avoid glitches at 2^32 + if (mSerialExtPeriod) { + uint32_t extsince = t - mSerialExtBaseTime; + + if (extsince >= 0x40000000) { + extsince += mSerialExtPeriod - 1; + mSerialExtBaseTime += extsince - extsince % mSerialExtPeriod; + } + } +} + +void ATPokeyEmulator::OnScheduledEvent(uint32_t id) { + switch(id) { + case kATPokeyEventKeyboardIRQ: + mpKeyboardIRQEvent = nullptr; + + if (mbKeyboardIRQPending) { + mbKeyboardIRQPending = false; + + if (mSKCTL & 2) + AssertKeyboardIRQ(); + } + break; + + case kATPokeyEventKeyboardScan: + { + mpKeyboardScanEvent = nullptr; + + if ((mb5200Mode || mbKeyboardScanEnabled) && (mSKCTL & 2)) { + mpKeyboardScanEvent = mpScheduler->AddEvent(114, this, kATPokeyEventKeyboardScan); + + const uint8_t kc = mKeyScanCode++ & 0x3F; + + // POKEY's keyboard circuitry is a two-bit state machine with a keyboard line and + // a comparator as input, and result/comparator latch signals as output. The state + // machine works as follows: + // + // state 0 (00): waiting for key + // key pressed -> state 1, load compare latch + // + // state 1 (01): waiting for key bounce + // key pressed, not same as compare latch -> state 0 + // key not pressed, same as compare latch -> state 0 + // key pressed, same as compare latch -> state 2 + // + // state 2 (11): waiting for key release + // key not pressed, same as compare latch -> state 3 + // + // state 3 (10): waiting for key debounce + // key pressed, same as compare latch -> state 2 + // key not pressed, same as compare latch -> state 0 + // + // If keyboard debounce (SKCTL bit 1) is disabled, the compare signal is always + // true. SKSTAT bit 2 reads 1 for states 0 and 1, and 0 for states 2 and 3. + // + // The state machine and the binary counter that run the external keyboard logic + // both run at HBLANK (15KHz) rate; this logic therefore cycles through the entire + // keyboard 245.3 times a second (NTSC), or 4.1 times a frame. It takes two HBLANKs + // for a key to be recognized, and since keys are mirrored four times on the 5200 + // keypad, this causes IRQs to be sent 8 times a frame. + + switch(kc) { + case 0x00: + mbControlKeyLatchedState = (mEffectiveKeyMatrix[0] & 0x100) != 0; + break; + + case 0x10: + { + const bool shiftState = (mEffectiveKeyMatrix[2] & 0x100) != 0; + + if (mbShiftKeyLatchedState != shiftState) { + mbShiftKeyLatchedState = shiftState; + + if (shiftState) + mSKSTAT &= ~0x08; + else + mSKSTAT |= 0x08; + } + } + break; + + case 0x30: + { + const bool breakKeyState = (mEffectiveKeyMatrix[6] & 0x100) != 0; + + if (mbBreakKeyLatchedState != breakKeyState) { + mbBreakKeyLatchedState = breakKeyState; + + if (breakKeyState) + AssertBreakIRQ(); + } + } + break; + } + + const bool keyState = (mEffectiveKeyMatrix[kc >> 3] & (1 << (kc & 7))) != 0; + + switch(mKeyScanState) { + case 0: // waiting for key + if (keyState) { + mKeyScanLatch = kc; + mKeyScanState = 1; + } + break; + + case 1: // waiting for key bounce + if (keyState) { + if (kc == mKeyScanLatch || !(mSKCTL & 1)) { + // same key down -- fire IRQ, save key, and continue + mKeyScanState = 2; + mKBCODE = kc | (mbShiftKeyLatchedState ? 0x40 : 0x00) | (mbControlKeyLatchedState ? 0x80 : 0x00); + mSKSTAT &= ~0x04; + if (mIRQEN & 0x40) { + // If keyboard IRQ is already active, set the keyboard overrun bit. + if (!(mIRQST & 0x40)) + mSKSTAT &= ~0x40; + else { + mIRQST &= ~0x40; + AssertIrq(false); + } + } + } else { + // different key down -- restart + mKeyScanState = 0; + } + } else if (kc == mKeyScanLatch || !(mSKCTL & 1)) { + // latched key no longer down -- restart + mKeyScanState = 0; + } + break; + + case 2: // waiting for key up + if (kc == mKeyScanLatch || !(mSKCTL & 1)) { + if (!keyState) { + mKeyScanState = 3; + } + } + break; + + case 3: // waiting for debounce + if (kc == mKeyScanLatch || !(mSKCTL & 1)) { + if (keyState) + mKeyScanState = 2; + else { + mSKSTAT |= 0x04; + mKeyScanState = 0; + } + } + break; + } + } + } + break; + + case kATPokeyEventTimer1Borrow: + mpTimerBorrowEvents[0] = NULL; + + // Check if there is a resync12 happening at the same time. If so, we need to suppress the + // timer tick as the resync will take priority -- there is a gate that blocks the borrow + // in that case. This only needs to occur if the borrow event is processed first, since + // the order is ambiguous; if the two-tone resync event is processed first, it will clear + // the borrow events and we won't get to this point. + if ((!mpEventResetTwoTones1 || mpScheduler->GetTicksToEvent(mpEventResetTwoTones1) != 0) + && (!mpEventResetTwoTones2 || mpScheduler->GetTicksToEvent(mpEventResetTwoTones2) != 0)) + { + FireTimer<0>(); + } + + mCounterBorrow[0] = 0; + if (!mbLinkedTimers12) { + mCounter[0] = mAUDFP1[0]; + } else { + // If we are operating at 1.79MHz, three cycles have already elapsed from the underflow to + // when the borrow goes through. + mCounter[0] = mbFastTimer1 ? 253 : 256; + + // We need the timer 2 counter updated so SetupTimers() can detect if a timer 2 borrow + // is happening, which affects interpretation of the timer 1 counter. + UpdateTimerCounter<1>(); + } + + SetupTimers(0x01); + break; + + case kATPokeyEventTimer2Borrow: + mpTimerBorrowEvents[1] = NULL; + + // see Timer1Borrow version + if ((!mpEventResetTwoTones1 || mpScheduler->GetTicksToEvent(mpEventResetTwoTones1) != 0) + && (!mpEventResetTwoTones2 || mpScheduler->GetTicksToEvent(mpEventResetTwoTones2) != 0)) + { + FireTimer<1>(); + } + + mCounterBorrow[1] = 0; + mCounter[1] = mAUDFP1[1]; + if (mbLinkedTimers12) { + mCounter[0] = mAUDFP1[0]; + mCounterBorrow[0] = 0; + SetupTimers(0x03); + } else + SetupTimers(0x02); + break; + + case kATPokeyEventTimer3Borrow: + mpTimerBorrowEvents[2] = NULL; + FireTimer<2>(); + + mCounterBorrow[2] = 0; + if (!mbLinkedTimers34) { + mCounter[2] = mAUDFP1[2]; + } else { + // If we are operating at 1.79MHz, three cycles have already elapsed from the underflow to + // when the borrow goes through. + mCounter[2] = mbFastTimer3 ? 253 : 256; + + // We need the timer 4 counter updated so SetupTimers() can detect if a timer 4 borrow + // is happening, which affects interpretation of the timer 3 counter. + UpdateTimerCounter<3>(); + } + + SetupTimers(0x04); + break; + + case kATPokeyEventTimer4Borrow: + mpTimerBorrowEvents[3] = NULL; + FireTimer<3>(); + + mCounter[3] = mAUDFP1[3]; + mCounterBorrow[3] = 0; + if (mbLinkedTimers34) { + mCounter[2] = mAUDFP1[2]; + mCounterBorrow[2] = 0; + SetupTimers(0x0C); + } else + SetupTimers(0x08); + break; + + case kATPokeyEventResetTwoTones1: + mpEventResetTwoTones1 = nullptr; + + // resync timers 1 and 2 + mCounter[0] = mAUDFP1[0]; + mCounter[1] = mAUDFP1[1]; + mCounterBorrow[0] = 0; + mCounterBorrow[1] = 0; + SetupTimers(0x03); + break; + + case kATPokeyEventResetTwoTones2: + mpEventResetTwoTones2 = nullptr; + + // resync timers 1 and 2 + mCounter[0] = mAUDFP1[0]; + mCounter[1] = mAUDFP1[1]; + mCounterBorrow[0] = 0; + mCounterBorrow[1] = 0; + SetupTimers(0x03); + break; + + case kATPokeyEventResetTimers: + mpResetTimersEvent = NULL; + mCounter[0] = mAUDFP1[0]; + mCounter[1] = mAUDFP1[1]; + mCounter[2] = mAUDFP1[2]; + mCounter[3] = mAUDFP1[3]; + + mpRenderer->ResetTimers(); + + for(int i=0; i<4; ++i) { + mCounterBorrow[i] = 0; + + if (mpTimerBorrowEvents[i]) { + mpScheduler->RemoveEvent(mpTimerBorrowEvents[i]); + mpTimerBorrowEvents[i] = NULL; + } + } + + SetupTimers(0x0f); + break; + + case kATPokeyEventSerialOutput: + mpEventSerialOutput = NULL; + + if (mSerialOutputCounter) + OnSerialOutputTick(); + break; + + case kATPokeyEventSerialInput: + mpEventSerialInput = NULL; + + if (mSerialInputCounter) + OnSerialInputTick(); + break; + } +} + +template +void ATPokeyEmulator::RecomputeTimerPeriod() { + static_assert(channel >= 0 && channel <= 3); + + const bool fastTimer = (channel == 0) ? mbFastTimer1 : (channel == 2) ? mbFastTimer3 : false; + const bool hiLinkedTimer = channel == 1 ? mbLinkedTimers12 : channel == 3 ? mbLinkedTimers34 : false; + const bool loLinkedTimer = channel == 0 ? mbLinkedTimers12 : channel == 2 ? mbLinkedTimers34 : false; + + uint32_t period; + if (hiLinkedTimer) { + constexpr int loChannel = channel & ~1; + const bool fastLinkedTimer = (channel == 1) ? mbFastTimer1 : (channel == 3) ? mbFastTimer3 : false; + + period = ((uint32_t)mAUDF[channel] << 8) + mAUDFP1[loChannel]; + + if (fastLinkedTimer) { + period += 6; + } else if (mbUse15KHzClock) + period *= 114; + else + period *= 28; + } else { + period = mAUDFP1[channel]; + + if (fastTimer) + period += 3; + else if (mbUse15KHzClock) + period *= 114; + else + period *= 28; + + if (loLinkedTimer) { + uint32_t fullPeriod; + + if (fastTimer) + fullPeriod = 256; + else if (mbUse15KHzClock) + fullPeriod = 256 * 114; + else + fullPeriod = 256 * 28; + + mTimerFullPeriod[channel >> 1] = fullPeriod; + } + } + + mTimerPeriod[channel] = period; +} + +void ATPokeyEmulator::RecomputeAllowedDeferredTimers() { + // Timer 2 IRQ, two-tone mode, or timer 2 as serial output clock prohibits deferring timer 2. + mbAllowDeferredTimer[1] = !kForceActiveTimers + && !(mIRQEN & 0x02) + && !(mSKCTL & 8) + && (mSKCTL & 0x60) != 0x60; + + // Timer 1 IRQ or two-tone mode prohibits deferring timer 1. + mbAllowDeferredTimer[0] = !kForceActiveTimers + && !(mIRQEN & 0x01) + && !(mSKCTL & 8); + + // Timer 4 IRQ, asynchronous receive mode, or using timer 4 as transmit clock prohibits deferring timer 4. + mbAllowDeferredTimer[3] = !kForceActiveTimers + && !(mIRQEN & 0x04) + && !(mSKCTL & 0x10) + && (mSKCTL & 0x60) != 0x20 + && (mSKCTL & 0x60) != 0x40; + + // Timer 3 deferring is ordinarily always possible. + mbAllowDeferredTimer[2] = !kForceActiveTimers; + + // Check if timers are linked; both timers must be able to defer together. + if (mbLinkedTimers12) { + if (!mbAllowDeferredTimer[0] || !mbAllowDeferredTimer[1]) { + mbAllowDeferredTimer[0] = false; + mbAllowDeferredTimer[1] = false; + } + } + + if (mbLinkedTimers34) { + if (!mbAllowDeferredTimer[2] || !mbAllowDeferredTimer[3]) { + mbAllowDeferredTimer[2] = false; + mbAllowDeferredTimer[3] = false; + } + } +} + +template +void ATPokeyEmulator::UpdateTimerCounter() { + static_assert(channel >= 0 && channel <= 3); + + VDASSERT(mCounter[0] > 0); + VDASSERT(mCounterBorrow[0] >= 0); + + const uint32_t t = ATSCHEDULER_GETTIME(mpScheduler); + + mCounterBorrow[channel] = 0; + + const bool fastTimer = channel == 0 || (channel == 1 && mbLinkedTimers12) ? mbFastTimer1 + : channel == 2 || (channel == 3 && mbLinkedTimers34) ? mbFastTimer3 : false; + const bool hiLinkedTimer = channel == 1 ? mbLinkedTimers12 : channel == 3 ? mbLinkedTimers34 : false; + const bool loLinkedTimer = channel == 0 ? mbLinkedTimers12 : channel == 2 ? mbLinkedTimers34 : false; + + if (loLinkedTimer) { + constexpr int hiChannel = channel | 1; + + // Compute the number of ticks until the next borrow event. The borrow occurs three cycles after + // the timer channel underflows; 16-bit channels take 6 cycles because the borrows from the two + // channels are cascaded. If we have a borrow event, we can simply use that time directly. + + int ticksLeft; + if (mbDeferredTimerEvents[hiChannel]) { + if ((sint32)(t - mDeferredTimerStarts[hiChannel]) < 0) + ticksLeft = mDeferredTimerStarts[hiChannel] - t; + else + ticksLeft = mDeferredTimerPeriods[hiChannel] - (t - mDeferredTimerStarts[hiChannel]) % mDeferredTimerPeriods[hiChannel]; + } else if (mpTimerBorrowEvents[hiChannel]) + ticksLeft = mpScheduler->GetTicksToEvent(mpTimerBorrowEvents[hiChannel]); + else + return; + + // Compute the low timer offset from the high timer offset. + [[maybe_unused]] int ticksLeft0 = ticksLeft; + + if (ticksLeft <= 3) { + // Three ticks or less means that the high timer is borrowing, so we're free running. We've + // already run through the three ticks of low timer borrow. + if (fastTimer) + mCounter[channel] = 256 - ticksLeft; + else + mCounter[channel] = 256; + } else { + ticksLeft -= 3; + + if (ticksLeft <= 3) { + // Low timer borrow is in progress. + mCounterBorrow[channel] = ticksLeft; + + if (fastTimer) + mCounter[channel] = 256 - ticksLeft; + else + mCounter[channel] = 256; + } else { + ticksLeft = (uint32_t)(ticksLeft - 3) % mTimerFullPeriod[channel >> 1]; + + if (!fastTimer) { + if (mbUse15KHzClock) + ticksLeft = (ticksLeft + 113) / 114; + else + ticksLeft = (ticksLeft + 27) / 28; + } + + mCounter[channel] = ticksLeft ? ticksLeft : 256; + } + } + + ATLOGPOKEYTI("Counter[%d] = %03X+%d (%d ticks left, full period = %d)\n", channel, mCounter[channel], mCounterBorrow[channel], ticksLeft0, mTimerFullPeriod[channel >> 1]); + + VDASSERT(mCounter[channel] > 0 && mCounter[channel] <= 256); + VDASSERT(mCounterBorrow[channel] >= 0); + } else { + // Compute the number of ticks until the next borrow event. The borrow occurs three cycles after + // the timer channel underflows; 16-bit channels take 6 cycles because the borrows from the two + // channels are cascaded. If we have a borrow event, we can simply use that time directly. + + int ticksLeft; + if (mbDeferredTimerEvents[channel]) { + if ((sint32)(t - mDeferredTimerStarts[channel]) < 0) + ticksLeft = mDeferredTimerStarts[channel] - t; + else + ticksLeft = mDeferredTimerPeriods[channel] - (t - mDeferredTimerStarts[channel]) % mDeferredTimerPeriods[channel]; + } else if (mpTimerBorrowEvents[channel]) + ticksLeft = mpScheduler->GetTicksToEvent(mpTimerBorrowEvents[channel]); + else + return; + + // If we're three ticks or less away, we have a borrow pending. At fast clock the timer will begin + // counting down immediately during the borrow; this is important to maintain 16-bit, 1.79MHz accuracy. + // At slow clock this can only happen once if the slow clock just happens to fall at the right time + // (can happen if 1.79MHz clocking is disabled). + // + // TODO: Handle slow tick happening in borrow land. + + [[maybe_unused]] int ticksLeft0 = ticksLeft; + if (ticksLeft <= 3) { + mCounterBorrow[channel] = ticksLeft; + + if (fastTimer && !hiLinkedTimer) + mCounter[channel] = 256 - ticksLeft; + else + mCounter[channel] = 256; + } else { + ticksLeft -= 3; + + if (hiLinkedTimer) { + if (ticksLeft <= 3) { + mCounter[channel] = 256; + mCounterBorrow[channel] = ticksLeft + 3; + } else { + ticksLeft -= 3; + + if (fastTimer) + mCounter[channel] = (ticksLeft + 255) >> 8; + else if (mbUse15KHzClock) + mCounter[channel] = (ticksLeft + 114*256 - 1) / (114*256); + else + mCounter[channel] = (ticksLeft + 28*256 - 1) / (28*256); + } + } else { + if (fastTimer) + mCounter[channel] = ticksLeft; + else if (mbUse15KHzClock) + mCounter[channel] = (ticksLeft + 113) / 114; + else + mCounter[channel] = (ticksLeft + 27) / 28; + } + } + + ATLOGPOKEYTI("Counter[%d] = %03X+%d (%d ticks left)\n", channel, mCounter[channel], mCounterBorrow[channel], ticksLeft0); + + VDASSERT(mCounter[channel] > 0 && mCounter[channel] <= 256); + VDASSERT(mCounterBorrow[channel] >= 0); + } +} + +void ATPokeyEmulator::SetupTimers(uint8_t channels) { + uint32_t t = ATSCHEDULER_GETTIME(mpScheduler); + int cyclesToNextSlowTick; + + if (mbUse15KHzClock) { + cyclesToNextSlowTick = 114 - (t - UpdateLast15KHzTime()); + + if (cyclesToNextSlowTick) + cyclesToNextSlowTick -= 114; + } else { + int slowTickOffset = t - UpdateLast64KHzTime(); + + cyclesToNextSlowTick = (28 - slowTickOffset); + + if (cyclesToNextSlowTick) + cyclesToNextSlowTick -= 28; + } + + const bool slowTickValid = (mSKCTL & 3) != 0; + + VDASSERT(!slowTickValid || (cyclesToNextSlowTick >= -114 && cyclesToNextSlowTick <= 0)); + + if (channels & 0x01) { + mpScheduler->UnsetEvent(mpTimerBorrowEvents[0]); + FlushDeferredTimerEvents(0); + + if (!mbFastTimer1 && !slowTickValid) { + // 15/64KHz clock is stopped and we're not running at 1.79MHz. In this case, we still + // fire the borrow event if we have one pending. + if (mCounterBorrow[0]) + mpTimerBorrowEvents[0] = mpScheduler->AddEvent(mCounterBorrow[0], this, kATPokeyEventTimer1Borrow); + } else { + // Computer number of ticks until the next borrow event. If we have a borrow pending, + // that's easy; otherwise, we have to look at the current counter value, slow tick offset, + // and mode. + uint32_t ticks = mCounterBorrow[0]; + + if (!ticks) { + // if we are linked and the high timer is borrowing, we're going to get reset soon and need + // to use the full period counter + if (mbLinkedTimers12 && mCounterBorrow[1]) { + ticks = mCounterBorrow[1] + mTimerPeriod[1] - 3; + } else { + ticks = mCounter[0]; + + if (!mbFastTimer1) { + if (mbUse15KHzClock) + ticks = ticks * 114; + else + ticks = ticks * 28; + + ticks += cyclesToNextSlowTick; + } + + // Borrow takes place three cycles after underflow. + ticks += 3; + } + } + + VDASSERT((sint32)ticks > 0); + + if (!mbAllowDeferredTimer[0]) { + ATLOGPOKEYTI("Timer 1 - active tick in %u cycles\n", ticks); + + mpTimerBorrowEvents[0] = mpScheduler->AddEvent(ticks, this, kATPokeyEventTimer1Borrow); + } else if (mbLinkedTimers12) { + const uint32_t loTime = t + ticks; + const uint32_t hiTime = loTime + 3 + mTimerFullPeriod[0] * (mCounter[1] - 1); + + ATLOGPOKEYTI("Timer 1 - passive linked tick in %u cycles\n", ticks); + SetupDeferredTimerEventsLinked(0, loTime, mTimerFullPeriod[0], hiTime, mTimerPeriod[1], mTimerPeriod[0] - 3); + } else { + ATLOGPOKEYTI("Timer 1 - passive tick in %u cycles\n", ticks); + SetupDeferredTimerEvents(0, t + ticks, mTimerPeriod[0]); + } + } + } + + if (channels & 0x02) { + mpScheduler->UnsetEvent(mpTimerBorrowEvents[1]); + FlushDeferredTimerEvents(1); + + if (mbLinkedTimers12) { + if (!mbFastTimer1 && !slowTickValid) { + // 15/64KHz clock is stopped and we're not running at 1.79MHz. In this case, we still + // fire the borrow event if we have one pending. + if (mCounterBorrow[1]) + mpTimerBorrowEvents[1] = mpScheduler->AddEvent(mCounterBorrow[1], this, kATPokeyEventTimer2Borrow); + } else { + // Computer number of ticks until the next borrow event. If we have a borrow pending, + // that's easy; otherwise, we have to look at the current counter value, slow tick offset, + // and mode. + uint32_t ticks = mCounterBorrow[1]; + + if (!ticks) { + ticks = mCounterBorrow[0]; + + if (!ticks) { + ticks = mCounter[0]; + + if (!mbFastTimer1) { + if (mbUse15KHzClock) + ticks = ticks * 114; + else + ticks = ticks * 28; + + ticks += cyclesToNextSlowTick; + } + + ticks += 3; + } + + if (mbFastTimer1) + ticks += (mCounter[1] - 1) << 8; + else if (mbUse15KHzClock) + ticks += (mCounter[1] - 1) * (256 * 114); + else + ticks += (mCounter[1] - 1) * (256 * 28); + + // Borrow takes place three cycles after underflow. + ticks += 3; + } + + VDASSERT((sint32)ticks > 0); + + if (!mbAllowDeferredTimer[1]) { + ATLOGPOKEYTI("Timer 2 - linked active tick in %u cycles -> %08X (%03X+%u:%03X+%u)\n", ticks, t + ticks, mCounter[1], mCounterBorrow[1], mCounter[0], mCounterBorrow[0]); + mpTimerBorrowEvents[1] = mpScheduler->AddEvent(ticks, this, kATPokeyEventTimer2Borrow); + } else { + ATLOGPOKEYTI("Timer 2 - linked passive tick in %u cycles -> %08X (%03X+%u:%03X+%u)\n", ticks, t + ticks, mCounter[1], mCounterBorrow[1], mCounter[0], mCounterBorrow[0]); + SetupDeferredTimerEvents(1, t + ticks, mTimerPeriod[1]); + } + } + } else { + if (!slowTickValid) { + // 15/64KHz clock is stopped and we're not running at 1.79MHz. In this case, we still + // fire the borrow event if we have one pending. + if (mCounterBorrow[1]) + mpTimerBorrowEvents[1] = mpScheduler->AddEvent(mCounterBorrow[1], this, kATPokeyEventTimer2Borrow); + } else { + // Computer number of ticks until the next borrow event. If we have a borrow pending, + // that easy; otherwise, we have to look at the current counter value, slow tick offset, + // and mode. + uint32_t ticks = mCounterBorrow[1]; + + if (!ticks) { + ticks = mCounter[1]; + + if (mbUse15KHzClock) + ticks = ticks * 114; + else + ticks = ticks * 28; + + ticks += cyclesToNextSlowTick; + + // Borrow takes place three cycles after underflow. + ticks += 3; + } + + VDASSERT((sint32)ticks > 0); + + if (!mbAllowDeferredTimer[1]) { + mpTimerBorrowEvents[1] = mpScheduler->AddEvent(ticks, this, kATPokeyEventTimer2Borrow); + } else { + SetupDeferredTimerEvents(1, t + ticks, mTimerPeriod[1]); + } + } + } + } + + if ((mSKCTL & 0x10) && mbSerialWaitingForStartBit) { + if (channels & 0x04) { + mpScheduler->UnsetEvent(mpTimerBorrowEvents[2]); + FlushDeferredTimerEvents(2); + + if (mCounterBorrow[2]) + mpTimerBorrowEvents[2] = mpScheduler->AddEvent(mCounterBorrow[2], this, kATPokeyEventTimer3Borrow); + } + + if (channels & 0x08) { + mpScheduler->UnsetEvent(mpTimerBorrowEvents[3]); + FlushDeferredTimerEvents(3); + + if (mCounterBorrow[3]) + mpTimerBorrowEvents[3] = mpScheduler->AddEvent(mCounterBorrow[3], this, kATPokeyEventTimer4Borrow); + } + } else { + if (channels & 0x04) { + mpScheduler->UnsetEvent(mpTimerBorrowEvents[2]); + FlushDeferredTimerEvents(2); + + if (!mbFastTimer3 && !slowTickValid) { + // 15/64KHz clock is stopped and we're not running at 1.79MHz. In this case, we still + // fire the borrow event if we have one pending. + if (mCounterBorrow[2]) + mpTimerBorrowEvents[2] = mpScheduler->AddEvent(mCounterBorrow[2], this, kATPokeyEventTimer3Borrow); + } else { + // Computer number of ticks until the next borrow event. If we have a borrow pending, + // that's easy; otherwise, we have to look at the current counter value, slow tick offset, + // and mode. + uint32_t ticks = mCounterBorrow[2]; + + if (!ticks) { + // if we are linked and the high timer is borrowing, we're going to get reset soon and need + // to use the full period counter + if (mbLinkedTimers34 && mCounterBorrow[3]) { + ticks = mCounterBorrow[3] + mTimerPeriod[3] - 3; + } else { + ticks = mCounter[2]; + + if (!mbFastTimer3) { + if (mbUse15KHzClock) + ticks = ticks * 114; + else + ticks = ticks * 28; + + ticks += cyclesToNextSlowTick; + } + + // Borrow takes place three cycles after underflow. + ticks += 3; + } + } + + VDASSERT((sint32)ticks > 0); + + // Check if we need an active event or if we can go to deferred mode. We only need an + // active event if timers are linked, timer 1 IRQ is enabled, or two-tone mode is active. + if (!mbAllowDeferredTimer[2]) + mpTimerBorrowEvents[2] = mpScheduler->AddEvent(ticks, this, kATPokeyEventTimer3Borrow); + else if (mbLinkedTimers34) { + const uint32_t loTime = t + ticks; + const uint32_t hiTime = loTime + 3 + mTimerFullPeriod[1] * (mCounter[3] - 1); + + SetupDeferredTimerEventsLinked(2, loTime, mTimerFullPeriod[1], hiTime, mTimerPeriod[3], mTimerPeriod[2]); + } else + SetupDeferredTimerEvents(2, t + ticks, mTimerPeriod[2]); + } + } + + if (channels & 0x08) { + mpScheduler->UnsetEvent(mpTimerBorrowEvents[3]); + FlushDeferredTimerEvents(3); + + if (mbLinkedTimers34) { + if (!mbFastTimer3 && !slowTickValid) { + // 15/64KHz clock is stopped and we're not running at 1.79MHz. In this case, we still + // fire the borrow event if we have one pending. + if (mCounterBorrow[3]) + mpTimerBorrowEvents[3] = mpScheduler->AddEvent(mCounterBorrow[3], this, kATPokeyEventTimer4Borrow); + } else { + // Computer number of ticks until the next borrow event. If we have a borrow pending, + // that's easy; otherwise, we have to look at the current counter value, slow tick offset, + // and mode. + uint32_t ticks = mCounterBorrow[3]; + + if (!ticks) { + ticks = mCounterBorrow[2]; + + if (!ticks) { + ticks = mCounter[2]; + + if (!mbFastTimer3) { + if (mbUse15KHzClock) + ticks = ticks * 114; + else + ticks = ticks * 28; + + ticks += cyclesToNextSlowTick; + } + + ticks += 3; + } + + if (mbFastTimer3) + ticks += (mCounter[3] - 1) << 8; + else if (mbUse15KHzClock) + ticks += (mCounter[3] - 1) * (114 * 256); + else + ticks += (mCounter[3] - 1) * (28 * 256); + + // Borrow takes place three cycles after underflow. + ticks += 3; + } + + VDASSERT(ticks > 0); + + if (!mbAllowDeferredTimer[3]) { + ATLOGPOKEYTI("Timer 4 - linked active tick in %u cycles -> %08X (%03X+%u:%03X+%u)\n", ticks, t + ticks, mCounter[3], mCounterBorrow[3], mCounter[2], mCounterBorrow[2]); + mpTimerBorrowEvents[3] = mpScheduler->AddEvent(ticks, this, kATPokeyEventTimer4Borrow); + } else { + ATLOGPOKEYTI("Timer 4 - linked passive tick in %u cycles -> %08X (%03X+%u:%03X+%u)\n", ticks, t + ticks, mCounter[3], mCounterBorrow[3], mCounter[2], mCounterBorrow[2]); + SetupDeferredTimerEvents(3, t + ticks, mTimerPeriod[3]); + } + } + } else { + if (!slowTickValid) { + // 15/64KHz clock is stopped and we're not running at 1.79MHz. In this case, we still + // fire the borrow event if we have one pending. + if (mCounterBorrow[3]) + mpTimerBorrowEvents[3] = mpScheduler->AddEvent(mCounterBorrow[3], this, kATPokeyEventTimer4Borrow); + } else { + // Computer number of ticks until the next borrow event. If we have a borrow pending, + // that easy; otherwise, we have to look at the current counter value, slow tick offset, + // and mode. + uint32_t ticks = mCounterBorrow[3]; + + if (!ticks) { + ticks = mCounter[3]; + + if (mbUse15KHzClock) + ticks = ticks * 114; + else + ticks = ticks * 28; + + ticks += cyclesToNextSlowTick; + + // Borrow takes place three cycles after underflow. + ticks += 3; + } + + VDASSERT(ticks > 0); + + // We only need the timer 4 event if the timers are linked, the timer 4 IRQ is enabled, + // asynchronous receive mode is enabled, or timer 4 is being used as the output clock. + if (!mbAllowDeferredTimer[3]) { + ATLOGPOKEYTI("Timer 4 - active tick in %u cycles (%03X+%u:%03X+%u)\n", ticks, mCounter[3], mCounterBorrow[3], mCounter[2], mCounterBorrow[2]); + mpTimerBorrowEvents[3] = mpScheduler->AddEvent(ticks, this, kATPokeyEventTimer4Borrow); + } else { + ATLOGPOKEYTI("Timer 4 - passive tick in %u cycles (%03X+%u:%03X+%u)\n", ticks, mCounter[3], mCounterBorrow[3], mCounter[2], mCounterBorrow[2]); + SetupDeferredTimerEvents(3, t + ticks, mTimerPeriod[3]); + } + } + } + } + } +} + +void ATPokeyEmulator::FlushDeferredTimerEvents(int channel) { + if (!mbDeferredTimerEvents[channel]) + return; + + // get current time + uint32_t t = ATSCHEDULER_GETTIME(mpScheduler); + + mbDeferredTimerEvents[channel] = false; + + mpRenderer->ClearChannelDeferredEvents(channel, t); +} + +void ATPokeyEmulator::SetupDeferredTimerEvents(int channel, uint32_t t0, uint32_t period) { + VDASSERT(!mbDeferredTimerEvents[channel]); + mbDeferredTimerEvents[channel] = true; + mDeferredTimerStarts[channel] = t0; + mDeferredTimerPeriods[channel] = period; + + mpRenderer->SetChannelDeferredEvents(channel, t0, period); +} + +void ATPokeyEmulator::SetupDeferredTimerEventsLinked(int channel, uint32_t t0, uint32_t period, uint32_t hit0, uint32_t hiperiod, uint32_t hilooffset) { + VDASSERT(!mbDeferredTimerEvents[channel]); + mbDeferredTimerEvents[channel] = true; + mDeferredTimerStarts[channel] = t0; + mDeferredTimerPeriods[channel] = period; + + mpRenderer->SetChannelDeferredEventsLinked(channel, t0, period, hit0, hiperiod, hilooffset); +} + +uint8_t ATPokeyEmulator::DebugReadByte(uint8_t reg) const { + reg &= mAddressMask; + + switch(reg) { + case 0x00: // $D200 POT0 + case 0x01: // $D201 POT1 + case 0x02: // $D202 POT2 + case 0x03: // $D203 POT3 + case 0x04: // $D204 POT4 + case 0x05: // $D205 POT5 + case 0x06: // $D206 POT6 + case 0x07: // $D207 POT7 + return const_cast(this)->ReadByte(reg); + case 0x08: // $D208 ALLPOT + const_cast(this)->UpdatePots(0); + return mALLPOT; + case 0x09: // $D209 KBCODE + return mKBCODE; + case 0x0A: // $D20A RANDOM + return const_cast(this)->ReadByte(reg); + case 0x0D: // $D20D SERIN + return mSERIN; + case 0x0E: + return mIRQST; + case 0x0F: + return const_cast(this)->ReadByte(reg); + } + + if (reg & 0x10) { + if (mpSlave) + return mpSlave->DebugReadByte(reg & 0x0f); + else + return DebugReadByte(reg & 0x0f); + } + + return 0xFF; +} + +uint8_t ATPokeyEmulator::ReadByte(uint8_t reg) { + reg &= mAddressMask; + + switch(reg) { + case 0x00: // $D200 POT0 + case 0x01: // $D201 POT1 + case 0x02: // $D202 POT2 + case 0x03: // $D203 POT3 + case 0x04: // $D204 POT4 + case 0x05: // $D205 POT5 + case 0x06: // $D206 POT6 + case 0x07: // $D207 POT7 + UpdatePots(0); + + if (mALLPOT & (1 << reg)) { + uint8_t count = mPotMasterCounter; + + // If we are in fast pot mode, we need to adjust the value that + // comes back. This is because the read occurs at a point where + // the count is unstable. The permutation below was determined + // on real hardware by reading POT1 at varying cycle offsets. + // Note that while all intermediate values read are even, the + // final count can be odd, so we must not apply this to latched + // values. Latched values can also vary but the mechanism is + // TBD. + if (mSKCTL & 4) { + const uint8_t kDeltaVec[16] = { 0, 1, 0, 3, 0, 1, 0, 7, 0, 1, 0, 3, 0, 1, 0, 15 }; + count ^= kDeltaVec[count & 15]; + } + + return count; + } + + return mPotLatches[reg]; + + case 0x08: // $D208 ALLPOT + UpdatePots(0); + return mALLPOT; + case 0x09: // $D209 KBCODE + return mKBCODE; + case 0x0A: // $D20A RANDOM + { + const bool initMode = !(mSKCTL & 3); + uint8_t forceMask = 0; + + if (initMode) { + uint64 offset = mpScheduler->GetTick64() - mPolyShutOffTime; + + if (offset > 10) + return 0xFF; + + if (offset) + forceMask = (uint8_t)(0xFFE00 >> (int)(uint32_t)offset); + } + + UpdatePolyTime(); + + const uint8_t *src = mAUDCTL & 0x80 ? &mpTables->mPolyBuffer[mPoly9Counter] : &mpTables->mPolyBuffer[mPoly17Counter]; + uint8_t v = 0; + + if (mAUDCTL & 0x80) { + for(int i=7; i>=0; --i) + v = (v + v) + ((src[i] & 2) >> 1); + } else { + for(int i=7; i>=0; --i) + v = (v + v) + (src[i] & 1); + } + + return ~v | forceMask; + } + case 0x0D: // $D20D SERIN + { + uint8_t c = mSERIN; + + if (mbTraceSIO) + (*mpConsoleOut)("POKEY: Reading SERIN value %02x (shiftreg: %02x)", c, mSerialInputShiftRegister); + + if (mbSerInBurstPendingData) { + mbSerInBurstPendingData = false; + + if (!mbSerInBurstPendingIRQ1 && !mbSerInBurstPendingIRQ2) { + for(IATPokeySIODevice *dev : mDevices) { + if (mbSerInBurstPendingData) + break; + + dev->PokeySerInReady(); + } + } + } + + return c; + } + break; + + case 0x0E: // $D20E IRQST + return mIRQST; + + case 0x0F: + { + uint8_t c = mSKSTAT; + + if (mpScheduler->GetTick64() >= mSerialDataInFlipTime) + c ^= 0x10; + + if (mbSerialSimulateInputPort) { + uint32_t dt = ATSCHEDULER_GETTIME(mpScheduler) - mSerialSimulateInputBaseTime; + uint32_t bitidx = dt / mSerialSimulateInputCyclesPerBit; + + if (bitidx >= 10) + mbSerialSimulateInputPort = false; + else { + c &= 0xef; + + if (mSerialSimulateInputData & (1 << bitidx)) + c |= 0x10; + } + } + + return c; + } + break; + + default: +// __debugbreak(); + break; + } + + if (reg & 0x10) + return mpSlave->ReadByte(reg & 0x0f); + + return 0xFF; +} + +void ATPokeyEmulator::WriteByte(uint8_t reg, uint8_t value) { + reg &= mAddressMask; + + mState.mReg[reg] = value; + + switch(reg) { + case 0x00: // $D200 AUDF1 + if (mAUDF[0] != value) { + mAUDF[0] = value; + mAUDFP1[0] = (int)value + 1; + + RecomputeTimerPeriod<0>(); + + if (mbLinkedTimers12) { + RecomputeTimerPeriod<1>(); + + // Both counters must be updated before we run SetupTimers(). + UpdateTimerCounter<0>(); + UpdateTimerCounter<1>(); + SetupTimers(0x03); + } else { + UpdateTimerCounter<0>(); + SetupTimers(0x01); + } + } + break; + case 0x01: // $D201 AUDC1 + if (mAUDC[0] != value) { + mAUDC[0] = value; + mpRenderer->SetAUDCx(0, value); + } + break; + case 0x02: // $D202 AUDF2 + if (mAUDF[1] != value) { + mAUDF[1] = value; + mAUDFP1[1] = (int)value + 1; + + RecomputeTimerPeriod<1>(); + + if (mbLinkedTimers12) { + // Both counters must be updated before we run SetupTimers(). + UpdateTimerCounter<0>(); + UpdateTimerCounter<1>(); + SetupTimers(0x03); + } else { + UpdateTimerCounter<1>(); + SetupTimers(0x02); + } + } + break; + case 0x03: // $D203 AUDC2 + if (mAUDC[1] != value) { + mAUDC[1] = value; + mpRenderer->SetAUDCx(1, value); + } + break; + case 0x04: // $D204 AUDF3 + if (mAUDF[2] != value) { + mAUDF[2] = value; + mAUDFP1[2] = (int)value + 1; + + RecomputeTimerPeriod<2>(); + + if (mbLinkedTimers34) { + RecomputeTimerPeriod<3>(); + + // Both counters must be updated before we run SetupTimers(). + UpdateTimerCounter<2>(); + UpdateTimerCounter<3>(); + SetupTimers(0x0C); + } else { + UpdateTimerCounter<2>(); + SetupTimers(0x04); + } + } + mbSerialRateChanged = true; + break; + case 0x05: // $D205 AUDC3 + if (mAUDC[2] != value) { + mAUDC[2] = value; + mpRenderer->SetAUDCx(2, value); + } + break; + case 0x06: // $D206 AUDF4 + if (mAUDF[3] != value) { + mAUDF[3] = value; + mAUDFP1[3] = (int)value + 1; + + RecomputeTimerPeriod<3>(); + + if (mbLinkedTimers34) { + // Both counters must be updated before we run SetupTimers(). + UpdateTimerCounter<2>(); + UpdateTimerCounter<3>(); + SetupTimers(0x0C); + } else { + UpdateTimerCounter<3>(); + SetupTimers(0x08); + } + } + mbSerialRateChanged = true; + break; + case 0x07: // $D207 AUDC4 + if (mAUDC[3] != value) { + mAUDC[3] = value; + mpRenderer->SetAUDCx(3, value); + } + break; + case 0x08: // $D208 AUDCTL + if (mAUDCTL != value) { + uint8_t delta = mAUDCTL ^ value; + if (delta & 0x29) + mbSerialRateChanged = true; + + UpdateTimerCounter<0>(); + UpdateTimerCounter<1>(); + UpdateTimerCounter<2>(); + UpdateTimerCounter<3>(); + + FlushDeferredTimerEvents(0); + FlushDeferredTimerEvents(1); + FlushDeferredTimerEvents(2); + FlushDeferredTimerEvents(3); + + mAUDCTL = value; + mbFastTimer1 = (mAUDCTL & 0x40) != 0; + mbFastTimer3 = (mAUDCTL & 0x20) != 0; + mbLinkedTimers12 = (mAUDCTL & 0x10) != 0; + mbLinkedTimers34 = (mAUDCTL & 0x08) != 0; + mbUse15KHzClock = (mAUDCTL & 0x01) != 0; + + mpRenderer->SetAUDCTL(value); + + if (delta & 0x18) + RecomputeAllowedDeferredTimers(); + + if (delta & 0x51) { + RecomputeTimerPeriod<0>(); + RecomputeTimerPeriod<1>(); + } + + if (delta & 0x29) { + RecomputeTimerPeriod<2>(); + RecomputeTimerPeriod<3>(); + } + + SetupTimers(0x0f); + } + break; + case 0x09: // $D209 STIMER + mpScheduler->SetEvent(4, this, kATPokeyEventResetTimers, mpResetTimersEvent); + break; + case 0x0A: // $D20A SKRES + mSKSTAT |= 0xe0; + break; + case 0x0B: // $D20B POTGO + StartPotScan(); + break; + case 0x0D: // $D20D SEROUT + if (mbTraceSIO) + (*mpConsoleOut)("POKEY: Sending serial byte %02x", value); + + // The only thing that writing to SEROUT does is load the register and set a latch + // indicating that a byte is ready. The actual load into the output shift register + // cannot occur until another serial clock pulse arrives. + mSEROUT = value; + if (mbSerOutValid && mbTraceSIO) + (*mpConsoleOut)("POKEY: Serial output overrun detected."); + + if (!mSerialOutputCounter) { + mSerialOutputCounter = 1; + + // check if we are doing external output clock + if (!(mSKCTL & 0x60) && mSerialExtPeriod) { + // yup -- start external clock + uint32_t delay = (ATSCHEDULER_GETTIME(mpScheduler) + 2) - mSerialExtBaseTime; + + if (delay >= 0x80000000) { + delay = (0 - delay) % mSerialExtPeriod; + + delay = mSerialExtPeriod - delay; + } else { + delay %= mSerialExtPeriod; + + if (!delay) + delay = mSerialExtPeriod; + } + + mpScheduler->SetEvent(delay, this, kATPokeyEventSerialOutput, mpEventSerialOutput); + } + } + + mbSerOutValid = true; + break; + case 0x0E: + if (mIRQEN != value) { + const uint8_t delta = (mIRQEN ^ value); + + mIRQEN = value; + + mIRQST |= ~value & 0xF7; + + if (mbBreakKeyState && (mIRQEN & mIRQST & 0x80)) { + mIRQST |= 0x80; + } + + if (!(mIRQEN & ~mIRQST)) + NegateIrq(true); + else + AssertIrq(true); + + // Check if any of the IRQ bits are being turned on and we are currently running that timer + // in deferred mode. If so, we need to yank it out of deferred mode. We don't do the + // opposite here; we wait until the existing timer expires to reinit the timer into + // deferred mode so as to not trigger on momentary clears. + if (delta & 0x07) { + RecomputeAllowedDeferredTimers(); + + if (delta & mIRQEN & 0x07) { + uint8_t timersToChange = 0; + + // timer 1 + if ((delta & 0x01) && mbDeferredTimerEvents[0]) + timersToChange |= 0x01; + + // timer 2 + if ((delta & 0x02) && mbDeferredTimerEvents[1]) + timersToChange |= 0x02; + + // timer 4 + if ((delta & 0x04) && mbDeferredTimerEvents[3]) + timersToChange |= 0x08; + + if (timersToChange) { + UpdateTimerCounter<0>(); + UpdateTimerCounter<1>(); + UpdateTimerCounter<2>(); + UpdateTimerCounter<3>(); + + SetupTimers(timersToChange); + } + } + } + + // check if the receive interrupt was toggled + if (delta & 0x20) { + if (value & 0x20) { + if (mbSerInBurstPendingIRQ2) { + mbSerInBurstPendingIRQ2 = false; + + if (!mbSerInBurstPendingData) { + for(IATPokeySIODevice *dev : mDevices) { + if (mbSerInBurstPendingData) + break; + + dev->PokeySerInReady(); + } + } + } + } else { + if (mbSerInBurstPendingIRQ1) { + mbSerInBurstPendingIRQ1 = false; + mbSerInBurstPendingIRQ2 = true; + } + } + } + } + break; + case 0x0F: + if (value != mSKCTL) { + UpdateTimerCounter<0>(); + UpdateTimerCounter<1>(); + UpdateTimerCounter<2>(); + UpdateTimerCounter<3>(); + + if (!(mSKCTL & 0x10) && (value & 0x10) && mbSerialWaitingForStartBit) { + // Restart timers 3 and 4 immediately. + mCounter[2] = mAUDFP1[2]; + mCounter[3] = mAUDFP1[3]; + } + + const uint8_t delta = value ^ mSKCTL; + + // update pots if fast pot scan mode has changed + if (delta & 0x04) { + // A time skew of two cycles is necessary to handle this case properly: + // + // STA POTGO + // STY SKCTL ;enable fast pot scan mode + // + // The counter is delayed by two cycles, but so is the change to fast mode. + UpdatePots(2); + } + + // force serial rate re-evaluation if any clocking mode bits have changed + if (delta & 0x70) + mbSerialRateChanged = true; + + bool prvInit = (mSKCTL & 3) == 0; + bool newInit = (value & 3) == 0; + + if (newInit != prvInit) { + if (newInit) { + mpScheduler->UnsetEvent(mpKeyboardScanEvent); + mpScheduler->UnsetEvent(mpKeyboardIRQEvent); + + // Don't reset poly counters at this point -- we need to keep them going in + // order to emulate the shift-out. + mPolyShutOffTime = mpScheduler->GetTick64(); + } else { + VDASSERT(!mpKeyboardScanEvent); + + // The 64KHz polynomial counter is a 5-bit LFSR that is reset to all 1s + // on init and XORs bits 4 and 2, if seen as shifting left. In order to + // achieve a 28 cycle period, a one is force fed when the register + // equals 00010. A side effect of this mechanism is that resetting the + // register actually starts it 19 cycles in. There is, however, an + // additional two clock delay from the shift register comparator to + // the timers. + + // The 15KHz polynomial counter is a 7-bit LFSR that is reset to all 0s + // on init and XNORs bits 7 and 6, if seen as shifting left. In order to + // achieve a 114 cycle period, a one is force fed when the register + // equals 1001001. + + mLast15KHzTime = ATSCHEDULER_GETTIME(mpScheduler) + 81 - 114; + mLast64KHzTime = ATSCHEDULER_GETTIME(mpScheduler) + 22 - 28; + + mPoly9Counter = 0; + mPoly17Counter = 0; + mLastPolyTime = ATSCHEDULER_GETTIME(mpScheduler) + 1; + } + + mpRenderer->SetInitMode(newInit); + + mSerialInputShiftRegister = 0; + mSerialOutputShiftRegister = 0; + mSerialOutputCounter = 0; + mSerialInputCounter = 0; + mbSerOutValid = false; + mbSerShiftValid = false; + mbSerialOutputState = false; + + // reset burst state + mbSerInBurstPendingData = false; + mbSerInBurstPendingIRQ1 = false; + mbSerInBurstPendingIRQ2 = false; + mSerOutBurstDeadline = 0; + + // reset serial input active bit + mSKSTAT |= 0x02; + + // assert serial output complete + mIRQST &= ~0x08; + + if (!(mIRQEN & ~mIRQST)) + NegateIrq(true); + else + AssertIrq(true); + + if (mpCassette) + mpCassette->PokeyResetSerialInput(); + + ++mSerialInputResetCounter; + } + + mSKCTL = value; + + // check for keyboard change + if (delta & 0x03) { + // check for keyboard being switched in and out of normal mode, for + // immediate scan (raw mode) + if (!mb5200Mode && !mbKeyboardScanEnabled) { + if (!(value & 1)) { + // Debounce is being turned off. We can't register any keys + // and will stop reporting a current one. + mSKSTAT |= 0x04; + + mKeyScanState = 0; + mKeyScanCode = 0; + } else if ((value & 3) == 3) { + // Returning to normal scan. Check if we have a key held and + // register it now. + if (mbShiftKeyState) + mSKSTAT &= ~0x08; + else + mSKSTAT |= 0x08; + + for(int i=0; i<8; ++i) { + if (mKeyMatrix[i] & 0xff) { + mSKSTAT &= ~0x04; + mKBCODE = (uint8_t)( + i*8 + + VDFindLowestSetBitFast(mKeyMatrix[i] & 0xFF) + + (mbShiftKeyState ? 0x40 : 0x00) + + (mbControlKeyState ? 0x80 : 0x00) + ); + QueueKeyboardIRQ(); + } + } + } + } + + // check for change in keyboard scan + if (delta & 0x02) { + if (!(value & 0x02)) { + // Keyboard scan is being disabled -- this resets the keyboard state machine + // and so bit 2 deasserts. However, this doesn't affect the shift key state + // (bit 3). + mSKSTAT |= 0x04; + + mKeyScanState = 0; + mKeyScanCode = 0; + } + + UpdateKeyboardScanEvent(); + } + } + + RecomputeAllowedDeferredTimers(); + SetupTimers(0x0f); + + // check if serial timer is stopped and terminate output byte if needed + if (!IsSerialOutputClockRunning()) + FlushSerialOutput(); + } + break; + + default: + if (reg & 0x10) { + mpSlave->WriteByte(reg & 0x0f, value); + } + break; + } +} + +void ATPokeyEmulator::DumpStatus(ATConsoleOutput& out) { + if (mpSlave) { + out.WriteLine("Primary POKEY:"); + DumpStatus(out, false); + out.WriteLine(""); + out.WriteLine("Secondary POKEY:"); + mpSlave->DumpStatus(out, true); + } else { + DumpStatus(out, false); + } +} + +void ATPokeyEmulator::SaveState(IATObjectState **pp) { + UpdateTimerCounter<0>(); + UpdateTimerCounter<1>(); + UpdateTimerCounter<2>(); + UpdateTimerCounter<3>(); + + vdrefptr obj(new ATSaveStatePokey); + vdrefptr obj2(new ATSaveStatePokeyInternal); + + for(int i=0; i<4; ++i) { + obj->mAUDF[i] = mAUDF[i]; + obj->mAUDC[i] = mAUDC[i]; + } + + obj->mAUDCTL = mAUDCTL; + obj->mIRQEN = mIRQEN; + obj->mIRQST = mIRQST; + obj->mSKCTL = mSKCTL; + obj->mSKSTAT = mSKSTAT; + obj->mALLPOT = mALLPOT; + obj->mKBCODE = mKBCODE; + + for(int i=0; i<4; ++i) + obj2->mTimerCounters[i] = mCounter[i]; + + for(int i=0; i<4; ++i) + obj2->mTimerBorrowCounters[i] = mCounterBorrow[i]; + + obj2->mTwoToneResetCounters[0] = mpEventResetTwoTones1 ? mpScheduler->GetTicksToEvent(mpEventResetTwoTones1) : 0; + obj2->mTwoToneResetCounters[1] = mpEventResetTwoTones2 ? mpScheduler->GetTicksToEvent(mpEventResetTwoTones2) : 0; + + const uint32_t t = ATSCHEDULER_GETTIME(mpScheduler); + obj2->mClock15Offset = t - UpdateLast15KHzTime(t); + VDASSERT(obj2->mClock15Offset < 114); + + obj2->mClock64Offset = t - UpdateLast64KHzTime(t); + VDASSERT(obj2->mClock64Offset < 28); + + int polyDelta = t - mLastPolyTime; + obj2->mPoly9Offset = (mPoly9Counter + polyDelta) % 511; + obj2->mPoly17Offset = (mPoly17Counter + polyDelta) % 131071; + obj2->mPolyShutOffTime = mpScheduler->GetTick64() - mPolyShutOffTime; + obj2->mSerInCounter = mSerialInputCounter; + obj2->mSerInShiftRegister = mSerialInputShiftRegister; + obj2->mbSerInDeferredLoad = mbSerInDeferredLoad; + obj2->mbSerInWaitingForStartBit = mbSerialWaitingForStartBit; + + obj2->mSerOutEventTime = mpEventSerialOutput ? mpScheduler->GetTicksToEvent(mpEventSerialOutput) : 0; + obj2->mSerOutShiftRegister = mSerialOutputShiftRegister; + obj2->mSerOutCounter = mSerialOutputCounter; + obj2->mbSerOutValid = mbSerOutValid; + obj2->mbSerOutShiftValid = mbSerShiftValid; + + obj2->mTraceByteIndex = mTraceByteIndex; + + obj2->mRendererState = mpRenderer->SaveState(); + + obj->mpInternalState = std::move(obj2); + + if (mpSlave) + mpSlave->SaveState(~obj->mpStereoPair); + + *pp = obj.release(); +} + +void ATPokeyEmulator::LoadState(const IATObjectState& state) { + const ATSaveStatePokey& pstate = atser_cast(state); + + for(int i=0; i<4; ++i) { + mAUDF[i] = pstate.mAUDF[i]; + mAUDC[i] = pstate.mAUDC[i]; + } + + mAUDCTL = pstate.mAUDCTL; + mIRQEN = pstate.mIRQEN; + mIRQST = pstate.mIRQST; + mSKCTL = pstate.mSKCTL; + mSKSTAT = pstate.mSKSTAT; + mALLPOT = pstate.mALLPOT; + mKBCODE = pstate.mKBCODE; + + const uint64 t64 = mpScheduler->GetTick64(); + const uint32_t t = (uint32_t)t64; + mLastPolyTime = t; + + for(auto& c : mCounter) + c = 1; + + for(auto& c : mCounterBorrow) + c = 0; + + mLast15KHzTime = t - 1; + mLast64KHzTime = t - 1; + mPoly9Counter = 0; + mPoly17Counter = 0; + mPolyShutOffTime = t64 - 1000; + mbSerInDeferredLoad = false; + mbSerialWaitingForStartBit = true; + mSerialInputCounter = 0; + mSerialOutputCounter = 0; + mSerialInputShiftRegister = 0; + mSerialOutputShiftRegister = 0; + + mpScheduler->UnsetEvent(mpEventResetTwoTones1); + mpScheduler->UnsetEvent(mpEventResetTwoTones2); + mpScheduler->UnsetEvent(mpEventSerialInput); + mpScheduler->UnsetEvent(mpEventSerialOutput); + + mTraceByteIndex = 0; + + if (pstate.mpInternalState) { + const ATSaveStatePokeyInternal& pistate = *pstate.mpInternalState; + + for(int i=0; i<4; ++i) + mCounter[i] = pistate.mTimerCounters[i]; + + for(int i=0; i<4; ++i) + mCounterBorrow[i] = pistate.mTimerBorrowCounters[i]; + + mLast15KHzTime = t - pistate.mClock15Offset; + mLast64KHzTime = t - pistate.mClock64Offset; + + mPoly9Counter = pistate.mPoly9Offset; + mPoly17Counter = pistate.mPoly17Offset; + mPolyShutOffTime = t64 - pistate.mPolyShutOffTime; + + mSerialInputCounter = pistate.mSerInCounter; + mSerialInputShiftRegister = pistate.mSerInShiftRegister; + mbSerInDeferredLoad = pistate.mbSerInDeferredLoad; + mbSerialWaitingForStartBit = pistate.mbSerInWaitingForStartBit; + + if (pistate.mSerOutEventTime) + mpScheduler->SetEvent(pistate.mSerOutEventTime, this, kATPokeyEventSerialOutput, mpEventSerialOutput); + + mSerialOutputShiftRegister = pistate.mSerOutShiftRegister; + mSerialOutputCounter = pistate.mSerOutCounter; + mbSerOutValid = pistate.mbSerOutValid; + mbSerShiftValid = pistate.mbSerOutShiftValid; + + if (pistate.mTwoToneResetCounters[0]) + mpScheduler->SetEvent(pistate.mTwoToneResetCounters[0], this, kATPokeyEventResetTwoTones1, mpEventResetTwoTones1); + + if (pistate.mTwoToneResetCounters[1]) + mpScheduler->SetEvent(pistate.mTwoToneResetCounters[1], this, kATPokeyEventResetTwoTones2, mpEventResetTwoTones2); + + mTraceByteIndex = pistate.mTraceByteIndex; + + mpRenderer->LoadState(pistate.mRendererState); + } else { + mpRenderer->LoadState({}); + } + + if (mpSlave) { + if (pstate.mpStereoPair) + mpSlave->LoadState(*pstate.mpStereoPair); + else + mpSlave->LoadState(ATSaveStatePokey{}); + } + + PostLoadState(); +} + +void ATPokeyEmulator::PostLoadState() { + const uint32_t t = ATSCHEDULER_GETTIME(mpScheduler); + + mbFastTimer1 = (mAUDCTL & 0x40) != 0; + mbFastTimer3 = (mAUDCTL & 0x20) != 0; + mbLinkedTimers12 = (mAUDCTL & 0x10) != 0; + mbLinkedTimers34 = (mAUDCTL & 0x08) != 0; + mbUse15KHzClock = (mAUDCTL & 0x01) != 0; + + mpRenderer->SetInitMode((mSKCTL & 3) == 0); + mpRenderer->SetAUDCTL(mAUDCTL); + for(int i=0; i<4; ++i) { + mAUDFP1[i] = mAUDF[i] + 1; + mpRenderer->SetAUDCx(i, mAUDC[i]); + } + + mLastPolyTime = t; + mbIrqAsserted = mIRQEN & ~mIRQST; + + if (mbIrqAsserted) + AssertIrq(false); + else + NegateIrq(false); + + + uint32_t keyboardTickOffset = 114 - (t - UpdateLast15KHzTime()); + + if (mbKeyboardIRQPending) + mpScheduler->SetEvent(keyboardTickOffset, this, kATPokeyEventKeyboardIRQ, mpKeyboardIRQEvent); + else + mpScheduler->UnsetEvent(mpKeyboardIRQEvent); + + UpdateKeyboardScanEvent(); + + RecomputeTimerPeriod<0>(); + RecomputeTimerPeriod<1>(); + RecomputeTimerPeriod<2>(); + RecomputeTimerPeriod<3>(); + RecomputeAllowedDeferredTimers(); + + SetupTimers(0x0f); + + if (mpSlave) + mpSlave->PostLoadState(); +} + +void ATPokeyEmulator::GetRegisterState(ATPokeyRegisterState& state) const { + state = mState; +} + +void ATPokeyEmulator::DumpStatus(ATConsoleOutput& out, bool isSlave) { + uint32_t t = ATSCHEDULER_GETTIME(mpScheduler); + + VDStringA s; + for(int i=0; i<4; ++i) { + s.sprintf("AUDF%u: %02x AUDC%u: %02x Output: %d", i+1, mAUDF[i], i+1, mAUDC[i], mpRenderer->GetChannelOutput(i)); + + if (mbDeferredTimerEvents[i]) { + uint32_t delay; + + if (mDeferredTimerStarts[i] - t - 1 < 0x7FFFFFFF) // wrap(start > t) => wrap(start - t) > 0 => 0 < (start - t) < 80000000 + delay = mDeferredTimerStarts[i] - t; + else + delay = mDeferredTimerPeriods[i] - (t - mDeferredTimerStarts[i]) % mDeferredTimerPeriods[i]; + + s.append_sprintf(" (%u cycles until fire) (passive: %d cycles)", delay, mDeferredTimerPeriods[i]); + } else if (mpTimerBorrowEvents[i]) + s.append_sprintf(" (%u cycles until fire) (active)", (int)mpScheduler->GetTicksToEvent(mpTimerBorrowEvents[i])); + + out.WriteLine(s.c_str()); + } + + out("AUDCTL: %02x%s%s%s%s%s%s%s%s" + , mAUDCTL + , mAUDCTL & 0x80 ? ", 9-bit poly" : ", 17-bit poly" + , mAUDCTL & 0x40 ? ", 1.79 ch1" : "" + , mAUDCTL & 0x20 ? ", 1.79 ch3" : "" + , mAUDCTL & 0x10 ? ", ch1+ch2" : "" + , mAUDCTL & 0x08 ? ", ch3+ch4" : "" + , mAUDCTL & 0x04 ? ", highpass 1+3" : "" + , mAUDCTL & 0x02 ? ", highpass 2+4" : "" + , mAUDCTL & 0x01 ? ", 15KHz" : ", 64KHz"); + + static const char *kRecvModes[4]={ + "recv ext", + "recv ch3+4 async", + "recv ch4", + "recv ch3+4 async", + }; + + static const char *kSendModes[8]={ + "send ext", + "send ext", + "send ch4", + "send ch4 async (x)", + "send ch4", + "send ch4 async (x)", + "send ch2", + "send ch2", + }; + + static const char *kInitModes[4]={ + "init mode", + "keyboard scan disabled", + "keyboard scan enabled w/o debounce", + "keyboard scan enabled", + }; + + out("SKCTL: %02x | %s | %s | %s | %s%s%s" + , mSKCTL + , kRecvModes[(mSKCTL >> 4) & 3] + , kSendModes[(mSKCTL >> 4) & 7] + , kInitModes[mSKCTL & 3] + , mSKCTL & 0x80 ? " | force break" : "" + , mSKCTL & 0x08 ? " | two-tone mode" : "" + , mSKCTL & 0x04 ? " | fast pot scan" : "" + ); + + if (mSerialInputCounter) + out("SERIN: %02X (shifting in %02X)", mSERIN, mSerialInputShiftRegister); + else + out("SERIN: %02X", mSERIN); + + out("SEROUT: %02x (%s)", mSEROUT, mbSerOutValid ? "pending" : "done"); + out(" shift register %02x (%d: %s)", mSerialOutputShiftRegister, mSerialOutputCounter, mSerialOutputCounter ? "pending" : "done"); + out("IRQEN: %02X%s%s%s%s%s%s%s%s" + , mIRQEN + , mIRQEN & 0x80 ? ", break key" : "" + , mIRQEN & 0x40 ? ", keyboard" : "" + , mIRQEN & 0x20 ? ", serin" : "" + , mIRQEN & 0x10 ? ", serout" : "" + , mIRQEN & 0x08 ? ", sertrans" : "" + , mIRQEN & 0x04 ? ", timer4" : "" + , mIRQEN & 0x02 ? ", timer2" : "" + , mIRQEN & 0x01 ? ", timer1" : "" + ); + out("IRQST: %02X%s%s%s%s%s%s%s%s" + , mIRQST + , mIRQST & 0x80 ? "" : ", break key" + , mIRQST & 0x40 ? "" : ", keyboard" + , mIRQST & 0x20 ? "" : ", serin" + , mIRQST & 0x10 ? "" : ", serout" + , mIRQST & 0x08 ? "" : ", sertrans" + , mIRQST & 0x04 ? "" : ", timer4" + , mIRQST & 0x02 ? "" : ", timer2" + , mIRQST & 0x01 ? "" : ", timer1" + ); + + s.sprintf("KBCODE: %02X", mKBCODE); + + if (mpKeyboardScanEvent) + s.append_sprintf(" (keyboard scan counter: %02X)", mKeyScanCode); + + out.WriteLine(s.c_str()); + + out("ALLPOT: %02X", mALLPOT); + + out.WriteLine(""); + out("Command line: %s", mbCommandLineState ? "asserted" : "negated"); +} + +void ATPokeyEmulator::FlushAudio(bool pushAudio, uint64 timestamp) { + ATFastMathScope fastMathScope; + + IATSyncAudioEdgePlayer *edgePlayer = mpAudioOut ? &mpAudioOut->AsMixer().GetEdgePlayer() : nullptr; + const auto endBlockResult = mpRenderer->EndBlock(edgePlayer); + + if (mpSlave) { + const auto slaveEndBlockResult = mpSlave->mpRenderer->EndBlock(edgePlayer); + + VDASSERT(endBlockResult.mSamples == slaveEndBlockResult.mSamples); + VDASSERT(endBlockResult.mTimestamp == slaveEndBlockResult.mTimestamp); + } + + if (mpAudioOut) { + // convert 32-bit timestamp to 64-bit + const uint64 now64 = mpScheduler->GetTick64(); + uint64 t64 = (now64 & ~UINT64_C(0xFFFFFFFF)) + endBlockResult.mTimestamp; + + if (t64 > now64 + UINT64_C(0x80000000)) + t64 -= UINT64_C(1) << 32; + + mpAudioOut->WriteAudio( + mpRenderer->GetOutputBuffer(), + mpSlave && mbStereoSoftEnable ? mpSlave->mpRenderer->GetOutputBuffer() : NULL, + endBlockResult.mSamples, + pushAudio, + t64); + } + + mpRenderer->StartBlock(); + + if (mpSlave) + mpSlave->mpRenderer->StartBlock(); +} + +void ATPokeyEmulator::SetTraceOutput(IATPokeyTraceOutput *output) { + mpTraceOutput = output; + mbTraceIrqPending = false; +} + +uint32_t ATPokeyEmulator::GetCyclesToTimerFire(uint32_t ch) const { + VDASSERT(ch < 4); + + if (!mbDeferredTimerEvents[ch]) + return mpTimerBorrowEvents[ch] ? mpScheduler->GetTicksToEvent(mpTimerBorrowEvents[ch]) : 0; + + uint32_t t = ATSCHEDULER_GETTIME(mpScheduler); + if ((uint32_t)(mDeferredTimerStarts[ch] - t - 1) < 0x7FFFFFFF) // wrap(start > t) => wrap(start - t) > 0 => 0 < (start - t) < 80000000 + return mDeferredTimerStarts[ch] - t; + else + return mDeferredTimerPeriods[ch] - (t - mDeferredTimerStarts[ch]) % mDeferredTimerPeriods[ch]; +} + +void ATPokeyEmulator::UpdateMixTable() { + static constexpr float kHalfCycleAccumulationFactor = 1.0f; + + if (mbNonlinearMixingEnabled) { + // Approximate measured voltage drops from individual POKEY volume bits, normalized so that + // maximum volume on all four channels adds to 1. These are supposed to be uneven, as they + // are on the actual hardware. + static constexpr float kATPokeyVolumeBitLevels[4] = { + 0.12f / 8.24f, + 0.26f / 8.24f, + 0.56f / 8.24f, + 1.12f / 8.24f, + }; + + // This is an approximate fit to raw sample data obtained from a scope on POKEY's output + // pin, tapped off before the amplifiers to avoid AC coupling effects. It's a simple + // exponential curve resulting in gradual saturation, normalized to hit almost [-1, 1] + // with the saturation clamp below and the x56 half-cycle oversampling. + static constexpr float kLinearThreshold = 0.14f; + static constexpr float kLinearSlope = 2.1f; + static constexpr float kCurveFactor = 2.85f; + + float *dst = mpTables->mMixTable; + float v23 = 0; + + for(int i=0; i<13; ++i) { + float v123 = v23; + for(int j=0; j<5; ++j) { + float x = v123; + for(int k=0; k<5; ++k) { + *dst++ = (xmReferenceDecayPerSample = 1.0f - expf(-(1.0f / 63920.0f) / kTau); + + // The amplifier in the analog audio circuit clamps out when the difference on its inputs + // is too great, until the reference voltage slews enough to reduce the delta. Relative to + // POKEY's max level, the clamp range is [-0.65, 0.5]. + + mpTables->mReferenceClampLo = -0.65f; + mpTables->mReferenceClampHi = 0.5f; + } else { + // When non-linear mixing is disabled, we ignore both the uneven volume bit steps and the + // saturation curve and just map linearly. + + float *dst = mpTables->mMixTable; + float v23 = 0; + + for(int i=0; i<13; ++i) { + float v123 = v23; + for(int j=0; j<5; ++j) { + float v0123 = v123; + for(int k=0; k<5; ++k) { + *dst++ = v0123; + v0123 -= 1.0f / 60.0f * kHalfCycleAccumulationFactor; + } + + v123 -= 2.0f / 60.0f * kHalfCycleAccumulationFactor; + } + + v23 -= 4.0f / 60.0f * kHalfCycleAccumulationFactor; + } + + mpTables->mReferenceDecayPerSample = 0.0f; + mpTables->mReferenceClampLo = -1.0f; + mpTables->mReferenceClampHi = 1.0f; + } + + // The XL/XE speaker is about as loud peak-to-peak as a channel at volume 6. + // However, it is added in later in the output circuitry and has different + // audio characteristics, so we must treat it separately. We do need to + // translate volume level 6 to a mix table index, but as it turns out, + // volume level 6 maps to mix index 6 (bit0 x 1 + bit1 x 1). + mpTables->mSpeakerLevel = mpTables->mMixTable[6]; +} + +void ATPokeyEmulator::UpdateKeyMatrix(int index, uint16_t mask, uint16_t state) { + uint16_t delta = (mKeyMatrix[index] ^ state) & mask; + + if (delta) { + mKeyMatrix[index] ^= delta; + + UpdateEffectiveKeyMatrix(); + } +} + +// Three or more keys pressed at the same time can produce phantom keys +// by connecting column lines to the active row line by other row lines: +// +// 1 . A<---B +// . | ^ +// . | | +// 2 --.----.--->C active row +// . | | +// . | | +// 3 . . . +// . | | +// . v v +// 1 2 3 +// sensed columns +// +// Here the connections formed by depressed keys A, B, and C cause a +// false detection of a fourth key at row 2, column 2. Trickier, however, +// is that such phantom keys can effectively serve as one of the keys +// necessary to produce other phantom keys: +// +// 1 . A<---B +// . | ^ +// . | | +// 2 --E----.--->C +// | | | +// | | | +// 3 --D----.----. active row +// . | | +// . v v +// 1 2 3 +// sensed columns +// +// In this case, phantom keys at row 2, column 2 and row 3, column 3 +// can be directly determined, but an additional phantom key at row +// 3, column 2 is also indirectly produced. This means that we need +// to produce the transitive closure of all interactions to get the +// full set of phantom keys. +// +// Why do we bother with this? Normally, pressing multiple keys does +// nothing because of debounce. However, there are two exceptions. +// First, Ctrl/Shift/Break participate in the matrix but are not +// affected by debounce. This causes Ctrl+Shift+X keys corresponding +// to scan codes $C0-C7 and $D0-D7 to not work. Second, the phantom +// keys ARE visible if debounce is disabled. +// +// To compute the transitive closure, we use an O(N^2) algorithm to +// compute the connected rows, and then an O(N) pass to propagate +// the unified column sets to all rows. Since only a few keys are +// pressed most of the time, we can save a lot of time by doing an +// early out for rows that have no active switches. +// +void ATPokeyEmulator::UpdateEffectiveKeyMatrix() { + int srcRows[8]; + bool activeKeys = false; + + memcpy(mEffectiveKeyMatrix, mKeyMatrix, sizeof mEffectiveKeyMatrix); + + // The computer line connects the control (KR2) signal via the same + // row select lines as the main keyboard on KR1, so both the main + // keyboard and the Ctrl/Shift/Break keys can interact to create + // phantom keys on both sides. The 5200 just connects the top button + // to KR2, so nothing in the main matrix can interfere. + const uint16_t crossConnectMask = mb5200Mode ? 0xFF : 0xFFFF; + + for(int i=0; i<8; ++i) { + int srcRow = i; + uint16_t connectedColumns = mEffectiveKeyMatrix[i] & crossConnectMask; + + if (connectedColumns) { + for(int j = i + 1; j < 8; ++j) { + if (mEffectiveKeyMatrix[j] & connectedColumns) { + connectedColumns |= mEffectiveKeyMatrix[j]; + srcRow = j; + } + } + + mEffectiveKeyMatrix[srcRow] |= connectedColumns & crossConnectMask; + + activeKeys = true; + } + + srcRows[i] = srcRow; + } + + if (!activeKeys) + return; + + for(int i=0; i<8; ++i) + mEffectiveKeyMatrix[i] |= mEffectiveKeyMatrix[srcRows[i]]; +} + +bool ATPokeyEmulator::CanPushKey(uint8_t scanCode) const { + // wait if keyboard IRQ is still pending (not necessarily active in IRQST yet!) + if (mbKeyboardIRQPending) + return false; + + // wait if keyboard IRQ is still active or disabled + if (!(mIRQST & mIRQEN & 0x40)) + return false; + + // wait if keyboard scan is disabled + if ((mSKCTL & 3) != 3) + return false; + + // wait if cooldown timer is still active to dodge the speaker / keyclick, unless we have + // credible evidence that the OS can accept another key sooner + const bool cooldownExpired = (mbUseKeyCooldownTimer && !mKeyCooldownTimer); + if (!mpConn->PokeyIsKeyPushOK(scanCode, cooldownExpired)) + return false; + + // looks fine to push a new key... + return true; +} + +void ATPokeyEmulator::TryPushNextKey() { + uint8_t c = mKeyQueue.front(); + mKeyQueue.pop_front(); + + PushKey(c, false, false, false, mbUseKeyCooldownTimer); +} + +void ATPokeyEmulator::SetKeyboardModes(bool cooked, bool scanEnabled) { + mbCookedKeyMode = cooked; + mbKeyboardScanEnabled = scanEnabled; + + UpdateKeyboardScanEvent(); +} + +void ATPokeyEmulator::UpdateKeyboardScanEvent() { + if ((mb5200Mode || mbKeyboardScanEnabled) && (mSKCTL & 2)) { + const uint32_t t = ATSCHEDULER_GETTIME(mpScheduler); + + mpScheduler->SetEvent(114 - (t - UpdateLast15KHzTime()), this, kATPokeyEventKeyboardScan, mpKeyboardScanEvent); + } else + mpScheduler->UnsetEvent(mpKeyboardScanEvent); +} + +void ATPokeyEmulator::QueueKeyboardIRQ() { + const uint32_t t = ATSCHEDULER_GETTIME(mpScheduler); + if (!mpKeyboardIRQEvent) + mpKeyboardIRQEvent = mpScheduler->AddEvent(114 - (t - UpdateLast15KHzTime()), this, kATPokeyEventKeyboardIRQ); + mbKeyboardIRQPending = true; +} + +void ATPokeyEmulator::AssertKeyboardIRQ() { + if (mIRQEN & 0x40) { + // If keyboard IRQ is already active, set the keyboard overrun bit. + if (!(mIRQST & 0x40)) + mSKSTAT &= ~0x40; + + mIRQST &= ~0x40; + AssertIrq(false); + } +} + +void ATPokeyEmulator::AssertBreakIRQ() { + if (mIRQEN & 0x80) { + mIRQST &= ~0x80; + AssertIrq(false); + } +} + +void ATPokeyEmulator::AssertIrq(bool cpuBased) { + if (!mbIrqAsserted) { + mbIrqAsserted = true; + + mpConn->PokeyAssertIRQ(cpuBased); + + if (mpTraceOutput) { + const uint64 t = mpScheduler->GetTick64() + (cpuBased ? 0 : -1); + mTraceIrqStart = t; + mbTraceIrqPending = true; + } + } +} + +void ATPokeyEmulator::NegateIrq(bool cpuBased) { + if (mbIrqAsserted) { + mbIrqAsserted = false; + + mpConn->PokeyNegateIRQ(cpuBased); + + if (mpTraceOutput && mbTraceIrqPending) { + mbTraceIrqPending = false; + + const uint64 t = mpScheduler->GetTick64() + (cpuBased ? 0 : -1); + + mpTraceOutput->AddIRQ(mTraceIrqStart, t); + } + } +} + +void ATPokeyEmulator::ProcessReceivedSerialByte() { + if (mbTraceSIO) + (*mpConsoleOut)("POKEY: Reasserting serial input IRQ. IRQEN=%02x, IRQST=%02x", mIRQEN, mIRQST); + + if (mIRQEN & 0x20) { + // check for overrun + if (!(mIRQST & 0x20)) { + + mSKSTAT &= 0xdf; + + if (mbTraceSIO) + (*mpConsoleOut)("POKEY: Serial input overrun detected (c=%02x; %02x %02x)", mSerialInputShiftRegister, mSERIN, mSerialInputShiftRegister); + } + + mIRQST &= ~0x20; + AssertIrq(false); + } + + mSERIN = mSerialInputShiftRegister; + mSKSTAT &= mSerialInputPendingStatus; +} + +void ATPokeyEmulator::SyncRenderers(ATPokeyRenderer *r) { + mpRenderer->SyncTo(*r); +} + +void ATPokeyEmulator::StartPotScan() { + // If we're in fast pot mode, update the pots now in case we're interrupting + // a scan. + if (mSKCTL & 4) + UpdatePots(0); + + mPotMasterCounter = 0; + + const uint32_t fastTime = ATSCHEDULER_GETTIME(mpScheduler); + const uint32_t slowTime = UpdateLast15KHzTime(); + + mPotLastScanTime = mpScheduler->GetTick64(); + mPotLastTimeFast = fastTime + 2; + mPotLastTimeSlow = slowTime; + + // If we're in slow pot mode, turn on the dumping caps and begin + // charging from level 0. + // + // If we're in fast pot mode, any pot lines that have already reached + // threshold will stay there unless they are being drained. For these + // lines, ALLPOT will indicate that the pot is done, but the POTn + // register will not update. The behavior is different if fast pot + // mode is activated *after* the pot scan has started, but we aren't + // handling that here. + // + if (!(mSKCTL & 4)) { + mALLPOT = 0xFF; + } else { + for(int i=0; i<8; ++i) { + if (mPotHiPositions[i] == 0xFF) + mALLPOT |= (1 << i); + } + } +} + +void ATPokeyEmulator::UpdatePots(uint32_t timeSkew) { + const uint32_t fastTime = ATSCHEDULER_GETTIME(mpScheduler) + timeSkew; + if ((fastTime - mPotLastTimeFast - 1) >= (uint32_t)0x7FFFFFFF) // wrap(fastTime <= mPotLastTimeFast) + return; + + const uint32_t slowTime = UpdateLast15KHzTime(); + + uint32_t count = mPotMasterCounter; + if (count < 229) { + if (mSKCTL & 4) { // fast pot scan + count += (fastTime - mPotLastTimeFast); + + if (count >= 229) + count = 229; + + } else { // slow pot scan + count += (slowTime - mPotLastTimeSlow) / 114; + + if (count >= 228) + count = 228; + } + } + + mPotMasterCounter = (uint8_t)count; + + mPotLastTimeFast = fastTime; + mPotLastTimeSlow = slowTime; + + if (mALLPOT) { + const uint8_t (&positions)[8] = *((mSKCTL & 4) ? &mPotHiPositions : &mPotPositions); + for(int i=0; i<8; ++i) { + if (!(mALLPOT & (1 << i))) + continue; + + if (mPotMasterCounter >= positions[i]) { + mALLPOT &= ~(1 << i); + + mPotLatches[i] = std::min(229, positions[i]); + } + } + } +} + +void ATPokeyEmulator::UpdateAddressDecoding() { + mAddressMask = mpSlave && mbStereoSoftEnable ? 0x1F : 0x0F; +} diff --git a/src/engine/platform/sound/pokey/pokeyrenderer.cpp b/src/engine/platform/sound/pokey/pokeyrenderer.cpp new file mode 100644 index 00000000..291ec86f --- /dev/null +++ b/src/engine/platform/sound/pokey/pokeyrenderer.cpp @@ -0,0 +1,1343 @@ +// Altirra - Atari 800/800XLf/5200 emulator +// Copyright (C) 2008-2011 Avery Lee +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +//========================================================================= +// POKEY renderer +// +// The POKEY renderer converts register change and timer events within +// the POKEY emulation to audio output. It exclusively handles portions of +// the audio circuits that have no feedback path to the 6502 and thus are +// not observable. Most of the logic simulated here is digital, except for +// downsample from 1.77/1.79MHz to 64KHz. +// +// The general approach is as follows: +// +// - Collected timing and register change events. Timing events are +// accumulated, while register change events are handled immediately +// and cause a flush. +// +// - The common cases of a timer running at a regular frequency are handled +// by the deferred mechanism, where the POKEY emulator pushes the initial +// timing parameters into the renderer and the renderer expands the +// periodic ticks. This greatly reduces the timer overhead in the POKEY +// emulator itself. The 16-bit linked timer case is a bit more +// complicated to handle the uneven ticking of the low timer. +// +// - Timer events from each channel are converted to output change events. +// This can involve sampling the polynomial counters for clocking and +// output noise. The polynomial tables are offset by the initial offset +// for the frame and then all channels independently sample noise off +// of the tables. +// +// - Timer events from ch3/4 are also converted to high pass update events +// for ch1/2. These have to be offset by half a cycle, so the output +// section uses half-ticks (3.55/3.58MHz). +// +// - The output path XORs the high-pass flip/flops into ch1/2, converts the +// four channel outputs to a mix level, then box filters the stairstep +// waveform to 1/28 rate (64KHz). This output is then sent to the audio +// sync mixer for mixing with non-POKEY sources, AC coupling filtering, +// and downsample to 44/48KHz. +// +//------------------------------------------------------------------------------ +// The signal processing path has been rewritten in Altirra 4.00 for higher +// quality and better performance: +// +// - The trick used in the main mixer has also been applied here, splitting +// the high-pass filter and cancelling the differentiator in the HPF with +// pulse rendering so that only changes in output voltage from POKEY need +// to be rendered pre-HPF. This avoids the need to step one sample at a time +// as output transitions are processed. +// +// - The sparseness of the pre-HPF buffer makes a higher-quality filter cheaper. +// We now use a bank of 56 x 8-tap filters, equivalent to a 447-tap low-pass +// filter at half-tick rate. This filter need only be applied to each +// transition as it is rendered into the output buffer. This significantly +// reduces aliasing for ultrasonic sounds compared to the 3.90 renderer. +// +// - The HPF also runs faster as only the integrator is needed, and is now run +// once at the end of each block instead of as part of sample generation. +// + +#include "at/ataudio/pokey.h" +#include "at/ataudio/pokeyrenderer.h" +#include "at/ataudio/pokeytables.h" + +ATLogChannel g_ATLCPokeyTEv(false, false, "POKEYTEV", "POKEY timer events (high traffic)"); + +namespace { + constexpr uint32 kAudioDelay = 2; + + constexpr uint8 kChannelOutputAndMask[7] = { + 0x3F, + 0x3F, + 0x3F, + 0x3F, + 0x3E, + 0x3D, + 0x3F, + }; + + constexpr uint8 kChannelOutputXorMask[7] = { + 0x11, + 0x22, + 0x04, + 0x08, + 0x00, + 0x00, + 0x00 + }; +} + +ATPokeyRenderer::ATPokeyRenderer() + : mpTables(NULL) + , mHighPassAccum(0) + , mOutputLevel(0) + , mExternalInput(0) + , mbSpeakerState(false) + , mOutputSampleCount(0) + , mpEdgeBuffer(new ATSyncAudioEdgeBuffer) +{ + mpEdgeBuffer->mLeftVolume = 1.0f; + mpEdgeBuffer->mRightVolume = 1.0f; + mpEdgeBuffer->mpDebugLabel = "PokeyRenderer"; + + for(int i=0; i<4; ++i) { + mbChannelEnabled[i] = true; + } +} + +ATPokeyRenderer::~ATPokeyRenderer() { +} + +void ATPokeyRenderer::Init(ATPokeyTables *tables) { + mpScheduler = sch; + mpTables = tables; + + mSerialPulse = 0.12f; + + ColdReset(); +} + +void ATPokeyRenderer::ColdReset() { + mbInitMode = true; + mPolyState.mInitMask = 0; + + // preset all noise and high-pass flip/flops + mNoiseFlipFlops = 0x0F; + mChannelOutputMask = 0x3F; + + for(int i=0; i<4; ++i) { + mDeferredEvents[i].mbEnabled = false; + } + + mOutputLevel = 0; + mOutputSampleCount = 0; + mHighPassAccum = 0; + + memset(mRawOutputBuffer, 0, sizeof mRawOutputBuffer); + + mpEdgeBuffer->mEdges.clear(); + + const uint32 t = ATSCHEDULER_GETTIME(mpScheduler); + mBlockStartTime = t; + mBlockStartTime2 = t * 2; + mLastFlushTime = t; + mPolyState.mLastPoly17Time = t; + mPolyState.mLastPoly9Time = t; + mPolyState.mLastPoly5Time = t; + mPolyState.mLastPoly4Time = t; + mPolyState.mPoly17Counter = 0; + mPolyState.mPoly9Counter = 0; + mPolyState.mPoly5Counter = 0; + mPolyState.mPoly4Counter = 0; + + for(ChannelEdges& edges : mChannelEdges) + edges.clear(); + + for(uint32& base : mChannelEdgeBases) + base = 0; + + mChangeQueue.clear(); + + // This must be done after everything else is inited, as it will start recomputing + // derived values. + mArchState = {}; + mRenderState = {}; + + mChannelOutputMask = 0x30; + + for(int i=0; i<4; ++i) + UpdateVolume(i); + + UpdateOutput(t); +} + +void ATPokeyRenderer::SyncTo(const ATPokeyRenderer& src) { + mLastFlushTime = src.mLastFlushTime; + mBlockStartTime = src.mBlockStartTime; + mBlockStartTime2 = src.mBlockStartTime2; + mOutputSampleCount = src.mOutputSampleCount; + + memset(mRawOutputBuffer, 0, sizeof(mRawOutputBuffer[0]) * mOutputSampleCount); +} + +void ATPokeyRenderer::SetChannelEnabled(int channel, bool enabled) { + VDASSERT(channel < 4); + if (mbChannelEnabled[channel] != enabled) { + const uint32 t = ATSCHEDULER_GETTIME(mpScheduler); + Flush(t); + + if (mpAudioLog) + LogOutputChange(t*2); + + mbChannelEnabled[channel] = enabled; + + UpdateVolume(channel); + UpdateOutput(t); + } +} + +void ATPokeyRenderer::SetAudioLog(ATPokeyAudioLog *log) { + mpAudioLog = log; + + if (log) { + log->mTicksPerSample = log->mCyclesPerSample * 2; + log->mFullScaleValue = log->mTicksPerSample * 15; + log->mSampleIndex = 0; + log->mLastOutputMask = 0; + log->mNumMixedSamples = 0; + + RestartAudioLog(true); + } +} + +void ATPokeyRenderer::RestartAudioLog(bool initial) { + if (!mpAudioLog) + return; + + const uint32 t = ATSCHEDULER_GETTIME(mpScheduler); + if (!initial) { + // finish remaining samples in log for this frame (note that the current sample has already + // been cleared and may be partially populated) + LogOutputChange(t*2); + } + + // restart sample buffer for new frame + const uint32 t2 = t*2; + mpAudioLog->mLastFrameSampleCount = mpAudioLog->mSampleIndex; + mpAudioLog->mStartingAudioTick = t2; + mpAudioLog->mLastAudioTick = t2; + mpAudioLog->mAccumulatedAudioTicks = 0; + mpAudioLog->mSampleIndex = 0; +} + +void ATPokeyRenderer::SetFiltersEnabled(bool enable) { + if (!enable) + mHighPassAccum = 0; +} + +void ATPokeyRenderer::SetInitMode(bool init) { + if (init == mbInitMode) + return; + + mbInitMode = init; + + QueueChangeEvent(ChangeType::Init, init ? 1 : 0); +} + +bool ATPokeyRenderer::SetSpeaker(bool newState) { + if (mbSpeakerState == newState) + return false; + + mbSpeakerState = newState; + + // The XL/XE speaker is about as loud peak-to-peak as a channel at volume 6. + // However, it is added in later in the output circuitry and has different + // audio characteristics, so we must treat it separately. + float delta = mpTables->mSpeakerLevel; + + if (newState) + delta = -delta; + + const uint32 t = ATSCHEDULER_GETTIME(mpScheduler); + mpEdgeBuffer->mEdges.push_back(ATSyncAudioEdge { t, delta }); + return true; +} + +void ATPokeyRenderer::SetAudioLine2(int v) { + if (mExternalInput != v) { + const uint32 t = ATSCHEDULER_GETTIME(mpScheduler); + + mpEdgeBuffer->mEdges.push_back(ATSyncAudioEdge { t, (float)(v - mExternalInput) / 60.0f }); + + mExternalInput = v; + } +} + +void ATPokeyRenderer::ResetTimers() { + QueueChangeEvent(ChangeType::ResetOutputs, 0); +} + +void ATPokeyRenderer::SetAUDCx(int index, uint8 value) { + if (mArchState.mAUDC[index] == value) + return; + + mArchState.mAUDC[index] = value; + + QueueChangeEvent((ChangeType)((int)ChangeType::Audc0 + index), value); +} + +void ATPokeyRenderer::SetAUDCTL(uint8 value) { + if (mArchState.mAUDCTL == value) + return; + + mArchState.mAUDCTL = value; + + QueueChangeEvent(ChangeType::Audctl, value); +} + +void ATPokeyRenderer::AddChannelEvent(int channel) { + ChannelEdges& ce = mChannelEdges[channel]; + const uint32 t = ATSCHEDULER_GETTIME(mpScheduler); + + VDASSERT(ce.size() == mChannelEdgeBases[channel] || t - ce.back() < 0x80000000); + ce.push_back(t); +} + +void ATPokeyRenderer::SetChannelDeferredEvents(int channel, uint32 start, uint32 period) { + VDASSERT(period < 7500000); + + DeferredEvent& ev = mDeferredEvents[channel]; + ev.mbEnabled = true; + ev.mbLinked = false; + ev.mNextTime = start; + ev.mPeriod = period; +} + +void ATPokeyRenderer::SetChannelDeferredEventsLinked(int channel, uint32 loStart, uint32 loPeriod, uint32 hiStart, uint32 hiPeriod, uint32 loOffset) { + VDASSERT(hiStart - loStart - 1 < 0x7FFFFFFFU); // wrapped(hiStart > loStart) + VDASSERT(loPeriod < 30000); + VDASSERT(hiPeriod < 7500000); + + DeferredEvent& ev = mDeferredEvents[channel]; + ev.mbEnabled = true; + ev.mbLinked = true; + ev.mNextTime = loStart; + ev.mPeriod = loPeriod; + ev.mNextHiTime = hiStart; + ev.mHiPeriod = hiPeriod; + ev.mHiLoOffset = loOffset; +} + +void ATPokeyRenderer::ClearChannelDeferredEvents(int channel, uint32 t) { + if (!mDeferredEvents[channel].mbEnabled) + return; + + FlushDeferredEvents(channel, t); + mDeferredEvents[channel].mbEnabled = false; +} + +void ATPokeyRenderer::AddSerialNoisePulse(uint32 t) { + if (mSerialPulseTimes.size() > 65536) + return; + + if (!mSerialPulseTimes.empty() && mSerialPulseTimes.back() - t < 0x80000000) + return; + + mSerialPulseTimes.push_back(t); +} + +void ATPokeyRenderer::StartBlock() { + memmove(mRawOutputBuffer, mRawOutputBuffer + mOutputSampleCount, 8*sizeof(float)); + memset(mRawOutputBuffer + 8, 0, mOutputSampleCount * sizeof(float)); + + const uint32 t = ATSCHEDULER_GETTIME(mpScheduler); + if (t - mBlockStartTime >= 28) + mBlockStartTime = t; + + mBlockStartTime2 = mBlockStartTime * 2; +} + +ATPokeyRenderer::EndBlockInfo ATPokeyRenderer::EndBlock(IATSyncAudioEdgePlayer *edgePlayer) { + const uint32 t = ATSCHEDULER_GETTIME(mpScheduler); + + Flush(t); + + // compute sample count + uint32 blockEndTime = t; + mOutputSampleCount = (blockEndTime - mBlockStartTime) / 28; + + if (mOutputSampleCount > kMaxWriteIndex) { + mOutputSampleCount = kMaxWriteIndex; + + blockEndTime = t; + } else { + blockEndTime = mBlockStartTime + 28 * mOutputSampleCount; + } + + // do final filtering on samples + PostFilter(); + + // copy mixed samples to the audio log + if (mpAudioLog) { + uint32 samplesToCopy = mpAudioLog->mMaxMixedSamples - mpAudioLog->mNumMixedSamples; + + if (samplesToCopy > mOutputSampleCount) + samplesToCopy = mOutputSampleCount; + + if (samplesToCopy) { + std::copy_n(mRawOutputBuffer, samplesToCopy, mpAudioLog->mpMixedSamples + mpAudioLog->mNumMixedSamples); + mpAudioLog->mNumMixedSamples += samplesToCopy; + } + } + + // merge noise samples + const uint32 sampleCount = mOutputSampleCount; + + if (!mSerialPulseTimes.empty()) { + const uint32 baseTime = t - sampleCount * 28; + float pulse = mSerialPulse; + + auto it = mSerialPulseTimes.begin(); + auto itEnd = mSerialPulseTimes.end(); + + while(it != itEnd) { + const uint32 pulseTime = *it; + const uint32 rawOffset = pulseTime - baseTime; + + if (rawOffset < 0x80000000) { + const uint32 sampleOffset = rawOffset / 28; + + if (sampleOffset >= sampleCount) + break; + + mRawOutputBuffer[sampleOffset] += pulse; + pulse = -pulse; + } + + ++it; + } + + mSerialPulse = pulse; + mSerialPulseTimes.erase(mSerialPulseTimes.begin(), it); + } + + if (edgePlayer) + edgePlayer->AddEdgeBuffer(mpEdgeBuffer); + else + mpEdgeBuffer->mEdges.clear(); + + mBlockStartTime = blockEndTime; + + return EndBlockInfo { + blockEndTime - 28 * sampleCount, + sampleCount + }; +} + +void ATPokeyRenderer::LoadState(const ATSaveStatePokeyRenderer& state) { + const uint32 t = ATSCHEDULER_GETTIME(mpScheduler); + + // Careful -- we save the polynomial counters in simulation time, but we + // have to roll them back to where sound generation currently is. + + mPolyState.mPoly4Counter = (state.mPoly4Offset + 15 - (t - mPolyState.mLastPoly4Time) % 15) % 15; + mPolyState.mPoly5Counter = (state.mPoly5Offset + 31 - (t - mPolyState.mLastPoly5Time) % 31) % 31; + mPolyState.mPoly9Counter = (state.mPoly9Offset + 511 - (t - mPolyState.mLastPoly9Time) % 511) % 511; + mPolyState.mPoly17Counter = (state.mPoly17Offset + 131071 - (t - mPolyState.mLastPoly17Time) % 131071) % 131071; + + mNoiseFlipFlops = state.mOutputFlipFlops & 0x0F; + mChannelOutputMask = state.mOutputFlipFlops & 0x3F; +} + +ATSaveStatePokeyRenderer ATPokeyRenderer::SaveState() const { + const uint32 t = ATSCHEDULER_GETTIME(mpScheduler); + ATSaveStatePokeyRenderer state {}; + + // Careful -- we can't update polynomial counters here like we do in the + // main POKEY module. That's because the polynomial counters have to be + // advanced by sound rendering and not by the simulation. + + state.mPoly4Offset = (mPolyState.mPoly4Counter + (mPolyState.mInitMask & (t - mPolyState.mLastPoly4Time))) % 15; + state.mPoly5Offset = (mPolyState.mPoly5Counter + (mPolyState.mInitMask & (t - mPolyState.mLastPoly5Time))) % 31; + state.mPoly9Offset = (mPolyState.mPoly9Counter + (mPolyState.mInitMask & (t - mPolyState.mLastPoly9Time))) % 511; + state.mPoly17Offset = (mPolyState.mPoly17Counter + (mPolyState.mInitMask & (t - mPolyState.mLastPoly17Time))) % 131071; + state.mOutputFlipFlops = mNoiseFlipFlops + (mChannelOutputMask & 0x30); + + // mbInitMode is restored by the POKEY emulator. + // AUDCTL is restored by the POKEY emulator. + // AUDCx are restored by the POKEY emulator. + + return state; +} + +void ATPokeyRenderer::QueueChangeEvent(ChangeType type, uint8 value) { + const uint32 t = ATSCHEDULER_GETTIME(mpScheduler); + mChangeQueue.push_back(ChangeEvent { t, type, value }); + + if (mChangeQueue.size() >= 40000) + Flush(t); +} + +void ATPokeyRenderer::ProcessChangeEvents(uint32 t) { + bool outputChanged = false; + + while(!mChangeQueue.empty()) { + const ChangeEvent& ce = mChangeQueue.front(); + + if ((uint32)(t - ce.mTime) >= UINT32_C(0x80000000)) + break; + + switch(ce.mType) { + case ChangeType::Audc0: + case ChangeType::Audc1: + case ChangeType::Audc2: + case ChangeType::Audc3: + mRenderState.mAUDC[(int)ce.mType - (int)ChangeType::Audc0] = ce.mValue; + UpdateVolume((int)ce.mType - (int)ChangeType::Audc0); + + outputChanged = true; + break; + + case ChangeType::Audctl: + { + const uint8 delta = mRenderState.mAUDCTL ^ ce.mValue; + + mRenderState.mAUDCTL = ce.mValue; + + if ((delta & 0x04) && !(mRenderState.mAUDCTL & 0x04)) { + if (!(mChannelOutputMask & 0x10)) { + mChannelOutputMask |= 0x10; + outputChanged = true; + } + } + + if ((delta & 0x02) && !(mRenderState.mAUDCTL & 0x02)) { + if (!(mChannelOutputMask & 0x20)) { + mChannelOutputMask |= 0x20; + outputChanged = true; + } + } + } + break; + + case ChangeType::Init: + mPolyState.mInitMask = ce.mValue ? 0 : UINT32_C(0xFFFFFFFF); + + // These offsets are specifically set so that the audio output patterns + // are correctly timed. + mPolyState.mPoly4Counter = 8 - kAudioDelay; + mPolyState.mPoly5Counter = 22 - kAudioDelay; + mPolyState.mPoly9Counter = 507 - kAudioDelay; + mPolyState.mPoly17Counter = 131067 - kAudioDelay; + mPolyState.mLastPoly17Time = t; + mPolyState.mLastPoly9Time = t; + mPolyState.mLastPoly5Time = t; + mPolyState.mLastPoly4Time = t; + break; + + case ChangeType::ResetOutputs: + // preset all noise flip/flops + mNoiseFlipFlops = 0xF; + mChannelOutputMask |= 0xF; + outputChanged = true; + break; + + case ChangeType::Flush: + break; + } + + mChangeQueue.pop_front(); + } + + if (outputChanged) { + UpdateOutput(t); + + if (mpAudioLog) + LogOutputChange(t*2); + } +} + +void ATPokeyRenderer::FlushDeferredEvents(int channel, uint32 t) { + DeferredEvent de = mDeferredEvents[channel]; + + ChannelEdges& ce = mChannelEdges[channel]; + + VDASSERT(ce.size() == mChannelEdgeBases[channel] || de.mNextTime - ce.back() < 0x80000000); // wrap(nextTime >= back) -> nextTime - back >= 0 + + if (de.mbLinked) { + while((sint32)(de.mNextTime - t) < 0) { + ce.push_back(de.mNextTime); + de.mNextTime += de.mPeriod; + + if ((sint32)(de.mNextTime - de.mNextHiTime) >= 0) { + de.mNextTime = de.mNextHiTime + de.mHiLoOffset; + de.mNextHiTime += de.mHiPeriod; + } + } + } else { + while((sint32)(de.mNextTime - t) < 0) { + ce.push_back(de.mNextTime); + de.mNextTime += de.mPeriod; + } + } + + mDeferredEvents[channel] = de; +} + +void ATPokeyRenderer::Flush(const uint32 t) { + mChangeQueue.push_back(ChangeEvent { t, ChangeType::Flush }); + + while(!mChangeQueue.empty()) { + const uint32 changeTime = mChangeQueue.front().mTime; + + while(mLastFlushTime != changeTime) { + uint32 dt = changeTime - mLastFlushTime; + if (dt) + Flush2(mLastFlushTime + std::min(dt, 0xC000)); + } + + if (mpAudioLog) + LogOutputChange(changeTime*2); + + ProcessChangeEvents(changeTime); + } +} + +void ATPokeyRenderer::Flush2(const uint32 t) { + // flush deferred events + bool haveAnyEvents = false; + + for(int i=0; i<4; ++i) { + if (mDeferredEvents[i].mbEnabled) + FlushDeferredEvents(i, t); + + if (mChannelEdgeBases[i] != mChannelEdges[i].size()) + haveAnyEvents = true; + } + + // Check if the noise flip-flops are different from the output channel + // mask for any channels that are not volume-only and have non-zero + // volume; if there are any, update the mask and the current output + // level. Any other channels that differ won't matter since their volume + // is either being overridden or zero; we can update them later when + // that changes. + uint8 dirtyOutputs = (mChannelOutputMask ^ mNoiseFlipFlops) & ~mVolumeOnlyMask & mNonZeroVolumeMask; + + if (dirtyOutputs) { + mChannelOutputMask ^= dirtyOutputs; + UpdateOutput2(mLastFlushTime*2); + + if (mpAudioLog) + LogOutputChange(mLastFlushTime*2); + } + + const uint32 baseTime = mLastFlushTime; + mLastFlushTime = t; + + g_ATLCPokeyTEv("=== processing %08X:%08X ===\n", baseTime, t); + + // early out if we have no events to process + if (!haveAnyEvents) + return; + + // realign polynomial tables to start of frame + if (mbInitMode) { + mPolyState.mPoly4Offset = + mPolyState.mPoly5Offset = + mPolyState.mPoly9Offset = + mPolyState.mPoly17Offset = (uintptr)mpTables->mInitModeBuffer; + } else { + mPolyState.UpdatePoly4Counter(baseTime); + mPolyState.UpdatePoly5Counter(baseTime); + mPolyState.UpdatePoly9Counter(baseTime); + mPolyState.UpdatePoly17Counter(baseTime); + + mPolyState.mPoly4Offset = (uintptr)mpTables->mPolyBuffer + mPolyState.mPoly4Counter; + mPolyState.mPoly5Offset = (uintptr)mpTables->mPolyBuffer + mPolyState.mPoly5Counter; + mPolyState.mPoly9Offset = (uintptr)mpTables->mPolyBuffer + mPolyState.mPoly9Counter; + mPolyState.mPoly17Offset = (uintptr)mpTables->mPolyBuffer + mPolyState.mPoly17Counter; + } + + for(int i=0; i<4; ++i) { + auto& srcEdges = mChannelEdges[i]; + uint32 srcBegin = mChannelEdgeBases[i]; + uint32 srcEnd = srcEdges.size(); + + // We should not have any edges in the future. We may have some edges slightly in the + // past since we keep a couple of cycles back to delay the audio output. + if (srcBegin != srcEnd) { + VDASSERT(ATWrapTime{srcEdges[srcBegin]} >= baseTime - kAudioDelay); + } + + auto& dstEdges = mSortedEdgesTemp[i]; + const uint32 numEdges = srcEnd - srcBegin; + + dstEdges.resize(numEdges + 1); + + uint32 *dst = dstEdges.data(); + + srcEdges.push_back(baseTime ^ UINT32_C(0x80000000)); + + const uint32 * const VDRESTRICT src = srcEdges.data() + srcBegin; + auto [dst2, src2] = (this->*GetFireTimerRoutine(i))(dst, src, baseTime - kAudioDelay, t - baseTime); + srcEdges.pop_back(); + + uint32 numSrcEdgesInRange = (uint32)(src2 - src); + uint32 numAudioEdges = numSrcEdgesInRange; + + while(numAudioEdges && ATWrapTime{src[numAudioEdges - 1]} >= t - kAudioDelay) + --numAudioEdges; + + *dst2++ = UINT32_C(0xFFFFFFFF); + + dstEdges.resize((size_t)(dst2 - dst)); + + if (i >= 2) { + // Merge in events to update the high-pass filter. If high-pass audio mode is enabled for + // ch1/2, we need to insert all of the events from ch3/4; otherwise, we only need the last + // event, if any. + uint32 hpClockStart = 0; + uint32 hpClockEnd = numSrcEdgesInRange; + + // push ending bound forward to encompass events that may have been skipped by the main + // audio update due to audio delay, but which are in scope for the high-pass since HP + // processing runs two cycles ahead + while(hpClockEnd != numEdges && ATWrapTime{src[hpClockEnd]} < t) + ++hpClockEnd; + + // push starting bound forward, for same reason + while(hpClockStart != hpClockEnd && ATWrapTime{src[hpClockStart]} < baseTime) + ++hpClockStart; + + if (hpClockStart != hpClockEnd) { + const bool hpEnabled = (mRenderState.mAUDCTL & (4 >> (i - 2))) != 0; + + if (hpEnabled) { + auto& hpTargetEdges = mSortedEdgesTemp[i - 2]; + + // high-pass is enabled -- offset events by audio delay, convert to HP update events, and merge + // into ch1/2 list + const uint32 numHpEvents = hpClockEnd - hpClockStart; + + mSortedEdgesHpTemp1.resize(numHpEvents + 1); + + // Compute update offset: +1 half cycle, clear high-pass bit, and rebias time. + // + // Add one half cycle to the high pass update so it's a half cycle earlier than + // the output flip/flop. On real hardware, HP never updates at the same time; + // it's either one half clock earlier or late, so there is no phase offset at + // which high pass is fully effective or ineffective. + + const uint32 hpUpdateCoding = 0x4000 + (i + 2) - (baseTime << 15); + + { + uint32 *VDRESTRICT hpDest = mSortedEdgesHpTemp1.data(); + for(uint32 j=0; j= 16 && mChannelEdgeBases[i]*4 >= srcEnd) { + srcEdges.erase(srcEdges.begin(), srcEdges.begin() + mChannelEdgeBases[i]); + mChannelEdgeBases[i] = 0; + } + } + + const uint32 n0 = (uint32)mSortedEdgesTemp[0].size() - 1; + const uint32 n1 = (uint32)mSortedEdgesTemp[1].size() - 1; + const uint32 n2 = (uint32)mSortedEdgesTemp[2].size() - 1; + const uint32 n3 = (uint32)mSortedEdgesTemp[3].size() - 1; + const uint32 n01 = n0 + n1; + const uint32 n23 = n2 + n3; + const uint32 n = n01 + n23; + + mSortedEdgesTemp2[0].resize(n01 + 1); + mSortedEdgesTemp2[1].resize(n23 + 1); + + if (n01) { + MergeOutputEvents(mSortedEdgesTemp[0].data(), + mSortedEdgesTemp[1].data(), + mSortedEdgesTemp2[0].data()); + } + + mSortedEdgesTemp2[0].back() = 0xFFFFFFFF; + + if (n23) { + MergeOutputEvents(mSortedEdgesTemp[2].data(), + mSortedEdgesTemp[3].data(), + mSortedEdgesTemp2[1].data()); + } + + mSortedEdgesTemp2[1].back() = 0xFFFFFFFF; + + // The merge order here is critical -- we need channels 3 and 4 to update before + // 1 and 2 in case high pass mode is enabled, because if 1+3 or 2+4 fire at the + // same time, the high pass is updated with the output state from the last cycle. + mSortedEdges.resize(n + 1); + + MergeOutputEvents(mSortedEdgesTemp2[1].data(), + mSortedEdgesTemp2[0].data(), + mSortedEdges.data()); + + if (g_ATLCPokeyTEv.IsEnabled()) { + uint32 lastRelTime[8] {}; + + for(uint32 i=0; i> 14; + const uint8 op = edge & 7; + uint32 dt = edgeRelTime - lastRelTime[op]; + + lastRelTime[op] = edgeRelTime; + + g_ATLCPokeyTEv("%08X.%c:%u (%+6u.%c)\n", (edge >> 15) + baseTime, edge & 0x4000 ? '5' : '0', op, dt >> 1, dt & 1 ? '5' : '0'); + } + } + + // if we have logging, we must log the edges before ProcessOutputEdges() updates the channel output state + if (mpAudioLog) + LogOutputEdges(baseTime*2, mSortedEdges.data(), n); + + ProcessOutputEdges(baseTime, mSortedEdges.data(), n); + mSortedEdges.clear(); +} + +void ATPokeyRenderer::MergeOutputEvents(const uint32 *VDRESTRICT src1, const uint32 *VDRESTRICT src2, uint32 *VDRESTRICT dst) { + uint32 a = *src1++; + uint32 b = *src2++; + + for(;;) { + if (b < a) { + *dst++ = b; + b = *src2++; + continue; + } + + if (a < b) { + *dst++ = a; + a = *src1++; + continue; + } + + if (a == 0xFFFFFFFF) + break; + + *dst++ = a; + a = *src1++; + } +} + +template +const ATPokeyRenderer::FireTimerRoutine ATPokeyRenderer::kFireRoutines[2][16]={ + // What we are trying to do here is minimize the amount of work done + // in the FireTimer() routine, in two ways: precompile code paths with + // specific functions enabled, and identify when the resultant signal + // does not change. + // + // AUDCx bit 7 controls clock selection and isn't tied to anything else, + // so it always needs to go through. + // + // AUDCx bit 5 enables pure tone mode. When set, it overrides bit 6. + // Therefore, we map [6:5] = 11 to 01. + // + // AUDCx bit 6 chooses the 4-bit LFSR or the 9/17-bit LFSR. It is + // overridden in the table as noted above. + // + // AUDCx bit 4 controls volume only mode. When it is set, we must still + // update the internal flip-flop states, but the volume + // level is locked and can't affect the output. + // + // We also check the volume on the channel. If it is zero, then the + // output also doesn't affect the volume and therefore we can skip the + // flush in that case as well. + // + // High-pass mode throws a wrench into the works. In that case, a pair + // of channels are tied together and we have to be careful about what + // optimizations are applied. We need to check volumes on both channels, + // and the high channel can cause signal changes if the low channel is + // un-muted even if the high channel is muted. + // + // The control value $00 is especially important as it is the init state + // used by the OS to silence the audio channels, and thus it should run + // quickly. It is annoying to us since it is an LFSR-based mode rather + // than volume level. + + { + &ATPokeyRenderer::FireTimer, // poly5 + poly9/17 + &ATPokeyRenderer::FireTimer, // poly5 + poly9/17 + &ATPokeyRenderer::FireTimer, // poly5 + tone + &ATPokeyRenderer::FireTimer, // poly5 + tone + &ATPokeyRenderer::FireTimer, // poly5 + poly4 + &ATPokeyRenderer::FireTimer, // poly5 + poly4 + &ATPokeyRenderer::FireTimer, // poly5 + tone + &ATPokeyRenderer::FireTimer, // poly5 + tone + &ATPokeyRenderer::FireTimer, // poly9/17 + &ATPokeyRenderer::FireTimer, // poly9/17 + &ATPokeyRenderer::FireTimer, // tone + &ATPokeyRenderer::FireTimer, // tone + &ATPokeyRenderer::FireTimer, // poly4 + &ATPokeyRenderer::FireTimer, // poly4 + &ATPokeyRenderer::FireTimer, // tone + &ATPokeyRenderer::FireTimer, // tone + }, + { + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + &ATPokeyRenderer::FireTimer, + }, +}; + +ATPokeyRenderer::FireTimerRoutine ATPokeyRenderer::GetFireTimerRoutine(int ch) const { + switch(ch) { + case 0: return GetFireTimerRoutine<0>(); + case 1: return GetFireTimerRoutine<1>(); + case 2: return GetFireTimerRoutine<2>(); + case 3: return GetFireTimerRoutine<3>(); + + default: + return nullptr; + } +} + +template +ATPokeyRenderer::FireTimerRoutine ATPokeyRenderer::GetFireTimerRoutine() const { + static_assert(activeChannel >= 0 && activeChannel < 4); + + // If high pass is enabled we must generate output events even if volume + // is zero so we can latch the noise flip/flop into the high-pass flip/flop. + // We need both the low channel and the high channel. + const bool highPassEnabled = (mRenderState.mAUDCTL & (activeChannel & 1 ? 0x02 : 0x04)) != 0; + const bool nonZeroVolume = mChannelVolume[activeChannel] || highPassEnabled; + + using FireTimerTable = FireTimerRoutine[2][16]; + const FireTimerTable *tab[2] = { + &kFireRoutines, + &kFireRoutines, + }; + const bool usePoly9 = (mRenderState.mAUDCTL & 0x80) != 0; + + return (*tab[usePoly9])[nonZeroVolume][mRenderState.mAUDC[activeChannel] >> 4]; +} + +template +std::pair ATPokeyRenderer::FireTimer(uint32 *VDRESTRICT dst, const uint32 *VDRESTRICT src, uint32 timeBase, uint32 timeLimit) { + static constexpr bool noiseEnabled = !(audcn & 0x20); + static constexpr bool poly5Enabled = !(audcn & 0x80); + static constexpr int polyOffset = 3 - activeChannel; + + constexpr uint8 channelBit = (1 << activeChannel); + uint32 noiseFF = (mNoiseFlipFlops & channelBit) ? 1 : 0; + + constexpr uint32 opcode = (uint32)activeChannel; + + // These aren't pointers because they aren't guaranteed to be within the array until offset + // by the time offset. + const uintptr poly4Base = mPolyState.mPoly4Offset + polyOffset; + const uintptr poly5Base = mPolyState.mPoly5Offset + polyOffset; + const uintptr poly9Base = mPolyState.mPoly9Offset + polyOffset; + const uintptr poly17Base = mPolyState.mPoly17Offset + polyOffset; + + for(;;) { + const uint32 timeOffset = (*src) - timeBase; + + if (timeOffset >= timeLimit) + break; + + ++src; + + if constexpr (poly5Enabled) { + uint8 poly5 = *(const uint8 *)(poly5Base + timeOffset); + + if (!(poly5 & 4)) + continue; + } + + if constexpr (noiseEnabled) { + uint32 noiseFFInput; + + if constexpr ((audcn & 0x40) != 0) { + const uint32 poly4 = *(const uint8 *)(poly4Base + timeOffset); + noiseFFInput = (poly4 & 8) >> 3; + } else if constexpr (T_UsePoly9) { + const uint32 poly9 = *(const uint8 *)(poly9Base + timeOffset); + noiseFFInput = (poly9 & 2) >> 1; + } else { + const uint32 poly17 = *(const uint8 *)(poly17Base + timeOffset); + noiseFFInput = (poly17 & 1); + } + + if constexpr (outputAffectsSignal) { + // Because we are using noise in this path, using a branch would be fairly expensive + // as it is highly likely to mispredict due to the random branch -- since we're + // literally driving it from a psuedorandom noise generator. Use a branchless + // algorithm instead. + + const uint32 outputChanged = noiseFF ^ noiseFFInput; + noiseFF = noiseFFInput; + + *dst = (timeOffset << 15) + opcode; + dst += outputChanged; + } else { + noiseFF = noiseFFInput; + } + } else { + // Noise isn't enabled -- hardcode some stuff since VC++ isn't able to + // deduce that toggling a bit will always cause the changed check to pass. + noiseFF ^= 1; + + if constexpr (outputAffectsSignal) { + // Update normal audio on full cycles; we reserve the half-cycle for high-pass update. + *dst++ = (timeOffset << 15) + opcode; + } + } + } + + if (noiseFF) + mNoiseFlipFlops |= channelBit; + else + mNoiseFlipFlops &= ~channelBit; + + return { dst, src }; +} + +void ATPokeyRenderer::ProcessOutputEdges(uint32 timeBase, const uint32 *edges, uint32 n) { + const uint16 v0 = mChannelVolMixIndex[0]; + const uint16 v1 = mChannelVolMixIndex[1]; + const uint16 v2 = mChannelVolMixIndex[2]; + const uint16 v3 = mChannelVolMixIndex[3]; + + union { + uint16 w[16]; + uint32 d[8]; + } v; + + v.d[0] = (uint32)v0 * VDFromLE32(0x00010000); + + const uint32 d1 = (uint32)v1 * 0x00010001; + v.d[1] = v.d[0] + d1; + + const uint32 d2 = (uint32)v2 * 0x00010001; + const uint32 d3 = (uint32)v3 * 0x00010001; + + v.d[2] = v.d[0] + d2; + v.d[3] = v.d[1] + d2; + + v.d[4] = v.d[0] + d3; + v.d[5] = v.d[1] + d3; + v.d[6] = v.d[2] + d3; + v.d[7] = v.d[3] + d3; + + uint32 timeBase2 = timeBase * 2; + + // The channel output mask normally holds the high-pass flip-flops in bits 4-5 and the + // noise FFs for ch1/2 in bits 0-1. For faster operation, we move the noise FFs + // up to bits 4-5 and pre-XOR the high-pass FFs into bits 0-1. This lets us just + // clear bits 0-1 for the high-pass without losing any state. + uint8 outputMask = (mChannelOutputMask & 0x0F) ^ (mChannelOutputMask >> 4) ^ ((mChannelOutputMask << 4) & 0x30); + + const uint8 volForceMask = mVolumeOnlyMask | 0x30; + + while(n--) { + const uint32 code = *edges++; + const uint32 t2 = timeBase2 + (code >> 14); + const uint8 op = (uint8)code; + + // apply AND/XOR masks for operation + outputMask = (outputMask & kChannelOutputAndMask[op]) ^ kChannelOutputXorMask[op]; + + UpdateOutput2(t2, v.w[(outputMask | volForceMask) - 0x30]); + } + + mChannelOutputMask = (outputMask & 0x3C) ^ (outputMask >> 4) ^ ((outputMask << 4) & 0x30); +} + +void ATPokeyRenderer::UpdateVolume(int index) { + const uint8 audc = mRenderState.mAUDC[index]; + mChannelVolume[index] = mbChannelEnabled[index] ? audc & 15 : 0; + + static constexpr uint8 kVolMixLookup[16] = { + 0, 1, 5, 6, + 25, 26, 30, 31, + 50, 51, 55, 56, + 75, 76, 80, 81 + }; + + mChannelVolMixIndex[index] = kVolMixLookup[mChannelVolume[index]]; + + if (audc & 0x10) + mVolumeOnlyMask |= (1 << index); + else + mVolumeOnlyMask &= ~(1 << index); + + if (audc & 0x0F) + mNonZeroVolumeMask |= (1 << index); + else + mNonZeroVolumeMask &= ~(1 << index); +} + +void ATPokeyRenderer::UpdateOutput(uint32 t) { + UpdateOutput2(t * 2); +} + +void ATPokeyRenderer::UpdateOutput2(uint32 t2) { + uint8 outputMask = (mChannelOutputMask ^ (mChannelOutputMask >> 4)) | mVolumeOnlyMask; + + uint32 v0 = mChannelVolMixIndex[0]; + uint32 v1 = mChannelVolMixIndex[1]; + uint32 v2 = mChannelVolMixIndex[2]; + uint32 v3 = mChannelVolMixIndex[3]; + uint32 vpok = ((outputMask & 0x01) ? v0 : 0) + + ((outputMask & 0x02) ? v1 : 0) + + ((outputMask & 0x04) ? v2 : 0) + + ((outputMask & 0x08) ? v3 : 0); + + UpdateOutput2(t2, vpok); +} + +void ATPokeyRenderer::UpdateOutput2(uint32 t2, uint32 vpok) { + uint32 t2Offset = t2 - mBlockStartTime2; + uint32 sampleOffset = t2Offset / 56; + uint32 samplePhase = t2Offset % 56; + + VDASSERT(sampleOffset < kMaxWriteIndex); + + float newLevel = mpTables->mMixTable[vpok]; + float delta = newLevel - mOutputLevel; + mOutputLevel = newLevel; + + if (sampleOffset < kMaxWriteIndex) { + float *VDRESTRICT dst = &mRawOutputBuffer[sampleOffset]; + + if (samplePhase > 28) { + const float *VDRESTRICT src = g_ATPokeyHiFilterTable.mFilter[56 - samplePhase]; + dst[0] += src[7] * delta; + dst[1] += src[6] * delta; + dst[2] += src[5] * delta; + dst[3] += src[4] * delta; + dst[4] += src[3] * delta; + dst[5] += src[2] * delta; + dst[6] += src[1] * delta; + dst[7] += src[0] * delta; + } else { + const float *VDRESTRICT src = g_ATPokeyHiFilterTable.mFilter[samplePhase]; + dst[0] += src[0] * delta; + dst[1] += src[1] * delta; + dst[2] += src[2] * delta; + dst[3] += src[3] * delta; + dst[4] += src[4] * delta; + dst[5] += src[5] * delta; + dst[6] += src[6] * delta; + dst[7] += src[7] * delta; + } + } +} + +void ATPokeyRenderer::PostFilter() { + const uint32 outputCount = mOutputSampleCount; + float hpAccum = mHighPassAccum; + const float coeff = 1.0f - mpTables->mReferenceDecayPerSample; + const float vmin = mpTables->mReferenceClampLo; + const float vmax = mpTables->mReferenceClampHi; + + // Apply the integrator half of the high-pass filter. See audiofilter.cpp + // for the theory on why we do this, as it applies the same trick to the + // main audio path. The difference here is the additional output clamp. + // + // Micro-optimization fun: the compiler does a poor job here and doesn't + // vectorize the min/max, but it's moot due to the loop carried multiply- + // add dependency chain which forces just over 8c/sample. This can be gotten + // down to 2c/sample with vectorized 128-bit code and 1c/sample with 128-bit + // FMA, but at 63Ksamples/sec it ends up being only ~0.3% of the host CPU. + + for(uint32 i=0; i vmax) + v = vmax; + + mRawOutputBuffer[i] = v; + } + + mHighPassAccum = hpAccum; + + // prevent denormals and check for NaNs + if (fabsf(mHighPassAccum) < 1e-20) + mHighPassAccum = 0; + + if (!isfinite(mHighPassAccum)) { + VDFAIL("NaN or Inf detected during post filtering."); + + mHighPassAccum = 0; + + memset(mRawOutputBuffer, 0, sizeof mRawOutputBuffer); + } +} + +void ATPokeyRenderer::LogOutputChange(uint32 t2) const { + const uint32 nullEdge = 6; + LogOutputEdges(t2, &nullEdge, 1); +} + +void ATPokeyRenderer::LogOutputEdges(uint32 timeBase2, const uint32 *edges, uint32 n) const { + if (mpAudioLog->mSampleIndex >= mpAudioLog->mMaxSamples) + return; + + VDASSERT(ATWrapTime{timeBase2} >= mpAudioLog->mLastAudioTick); + + const uint8 v0 = mChannelVolume[0]; + const uint8 v1 = mChannelVolume[1]; + const uint8 v2 = mChannelVolume[2]; + const uint8 v3 = mChannelVolume[3]; + + uint8 outputMask = (mChannelOutputMask & 0x0F) ^ (mChannelOutputMask >> 4) ^ ((mChannelOutputMask << 4) & 0x30); + uint8 lastOutputMask = mpAudioLog->mLastOutputMask; + uint32 tickAccum = mpAudioLog->mAccumulatedAudioTicks; + uint32 lastTick = mpAudioLog->mLastAudioTick; + uint32 sampleIndex = mpAudioLog->mSampleIndex; + const uint32 maxSamples = mpAudioLog->mMaxSamples; + const uint32 ticksPerSample = mpAudioLog->mTicksPerSample; + ATPokeyAudioState *VDRESTRICT samplePtr = &mpAudioLog->mpStates[sampleIndex]; + + while(n--) { + const uint32 code = *edges++; + const uint32 t2 = timeBase2 + (code >> 14); + const uint8 op = (uint8)code; + + // apply AND/XOR masks for operation + outputMask = (outputMask & kChannelOutputAndMask[op]) ^ kChannelOutputXorMask[op]; + + uint32 dt = t2 - lastTick; + + // we need to generate samples up to the current point with the _last_ output + // mask, since the new value is for past this edge + int ch0 = lastOutputMask & 1 ? v0 : 0; + int ch1 = lastOutputMask & 2 ? v1 : 0; + int ch2 = lastOutputMask & 4 ? v2 : 0; + int ch3 = lastOutputMask & 8 ? v3 : 0; + lastOutputMask = outputMask | mVolumeOnlyMask; + + VDASSERT((lastTick - mpAudioLog->mStartingAudioTick) - (sampleIndex * mpAudioLog->mTicksPerSample) < mpAudioLog->mTicksPerSample); + + if ((uint32)(dt - 1) < UINT32_C(0x80000000)) { + lastTick = t2; + + do { + uint32 sampleTicks = ticksPerSample - tickAccum; + + if (sampleTicks > dt) + sampleTicks = dt; + + if (tickAccum == 0) + *samplePtr = {}; + + tickAccum += sampleTicks; + dt -= sampleTicks; + + samplePtr->mChannelOutputs[0] += ch0 * sampleTicks; + samplePtr->mChannelOutputs[1] += ch1 * sampleTicks; + samplePtr->mChannelOutputs[2] += ch2 * sampleTicks; + samplePtr->mChannelOutputs[3] += ch3 * sampleTicks; + + if (tickAccum == ticksPerSample) { + tickAccum = 0; + + ++samplePtr; + + if (++sampleIndex >= maxSamples) + goto log_full; + } + } while(dt); + } + } + +log_full: + mpAudioLog->mAccumulatedAudioTicks = tickAccum; + mpAudioLog->mLastAudioTick = lastTick; + mpAudioLog->mSampleIndex = sampleIndex; + mpAudioLog->mLastOutputMask = lastOutputMask; +} + +void ATPokeyRenderer::PolyState::UpdatePoly17Counter(uint32 t) { + int polyDelta = t - mLastPoly17Time; + mPoly17Counter += polyDelta & mInitMask; + mLastPoly17Time = t; + + if (mPoly17Counter >= 131071) + mPoly17Counter %= 131071; +} + +void ATPokeyRenderer::PolyState::UpdatePoly9Counter(uint32 t) { + int polyDelta = t - mLastPoly9Time; + mPoly9Counter += polyDelta & mInitMask; + mLastPoly9Time = t; + + if (mPoly9Counter >= 511) + mPoly9Counter %= 511; +} + +void ATPokeyRenderer::PolyState::UpdatePoly5Counter(uint32 t) { + int polyDelta = t - mLastPoly5Time; + mPoly5Counter += polyDelta & mInitMask; + mLastPoly5Time = t; + + if (mPoly5Counter >= 31) + mPoly5Counter %= 31; +} + +void ATPokeyRenderer::PolyState::UpdatePoly4Counter(uint32 t) { + int polyDelta = t - mLastPoly4Time; + VDASSERT(polyDelta >= 0); + mPoly4Counter += polyDelta & mInitMask; + mLastPoly4Time = t; + + if (mPoly4Counter >= 15) + mPoly4Counter %= 15; +} diff --git a/src/engine/platform/sound/pokey/pokeytables.cpp b/src/engine/platform/sound/pokey/pokeytables.cpp new file mode 100644 index 00000000..e07f2fc2 --- /dev/null +++ b/src/engine/platform/sound/pokey/pokeytables.cpp @@ -0,0 +1,165 @@ +// Altirra - Atari 800/800XL/5200 emulator +// Copyright (C) 2008-2011 Avery Lee +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +#include "at/ataudio/pokeytables.h" + +ATPokeyTables::ATPokeyTables() { + // The 4-bit and 5-bit polynomial counters are of the XNOR variety, which means + // that the all-1s case causes a lockup. The INIT mode shifts zeroes into the + // register. + + uint32 poly4 = 0; + for(int i=0; i<131071; ++i) { + poly4 = (poly4+poly4) + (~((poly4 >> 2) ^ (poly4 >> 3)) & 1); + + mPolyBuffer[i] = (poly4 & 1) << 3; + } + + uint32 poly5 = 0; + for(int i=0; i<131071; ++i) { + poly5 = (poly5+poly5) + (~((poly5 >> 2) ^ (poly5 >> 4)) & 1); + + mPolyBuffer[i] |= (poly5 & 1) << 2; + } + + // The 17-bit polynomial counter is also of the XNOR variety, but one big + // difference is that you're allowed to read out 8 bits of it. The RANDOM + // register actually reports the INVERTED state of these bits (the Q' output + // of the flip flops is connected to the data bus). This means that even + // though we clear the register to 0, it reads as FF. + // + // From the perspective of the CPU through RANDOM, the LFSR shifts to the + // right, and new bits appear on the left. The equivalent operation for the + // 9-bit LFSR would be to set carry equal to (D0 ^ D5) and then execute + // a ROR. + uint32 poly9 = 0; + for(int i=0; i<131071; ++i) { + // Note: This one is actually verified against a real Atari. + // At WSYNC time, the pattern goes: 00 DF EE 16 B9.... + poly9 = (poly9 >> 1) + (~((poly9 << 8) ^ (poly9 << 3)) & 0x100); + + mPolyBuffer[i] |= (poly9 & 1) << 1; + } + + // The 17-bit mode inserts an additional 8 register bits immediately after + // the XNOR. The generator polynomial is unchanged. + uint32 poly17 = 0; + for(int i=0; i<131071; ++i) { + poly17 = (poly17 >> 1) + (~((poly17 << 16) ^ (poly17 << 11)) & 0x10000); + + mPolyBuffer[i] |= (poly17 >> 8) & 1; + } + + memcpy(mPolyBuffer + 131071, mPolyBuffer, 131071); + + memset(mInitModeBuffer, 0xFF, sizeof mInitModeBuffer); +} + +constexpr ATPokeyHiFilterTable ATMakePokeyHiFilterTable() { + constexpr double kRawFilter[][8] = { + // Coefficients for a windowed-sinc LPF at 24KHz cutoff with Kaiser window. Scilab derivation: + // + // pi=atan(1)*4; fc = 24000/(7159090/2); M=sinc(2*pi*fc*[-223:1:223]).*(2*fc).*window('kr',447,8.6); plot(M); M2=matrix(cat(2,[0],M),[56 8]); + // mprintf("{ %10.7ff, %10.7ff, %10.7ff, %10.7ff, %10.7ff, %10.7ff, %10.7ff, %10.7ff, },\n", M2) + // [hm, fr] = frmag(M, 1024); plot(fr, hm) + + { 0.0000000f, 0.0000885f, -0.0009574f, 0.0030938f, 0.0134095f, 0.0030938f, -0.0009574f, 0.0000885f, }, + { 0.0000001f, 0.0000887f, -0.0009856f, 0.0033114f, 0.0134045f, 0.0028807f, -0.0009282f, 0.0000879f, }, + { 0.0000002f, 0.0000886f, -0.0010127f, 0.0035331f, 0.0133893f, 0.0026721f, -0.0008981f, 0.0000870f, }, + { 0.0000003f, 0.0000880f, -0.0010385f, 0.0037588f, 0.0133641f, 0.0024683f, -0.0008673f, 0.0000858f, }, + { 0.0000005f, 0.0000870f, -0.0010629f, 0.0039882f, 0.0133288f, 0.0022694f, -0.0008359f, 0.0000844f, }, + { 0.0000007f, 0.0000856f, -0.0010857f, 0.0042211f, 0.0132836f, 0.0020756f, -0.0008041f, 0.0000827f, }, + { 0.0000009f, 0.0000836f, -0.0011068f, 0.0044572f, 0.0132284f, 0.0018870f, -0.0007718f, 0.0000808f, }, + { 0.0000012f, 0.0000811f, -0.0011260f, 0.0046964f, 0.0131635f, 0.0017037f, -0.0007393f, 0.0000787f, }, + { 0.0000016f, 0.0000780f, -0.0011431f, 0.0049382f, 0.0130888f, 0.0015259f, -0.0007067f, 0.0000764f, }, + { 0.0000020f, 0.0000743f, -0.0011581f, 0.0051825f, 0.0130046f, 0.0013537f, -0.0006740f, 0.0000740f, }, + { 0.0000024f, 0.0000700f, -0.0011707f, 0.0054289f, 0.0129110f, 0.0011872f, -0.0006414f, 0.0000715f, }, + { 0.0000030f, 0.0000651f, -0.0011808f, 0.0056771f, 0.0128081f, 0.0010264f, -0.0006090f, 0.0000689f, }, + { 0.0000036f, 0.0000594f, -0.0011882f, 0.0059269f, 0.0126961f, 0.0008714f, -0.0005767f, 0.0000662f, }, + { 0.0000042f, 0.0000531f, -0.0011927f, 0.0061779f, 0.0125751f, 0.0007223f, -0.0005448f, 0.0000635f, }, + { 0.0000049f, 0.0000460f, -0.0011942f, 0.0064298f, 0.0124455f, 0.0005791f, -0.0005133f, 0.0000607f, }, + { 0.0000058f, 0.0000381f, -0.0011926f, 0.0066822f, 0.0123074f, 0.0004419f, -0.0004823f, 0.0000579f, }, + { 0.0000066f, 0.0000294f, -0.0011876f, 0.0069349f, 0.0121610f, 0.0003106f, -0.0004518f, 0.0000551f, }, + { 0.0000076f, 0.0000199f, -0.0011791f, 0.0071875f, 0.0120066f, 0.0001852f, -0.0004218f, 0.0000523f, }, + { 0.0000087f, 0.0000095f, -0.0011669f, 0.0074396f, 0.0118444f, 0.0000658f, -0.0003926f, 0.0000495f, }, + { 0.0000098f, -0.0000017f, -0.0011508f, 0.0076910f, 0.0116746f, -0.0000476f, -0.0003640f, 0.0000467f, }, + { 0.0000111f, -0.0000138f, -0.0011308f, 0.0079411f, 0.0114977f, -0.0001552f, -0.0003362f, 0.0000440f, }, + { 0.0000124f, -0.0000269f, -0.0011067f, 0.0081898f, 0.0113138f, -0.0002568f, -0.0003092f, 0.0000413f, }, + { 0.0000139f, -0.0000408f, -0.0010782f, 0.0084366f, 0.0111233f, -0.0003527f, -0.0002830f, 0.0000387f, }, + { 0.0000154f, -0.0000558f, -0.0010453f, 0.0086812f, 0.0109264f, -0.0004427f, -0.0002577f, 0.0000361f, }, + { 0.0000170f, -0.0000716f, -0.0010078f, 0.0089232f, 0.0107235f, -0.0005271f, -0.0002332f, 0.0000336f, }, + { 0.0000188f, -0.0000884f, -0.0009656f, 0.0091622f, 0.0105149f, -0.0006058f, -0.0002097f, 0.0000312f, }, + { 0.0000206f, -0.0001062f, -0.0009185f, 0.0093980f, 0.0103009f, -0.0006791f, -0.0001871f, 0.0000289f, }, + { 0.0000225f, -0.0001250f, -0.0008664f, 0.0096301f, 0.0100819f, -0.0007468f, -0.0001654f, 0.0000267f, }, + { 0.0000246f, -0.0001447f, -0.0008092f, 0.0098581f, 0.0098581f, -0.0008092f, -0.0001447f, 0.0000246f, }, + { 0.0000267f, -0.0001654f, -0.0007468f, 0.0100819f, 0.0096301f, -0.0008664f, -0.0001250f, 0.0000225f, }, + { 0.0000289f, -0.0001871f, -0.0006791f, 0.0103009f, 0.0093980f, -0.0009185f, -0.0001062f, 0.0000206f, }, + { 0.0000312f, -0.0002097f, -0.0006058f, 0.0105149f, 0.0091622f, -0.0009656f, -0.0000884f, 0.0000188f, }, + { 0.0000336f, -0.0002332f, -0.0005271f, 0.0107235f, 0.0089232f, -0.0010078f, -0.0000716f, 0.0000170f, }, + { 0.0000361f, -0.0002577f, -0.0004427f, 0.0109264f, 0.0086812f, -0.0010453f, -0.0000558f, 0.0000154f, }, + { 0.0000387f, -0.0002830f, -0.0003527f, 0.0111233f, 0.0084366f, -0.0010782f, -0.0000408f, 0.0000139f, }, + { 0.0000413f, -0.0003092f, -0.0002568f, 0.0113138f, 0.0081898f, -0.0011067f, -0.0000269f, 0.0000124f, }, + { 0.0000440f, -0.0003362f, -0.0001552f, 0.0114977f, 0.0079411f, -0.0011308f, -0.0000138f, 0.0000111f, }, + { 0.0000467f, -0.0003640f, -0.0000476f, 0.0116746f, 0.0076910f, -0.0011508f, -0.0000017f, 0.0000098f, }, + { 0.0000495f, -0.0003926f, 0.0000658f, 0.0118444f, 0.0074396f, -0.0011669f, 0.0000095f, 0.0000087f, }, + { 0.0000523f, -0.0004218f, 0.0001852f, 0.0120066f, 0.0071875f, -0.0011791f, 0.0000199f, 0.0000076f, }, + { 0.0000551f, -0.0004518f, 0.0003106f, 0.0121610f, 0.0069349f, -0.0011876f, 0.0000294f, 0.0000066f, }, + { 0.0000579f, -0.0004823f, 0.0004419f, 0.0123074f, 0.0066822f, -0.0011926f, 0.0000381f, 0.0000058f, }, + { 0.0000607f, -0.0005133f, 0.0005791f, 0.0124455f, 0.0064298f, -0.0011942f, 0.0000460f, 0.0000049f, }, + { 0.0000635f, -0.0005448f, 0.0007223f, 0.0125751f, 0.0061779f, -0.0011927f, 0.0000531f, 0.0000042f, }, + { 0.0000662f, -0.0005767f, 0.0008714f, 0.0126961f, 0.0059269f, -0.0011882f, 0.0000594f, 0.0000036f, }, + { 0.0000689f, -0.0006090f, 0.0010264f, 0.0128081f, 0.0056771f, -0.0011808f, 0.0000651f, 0.0000030f, }, + { 0.0000715f, -0.0006414f, 0.0011872f, 0.0129110f, 0.0054289f, -0.0011707f, 0.0000700f, 0.0000024f, }, + { 0.0000740f, -0.0006740f, 0.0013537f, 0.0130046f, 0.0051825f, -0.0011581f, 0.0000743f, 0.0000020f, }, + { 0.0000764f, -0.0007067f, 0.0015259f, 0.0130888f, 0.0049382f, -0.0011431f, 0.0000780f, 0.0000016f, }, + { 0.0000787f, -0.0007393f, 0.0017037f, 0.0131635f, 0.0046964f, -0.0011260f, 0.0000811f, 0.0000012f, }, + { 0.0000808f, -0.0007718f, 0.0018870f, 0.0132284f, 0.0044572f, -0.0011068f, 0.0000836f, 0.0000009f, }, + { 0.0000827f, -0.0008041f, 0.0020756f, 0.0132836f, 0.0042211f, -0.0010857f, 0.0000856f, 0.0000007f, }, + { 0.0000844f, -0.0008359f, 0.0022694f, 0.0133288f, 0.0039882f, -0.0010629f, 0.0000870f, 0.0000005f, }, + { 0.0000858f, -0.0008673f, 0.0024683f, 0.0133641f, 0.0037588f, -0.0010385f, 0.0000880f, 0.0000003f, }, + { 0.0000870f, -0.0008981f, 0.0026721f, 0.0133893f, 0.0035331f, -0.0010127f, 0.0000886f, 0.0000002f, }, + { 0.0000879f, -0.0009282f, 0.0028807f, 0.0134045f, 0.0033114f, -0.0009856f, 0.0000887f, 0.0000001f, }, + }; + + static_assert(vdcountof(kRawFilter) == 56); + + ATPokeyHiFilterTable table {}; + + for(int i=0; i<=28; ++i) { + float sum = 0; + + for(int j=0; j<8; ++j) { + table.mFilter[i][j] = (float)kRawFilter[i][7-j]; + + sum += table.mFilter[i][j]; + } + + float scale = 1.0f / sum; + + for(float& v : table.mFilter[i]) + v *= scale; + } + + return table; +} + +constexpr ATPokeyHiFilterTable ATMakePokeyHiFilterTable2() { + constexpr ATPokeyHiFilterTable table = ATMakePokeyHiFilterTable(); + return table; +} + +extern const ATPokeyHiFilterTable g_ATPokeyHiFilterTable = ATMakePokeyHiFilterTable2();