Windows Port: AVI recording now maintains many more frames in memory. Also, the framebuffer color conversion threads and file writing thread are more free running threads.

- These changes help to stabilize the performance of AVI recording, making it less sensitive to sudden changes in disk writing speed.
- The maximum amount of frames maintained in memory will either be 1.5 GB worth or 180 frames (or 3 seconds) worth, whichever is less.
This commit is contained in:
rogerman 2018-03-02 13:09:39 -08:00
parent db1a19ad86
commit 8763a6169a
2 changed files with 231 additions and 58 deletions

View File

@ -61,8 +61,8 @@ static void* RunConvertVideoSlice888XTo888(void *arg)
static void* RunAviFileWrite(void *arg)
{
AVIFileWriteParam *fileWriteParam = (AVIFileWriteParam *)arg;
fileWriteParam->fs->WriteOneFrame(fileWriteParam->srcVideo, fileWriteParam->videoBufferSize, fileWriteParam->srcAudio, fileWriteParam->audioBufferSize);
AVIFileStream *fs = (AVIFileStream *)arg;
fs->WriteAllFrames();
return NULL;
}
@ -97,15 +97,40 @@ AVIFileStream::AVIFileStream()
_expectedFrameSize = 0;
_writtenVideoFrameCount = 0;
_writtenAudioSampleCount = 0;
_writtenBytes = 0;
_writtenBytes = 0;
_semQueue = NULL;
_mutexQueue = slock_new();
}
AVIFileStream::~AVIFileStream()
{
this->Close();
this->Close(FSCA_WriteRemainingInQueue);
slock_free(this->_mutexQueue);
ssem_free(this->_semQueue);
}
HRESULT AVIFileStream::Open(const char *fileName, BITMAPINFOHEADER *bmpFormat, WAVEFORMATEX *wavFormat)
size_t AVIFileStream::GetExpectedFrameSize(const BITMAPINFOHEADER *bmpFormat, const WAVEFORMATEX *wavFormat)
{
size_t expectedFrameSize = 0; // The expected frame size is the video frame size plus the sum of the audio samples.
if (bmpFormat != NULL)
{
expectedFrameSize += bmpFormat->biSizeImage;
}
if (wavFormat != NULL)
{
// Since the number of audio samples may not be exactly the same for each video frame,
// we double the expected size of the audio buffer for safety.
expectedFrameSize += ((wavFormat->nAvgBytesPerSec / 60) * 2);
}
return expectedFrameSize;
}
HRESULT AVIFileStream::Open(const char *fileName, BITMAPINFOHEADER *bmpFormat, WAVEFORMATEX *wavFormat, size_t pendingFrameCount)
{
HRESULT error = S_OK;
@ -154,10 +179,9 @@ HRESULT AVIFileStream::Open(const char *fileName, BITMAPINFOHEADER *bmpFormat, W
this->_streamInfo[AUDIO_STREAM].dwSampleSize = this->_wavFormat.nBlockAlign;
}
// The expected frame size is the video frame size plus the sum of the audio samples.
// Since the number of audio samples may not be exactly the same for each video frame,
// we double the expected size of the audio buffer for safety.
this->_expectedFrameSize = this->_bmpFormat.biSizeImage + ((this->_wavFormat.nAvgBytesPerSec / 60) * 2);
this->_expectedFrameSize = AVIFileStream::GetExpectedFrameSize(bmpFormat, wavFormat);
_semQueue = ssem_new(pendingFrameCount);
}
else
{
@ -244,13 +268,58 @@ HRESULT AVIFileStream::Open(const char *fileName, BITMAPINFOHEADER *bmpFormat, W
return error;
}
void AVIFileStream::Close()
void AVIFileStream::Close(FileStreamCloseAction theAction)
{
if (this->_file == NULL)
{
return;
}
switch (theAction)
{
case FSCA_PurgeQueue:
{
AVIWriteQueue clearQueue;
slock_lock(this->_mutexQueue);
this->_writeQueue.swap(clearQueue);
slock_unlock(this->_mutexQueue);
const size_t remainingFrameCount = (size_t)ssem_get(this->_semQueue);
for (size_t i = 0; i < remainingFrameCount; i++)
{
ssem_signal(this->_semQueue);
}
break;
}
case FSCA_WriteRemainingInQueue:
{
do
{
slock_lock(this->_mutexQueue);
if (this->_writeQueue.empty())
{
slock_unlock(this->_mutexQueue);
break;
}
const AVIFileWriteParam param = this->_writeQueue.front();
slock_unlock(this->_mutexQueue);
this->WriteOneFrame(param);
slock_lock(this->_mutexQueue);
this->_writeQueue.pop();
slock_unlock(this->_mutexQueue);
ssem_signal(this->_semQueue);
} while (true);
break;
}
}
// _compressedStream[AUDIO_STREAM] is just a copy of _stream[AUDIO_STREAM]
if (this->_compressedStream[AUDIO_STREAM])
{
@ -284,6 +353,38 @@ bool AVIFileStream::IsValid()
return (this->_file != NULL);
}
void AVIFileStream::QueueAdd(u8 *srcVideo, const size_t videoBufferSize, u8 *srcAudio, const size_t audioBufferSize)
{
AVIFileWriteParam newParam;
newParam.fs = this;
newParam.srcVideo = srcVideo;
newParam.videoBufferSize = videoBufferSize;
newParam.srcAudio = srcAudio;
newParam.audioBufferSize = audioBufferSize;
ssem_wait(this->_semQueue);
slock_lock(this->_mutexQueue);
this->_writeQueue.push(newParam);
slock_unlock(this->_mutexQueue);
}
void AVIFileStream::QueueWait()
{
// If the queue if full, the ssem_wait() will force a wait until the current frame is finished writing.
ssem_wait(this->_semQueue);
ssem_signal(this->_semQueue);
}
size_t AVIFileStream::GetQueueSize()
{
slock_lock(this->_mutexQueue);
const size_t queueSize = this->_writeQueue.size();
slock_unlock(this->_mutexQueue);
return queueSize;
}
HRESULT AVIFileStream::FlushVideo(u8 *srcBuffer, const LONG bufferSize)
{
HRESULT error = S_OK;
@ -341,21 +442,21 @@ HRESULT AVIFileStream::FlushAudio(u8 *srcBuffer, const LONG bufferSize)
return error;
}
HRESULT AVIFileStream::WriteOneFrame(u8 *srcVideo, const LONG videoBufferSize, u8 *srcAudio, const LONG audioBufferSize)
HRESULT AVIFileStream::WriteOneFrame(const AVIFileWriteParam &param)
{
HRESULT error = S_OK;
error = this->FlushVideo(srcVideo, videoBufferSize);
error = this->FlushVideo(param.srcVideo, param.videoBufferSize);
if (FAILED(error))
{
this->Close();
this->Close(FSCA_PurgeQueue);
return error;
}
error = this->FlushAudio(srcAudio, audioBufferSize);
error = this->FlushAudio(param.srcAudio, param.audioBufferSize);
if (FAILED(error))
{
this->Close();
this->Close(FSCA_PurgeQueue);
return error;
}
@ -363,14 +464,14 @@ HRESULT AVIFileStream::WriteOneFrame(u8 *srcVideo, const LONG videoBufferSize, u
const size_t futureFrameSize = this->_writtenBytes + this->_expectedFrameSize;
if (futureFrameSize >= MAX_AVI_FILE_SIZE)
{
this->Close();
this->Close(FSCA_DoNothing);
this->_segmentNumber++;
error = this->Open(NULL, NULL, NULL);
error = this->Open(NULL, NULL, NULL, 0);
if (FAILED(error))
{
EMU_PrintError("Error creating new AVI segment.");
this->Close();
this->Close(FSCA_PurgeQueue);
return error;
}
}
@ -378,22 +479,55 @@ HRESULT AVIFileStream::WriteOneFrame(u8 *srcVideo, const LONG videoBufferSize, u
return error;
}
HRESULT AVIFileStream::WriteAllFrames()
{
HRESULT error = S_OK;
do
{
slock_lock(this->_mutexQueue);
if (this->_writeQueue.empty())
{
slock_unlock(this->_mutexQueue);
break;
}
const AVIFileWriteParam param = this->_writeQueue.front();
slock_unlock(this->_mutexQueue);
error = this->WriteOneFrame(param);
slock_lock(this->_mutexQueue);
this->_writeQueue.pop();
slock_unlock(this->_mutexQueue);
ssem_signal(this->_semQueue);
if (FAILED(error))
{
return error;
}
} while (true);
return error;
}
NDSCaptureObject::NDSCaptureObject()
{
// create the video stream
// Create the format structs.
memset(&_bmpFormat, 0, sizeof(BITMAPINFOHEADER));
_bmpFormat.biSize = 0x28;
_bmpFormat.biPlanes = 1;
_bmpFormat.biBitCount = 24;
_pendingVideoBuffer = NULL;
memset(_pendingAudioBuffer, 0, AUDIO_STREAM_BUFFER_SIZE * PENDING_BUFFER_COUNT * sizeof(u8));
memset(&_wavFormat, 0, sizeof(WAVEFORMATEX));
_pendingVideoBuffer = NULL;
_pendingAudioBuffer = NULL;
_pendingAudioWriteSize = NULL;
_pendingBufferCount = 0;
_currentBufferIndex = 0;
_pendingAudioWriteSize[0] = 0;
_pendingAudioWriteSize[1] = 0;
// Create the colorspace conversion threads.
_numThreads = CommonSettings.num_cores;
@ -419,12 +553,6 @@ NDSCaptureObject::NDSCaptureObject()
// Generate the AVI file streams.
_fs = new AVIFileStream;
_fileWriteParam.fs = _fs;
_fileWriteParam.srcVideo = NULL;
_fileWriteParam.videoBufferSize = 0;
_fileWriteParam.srcAudio = this->_pendingAudioBuffer;
_fileWriteParam.audioBufferSize = 0;
_fileWriteThread = new Task();
_fileWriteThread->start(false);
}
@ -437,13 +565,23 @@ NDSCaptureObject::NDSCaptureObject(size_t frameWidth, size_t frameHeight, const
_bmpFormat.biHeight = frameHeight * 2;
_bmpFormat.biSizeImage = _bmpFormat.biWidth * _bmpFormat.biHeight * 3;
_pendingVideoBuffer = (u8*)malloc_alignedCacheLine(_bmpFormat.biSizeImage * PENDING_BUFFER_COUNT);
if (wfex != NULL)
{
_wavFormat = *wfex;
}
const size_t expectedFrameSize = AVIFileStream::GetExpectedFrameSize(&_bmpFormat, wfex);
_pendingBufferCount = MAX_PENDING_BUFFER_SIZE / expectedFrameSize;
if (_pendingBufferCount > MAX_PENDING_FRAME_COUNT)
{
_pendingBufferCount = MAX_PENDING_FRAME_COUNT;
}
_pendingVideoBuffer = (u8 *)malloc_alignedCacheLine(_bmpFormat.biSizeImage * _pendingBufferCount * sizeof(u8));
_pendingAudioBuffer = (u8 *)malloc_alignedCacheLine(AUDIO_STREAM_BUFFER_SIZE * _pendingBufferCount * sizeof(u8));
_pendingAudioWriteSize = (size_t *)calloc(_pendingBufferCount, sizeof(size_t));
const size_t linesPerThread = (_numThreads > 0) ? _bmpFormat.biHeight / _numThreads : _bmpFormat.biHeight;
if (_numThreads == 0)
@ -482,9 +620,6 @@ NDSCaptureObject::NDSCaptureObject(size_t frameWidth, size_t frameHeight, const
_convertParam[i].frameWidth = _bmpFormat.biWidth;
}
}
_fileWriteParam.srcVideo = _pendingVideoBuffer;
_fileWriteParam.videoBufferSize = _bmpFormat.biSizeImage;
}
NDSCaptureObject::~NDSCaptureObject()
@ -501,16 +636,18 @@ NDSCaptureObject::~NDSCaptureObject()
delete this->_fs;
free_aligned(this->_pendingVideoBuffer);
free_aligned(this->_pendingAudioBuffer);
free(this->_pendingAudioWriteSize);
}
HRESULT NDSCaptureObject::OpenFileStream(const char *fileName)
{
return this->_fs->Open(fileName, &this->_bmpFormat, &this->_wavFormat);
return this->_fs->Open(fileName, &this->_bmpFormat, &this->_wavFormat, this->_pendingBufferCount);
}
void NDSCaptureObject::CloseFileStream()
{
this->_fs->Close();
this->_fs->Close(FSCA_WriteRemainingInQueue);
}
bool NDSCaptureObject::IsFileStreamValid()
@ -520,34 +657,41 @@ bool NDSCaptureObject::IsFileStreamValid()
void NDSCaptureObject::StartFrame()
{
for (size_t i = 0; i < this->_numThreads; i++)
{
this->_convertThread[i]->finish();
}
// If the queue is full, then we need to wait for some frames to finish writing
// before we continue adding new frames to the queue.
this->_fs->QueueWait();
this->_currentBufferIndex = ((this->_currentBufferIndex + 1) % PENDING_BUFFER_COUNT);
this->_currentBufferIndex = ((this->_currentBufferIndex + 1) % this->_pendingBufferCount);
this->_pendingAudioWriteSize[this->_currentBufferIndex] = 0;
}
void NDSCaptureObject::StreamWriteStart()
{
const size_t bufferIndex = this->_currentBufferIndex;
const size_t queueSize = this->_fs->GetQueueSize();
const bool isQueueEmpty = (queueSize == 0);
if (this->_bmpFormat.biSizeImage > 0)
// If there are no frames in the current write queue, then we know that the current
// pending video frame will be written immediately. If this is the case, then we
// need to force the video conversion to finish so that we can write out the frame.
if (isQueueEmpty)
{
for (size_t i = 0; i < this->_numThreads; i++)
if (this->_bmpFormat.biSizeImage > 0)
{
this->_convertThread[i]->finish();
for (size_t i = 0; i < this->_numThreads; i++)
{
this->_convertThread[i]->finish();
}
}
}
this->_fileWriteThread->finish();
this->_fs->QueueAdd(this->_pendingVideoBuffer + (this->_bmpFormat.biSizeImage * bufferIndex), this->_bmpFormat.biSizeImage,
this->_pendingAudioBuffer + (AUDIO_STREAM_BUFFER_SIZE * bufferIndex), this->_pendingAudioWriteSize[bufferIndex]);
this->_fileWriteParam.srcVideo = this->_pendingVideoBuffer + (this->_bmpFormat.biSizeImage * bufferIndex);
this->_fileWriteParam.srcAudio = this->_pendingAudioBuffer + (AUDIO_STREAM_BUFFER_SIZE * bufferIndex);
this->_fileWriteParam.audioBufferSize = this->_pendingAudioWriteSize[bufferIndex];
this->_fileWriteThread->execute(&RunAviFileWrite, &this->_fileWriteParam);
if (isQueueEmpty)
{
this->_fileWriteThread->execute(&RunAviFileWrite, this->_fs);
}
}
void NDSCaptureObject::StreamWriteFinish()
@ -608,6 +752,7 @@ void NDSCaptureObject::ReadVideoFrame(const void *srcVideoFrame, const size_t in
{
for (size_t i = 0; i < this->_numThreads; i++)
{
this->_convertThread[i]->finish();
this->_convertParam[i].src = (u16 *)srcVideoFrame + this->_convertParam[i].srcOffset;
this->_convertParam[i].dst = convertBufferHead + this->_convertParam[i].dstOffset;
this->_convertThread[i]->execute(&RunConvertVideoSlice555XTo888, &this->_convertParam[i]);
@ -626,6 +771,7 @@ void NDSCaptureObject::ReadVideoFrame(const void *srcVideoFrame, const size_t in
{
for (size_t i = 0; i < this->_numThreads; i++)
{
this->_convertThread[i]->finish();
this->_convertParam[i].src = (u32 *)srcVideoFrame + this->_convertParam[i].srcOffset;
this->_convertParam[i].dst = convertBufferHead + this->_convertParam[i].dstOffset;
this->_convertThread[i]->execute(&RunConvertVideoSlice888XTo888, &this->_convertParam[i]);

View File

@ -21,6 +21,11 @@
#include <windows.h>
#include <vfw.h>
#include <queue>
#include <rthreads/rthreads.h>
#include <rthreads/rsemaphore.h>
#include "GPU.h"
#include "SPU.h"
#include "utils/task.h"
@ -31,7 +36,16 @@
#define AUDIO_STREAM_BUFFER_SIZE ((DESMUME_SAMPLE_RATE * sizeof(u16) * 2) / 30) // 16-bit samples, 2 channels, 2 frames (need only 1 frame's worth, but have 2 frame's worth for safety)
#define MAX_AVI_FILE_SIZE (2ULL * 1024ULL * 1024ULL * 1024ULL) // Max file size should be 2 GB due to the Video for Windows AVI limit.
#define PENDING_BUFFER_COUNT 2 // Number of pending buffers to maintain for file writes. Each pending buffer will store 1 frame's worth of the audio/video streams.
#define MAX_PENDING_BUFFER_SIZE (1536ULL * 1024ULL * 1024ULL) // Max pending buffer size should not exceed 1.5 GB.
#define MAX_PENDING_FRAME_COUNT 180 // Maintain up to 180 frames in memory for current and future file writes. This is equivalent to 3 seconds worth of frames.
enum FileStreamCloseAction
{
FSCA_DoNothing = 0,
FSCA_PurgeQueue = 1,
FSCA_WriteRemainingInQueue = 2
};
class NDSCaptureObject;
class AVIFileStream;
@ -62,6 +76,8 @@ struct AVIFileWriteParam
};
typedef struct AVIFileWriteParam AVIFileWriteParam;
typedef std::queue<AVIFileWriteParam> AVIWriteQueue;
class AVIFileStream
{
private:
@ -86,17 +102,28 @@ protected:
LONG _writtenVideoFrameCount;
LONG _writtenAudioSampleCount;
ssem_t *_semQueue;
slock_t *_mutexQueue;
AVIWriteQueue _writeQueue;
public:
AVIFileStream();
~AVIFileStream();
HRESULT Open(const char *fileName, BITMAPINFOHEADER *bmpFormat, WAVEFORMATEX *wavFormat);
void Close();
static size_t GetExpectedFrameSize(const BITMAPINFOHEADER *bmpFormat, const WAVEFORMATEX *wavFormat);
HRESULT Open(const char *fileName, BITMAPINFOHEADER *bmpFormat, WAVEFORMATEX *wavFormat, size_t pendingFrameCount);
void Close(FileStreamCloseAction theAction);
bool IsValid();
void QueueAdd(u8 *srcVideo, const size_t videoBufferSize, u8 *srcAudio, const size_t audioBufferSize);
void QueueWait();
size_t GetQueueSize();
HRESULT FlushVideo(u8 *srcBuffer, const LONG bufferSize);
HRESULT FlushAudio(u8 *srcBuffer, const LONG bufferSize);
HRESULT WriteOneFrame(u8 *srcVideo, const LONG videoBufferSize, u8 *srcAudio, const LONG audioBufferSize);
HRESULT WriteOneFrame(const AVIFileWriteParam &param);
HRESULT WriteAllFrames();
};
class NDSCaptureObject
@ -108,15 +135,15 @@ protected:
WAVEFORMATEX _wavFormat;
u8 *_pendingVideoBuffer;
u8 _pendingAudioBuffer[AUDIO_STREAM_BUFFER_SIZE * PENDING_BUFFER_COUNT];
size_t _pendingAudioWriteSize[2];
u8 *_pendingAudioBuffer;
size_t *_pendingAudioWriteSize;
size_t _pendingBufferCount;
size_t _currentBufferIndex;
size_t _numThreads;
Task *_fileWriteThread;
Task *_convertThread[MAX_CONVERT_THREADS];
VideoConvertParam _convertParam[MAX_CONVERT_THREADS];
AVIFileWriteParam _fileWriteParam;
public:
NDSCaptureObject();