diff --git a/include/mgba/core/core.h b/include/mgba/core/core.h index 2c226c577..170634bc2 100644 --- a/include/mgba/core/core.h +++ b/include/mgba/core/core.h @@ -26,10 +26,10 @@ CXX_GUARD_START enum mPlatform { PLATFORM_NONE = -1, #ifdef M_CORE_GBA - PLATFORM_GBA, + PLATFORM_GBA = 0, #endif #ifdef M_CORE_GB - PLATFORM_GB, + PLATFORM_GB = 1, #endif }; @@ -40,6 +40,7 @@ enum mCoreChecksumType { struct mCoreConfig; struct mCoreSync; struct mStateExtdata; +struct mVideoLogContext; struct mCore { void* cpu; void* board; @@ -145,6 +146,9 @@ struct mCore { size_t (*listAudioChannels)(const struct mCore*, const struct mCoreChannelInfo**); void (*enableVideoLayer)(struct mCore*, size_t id, bool enable); void (*enableAudioChannel)(struct mCore*, size_t id, bool enable); + + void (*startVideoLog)(struct mCore*, struct mVideoLogContext*); + void (*endVideoLog)(struct mCore*); }; #if !defined(MINIMAL_CORE) || MINIMAL_CORE < 2 diff --git a/include/mgba/core/video-logger.h b/include/mgba/core/video-logger.h index 207385ea2..f204e9bb5 100644 --- a/include/mgba/core/video-logger.h +++ b/include/mgba/core/video-logger.h @@ -27,6 +27,7 @@ struct mVideoLoggerDirtyInfo { uint32_t padding; }; +struct VFile; struct mVideoLogger { bool (*writeData)(struct mVideoLogger* logger, const void* data, size_t length); bool (*readData)(struct mVideoLogger* logger, void* data, size_t length, bool block); @@ -45,8 +46,40 @@ struct mVideoLogger { uint16_t* vram; uint16_t* oam; uint16_t* palette; + + struct VFile* vf; }; +struct mVideoLogChannel { + uint32_t type; + void* initialState; + size_t initialStateSize; + struct VFile* channelData; +}; + +struct mVideoLogContext { + void* initialState; + size_t initialStateSize; + uint32_t nChannels; + struct mVideoLogChannel channels[32]; +}; + +struct mVideoLogHeader { + char magic[4]; + uint32_t platform; + uint32_t nChannels; + uint32_t initialStatePointer; + uint32_t channelPointers[32]; +}; + +struct mVideoLogChannelHeader { + uint32_t type; + uint32_t channelInitialStatePointer; + uint32_t channelSize; + uint32_t reserved; +}; + +void mVideoLoggerRendererCreate(struct mVideoLogger* logger); void mVideoLoggerRendererInit(struct mVideoLogger* logger); void mVideoLoggerRendererDeinit(struct mVideoLogger* logger); void mVideoLoggerRendererReset(struct mVideoLogger* logger); @@ -61,6 +94,11 @@ void mVideoLoggerRendererFlush(struct mVideoLogger* logger); bool mVideoLoggerRendererRun(struct mVideoLogger* logger); +struct mCore; +struct mVideoLogContext* mVideoLoggerCreate(struct mCore* core); +void mVideoLoggerDestroy(struct mCore* core, struct mVideoLogContext*); +void mVideoLoggerWrite(struct mCore* core, struct mVideoLogContext*, struct VFile*); + CXX_GUARD_END #endif diff --git a/include/mgba/internal/gba/renderers/proxy.h b/include/mgba/internal/gba/renderers/proxy.h index dd16da688..06b8b6bcc 100644 --- a/include/mgba/internal/gba/renderers/proxy.h +++ b/include/mgba/internal/gba/renderers/proxy.h @@ -31,6 +31,8 @@ struct GBAVideoProxyRenderer { }; void GBAVideoProxyRendererCreate(struct GBAVideoProxyRenderer* renderer, struct GBAVideoRenderer* backend); +void GBAVideoProxyRendererShim(struct GBAVideo* video, struct GBAVideoProxyRenderer* renderer); +void GBAVideoProxyRendererUnshim(struct GBAVideo* video, struct GBAVideoProxyRenderer* renderer); CXX_GUARD_END diff --git a/src/core/video-logger.c b/src/core/video-logger.c index 507b1ed3e..15359402b 100644 --- a/src/core/video-logger.c +++ b/src/core/video-logger.c @@ -5,13 +5,26 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include +#include #include +#include + +const char mVL_MAGIC[] = "mVL\0"; + +static bool _writeData(struct mVideoLogger* logger, const void* data, size_t length); +static bool _readData(struct mVideoLogger* logger, void* data, size_t length, bool block); static inline size_t _roundUp(size_t value, int shift) { value += (1 << shift) - 1; return value >> shift; } +void mVideoLoggerRendererCreate(struct mVideoLogger* logger) { + logger->writeData = _writeData; + logger->readData = _readData; + logger->vf = NULL; +} + void mVideoLoggerRendererInit(struct mVideoLogger* logger) { logger->palette = anonymousMemoryMap(logger->paletteSize); logger->vram = anonymousMemoryMap(logger->vramSize); @@ -134,3 +147,79 @@ bool mVideoLoggerRendererRun(struct mVideoLogger* logger) { } return true; } + +static bool _writeData(struct mVideoLogger* logger, const void* data, size_t length) { + return logger->vf->write(logger->vf, data, length) == (ssize_t) length; +} + +static bool _readData(struct mVideoLogger* logger, void* data, size_t length, bool block) { + return logger->vf->read(logger->vf, data, length) == (ssize_t) length || !block; +} + +struct mVideoLogContext* mVideoLoggerCreate(struct mCore* core) { + struct mVideoLogContext* context = malloc(sizeof(*context)); + core->startVideoLog(core, context); + return context; +} + +void mVideoLoggerDestroy(struct mCore* core, struct mVideoLogContext* context) { + if (core) { + core->endVideoLog(core); + } + free(context); +} + +void mVideoLoggerWrite(struct mCore* core, struct mVideoLogContext* context, struct VFile* vf) { + struct mVideoLogHeader header = {{0}}; + memcpy(header.magic, mVL_MAGIC, sizeof(mVL_MAGIC)); + + enum mPlatform platform = core->platform(core); + STORE_32LE(platform, 0, &header.platform); + STORE_32LE(context->nChannels, 0, &header.nChannels); + + ssize_t pointer = vf->seek(vf, sizeof(header), SEEK_SET); + if (context->initialStateSize) { + ssize_t written = vf->write(vf, context->initialState, context->initialStateSize); + if (written > 0) { + STORE_32LE(pointer, 0, &header.initialStatePointer); + pointer += written; + } else { + header.initialStatePointer = 0; + } + } else { + header.initialStatePointer = 0; + } + + size_t i; + for (i = 0; i < context->nChannels && i < 32; ++i) { + struct VFile* channel = context->channels[i].channelData; + void* block = channel->map(channel, channel->size(channel), MAP_READ); + + struct mVideoLogChannelHeader chHeader = {0}; + STORE_32LE(context->channels[i].type, 0, &chHeader.type); + STORE_32LE(channel->size(channel), 0, &chHeader.channelSize); + + if (context->channels[i].initialStateSize) { + ssize_t written = vf->write(vf, context->channels[i].initialState, context->channels[i].initialStateSize); + if (written > 0) { + STORE_32LE(pointer, 0, &chHeader.channelInitialStatePointer); + pointer += written; + } else { + chHeader.channelInitialStatePointer = 0; + } + } + STORE_32LE(pointer, 0, &header.channelPointers[i]); + ssize_t written = vf->write(vf, &chHeader, sizeof(chHeader)); + if (written != sizeof(chHeader)) { + continue; + } + pointer += written; + written = vf->write(vf, block, channel->size(channel)); + if (written != channel->size(channel)) { + break; + } + pointer += written; + } + vf->seek(vf, 0, SEEK_SET); + vf->write(vf, &header, sizeof(header)); +} diff --git a/src/gba/core.c b/src/gba/core.c index 075f87b6e..7b1417ab6 100644 --- a/src/gba/core.c +++ b/src/gba/core.c @@ -42,6 +42,8 @@ const static struct mCoreChannelInfo _GBAAudioChannels[] = { struct GBACore { struct mCore d; struct GBAVideoSoftwareRenderer renderer; + struct GBAVideoProxyRenderer logProxy; + struct mVideoLogContext* logContext; #ifndef DISABLE_THREADING struct GBAVideoThreadProxyRenderer threadProxy; int threadedVideo; @@ -69,6 +71,7 @@ static bool _GBACoreInit(struct mCore* core) { gbacore->overrides = NULL; gbacore->debuggerPlatform = NULL; gbacore->cheatDevice = NULL; + gbacore->logContext = NULL; GBACreate(gba); // TODO: Restore cheats @@ -646,6 +649,37 @@ static void _GBACoreEnableAudioChannel(struct mCore* core, size_t id, bool enabl } } +static void _GBACoreStartVideoLog(struct mCore* core, struct mVideoLogContext* context) { + struct GBACore* gbacore = (struct GBACore*) core; + struct GBA* gba = core->board; + gbacore->logContext = context; + + GBAVideoProxyRendererCreate(&gbacore->logProxy, gba->video.renderer); + + context->initialStateSize = core->stateSize(core); + context->initialState = anonymousMemoryMap(context->initialStateSize); + core->saveState(core, context->initialState); + + struct VFile* vf = VFileMemChunk(NULL, 0); + context->nChannels = 1; + context->channels[0].initialState = NULL; + context->channels[0].initialStateSize = 0; + context->channels[0].channelData = vf; + gbacore->logProxy.logger.vf = vf; + gbacore->logProxy.block = false; + + GBAVideoProxyRendererShim(&gba->video, &gbacore->logProxy); +} + +static void _GBACoreEndVideoLog(struct mCore* core) { + struct GBACore* gbacore = (struct GBACore*) core; + struct GBA* gba = core->board; + GBAVideoProxyRendererUnshim(&gba->video, &gbacore->logProxy); + + mappedMemoryFree(gbacore->logContext->initialState, gbacore->logContext->initialStateSize); + gbacore->logContext->channels[0].channelData->close(gbacore->logContext->channels[0].channelData); +} + struct mCore* GBACoreCreate(void) { struct GBACore* gbacore = malloc(sizeof(*gbacore)); struct mCore* core = &gbacore->d; @@ -718,5 +752,7 @@ struct mCore* GBACoreCreate(void) { core->listAudioChannels = _GBACoreListAudioChannels; core->enableVideoLayer = _GBACoreEnableVideoLayer; core->enableAudioChannel = _GBACoreEnableAudioChannel; + core->startVideoLog = _GBACoreStartVideoLog; + core->endVideoLog = _GBACoreEndVideoLog; return core; } diff --git a/src/gba/renderers/proxy.c b/src/gba/renderers/proxy.c index b80d82374..6e543c884 100644 --- a/src/gba/renderers/proxy.c +++ b/src/gba/renderers/proxy.c @@ -25,6 +25,8 @@ static bool _parsePacket(struct mVideoLogger* logger, const struct mVideoLoggerD static uint16_t* _vramBlock(struct mVideoLogger* logger, uint32_t address); void GBAVideoProxyRendererCreate(struct GBAVideoProxyRenderer* renderer, struct GBAVideoRenderer* backend) { + mVideoLoggerRendererCreate(&renderer->logger); + renderer->d.init = GBAVideoProxyRendererInit; renderer->d.reset = GBAVideoProxyRendererReset; renderer->d.deinit = GBAVideoProxyRendererDeinit; @@ -43,9 +45,11 @@ void GBAVideoProxyRendererCreate(struct GBAVideoProxyRenderer* renderer, struct renderer->d.disableBG[3] = false; renderer->d.disableOBJ = false; + renderer->init = NULL; + renderer->deinit = NULL; + renderer->reset = NULL; + renderer->logger.context = renderer; - renderer->logger.writeData = NULL; - renderer->logger.readData = NULL; renderer->logger.parsePacket = _parsePacket; renderer->logger.vramBlock = _vramBlock; renderer->logger.paletteSize = SIZE_PALETTE_RAM; @@ -55,9 +59,7 @@ void GBAVideoProxyRendererCreate(struct GBAVideoProxyRenderer* renderer, struct renderer->backend = backend; } -void GBAVideoProxyRendererInit(struct GBAVideoRenderer* renderer) { - struct GBAVideoProxyRenderer* proxyRenderer = (struct GBAVideoProxyRenderer*) renderer; - +static void _init(struct GBAVideoProxyRenderer* proxyRenderer) { mVideoLoggerRendererInit(&proxyRenderer->logger); if (proxyRenderer->block) { @@ -67,20 +69,65 @@ void GBAVideoProxyRendererInit(struct GBAVideoRenderer* renderer) { proxyRenderer->backend->cache = NULL; } - proxyRenderer->init(proxyRenderer); + if (proxyRenderer->init) { + proxyRenderer->init(proxyRenderer); + } +} + +static void _reset(struct GBAVideoProxyRenderer* proxyRenderer) { + memcpy(proxyRenderer->logger.oam, &proxyRenderer->d.oam->raw, SIZE_OAM); + memcpy(proxyRenderer->logger.palette, &proxyRenderer->d.palette, SIZE_PALETTE_RAM); + memcpy(proxyRenderer->logger.vram, &proxyRenderer->d.vram, SIZE_VRAM); + + mVideoLoggerRendererReset(&proxyRenderer->logger); + + if (proxyRenderer->reset) { + proxyRenderer->reset(proxyRenderer); + } +} + +void GBAVideoProxyRendererShim(struct GBAVideo* video, struct GBAVideoProxyRenderer* renderer) { + if (video->renderer != renderer->backend) { + return; + } + renderer->d.cache = video->renderer->cache; + video->renderer = &renderer->d; + renderer->d.palette = video->palette; + renderer->d.vram = video->vram; + renderer->d.oam = &video->oam; + _init(renderer); + _reset(renderer); +} + +void GBAVideoProxyRendererUnshim(struct GBAVideo* video, struct GBAVideoProxyRenderer* renderer) { + if (video->renderer != &renderer->d) { + return; + } + renderer->backend->cache = video->renderer->cache; + video->renderer = renderer->backend; + renderer->backend->palette = video->palette; + renderer->backend->vram = video->vram; + renderer->backend->oam = &video->oam; + + if (renderer->deinit) { + renderer->deinit(renderer); + } + + mVideoLoggerRendererDeinit(&renderer->logger); +} + +void GBAVideoProxyRendererInit(struct GBAVideoRenderer* renderer) { + struct GBAVideoProxyRenderer* proxyRenderer = (struct GBAVideoProxyRenderer*) renderer; + + _init(proxyRenderer); proxyRenderer->backend->init(proxyRenderer->backend); } void GBAVideoProxyRendererReset(struct GBAVideoRenderer* renderer) { struct GBAVideoProxyRenderer* proxyRenderer = (struct GBAVideoProxyRenderer*) renderer; - memcpy(proxyRenderer->logger.oam, &renderer->oam->raw, SIZE_OAM); - memcpy(proxyRenderer->logger.palette, renderer->palette, SIZE_PALETTE_RAM); - memcpy(proxyRenderer->logger.vram, renderer->vram, SIZE_VRAM); - mVideoLoggerRendererReset(&proxyRenderer->logger); - - proxyRenderer->reset(proxyRenderer); + _reset(proxyRenderer); proxyRenderer->backend->reset(proxyRenderer->backend); } @@ -88,7 +135,9 @@ void GBAVideoProxyRendererReset(struct GBAVideoRenderer* renderer) { void GBAVideoProxyRendererDeinit(struct GBAVideoRenderer* renderer) { struct GBAVideoProxyRenderer* proxyRenderer = (struct GBAVideoProxyRenderer*) renderer; - proxyRenderer->deinit(proxyRenderer); + if (proxyRenderer->deinit) { + proxyRenderer->deinit(proxyRenderer); + } proxyRenderer->backend->deinit(proxyRenderer->backend); diff --git a/src/gba/renderers/thread-proxy.c b/src/gba/renderers/thread-proxy.c index 80bb84f82..a47a3f727 100644 --- a/src/gba/renderers/thread-proxy.c +++ b/src/gba/renderers/thread-proxy.c @@ -39,6 +39,7 @@ void GBAVideoThreadProxyRendererCreate(struct GBAVideoThreadProxyRenderer* rende renderer->d.logger.writeData = _writeData; renderer->d.logger.readData = _readData; + renderer->d.logger.vf = NULL; } void GBAVideoThreadProxyRendererInit(struct GBAVideoProxyRenderer* renderer) { diff --git a/src/platform/qt/GameController.cpp b/src/platform/qt/GameController.cpp index 8cf59c8f1..7aa4657a5 100644 --- a/src/platform/qt/GameController.cpp +++ b/src/platform/qt/GameController.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #ifdef M_CORE_GBA #include #include @@ -71,6 +72,7 @@ GameController::GameController(QObject* parent) , m_loadStateFlags(SAVESTATE_SCREENSHOT | SAVESTATE_RTC) , m_preload(false) , m_override(nullptr) + , m_vl(nullptr) { #ifdef M_CORE_GBA m_lux.p = this; @@ -1199,6 +1201,27 @@ void GameController::disableLogLevel(int levels) { m_logLevels &= ~levels; } +void GameController::startVideoLog(const QString& path) { + if (!isLoaded() || m_vl) { + return; + } + m_vlPath = path; + m_vl = mVideoLoggerCreate(m_threadContext.core); +} + +void GameController::endVideoLog() { + if (!m_vl) { + return; + } + if (isLoaded()) { + VFile* vf = VFileDevice::open(m_vlPath, O_WRONLY | O_CREAT | O_TRUNC); + mVideoLoggerWrite(m_threadContext.core, m_vl, vf); + vf->close(vf); + } + mVideoLoggerDestroy(m_threadContext.core, m_vl); + m_vf = nullptr; +} + void GameController::pollEvents() { if (!m_inputController) { return; diff --git a/src/platform/qt/GameController.h b/src/platform/qt/GameController.h index 3fe79d087..66b4b4012 100644 --- a/src/platform/qt/GameController.h +++ b/src/platform/qt/GameController.h @@ -29,6 +29,7 @@ struct GBAAudio; struct mCoreConfig; struct mDebugger; struct mTileCache; +struct mVideoLogContext; namespace QGBA { @@ -174,6 +175,9 @@ public slots: void enableLogLevel(int); void disableLogLevel(int); + void startVideoLog(const QString& path); + void endVideoLog(); + private slots: void openGame(bool bios = false); void crashGame(const QString& crashMessage); @@ -242,6 +246,9 @@ private: mAVStream* m_stream; + mVideoLogContext* m_vl; + QString m_vlPath; + struct GameControllerLux : GBALuminanceSource { GameController* p; uint8_t value; diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp index 112ee95bf..17800c082 100644 --- a/src/platform/qt/Window.cpp +++ b/src/platform/qt/Window.cpp @@ -478,6 +478,13 @@ void Window::openAboutScreen() { openView(about); } +void Window::startVideoLog() { + QString filename = GBAApp::app()->getSaveFileName(this, tr("Select video log"), tr("Video logs (*.mvl)")); + if (!filename.isEmpty()) { + m_controller->startVideoLog(filename); + } +} + template std::function Window::openTView(A arg) { return [=]() { @@ -1350,6 +1357,16 @@ void Window::setupMenu(QMenuBar* menubar) { addControlledAction(avMenu, recordGIF, "recordGIF"); #endif + QAction* recordVL = new QAction(tr("Record video log..."), avMenu); + connect(recordVL, SIGNAL(triggered()), this, SLOT(startVideoLog())); + addControlledAction(avMenu, recordVL, "recordVL"); + m_gameActions.append(recordVL); + + QAction* stopVL = new QAction(tr("Stop video log"), avMenu); + connect(stopVL, SIGNAL(triggered()), m_controller, SLOT(endVideoLog())); + addControlledAction(avMenu, stopVL, "stopVL"); + m_gameActions.append(stopVL); + avMenu->addSeparator(); m_videoLayers = avMenu->addMenu(tr("Video layers")); m_shortcutController->addMenu(m_videoLayers, avMenu); diff --git a/src/platform/qt/Window.h b/src/platform/qt/Window.h index f224a28bf..bd24621bd 100644 --- a/src/platform/qt/Window.h +++ b/src/platform/qt/Window.h @@ -81,6 +81,8 @@ public slots: void openSettingsWindow(); void openAboutScreen(); + void startVideoLog(); + #ifdef USE_DEBUGGERS void consoleOpen(); #endif