diff --git a/desmume/src/frontend/cocoa/ClientAVCaptureObject.cpp b/desmume/src/frontend/cocoa/ClientAVCaptureObject.cpp
new file mode 100644
index 000000000..b86280f99
--- /dev/null
+++ b/desmume/src/frontend/cocoa/ClientAVCaptureObject.cpp
@@ -0,0 +1,563 @@
+/*
+ Copyright (C) 2018 DeSmuME team
+
+ This file 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 file 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 the this software. If not, see .
+ */
+
+#include "ClientAVCaptureObject.h"
+
+#include "../../common.h"
+#include "../../NDSSystem.h"
+#include "../../GPU.h"
+
+
+static void* RunConvertVideoSlice555XTo888(void *arg)
+{
+ VideoConvertParam *convertParam = (VideoConvertParam *)arg;
+ convertParam->captureObj->ConvertVideoSlice555Xto888(*convertParam);
+
+ return NULL;
+}
+
+static void* RunConvertVideoSlice888XTo888(void *arg)
+{
+ VideoConvertParam *convertParam = (VideoConvertParam *)arg;
+ convertParam->captureObj->ConvertVideoSlice888Xto888(*convertParam);
+
+ return NULL;
+}
+
+static void* RunAviFileWrite(void *arg)
+{
+ ClientAVCaptureFileStream *fs = (ClientAVCaptureFileStream *)arg;
+ fs->WriteAllFrames();
+
+ return NULL;
+}
+
+ClientAVCaptureFileStream::ClientAVCaptureFileStream()
+{
+ _videoWidth = GPU_FRAMEBUFFER_NATIVE_WIDTH;
+ _videoHeight = GPU_FRAMEBUFFER_NATIVE_HEIGHT;
+
+ _fileTypeID = AVFileTypeID_mp4_H264_AAC;
+ _fileTypeContainerID = (AVFileTypeContainerID)(_fileTypeID & AVFILETYPEID_CONTAINERMASK);
+ _fileTypeVideoCodecID = (AVFileTypeVideoCodecID)(_fileTypeID & AVFILETYPEID_VIDEOCODECMASK);
+ _fileTypeAudioCodecID = (AVFileTypeAudioCodecID)(_fileTypeID & AVFILETYPEID_AUDIOCODECMASK);
+
+ _baseFileName = "untitled";
+ _baseFileNameExt = "mp4";
+ _segmentNumber = 0;
+
+ _maxSegmentSize = (4ULL * 1024ULL * 1024ULL * 1024ULL * 1024ULL); // Default to 4TB segments.
+ _expectedMaxFrameSize = 0;
+ _writtenVideoFrameCount = 0;
+ _writtenAudioSampleCount = 0;
+ _writtenBytes = 0;
+
+ _semQueue = NULL;
+ _mutexQueue = slock_new();
+}
+
+ClientAVCaptureFileStream::~ClientAVCaptureFileStream()
+{
+ slock_free(this->_mutexQueue);
+ ssem_free(this->_semQueue);
+}
+
+void ClientAVCaptureFileStream::InitBaseProperties(const AVFileTypeID fileTypeID, const std::string &fileName,
+ const size_t videoWidth, const size_t videoHeight,
+ size_t pendingFrameCount)
+{
+ // Set up the stream formats that will be used for all AVI segments.
+ this->_fileTypeID = fileTypeID;
+ this->_fileTypeContainerID = (AVFileTypeContainerID)(fileTypeID & AVFILETYPEID_CONTAINERMASK);
+ this->_fileTypeVideoCodecID = (AVFileTypeVideoCodecID)(fileTypeID & AVFILETYPEID_VIDEOCODECMASK);
+ this->_fileTypeAudioCodecID = (AVFileTypeAudioCodecID)(fileTypeID & AVFILETYPEID_AUDIOCODECMASK);
+
+ switch (this->_fileTypeContainerID)
+ {
+ case AVFileTypeContainerID_mp4:
+ this->_baseFileNameExt = "mp4";
+ break;
+
+ case AVFileTypeContainerID_avi:
+ this->_baseFileNameExt = "avi";
+ break;
+
+ case AVFileTypeContainerID_mkv:
+ this->_baseFileNameExt = "mkv";
+ break;
+
+ case AVFileTypeContainerID_mov:
+ this->_baseFileNameExt = "mov";
+ break;
+
+ case AVFileTypeContainerID_m4a:
+ this->_baseFileNameExt = "m4a";
+ break;
+
+ case AVFileTypeContainerID_aiff:
+ this->_baseFileNameExt = "aiff";
+ break;
+
+ case AVFileTypeContainerID_flac:
+ this->_baseFileNameExt = "flac";
+ break;
+
+ case AVFileTypeContainerID_wav:
+ this->_baseFileNameExt = "wav";
+ break;
+
+ default:
+ break;
+ }
+
+ this->_baseFileName = fileName;
+ this->_videoWidth = videoWidth;
+ this->_videoHeight = videoHeight;
+
+ const size_t videoFrameSize = videoWidth * videoHeight * 3; // The video frame will always be in the RGB888 colorspace.
+ const size_t audioBlockSize = sizeof(int16_t) * 2;
+ const size_t audioFrameSize = ((DESMUME_SAMPLE_RATE * audioBlockSize) / 30);
+
+ this->_expectedMaxFrameSize = videoFrameSize + audioFrameSize;
+
+ _semQueue = ssem_new(pendingFrameCount);
+}
+
+AVFileTypeVideoCodecID ClientAVCaptureFileStream::GetVideoCodecID()
+{
+ return this->_fileTypeVideoCodecID;
+}
+
+AVFileTypeAudioCodecID ClientAVCaptureFileStream::GetAudioCodecID()
+{
+ return this->_fileTypeAudioCodecID;
+}
+
+bool ClientAVCaptureFileStream::IsValid()
+{
+ // Implementations need to override this method.
+ return false;
+}
+
+void ClientAVCaptureFileStream::QueueAdd(uint8_t *srcVideo, const size_t videoBufferSize, uint8_t *srcAudio, const size_t audioBufferSize)
+{
+ AVStreamWriteParam newParam;
+ 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 ClientAVCaptureFileStream::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 ClientAVCaptureFileStream::GetQueueSize()
+{
+ slock_lock(this->_mutexQueue);
+ const size_t queueSize = this->_writeQueue.size();
+ slock_unlock(this->_mutexQueue);
+
+ return queueSize;
+}
+
+ClientAVCaptureError ClientAVCaptureFileStream::FlushVideo(uint8_t *srcBuffer, const size_t bufferSize)
+{
+ // Do nothing. This is implementation dependent.
+ return ClientAVCaptureError_None;
+}
+
+ClientAVCaptureError ClientAVCaptureFileStream::FlushAudio(uint8_t *srcBuffer, const size_t bufferSize)
+{
+ // Do nothing. This is implementation dependent.
+ return ClientAVCaptureError_None;
+}
+
+ClientAVCaptureError ClientAVCaptureFileStream::WriteOneFrame(const AVStreamWriteParam ¶m)
+{
+ // Do nothing. This is implementation dependent.
+ return ClientAVCaptureError_None;
+}
+
+ClientAVCaptureError ClientAVCaptureFileStream::WriteAllFrames()
+{
+ ClientAVCaptureError error = ClientAVCaptureError_None;
+
+ do
+ {
+ slock_lock(this->_mutexQueue);
+ if (this->_writeQueue.empty())
+ {
+ slock_unlock(this->_mutexQueue);
+ break;
+ }
+
+ const AVStreamWriteParam 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 (error)
+ {
+ return error;
+ }
+
+ } while (true);
+
+ return error;
+}
+
+ClientAVCaptureObject::ClientAVCaptureObject()
+{
+ __InstanceInit(0, 0);
+}
+
+ClientAVCaptureObject::ClientAVCaptureObject(size_t videoFrameWidth, size_t videoFrameHeight)
+{
+ __InstanceInit(videoFrameWidth, videoFrameHeight);
+
+ _pendingVideoBuffer = (u8 *)malloc_alignedCacheLine(_videoFrameSize * _pendingBufferCount);
+ _pendingAudioBuffer = (u8 *)malloc_alignedCacheLine(_audioFrameSize * _pendingBufferCount);
+ _pendingAudioWriteSize = (size_t *)calloc(_pendingBufferCount, sizeof(size_t));
+
+ const size_t linesPerThread = (_numThreads > 0) ? videoFrameHeight / _numThreads : videoFrameHeight;
+
+ if (_numThreads == 0)
+ {
+ for (size_t i = 0; i < MAX_CONVERT_THREADS; i++)
+ {
+ _convertParam[i].firstLineIndex = 0;
+ _convertParam[i].lastLineIndex = linesPerThread - 1;
+ _convertParam[i].srcOffset = 0;
+ _convertParam[i].dstOffset = (videoFrameWidth * (videoFrameHeight - 1) * 3);
+ _convertParam[i].frameWidth = videoFrameWidth;
+ }
+ }
+ else
+ {
+ for (size_t i = 0; i < _numThreads; i++)
+ {
+ if (i == 0)
+ {
+ _convertParam[i].firstLineIndex = 0;
+ _convertParam[i].lastLineIndex = linesPerThread - 1;
+ }
+ else if (i == (_numThreads - 1))
+ {
+ _convertParam[i].firstLineIndex = _convertParam[i - 1].lastLineIndex + 1;
+ _convertParam[i].lastLineIndex = videoFrameHeight - 1;
+ }
+ else
+ {
+ _convertParam[i].firstLineIndex = _convertParam[i - 1].lastLineIndex + 1;
+ _convertParam[i].lastLineIndex = _convertParam[i].firstLineIndex + linesPerThread - 1;
+ }
+
+ _convertParam[i].srcOffset = (videoFrameWidth * _convertParam[i].firstLineIndex);
+ _convertParam[i].dstOffset = (videoFrameWidth * (videoFrameHeight - (_convertParam[i].firstLineIndex + 1)) * 3);
+ _convertParam[i].frameWidth = videoFrameWidth;
+ }
+ }
+}
+
+void ClientAVCaptureObject::__InstanceInit(size_t videoFrameWidth, size_t videoFrameHeight)
+{
+ _fs = NULL;
+ _isCapturingVideo = false;
+ _isCapturingAudio = false;
+
+ _mutexCaptureFlags = slock_new();
+
+ _videoFrameWidth = videoFrameWidth;
+ _videoFrameHeight = videoFrameHeight;
+ _videoFrameSize = videoFrameWidth * videoFrameHeight * 3; // The video frame will always be in the RGB888 colorspace.
+ _audioBlockSize = sizeof(int16_t) * 2;
+ _audioFrameSize = ((DESMUME_SAMPLE_RATE * _audioBlockSize) / 30);
+
+ _pendingVideoBuffer = NULL;
+ _pendingAudioBuffer = NULL;
+ _pendingAudioWriteSize = NULL;
+ _currentBufferIndex = 0;
+
+ _pendingBufferCount = MAX_PENDING_BUFFER_SIZE / (_videoFrameSize + _audioFrameSize);
+ if (_pendingBufferCount > MAX_PENDING_FRAME_COUNT)
+ {
+ _pendingBufferCount = MAX_PENDING_FRAME_COUNT;
+ }
+
+ // Create the colorspace conversion threads.
+ _numThreads = CommonSettings.num_cores;
+
+ if (_numThreads > MAX_CONVERT_THREADS)
+ {
+ _numThreads = MAX_CONVERT_THREADS;
+ }
+ else if (_numThreads < 2)
+ {
+ _numThreads = 0;
+ }
+
+ for (size_t i = 0; i < _numThreads; i++)
+ {
+ memset(&_convertParam[i], 0, sizeof(VideoConvertParam));
+ _convertParam[i].captureObj = this;
+
+ _convertThread[i] = new Task();
+ _convertThread[i]->start(false);
+ }
+
+ _fileWriteThread = new Task();
+ _fileWriteThread->start(false);
+}
+
+ClientAVCaptureObject::~ClientAVCaptureObject()
+{
+ this->_fileWriteThread->finish();
+ delete this->_fileWriteThread;
+
+ for (size_t i = 0; i < this->_numThreads; i++)
+ {
+ this->_convertThread[i]->finish();
+ delete this->_convertThread[i];
+ }
+
+ free_aligned(this->_pendingVideoBuffer);
+ free_aligned(this->_pendingAudioBuffer);
+ free(this->_pendingAudioWriteSize);
+
+ slock_free(this->_mutexCaptureFlags);
+}
+
+ClientAVCaptureFileStream* ClientAVCaptureObject::GetOutputFileStream()
+{
+ return this->_fs;
+}
+
+void ClientAVCaptureObject::SetOutputFileStream(ClientAVCaptureFileStream *fs)
+{
+ if (fs == NULL)
+ {
+ slock_lock(this->_mutexCaptureFlags);
+ this->_isCapturingVideo = false;
+ this->_isCapturingAudio = false;
+ slock_unlock(this->_mutexCaptureFlags);
+ }
+
+ this->_fs = fs;
+
+ if (fs != NULL)
+ {
+ slock_lock(this->_mutexCaptureFlags);
+ this->_isCapturingVideo = (fs->GetVideoCodecID() != AVFileTypeVideoCodecID_None);
+ this->_isCapturingAudio = (fs->GetAudioCodecID() != AVFileTypeAudioCodecID_None);
+ slock_unlock(this->_mutexCaptureFlags);
+ }
+}
+
+bool ClientAVCaptureObject::IsFileStreamValid()
+{
+ if (this->_fs == NULL)
+ {
+ return false;
+ }
+
+ return this->_fs->IsValid();
+}
+
+bool ClientAVCaptureObject::IsCapturingVideo()
+{
+ slock_lock(this->_mutexCaptureFlags);
+ const bool isCapturingVideo = this->_isCapturingVideo;
+ slock_unlock(this->_mutexCaptureFlags);
+
+ return isCapturingVideo;
+}
+
+bool ClientAVCaptureObject::IsCapturingAudio()
+{
+ slock_lock(this->_mutexCaptureFlags);
+ const bool isCapturingAudio = this->_isCapturingAudio;
+ slock_unlock(this->_mutexCaptureFlags);
+
+ return isCapturingAudio;
+}
+
+void ClientAVCaptureObject::StartFrame()
+{
+ if (!this->IsFileStreamValid())
+ {
+ return;
+ }
+
+ // 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) % this->_pendingBufferCount);
+ this->_pendingAudioWriteSize[this->_currentBufferIndex] = 0;
+}
+
+void ClientAVCaptureObject::StreamWriteStart()
+{
+ if (!this->IsFileStreamValid())
+ {
+ return;
+ }
+
+ const size_t bufferIndex = this->_currentBufferIndex;
+ const size_t queueSize = this->_fs->GetQueueSize();
+ const bool isQueueEmpty = (queueSize == 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)
+ {
+ if (this->_videoFrameSize > 0)
+ {
+ for (size_t i = 0; i < this->_numThreads; i++)
+ {
+ this->_convertThread[i]->finish();
+ }
+ }
+ }
+
+ this->_fs->QueueAdd(this->_pendingVideoBuffer + (this->_videoFrameSize * bufferIndex), this->_videoFrameSize,
+ this->_pendingAudioBuffer + (AUDIO_STREAM_BUFFER_SIZE * bufferIndex), this->_pendingAudioWriteSize[bufferIndex]);
+
+ if (isQueueEmpty)
+ {
+ this->_fileWriteThread->execute(&RunAviFileWrite, this->_fs);
+ }
+}
+
+void ClientAVCaptureObject::StreamWriteFinish()
+{
+ this->_fileWriteThread->finish();
+}
+
+//converts 16bpp to 24bpp and flips
+void ClientAVCaptureObject::ConvertVideoSlice555Xto888(const VideoConvertParam ¶m)
+{
+ const u16 *__restrict src = (const u16 *__restrict)param.src;
+ u8 *__restrict dst = param.dst;
+
+ for (size_t y = param.firstLineIndex; y <= param.lastLineIndex; y++)
+ {
+ ColorspaceConvertBuffer555XTo888(src, dst, param.frameWidth);
+ src += param.frameWidth;
+ dst -= param.frameWidth * 3;
+ }
+}
+
+//converts 32bpp to 24bpp and flips
+void ClientAVCaptureObject::ConvertVideoSlice888Xto888(const VideoConvertParam ¶m)
+{
+ const u32 *__restrict src = (const u32 *__restrict)param.src;
+ u8 *__restrict dst = param.dst;
+
+ for (size_t y = param.firstLineIndex; y <= param.lastLineIndex; y++)
+ {
+ ColorspaceConvertBuffer888XTo888(src, dst, param.frameWidth);
+ src += param.frameWidth;
+ dst -= param.frameWidth * 3;
+ }
+}
+
+void ClientAVCaptureObject::ReadVideoFrame(const void *srcVideoFrame, const size_t inFrameWidth, const size_t inFrameHeight, const NDSColorFormat colorFormat)
+{
+ //dont do anything if prescale has changed, it's just going to be garbage
+ if (!this->_isCapturingVideo ||
+ (srcVideoFrame == NULL) ||
+ (this->_videoFrameSize == 0) ||
+ (this->_videoFrameWidth != inFrameWidth) ||
+ (this->_videoFrameHeight != (inFrameHeight * 2)))
+ {
+ return;
+ }
+
+ const size_t bufferIndex = this->_currentBufferIndex;
+ u8 *convertBufferHead = this->_pendingVideoBuffer + (this->_videoFrameSize * bufferIndex);
+
+ if (colorFormat == NDSColorFormat_BGR555_Rev)
+ {
+ if (this->_numThreads == 0)
+ {
+ this->_convertParam[0].src = (u16 *)srcVideoFrame;
+ this->_convertParam[0].dst = convertBufferHead + this->_convertParam[0].dstOffset;
+ this->ConvertVideoSlice555Xto888(this->_convertParam[0]);
+ }
+ else
+ {
+ 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]);
+ }
+ }
+ }
+ else
+ {
+ if (this->_numThreads == 0)
+ {
+ this->_convertParam[0].src = (u32 *)srcVideoFrame;
+ this->_convertParam[0].dst = convertBufferHead + this->_convertParam[0].dstOffset;
+ this->ConvertVideoSlice888Xto888(this->_convertParam[0]);
+ }
+ else
+ {
+ 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]);
+ }
+ }
+ }
+}
+
+void ClientAVCaptureObject::ReadAudioFrames(const void *srcAudioBuffer, const size_t inSampleCount)
+{
+ if (!this->_isCapturingAudio || (srcAudioBuffer == NULL))
+ {
+ return;
+ }
+
+ const size_t bufferIndex = this->_currentBufferIndex;
+ const size_t soundSize = inSampleCount * this->_audioBlockSize;
+
+ memcpy(this->_pendingAudioBuffer + (AUDIO_STREAM_BUFFER_SIZE * bufferIndex) + this->_pendingAudioWriteSize[bufferIndex], srcAudioBuffer, soundSize);
+ this->_pendingAudioWriteSize[bufferIndex] += soundSize;
+}
diff --git a/desmume/src/frontend/cocoa/ClientAVCaptureObject.h b/desmume/src/frontend/cocoa/ClientAVCaptureObject.h
new file mode 100644
index 000000000..c6afedcfa
--- /dev/null
+++ b/desmume/src/frontend/cocoa/ClientAVCaptureObject.h
@@ -0,0 +1,260 @@
+/*
+ Copyright (C) 2018 DeSmuME team
+
+ This file 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 file 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 the this software. If not, see .
+ */
+
+#ifndef _CLIENT_AV_CAPTURE_OBJECT_H_
+#define _CLIENT_AV_CAPTURE_OBJECT_H_
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include "utils/colorspacehandler/colorspacehandler.h"
+#include "utils/task.h"
+#include "../../SPU.h"
+
+#define VIDEO_STREAM 0
+#define AUDIO_STREAM 1
+#define MAX_CONVERT_THREADS 32
+
+#define AUDIO_STREAM_BUFFER_SIZE ((DESMUME_SAMPLE_RATE * sizeof(int16_t) * 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 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 AVFileTypeContainerID
+{
+ AVFileTypeContainerID_mp4 = 0x000000,
+ AVFileTypeContainerID_avi = 0x010000,
+ AVFileTypeContainerID_mkv = 0x020000,
+ AVFileTypeContainerID_mov = 0x030000,
+
+ AVFileTypeContainerID_m4a = 0x800000,
+ AVFileTypeContainerID_aiff = 0x810000,
+ AVFileTypeContainerID_flac = 0x820000,
+ AVFileTypeContainerID_wav = 0x830000
+};
+
+enum AVFileTypeVideoCodecID
+{
+ AVFileTypeVideoCodecID_H264 = 0x000000,
+ AVFileTypeVideoCodecID_HVEC = 0x001000,
+ AVFileTypeVideoCodecID_HuffYUV = 0x002000,
+ AVFileTypeVideoCodecID_FFV1 = 0x003000,
+ AVFileTypeVideoCodecID_ProRes = 0x004000,
+
+ AVFileTypeVideoCodecID_None = 0x00FF00
+};
+
+enum AVFileTypeAudioCodecID
+{
+ AVFileTypeAudioCodecID_AAC = 0x000000,
+ AVFileTypeAudioCodecID_PCMs16LE = 0x000010,
+ AVFileTypeAudioCodecID_PCMs16BE = 0x000020,
+ AVFileTypeAudioCodecID_ALAC = 0x000030,
+ AVFileTypeAudioCodecID_FLAC = 0x000040,
+
+ AVFileTypeAudioCodecID_None = 0x0000FF
+};
+
+enum AVFileTypeID
+{
+ AVFileTypeID_mp4_H264_AAC = (AVFileTypeContainerID_mp4 | AVFileTypeVideoCodecID_H264 | AVFileTypeAudioCodecID_AAC), // dec = 0
+ AVFileTypeID_mp4_HVEC_AAC = (AVFileTypeContainerID_mp4 | AVFileTypeVideoCodecID_HVEC | AVFileTypeAudioCodecID_AAC), // dec = 4096
+ AVFileTypeID_avi_HuffYUV_PCMs16LE = (AVFileTypeContainerID_avi | AVFileTypeVideoCodecID_HuffYUV | AVFileTypeAudioCodecID_PCMs16LE), // dec = 73744
+ AVFileTypeID_mkv_FFV1_FLAC = (AVFileTypeContainerID_mkv | AVFileTypeVideoCodecID_FFV1 | AVFileTypeAudioCodecID_FLAC), // dec = 143424
+ AVFileTypeID_mov_H264_ALAC = (AVFileTypeContainerID_mov | AVFileTypeVideoCodecID_H264 | AVFileTypeAudioCodecID_ALAC), // dec = 196656
+ AVFileTypeID_mov_ProRes_ALAC = (AVFileTypeContainerID_mov | AVFileTypeVideoCodecID_ProRes | AVFileTypeAudioCodecID_ALAC), // dec = 213040
+
+ AVFileTypeID_m4a_AAC = (AVFileTypeContainerID_m4a | AVFileTypeVideoCodecID_None | AVFileTypeAudioCodecID_AAC), // dec = 8453888
+ AVFileTypeID_m4a_ALAC = (AVFileTypeContainerID_m4a | AVFileTypeVideoCodecID_None | AVFileTypeAudioCodecID_ALAC), // dec = 8453936
+ AVFileTypeID_aiff_PCMs16BE = (AVFileTypeContainerID_aiff | AVFileTypeVideoCodecID_None | AVFileTypeAudioCodecID_PCMs16BE), // dec = 8519456
+ AVFileTypeID_flac_FLAC = (AVFileTypeContainerID_flac | AVFileTypeVideoCodecID_None | AVFileTypeAudioCodecID_FLAC), // dec = 8585024
+ AVFileTypeID_wav_PCMs16LE = (AVFileTypeContainerID_wav | AVFileTypeVideoCodecID_None | AVFileTypeAudioCodecID_PCMs16LE), // dec = 8650512
+};
+
+enum
+{
+ AVFILETYPEID_CONTAINERMASK = 0xFF0000,
+ AVFILETYPEID_VIDEOCODECMASK = 0x00F000,
+ AVFILETYPEID_VIDEOCODECOPTMASK = 0x000F00,
+ AVFILETYPEID_AUDIOCODECMASK = 0x0000F0,
+ AVFILETYPEID_AUDIOCODECOPTMASK = 0x00000F
+};
+
+enum FileStreamCloseAction
+{
+ FSCA_DoNothing = 0,
+ FSCA_PurgeQueue = 1,
+ FSCA_WriteRemainingInQueue = 2
+};
+
+enum ClientAVCaptureError
+{
+ ClientAVCaptureError_None = 0,
+ ClientAVCaptureError_GenericError = 1,
+ ClientAVCaptureError_FileOpenError = 1000,
+ ClientAVCaptureError_GenericFormatCodecError = 2000,
+ ClientAVCaptureError_InvalidContainerFormat = 2000,
+ ClientAVCaptureError_InvalidVideoCodec = 2000,
+ ClientAVCaptureError_InvalidAudioCodec = 2000,
+ ClientAVCaptureError_VideoStreamError = 2000,
+ ClientAVCaptureError_VideoFrameCreationError = 2000,
+ ClientAVCaptureError_AudioStreamError = 2000,
+ ClientAVCaptureError_AudioFrameCreationError = 2000,
+ ClientAVCaptureError_FrameEncodeError = 2000,
+ ClientAVCaptureError_FrameWriteError = 2000
+};
+
+class ClientAVCaptureObject;
+
+struct VideoConvertParam
+{
+ ClientAVCaptureObject *captureObj;
+
+ const void *src;
+ uint8_t *dst;
+
+ size_t srcOffset;
+ size_t dstOffset;
+
+ size_t firstLineIndex;
+ size_t lastLineIndex;
+ size_t frameWidth;
+};
+typedef struct VideoConvertParam VideoConvertParam;
+
+struct AVStreamWriteParam
+{
+ u8 *srcVideo;
+ u8 *srcAudio;
+ size_t videoBufferSize;
+ size_t audioBufferSize;
+};
+typedef struct AVStreamWriteParam AVStreamWriteParam;
+
+typedef std::queue AVStreamWriteQueue;
+
+class ClientAVCaptureFileStream
+{
+protected:
+ size_t _videoWidth;
+ size_t _videoHeight;
+
+ std::string _baseFileName;
+ std::string _baseFileNameExt;
+ size_t _segmentNumber;
+
+ AVFileTypeID _fileTypeID;
+ AVFileTypeContainerID _fileTypeContainerID;
+ AVFileTypeVideoCodecID _fileTypeVideoCodecID;
+ AVFileTypeAudioCodecID _fileTypeAudioCodecID;
+
+ uint64_t _maxSegmentSize;
+ uint64_t _expectedMaxFrameSize;
+ uint64_t _writtenBytes;
+ size_t _writtenVideoFrameCount;
+ size_t _writtenAudioSampleCount;
+
+ ssem_t *_semQueue;
+ slock_t *_mutexQueue;
+ AVStreamWriteQueue _writeQueue;
+
+public:
+ ClientAVCaptureFileStream();
+ virtual ~ClientAVCaptureFileStream();
+
+ void InitBaseProperties(const AVFileTypeID fileTypeID, const std::string &fileName,
+ const size_t videoWidth, const size_t videoHeight,
+ size_t pendingFrameCount);
+
+ AVFileTypeVideoCodecID GetVideoCodecID();
+ AVFileTypeAudioCodecID GetAudioCodecID();
+
+ virtual ClientAVCaptureError Open() = 0;
+ virtual void Close(FileStreamCloseAction theAction) = 0;
+ virtual bool IsValid();
+
+ void QueueAdd(uint8_t *srcVideo, const size_t videoBufferSize, uint8_t *srcAudio, const size_t audioBufferSize);
+ void QueueWait();
+ size_t GetQueueSize();
+
+ virtual ClientAVCaptureError FlushVideo(uint8_t *srcBuffer, const size_t bufferSize);
+ virtual ClientAVCaptureError FlushAudio(uint8_t *srcBuffer, const size_t bufferSize);
+ virtual ClientAVCaptureError WriteOneFrame(const AVStreamWriteParam ¶m);
+
+ ClientAVCaptureError WriteAllFrames();
+};
+
+class ClientAVCaptureObject
+{
+private:
+ void __InstanceInit(size_t videoFrameWidth, size_t videoFrameHeight);
+
+protected:
+ ClientAVCaptureFileStream *_fs;
+
+ bool _isCapturingVideo;
+ bool _isCapturingAudio;
+
+ size_t _videoFrameWidth;
+ size_t _videoFrameHeight;
+ size_t _videoFrameSize;
+ size_t _audioBlockSize;
+ size_t _audioFrameSize;
+
+ uint8_t *_pendingVideoBuffer;
+ uint8_t *_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];
+
+ slock_t *_mutexCaptureFlags;
+
+public:
+ ClientAVCaptureObject();
+ ClientAVCaptureObject(size_t videoFrameWidth, size_t videoFrameHeight);
+ ~ClientAVCaptureObject();
+
+ ClientAVCaptureFileStream* GetOutputFileStream();
+ void SetOutputFileStream(ClientAVCaptureFileStream *fs);
+ bool IsFileStreamValid();
+
+ bool IsCapturingVideo();
+ bool IsCapturingAudio();
+
+ void StartFrame();
+ void StreamWriteStart();
+ void StreamWriteFinish();
+
+ void ConvertVideoSlice555Xto888(const VideoConvertParam ¶m);
+ void ConvertVideoSlice888Xto888(const VideoConvertParam ¶m);
+
+ void ReadVideoFrame(const void *srcVideoFrame, const size_t inFrameWidth, const size_t inFrameHeight, const NDSColorFormat colorFormat);
+ void ReadAudioFrames(const void *srcAudioBuffer, const size_t inSampleCount);
+};
+
+#endif // _CLIENT_AV_CAPTURE_OBJECT_H_
diff --git a/desmume/src/frontend/cocoa/ClientExecutionControl.cpp b/desmume/src/frontend/cocoa/ClientExecutionControl.cpp
index d5da45bec..2abc65d3e 100644
--- a/desmume/src/frontend/cocoa/ClientExecutionControl.cpp
+++ b/desmume/src/frontend/cocoa/ClientExecutionControl.cpp
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2017 DeSmuME team
+ Copyright (C) 2017-2018 DeSmuME team
This file is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -18,13 +18,29 @@
#include
#include
+#include "../../armcpu.h"
#include "../../GPU.h"
#include "../../movie.h"
#include "../../NDSSystem.h"
+#include "../../gdbstub.h"
#include "../../rtc.h"
#include "ClientExecutionControl.h"
+// Need to include assert.h this way so that GDB stub will work
+// with an optimized build.
+#if defined(GDB_STUB) && defined(NDEBUG)
+#define TEMP_NDEBUG
+#undef NDEBUG
+#endif
+
+#include
+
+#if defined(TEMP_NDEBUG)
+#undef TEMP_NDEBUG
+#define NDEBUG
+#endif
+
ClientExecutionControl::ClientExecutionControl()
{
@@ -40,6 +56,15 @@ ClientExecutionControl::ClientExecutionControl()
_framesToSkip = 0;
_prevExecBehavior = ExecutionBehavior_Pause;
+ _isGdbStubStarted = false;
+ _enableGdbStubARM9 = false;
+ _enableGdbStubARM7 = false;
+ _gdbStubPortARM9 = 0;
+ _gdbStubPortARM7 = 0;
+ _gdbStubHandleARM9 = NULL;
+ _gdbStubHandleARM7 = NULL;
+ _isInDebugTrap = false;
+
_settingsPending.cpuEngineID = CPUEmulationEngineID_Interpreter;
_settingsPending.JITMaxBlockSize = 12;
_settingsPending.slot1DeviceType = NDS_SLOT1_RETAIL_AUTO;
@@ -71,6 +96,8 @@ ClientExecutionControl::ClientExecutionControl()
_settingsPending.execBehavior = ExecutionBehavior_Pause;
_settingsPending.jumpBehavior = FrameJumpBehavior_Forward;
+ _settingsPending.avCaptureObject = NULL;
+
_settingsApplied = _settingsPending;
_settingsApplied.filePathARM9BIOS = _settingsPending.filePathARM9BIOS;
_settingsApplied.filePathARM7BIOS = _settingsPending.filePathARM7BIOS;
@@ -100,6 +127,29 @@ ClientExecutionControl::~ClientExecutionControl()
pthread_mutex_destroy(&this->_mutexOutputPostNDSExec);
}
+ClientAVCaptureObject* ClientExecutionControl::GetClientAVCaptureObject()
+{
+ pthread_mutex_lock(&this->_mutexSettingsPendingOnNDSExec);
+ ClientAVCaptureObject *theCaptureObject = this->_settingsPending.avCaptureObject;
+ pthread_mutex_unlock(&this->_mutexSettingsPendingOnNDSExec);
+
+ return theCaptureObject;
+}
+
+ClientAVCaptureObject* ClientExecutionControl::GetClientAVCaptureObjectApplied()
+{
+ return this->_settingsApplied.avCaptureObject;
+}
+
+void ClientExecutionControl::SetClientAVCaptureObject(ClientAVCaptureObject *theCaptureObject)
+{
+ pthread_mutex_lock(&this->_mutexSettingsPendingOnNDSExec);
+ this->_settingsPending.avCaptureObject = theCaptureObject;
+
+ this->_newSettingsPendingOnNDSExec = true;
+ pthread_mutex_unlock(&this->_mutexSettingsPendingOnNDSExec);
+}
+
ClientInputHandler* ClientExecutionControl::GetClientInputHandler()
{
return this->_inputHandler;
@@ -646,6 +696,138 @@ void ClientExecutionControl::SetFrameJumpTarget(uint64_t newJumpTarget)
pthread_mutex_unlock(&this->_mutexSettingsPendingOnExecutionLoopStart);
}
+bool ClientExecutionControl::IsGDBStubARM9Enabled()
+{
+ return this->_enableGdbStubARM9;
+}
+
+void ClientExecutionControl::SetGDBStubARM9Enabled(bool theState)
+{
+ this->_enableGdbStubARM9 = theState;
+}
+
+bool ClientExecutionControl::IsGDBStubARM7Enabled()
+{
+ return this->_enableGdbStubARM7;
+}
+
+void ClientExecutionControl::SetGDBStubARM7Enabled(bool theState)
+{
+ this->_enableGdbStubARM7 = theState;
+}
+
+uint16_t ClientExecutionControl::GetGDBStubARM9Port()
+{
+ return this->_gdbStubPortARM9;
+}
+
+void ClientExecutionControl::SetGDBStubARM9Port(uint16_t portNumber)
+{
+ this->_gdbStubPortARM9 = portNumber;
+}
+
+uint16_t ClientExecutionControl::GetGDBStubARM7Port()
+{
+ return this->_gdbStubPortARM7;
+}
+
+void ClientExecutionControl::SetGDBStubARM7Port(uint16_t portNumber)
+{
+ this->_gdbStubPortARM7 = portNumber;
+}
+
+bool ClientExecutionControl::IsGDBStubStarted()
+{
+ return this->_isGdbStubStarted;
+}
+
+void ClientExecutionControl::SetIsGDBStubStarted(bool theState)
+{
+#ifdef GDB_STUB
+ if (theState)
+ {
+ gdbstub_mutex_init();
+
+ if (this->_enableGdbStubARM9)
+ {
+ const uint16_t arm9Port = this->_gdbStubPortARM9;
+ if(arm9Port > 0)
+ {
+ this->_gdbStubHandleARM9 = createStub_gdb(arm9Port, &NDS_ARM9, &arm9_direct_memory_iface);
+ if (this->_gdbStubHandleARM9 == NULL)
+ {
+ printf("Failed to create ARM9 gdbstub on port %d\n", arm9Port);
+ }
+ else
+ {
+ activateStub_gdb(this->_gdbStubHandleARM9);
+ }
+ }
+ }
+ else
+ {
+ destroyStub_gdb(this->_gdbStubHandleARM9);
+ this->_gdbStubHandleARM9 = NULL;
+ }
+
+ if (this->_enableGdbStubARM7)
+ {
+ const uint16_t arm7Port = this->_gdbStubPortARM7;
+ if (arm7Port > 0)
+ {
+ this->_gdbStubHandleARM7 = createStub_gdb(arm7Port, &NDS_ARM7, &arm7_base_memory_iface);
+ if (this->_gdbStubHandleARM7 == NULL)
+ {
+ printf("Failed to create ARM7 gdbstub on port %d\n", arm7Port);
+ }
+ else
+ {
+ activateStub_gdb(this->_gdbStubHandleARM7);
+ }
+ }
+ }
+ else
+ {
+ destroyStub_gdb(this->_gdbStubHandleARM7);
+ this->_gdbStubHandleARM7 = NULL;
+ }
+ }
+ else
+ {
+ destroyStub_gdb(this->_gdbStubHandleARM9);
+ this->_gdbStubHandleARM9 = NULL;
+
+ destroyStub_gdb(this->_gdbStubHandleARM7);
+ this->_gdbStubHandleARM7 = NULL;
+
+ gdbstub_mutex_destroy();
+ }
+#endif
+ if ( (this->_gdbStubHandleARM9 == NULL) && (this->_gdbStubHandleARM7 == NULL) )
+ {
+ theState = false;
+ }
+
+ this->_isGdbStubStarted = theState;
+}
+
+bool ClientExecutionControl::IsInDebugTrap()
+{
+ return this->_isInDebugTrap;
+}
+
+void ClientExecutionControl::SetIsInDebugTrap(bool theState)
+{
+ // If we're transitioning out of the debug trap, then ignore
+ // frame skipping this time.
+ if (this->_isInDebugTrap && !theState)
+ {
+ this->ResetFramesToSkip();
+ }
+
+ this->_isInDebugTrap = theState;
+}
+
ExecutionBehavior ClientExecutionControl::GetPreviousExecutionBehavior()
{
pthread_mutex_lock(&this->_mutexSettingsPendingOnExecutionLoopStart);
@@ -859,6 +1041,8 @@ void ClientExecutionControl::ApplySettingsOnNDSExec()
this->_settingsApplied.enableFrameSkip = this->_settingsPending.enableFrameSkip;
+ this->_settingsApplied.avCaptureObject = this->_settingsPending.avCaptureObject;
+
const bool needResetFramesToSkip = this->_needResetFramesToSkip;
this->_needResetFramesToSkip = false;
@@ -1067,3 +1251,33 @@ void ClientExecutionControl::WaitUntilAbsoluteTime(double deadlineAbsoluteTime)
{
mach_wait_until((uint64_t)deadlineAbsoluteTime);
}
+
+void* createThread_gdb(void (*thread_function)(void *data), void *thread_data)
+{
+ // Create the thread using POSIX routines.
+ pthread_attr_t attr;
+ pthread_t* posixThreadID = (pthread_t*)malloc(sizeof(pthread_t));
+
+ assert(!pthread_attr_init(&attr));
+ assert(!pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE));
+
+ int threadError = pthread_create(posixThreadID, &attr, (void* (*)(void *))thread_function, thread_data);
+
+ assert(!pthread_attr_destroy(&attr));
+
+ if (threadError != 0)
+ {
+ // Report an error.
+ return NULL;
+ }
+ else
+ {
+ return posixThreadID;
+ }
+}
+
+void joinThread_gdb(void *thread_handle)
+{
+ pthread_join(*(pthread_t *)thread_handle, NULL);
+ free(thread_handle);
+}
diff --git a/desmume/src/frontend/cocoa/ClientExecutionControl.h b/desmume/src/frontend/cocoa/ClientExecutionControl.h
index 40ea2e31c..06908665c 100644
--- a/desmume/src/frontend/cocoa/ClientExecutionControl.h
+++ b/desmume/src/frontend/cocoa/ClientExecutionControl.h
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2017 DeSmuME team
+ Copyright (C) 2017-2018 DeSmuME team
This file is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -22,6 +22,7 @@
#include