Sanitize and match emulation timing

-> no more perceivable audio latency
-> fewer underruns
This commit is contained in:
Christian Speckner 2018-05-05 00:47:48 +02:00
parent 9079d77de0
commit d2c930886b
20 changed files with 240 additions and 73 deletions

View File

@ -1,22 +1,13 @@
# Debugging
* Log and check how queue fills
# Bugs
* Fix timeslice size
# Refactoring
* Move timing related constants and logic to separate class
# Verify
* Verify that the base unit for chrono is seconds
* Verify that FPS are still measured correctly
# Missing features
* Reimplement target FPS mode
* Implement Lanzcos resampling
* Fixup OpenGL sync
* Fixup OpenGL sync, ensure that FB only rerenders after a frame has been generated
# Cleanup
* Document EmulationTiming

View File

@ -21,7 +21,7 @@ using std::mutex;
using std::lock_guard;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
AudioQueue::AudioQueue(uInt32 fragmentSize, uInt8 capacity, bool isStereo, uInt16 sampleRate)
AudioQueue::AudioQueue(uInt32 fragmentSize, uInt32 capacity, bool isStereo, uInt16 sampleRate)
: myFragmentSize(fragmentSize),
myIsStereo(isStereo),
mySampleRate(sampleRate),
@ -34,7 +34,7 @@ AudioQueue::AudioQueue(uInt32 fragmentSize, uInt8 capacity, bool isStereo, uInt1
myFragmentBuffer = new Int16[myFragmentSize * sampleSize * (capacity + 2)];
for (uInt8 i = 0; i < capacity; i++)
for (uInt32 i = 0; i < capacity; i++)
myFragmentQueue[i] = myAllFragments[i] = myFragmentBuffer + i * sampleSize * myFragmentSize;
myAllFragments[capacity] = myFirstFragmentForEnqueue =
@ -51,13 +51,13 @@ AudioQueue::~AudioQueue()
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt8 AudioQueue::capacity() const
uInt32 AudioQueue::capacity() const
{
return myFragmentQueue.size();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt8 AudioQueue::size()
uInt32 AudioQueue::size()
{
lock_guard<mutex> guard(myMutex);

View File

@ -45,7 +45,7 @@ class AudioQueue
@param isStereo Whether samples are stereo or mono.
@param sampleRate The sample rate. This is not used, but can be queried.
*/
AudioQueue(uInt32 fragmentSize, uInt8 capacity, bool isStereo, uInt16 sampleRate);
AudioQueue(uInt32 fragmentSize, uInt32 capacity, bool isStereo, uInt16 sampleRate);
/**
We need a destructor to deallocate the individual fragment buffers.
@ -55,12 +55,12 @@ class AudioQueue
/**
Capacity getter.
*/
uInt8 capacity() const;
uInt32 capacity() const;
/**
Size getter.
*/
uInt8 size();
uInt32 size();
/**
Stereo / mono getter.
@ -122,10 +122,10 @@ class AudioQueue
Int16* myFragmentBuffer;
// The nubmer if queued fragments
uInt8 mySize;
uInt32 mySize;
// The next fragment.
uInt8 myNextFragment;
uInt32 myNextFragment;
// We need a mutex for thread safety.
std::mutex myMutex;

View File

@ -21,6 +21,8 @@
#include "bspf.hxx"
#include "Sound.hxx"
#include "OSystem.hxx"
#include "AudioQueue.hxx"
#include "EmulationTiming.hxx"
/**
This class implements a Null sound object, where-by sound generation
@ -57,7 +59,7 @@ class SoundNull : public Sound
Initializes the sound device. This must be called before any
calls are made to derived methods.
*/
void open() override { }
void open(shared_ptr<AudioQueue>, EmulationTiming*) override { }
/**
Should be called to close the sound device. Once called the sound

View File

@ -29,6 +29,7 @@
#include "Console.hxx"
#include "SoundSDL2.hxx"
#include "AudioQueue.hxx"
#include "EmulationTiming.hxx"
namespace {
inline Int16 applyVolume(Int16 sample, Int32 volumeFactor)
@ -116,8 +117,10 @@ void SoundSDL2::setEnabled(bool state)
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL2::open(shared_ptr<AudioQueue> audioQueue)
void SoundSDL2::open(shared_ptr<AudioQueue> audioQueue, EmulationTiming* emulationTiming)
{
this->emulationTiming = emulationTiming;
myOSystem.logMessage("SoundSDL2::open started ...", 2);
mute(true);
@ -241,7 +244,7 @@ uInt32 SoundSDL2::getSampleRate() const
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void SoundSDL2::processFragment(Int16* stream, uInt32 length)
{
if (myUnderrun && myAudioQueue->size() > 0) {
if (myUnderrun && myAudioQueue->size() > emulationTiming->prebufferFragmentCount()) {
myUnderrun = false;
myCurrentFragment = myAudioQueue->dequeue(myCurrentFragment);
myFragmentIndex = 0;
@ -273,8 +276,10 @@ void SoundSDL2::processFragment(Int16* stream, uInt32 length)
Int16* nextFragment = myAudioQueue->dequeue(myCurrentFragment);
if (nextFragment)
myCurrentFragment = nextFragment;
else
else {
myUnderrun = true;
(cout << "audio underrun!\n").flush();
}
}
if (isStereo) {

View File

@ -22,6 +22,7 @@
class OSystem;
class AudioQueue;
class EmulationTiming;
#include "SDL_lib.hxx"
@ -59,7 +60,7 @@ class SoundSDL2 : public Sound
Initializes the sound device. This must be called before any
calls are made to derived methods.
*/
void open(shared_ptr<AudioQueue> audioQueue) override;
void open(shared_ptr<AudioQueue> audioQueue, EmulationTiming* emulationTiming) override;
/**
Should be called to close the sound device. Once called the sound
@ -124,6 +125,8 @@ class SoundSDL2 : public Sound
shared_ptr<AudioQueue> myAudioQueue;
EmulationTiming* emulationTiming;
Int16* myCurrentFragment;
uInt32 myTimeIndex;
uInt32 myFragmentIndex;

View File

@ -513,7 +513,7 @@ void Debugger::nextFrame(int frames)
unlockSystem();
while(frames)
{
myOSystem.console().tia().update();
myOSystem.console().tia().update(myOSystem.console().emulationTiming().maxCyclesPerTimeslice());
--frames;
}
lockSystem();

View File

@ -72,7 +72,6 @@
namespace {
constexpr uInt8 YSTART_EXTRA = 2;
constexpr uInt8 AUDIO_QUEUE_HALF_FRAMES_PER_FRAGMENT = 1;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -358,6 +357,7 @@ void Console::toggleFormat(int direction)
setTIAProperties();
myTIA->frameReset();
initializeVideo(); // takes care of refreshing the screen
initializeAudio(); // ensure that audio synthesis is set up to match emulation speed
myOSystem.frameBuffer().showMessage(message);
@ -556,7 +556,7 @@ void Console::initializeAudio()
createAudioQueue();
myTIA->setAudioQueue(myAudioQueue);
myOSystem.sound().open(myAudioQueue);
myOSystem.sound().open(myAudioQueue, &myEmulationTiming);
}
/* Original frying research and code by Fred Quimby.
@ -699,37 +699,18 @@ void Console::setTIAProperties()
myTIA->setYStart(ystart != 0 ? ystart : myAutodetectedYstart);
myTIA->setHeight(height);
myEmulationTiming.updateFrameLayout(myTIA->frameLayout());
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void Console::createAudioQueue()
{
uInt32 fragmentSize, sampleRate;
switch (myConsoleTiming) {
case ConsoleTiming::ntsc:
fragmentSize = 262 * AUDIO_QUEUE_HALF_FRAMES_PER_FRAGMENT;
sampleRate = 2 * 262 * 60;
break;
case ConsoleTiming::pal:
case ConsoleTiming::secam:
fragmentSize = 312 * AUDIO_QUEUE_HALF_FRAMES_PER_FRAGMENT;
sampleRate = 2 * 312 * 50;
break;
default:
throw runtime_error("invalid console timing");
}
uInt32 queueSize =
(2 * myOSystem.sound().getFragmentSize() * sampleRate) / (fragmentSize * myOSystem.sound().getSampleRate());
myAudioQueue = make_shared<AudioQueue>(
fragmentSize,
queueSize > 0 ? queueSize : 1,
myEmulationTiming.audioFragmentSize(),
myEmulationTiming.audioQueueCapacity(myOSystem.sound().getSampleRate(), myOSystem.sound().getFragmentSize()),
myProperties.get(Cartridge_Sound) == "STEREO",
sampleRate
myEmulationTiming.audioSampleRate()
);
}

View File

@ -37,6 +37,7 @@ class AudioQueue;
#include "Serializable.hxx"
#include "EventHandlerConstants.hxx"
#include "NTSCFilter.hxx"
#include "EmulationTiming.hxx"
#include "frame-manager/AbstractFrameManager.hxx"
/**
@ -190,6 +191,11 @@ class Console : public Serializable
*/
void stateChanged(EventHandlerState state);
/**
Retrieve emulation timing provider.
*/
EmulationTiming& emulationTiming() { return myEmulationTiming; }
public:
/**
Toggle between NTSC/PAL/SECAM (and variants) display format.
@ -416,7 +422,9 @@ class Console : public Serializable
// Contains timing information for this console
ConsoleTiming myConsoleTiming;
uInt32 myFramerate;
// Emulation timing provider. This ties together the timing of the core emulation loop
// and the audio synthesis parameters
EmulationTiming myEmulationTiming;
// Table of RGB values for NTSC, PAL and SECAM
static uInt32 ourNTSCPalette[256];

View File

@ -0,0 +1,103 @@
//============================================================================
//
// SSSS tt lll lll
// SS SS tt ll ll
// SS tttttt eeee ll ll aaaa
// SSSS tt ee ee ll ll aa
// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
// SS SS tt ee ll ll aa aa
// SSSS ttt eeeee llll llll aaaaa
//
// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony
// and the Stella Team
//
// See the file "License.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//============================================================================
#include "EmulationTiming.hxx"
namespace {
constexpr uInt32 AUDIO_HALF_FRAMES_PER_FRAGMENT = 1;
constexpr uInt32 QUEUE_CAPACITY_SAFETY_FACTOR = 2;
constexpr uInt32 PREBUFFER_FRAGMENT_COUNT = 4;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
EmulationTiming::EmulationTiming(FrameLayout frameLayout) : frameLayout(frameLayout) {}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void EmulationTiming::updateFrameLayout(FrameLayout frameLayout) {
this->frameLayout = frameLayout;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 EmulationTiming::maxCyclesPerTimeslice() const {
return (3 * cyclesPerFrame()) / 2;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 EmulationTiming::minCyclesPerTimeslice() const {
return cyclesPerFrame() / 2;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 EmulationTiming::linesPerFrame() const {
switch (frameLayout) {
case FrameLayout::ntsc:
return 262;
case FrameLayout::pal:
return 312;
default:
throw runtime_error("invalid frame layout");
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 EmulationTiming::cyclesPerFrame() const {
return 76 * linesPerFrame();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 EmulationTiming::framesPerSecond() const {
switch (frameLayout) {
case FrameLayout::ntsc:
return 60;
case FrameLayout::pal:
return 50;
default:
throw runtime_error("invalid frame layout");
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 EmulationTiming::cyclesPerSecond() const {
return cyclesPerFrame() * framesPerSecond();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 EmulationTiming::audioFragmentSize() const {
return AUDIO_HALF_FRAMES_PER_FRAGMENT * linesPerFrame();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 EmulationTiming::audioSampleRate() const {
return 2 * linesPerFrame() * framesPerSecond();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 EmulationTiming::audioQueueCapacity(uInt32 playbackRate, uInt32 playbackFragmentSize) const {
uInt32 capacity = (playbackFragmentSize * audioSampleRate()) / (audioFragmentSize() * playbackRate) + 1;
uInt32 minCapacity = (maxCyclesPerTimeslice() * audioSampleRate()) / (audioFragmentSize() * cyclesPerSecond()) + 1;
return std::max(prebufferFragmentCount() + 1, QUEUE_CAPACITY_SAFETY_FACTOR * std::max(capacity, minCapacity));
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt32 EmulationTiming::prebufferFragmentCount() const {
return PREBUFFER_FRAGMENT_COUNT;
}

View File

@ -0,0 +1,57 @@
//============================================================================
//
// SSSS tt lll lll
// SS SS tt ll ll
// SS tttttt eeee ll ll aaaa
// SSSS tt ee ee ll ll aa
// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
// SS SS tt ee ll ll aa aa
// SSSS ttt eeeee llll llll aaaaa
//
// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony
// and the Stella Team
//
// See the file "License.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//============================================================================
#ifndef EMULATION_TIMING_HXX
#define EMULATION_TIMING_HXX
#include "bspf.hxx"
#include "FrameLayout.hxx"
class EmulationTiming {
public:
EmulationTiming(FrameLayout frameLayout = FrameLayout::ntsc);
void updateFrameLayout(FrameLayout frameLayout);
uInt32 maxCyclesPerTimeslice() const;
uInt32 minCyclesPerTimeslice() const;
uInt32 linesPerFrame() const;
uInt32 cyclesPerFrame() const;
uInt32 framesPerSecond() const;
uInt32 cyclesPerSecond() const;
uInt32 audioFragmentSize() const;
uInt32 audioSampleRate() const;
uInt32 audioQueueCapacity(uInt32 playbackRate, uInt32 playbackFragmentSize) const;
uInt32 prebufferFragmentCount() const;
private:
FrameLayout frameLayout;
};
#endif // EMULATION_TIMING_HXX

View File

@ -258,7 +258,7 @@ FBInitStatus FrameBuffer::createDisplay(const string& title,
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Int64 FrameBuffer::update()
Int64 FrameBuffer::update(uInt32 maxCycles)
{
// Determine which mode we are in (from the EventHandler)
// Take care of S_EMULATE mode here, otherwise let the GUI
@ -274,7 +274,7 @@ Int64 FrameBuffer::update()
// Run the console for one frame
// Note that the debugger can cause a breakpoint to occur, which changes
// the EventHandler state 'behind our back' - we need to check for that
cycles = myOSystem.console().tia().update();
cycles = myOSystem.console().tia().update(maxCycles);
#ifdef DEBUGGER_SUPPORT
if(myOSystem.eventHandler().state() != EventHandlerState::EMULATION) break;
#endif

View File

@ -116,7 +116,7 @@ class FrameBuffer
drawing the TIA, any pending menus, etc. Returns the numbers of CPU cycles
spent during emulation, or -1 if not applicable.
*/
Int64 update();
Int64 update(uInt32 maxCycles = 50000);
/**
Shows a message onscreen.

View File

@ -229,7 +229,7 @@ bool M6502::execute(uInt32 number)
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
inline bool M6502::_execute(uInt32 number)
inline bool M6502::_execute(uInt32 cycles)
{
// Clear all of the execution status bits except for the fatal error bit
myExecutionStatus &= FatalErrorBit;
@ -239,10 +239,12 @@ inline bool M6502::_execute(uInt32 number)
M6532& riot = mySystem->m6532();
#endif
uInt32 currentCycles = 0;
// Loop until execution is stopped or a fatal error occurs
for(;;)
{
for(; !myExecutionStatus && (number != 0); --number)
for(; !myExecutionStatus && (currentCycles < cycles * SYSTEM_CYCLES_PER_CPU); currentCycles += SYSTEM_CYCLES_PER_CPU)
{
#ifdef DEBUGGER_SUPPORT
if(myJustHitReadTrapFlag || myJustHitWriteTrapFlag)
@ -329,7 +331,7 @@ inline bool M6502::_execute(uInt32 number)
}
// See if we've executed the specified number of instructions
if(number == 0)
if (currentCycles >= cycles * SYSTEM_CYCLES_PER_CPU)
{
// Yes, so answer that everything finished fine
return true;

View File

@ -110,10 +110,12 @@ class M6502 : public Serializable
is executed, someone stops execution, or an error occurs. Answers
true iff execution stops normally.
@param number Indicates the number of instructions to execute
@param number Indicates the number of cycles to execute. Not that the actual
granularity of the CPU is instructions, so this is only accurate up to
a couple of cycles
@return true iff execution stops normally
*/
bool execute(uInt32 number);
bool execute(uInt32 cycles);
/**
Tell the processor to stop executing instructions. Invoking this
@ -318,7 +320,7 @@ class M6502 : public Serializable
This is the actual dispatch function that does the grunt work. M6502::execute
wraps it and makes sure that any pending halt is processed before returning.
*/
bool _execute(uInt32 number);
bool _execute(uInt32 cycles);
#ifdef DEBUGGER_SUPPORT
/**

View File

@ -646,10 +646,21 @@ void OSystem::mainLoop()
myEventHandler->poll(getTicks());
if(myQuitLoop) break; // Exit if the user wants to quit
Int64 cycles = myFrameBuffer->update();
Int64 totalCycles = 0;
const Int64 minCycles = myConsole ? myConsole->emulationTiming().minCyclesPerTimeslice() : 50000;
const Int64 maxCycles = myConsole ? myConsole->emulationTiming().maxCyclesPerTimeslice() : 0;
const uInt32 cyclesPerSecond = myConsole ? myConsole->emulationTiming().cyclesPerSecond() : 1;
do {
Int64 cycles = myFrameBuffer->update(totalCycles > 0 ? minCycles - totalCycles : maxCycles);
if (cycles < 0) break;
totalCycles += cycles;
} while (totalCycles < minCycles);
duration<double> timeslice (
(cycles >= 0) ?
static_cast<double>(cycles) / static_cast<double>(76 * ((myConsole->timing() == ConsoleTiming::ntsc) ? (262 * 60) : (312 * 50))) :
(totalCycles > 0) ?
static_cast<double>(totalCycles) / static_cast<double>(cyclesPerSecond) :
1. / 30.
);
@ -659,7 +670,7 @@ void OSystem::mainLoop()
if (duration_cast<duration<double>>(now - virtualTime).count() > 0)
virtualTime = now;
else if (virtualTime > now) {
if (busyWait && cycles >= 0) {
if (busyWait && totalCycles > 0) {
while (high_resolution_clock::now() < virtualTime);
}
else std::this_thread::sleep_until(virtualTime);

View File

@ -20,6 +20,7 @@
class OSystem;
class AudioQueue;
class EmulationTiming;
#include "bspf.hxx"
@ -51,7 +52,7 @@ class Sound
Start the sound system, initializing it if necessary. This must be
called before any calls are made to derived methods.
*/
virtual void open(shared_ptr<AudioQueue> audioQueue) = 0;
virtual void open(shared_ptr<AudioQueue>, EmulationTiming*) = 0;
/**
Should be called to stop the sound system. Once called the sound

View File

@ -53,6 +53,7 @@ MODULE_OBJS := \
src/emucore/Control.o \
src/emucore/Driving.o \
src/emucore/EventHandler.o \
src/emucore/EmulationTiming.o \
src/emucore/FrameBuffer.o \
src/emucore/FBSurface.o \
src/emucore/FSNode.o \

View File

@ -805,11 +805,11 @@ bool TIA::loadDisplay(Serializer& in)
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt64 TIA::update()
uInt64 TIA::update(uInt32 maxCycles)
{
uInt64 timestampOld = myTimestamp;
mySystem->m6502().execute(25000);
mySystem->m6502().execute(maxCycles);
updateEmulation();
return (myTimestamp - timestampOld) / 3;

View File

@ -199,7 +199,7 @@ class TIA : public Device
desired frame rate to update the TIA. Invoking this method will update
the graphics buffer and generate the corresponding audio samples.
*/
uInt64 update();
uInt64 update(uInt32 maxCycles = 50000);
/**
Returns a pointer to the internal frame buffer.