From 8763a6169aa3442f8d1165af803cee93e72a676a Mon Sep 17 00:00:00 2001 From: rogerman Date: Fri, 2 Mar 2018 13:09:39 -0800 Subject: [PATCH] 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. --- desmume/src/frontend/windows/aviout.cpp | 248 +++++++++++++++++++----- desmume/src/frontend/windows/aviout.h | 41 +++- 2 files changed, 231 insertions(+), 58 deletions(-) diff --git a/desmume/src/frontend/windows/aviout.cpp b/desmume/src/frontend/windows/aviout.cpp index 64aa8088c..cd75bcabd 100755 --- a/desmume/src/frontend/windows/aviout.cpp +++ b/desmume/src/frontend/windows/aviout.cpp @@ -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 ¶m) { 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]); diff --git a/desmume/src/frontend/windows/aviout.h b/desmume/src/frontend/windows/aviout.h index b2595f58b..f9edf44b3 100755 --- a/desmume/src/frontend/windows/aviout.h +++ b/desmume/src/frontend/windows/aviout.h @@ -21,6 +21,11 @@ #include #include +#include + +#include +#include + #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 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 ¶m); + 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();