Merge branch 'master' into medusa
jump to
@@ -35,6 +35,7 @@ - GB MBC: Fix MBC1 mode changing behavior
- GB MBC: Fix MBC1 RAM enable bit selection - GB MBC: Fix MBC2 bit selection - GB Video: Fix state after skipping BIOS (fixes mgba.io/i/1715 and mgba.io/i/1716) + - GB Video: Always initialize palette - GBA: Fix timing advancing too quickly in rare cases - GBA BIOS: Implement dummy sound driver calls - GBA BIOS: Improve HLE BIOS timing@@ -46,6 +47,7 @@ - GBA SIO: Fix copying Normal mode transfer values
- GBA Video: Latch scanline at end of Hblank (fixes mgba.io/i/1319) - GBA Video: Fix Hblank timing - GBA Video: Fix invalid read in mode 4 mosaic + - GBA Video: Fix color of disabled screen - SM83: Emulate HALT bug Other fixes: - All: Improve export headers (fixes mgba.io/i/1738)@@ -55,6 +57,7 @@ - CMake: Fix build with libzip 1.7
- Core: Ensure ELF regions can be written before trying - Debugger: Don't skip undefined instructions when debugger attached - FFmpeg: Fix some small memory leaks + - FFmpeg: Fix encoding of time base - GB Core: Fix extracting SRAM when none is present - GBA Savedata: Fix extracting save when not yet configured in-game - Qt: Force OpenGL paint engine creation thread (fixes mgba.io/i/1642)
@@ -60,6 +60,7 @@ endif()
set(BUILD_PERF OFF CACHE BOOL "Build performance profiling tool") set(BUILD_TEST OFF CACHE BOOL "Build testing harness") set(BUILD_SUITE OFF CACHE BOOL "Build test suite") + set(BUILD_CINEMA OFF CACHE BOOL "Build video tests suite") set(BUILD_EXAMPLE OFF CACHE BOOL "Build example frontends") set(BUILD_PYTHON OFF CACHE BOOL "Build Python bindings") set(BUILD_STATIC OFF CACHE BOOL "Build a static library")@@ -1250,6 +1251,7 @@ message(STATUS " SDL (${SDL_VERSION}): ${BUILD_SDL}")
message(STATUS " Profiling: ${BUILD_PERF}") message(STATUS " Test harness: ${BUILD_TEST}") message(STATUS " Test suite: ${BUILD_SUITE}") + message(STATUS " Video test suite: ${BUILD_CINEMA}") message(STATUS " Python bindings: ${BUILD_PYTHON}") message(STATUS " Examples: ${BUILD_EXAMPLE}") message(STATUS "Cores:")
@@ -10,6 +10,8 @@ #include <mgba-util/common.h>
CXX_GUARD_START +#include <mgba/core/core.h> + #include <mgba-util/circle-buffer.h> #define mVL_MAX_CHANNELS 32@@ -115,7 +117,7 @@ void mVideoLogContextSetOutput(struct mVideoLogContext*, struct VFile*);
void mVideoLogContextWriteHeader(struct mVideoLogContext*, struct mCore* core); bool mVideoLogContextLoad(struct mVideoLogContext*, struct VFile*); -void mVideoLogContextDestroy(struct mCore* core, struct mVideoLogContext*); +void mVideoLogContextDestroy(struct mCore* core, struct mVideoLogContext*, bool closeVF); void mVideoLogContextRewind(struct mVideoLogContext*, struct mCore*); void* mVideoLogContextInitialState(struct mVideoLogContext*, size_t* size);@@ -128,6 +130,7 @@ void mVideoLoggerInjectVideoRegister(struct mVideoLogger* logger, uint32_t address, uint16_t value);
void mVideoLoggerInjectPalette(struct mVideoLogger* logger, uint32_t address, uint16_t value); void mVideoLoggerInjectOAM(struct mVideoLogger* logger, uint32_t address, uint16_t value); +enum mPlatform mVideoLogIsCompatible(struct VFile*); struct mCore* mVideoLogCoreFind(struct VFile*); CXX_GUARD_END
@@ -56,7 +56,7 @@ #else
GBA_COLOR_WHITE = 0x7FFF, #endif #else - GBA_COLOR_WHITE = 0x00F8F8F8, + GBA_COLOR_WHITE = 0x00FFFFFF, #endif OFFSET_PRIORITY = 30, OFFSET_INDEX = 28,
@@ -369,6 +369,8 @@ encoder->video->width = encoder->width;
encoder->video->height = encoder->height; encoder->video->time_base = (AVRational) { encoder->frameCycles * encoder->frameskip, encoder->cycles }; encoder->video->framerate = (AVRational) { encoder->cycles, encoder->frameCycles * encoder->frameskip }; + encoder->videoStream->time_base = encoder->video->time_base; + encoder->videoStream->avg_frame_rate = encoder->video->framerate; encoder->video->pix_fmt = encoder->pixFormat; encoder->video->gop_size = 60; encoder->video->max_b_frames = 3;
@@ -5,7 +5,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include <mgba/feature/video-logger.h> -#include <mgba/core/core.h> #include <mgba-util/memory.h> #include <mgba-util/vfs.h> #include <mgba-util/math.h>@@ -724,7 +723,7 @@ context->backing->write(context->backing, writeBuffer, read);
} } -void mVideoLogContextDestroy(struct mCore* core, struct mVideoLogContext* context) { +void mVideoLogContextDestroy(struct mCore* core, struct mVideoLogContext* context, bool closeVF) { if (context->write) { _flushBuffer(context);@@ -750,6 +749,10 @@ inflateEnd(&context->channels[i].inflateStream);
context->channels[i].inflating = false; } #endif + } + + if (closeVF && context->backing) { + context->backing->close(context->backing); } free(context);@@ -1033,7 +1036,7 @@ }
return read; } -struct mCore* mVideoLogCoreFind(struct VFile* vf) { +static const struct mVLDescriptor* _mVideoLogDescriptor(struct VFile* vf) { if (!vf) { return NULL; }@@ -1054,6 +1057,25 @@ for (descriptor = &_descriptors[0]; descriptor->platform != PLATFORM_NONE; ++descriptor) {
if (platform == descriptor->platform) { break; } + } + if (descriptor->platform == PLATFORM_NONE) { + return NULL; + } + return descriptor; +} + +enum mPlatform mVideoLogIsCompatible(struct VFile* vf) { + const struct mVLDescriptor* descriptor = _mVideoLogDescriptor(vf); + if (descriptor) { + return descriptor->platform; + } + return PLATFORM_NONE; +} + +struct mCore* mVideoLogCoreFind(struct VFile* vf) { + const struct mVLDescriptor* descriptor = _mVideoLogDescriptor(vf); + if (!descriptor) { + return NULL; } struct mCore* core = NULL; if (descriptor->open) {
@@ -1091,7 +1091,7 @@
static void _GBVLPDeinit(struct mCore* core) { struct GBCore* gbcore = (struct GBCore*) core; if (gbcore->logContext) { - mVideoLogContextDestroy(core, gbcore->logContext); + mVideoLogContextDestroy(core, gbcore->logContext, true); } _GBCoreDeinit(core); }@@ -1120,7 +1120,7 @@ static bool _GBVLPLoadROM(struct mCore* core, struct VFile* vf) {
struct GBCore* gbcore = (struct GBCore*) core; gbcore->logContext = mVideoLogContextCreate(NULL); if (!mVideoLogContextLoad(gbcore->logContext, vf)) { - mVideoLogContextDestroy(core, gbcore->logContext); + mVideoLogContextDestroy(core, gbcore->logContext, false); gbcore->logContext = NULL; return false; }
@@ -213,6 +213,8 @@ softwareRenderer->lookup[i] = i;
softwareRenderer->lookup[i] = i; softwareRenderer->lookup[i] = i; } + + memset(softwareRenderer->palette, 0, sizeof(softwareRenderer->palette)); } static void GBVideoSoftwareRendererDeinit(struct GBVideoRenderer* renderer) {
@@ -894,6 +894,9 @@ LOAD_32LE(when, 0, &state->video.nextFrame);
mTimingSchedule(&video->p->timing, &video->frameEvent, when); } + video->renderer->deinit(video->renderer); + video->renderer->init(video->renderer, video->p->model, video->sgbBorders); + size_t i; for (i = 0; i < 64; ++i) { LOAD_16LE(video->palette[i], i * 2, state->video.palette);@@ -905,7 +908,4 @@ memcpy(&video->oam.raw, state->oam, GB_SIZE_OAM);
_cleanOAM(video, video->ly); GBVideoSwitchBank(video, video->vramCurrentBank); - - video->renderer->deinit(video->renderer); - video->renderer->init(video->renderer, video->p->model, video->sgbBorders); }
@@ -1248,7 +1248,7 @@
static void _GBAVLPDeinit(struct mCore* core) { struct GBACore* gbacore = (struct GBACore*) core; if (gbacore->logContext) { - mVideoLogContextDestroy(core, gbacore->logContext); + mVideoLogContextDestroy(core, gbacore->logContext, true); } _GBACoreDeinit(core); }@@ -1277,7 +1277,7 @@ static bool _GBAVLPLoadROM(struct mCore* core, struct VFile* vf) {
struct GBACore* gbacore = (struct GBACore*) core; gbacore->logContext = mVideoLogContextCreate(NULL); if (!mVideoLogContextLoad(gbacore->logContext, vf)) { - mVideoLogContextDestroy(core, gbacore->logContext); + mVideoLogContextDestroy(core, gbacore->logContext, false); gbacore->logContext = NULL; return false; }
@@ -8,15 +8,17 @@
class Tracer(object): def __init__(self, core): self.core = core - self.framebuffer = Image(*core.desired_video_dimensions()) - self.core.set_video_buffer(self.framebuffer) self._video_fifo = [] def yield_frames(self, skip=0, limit=None): + self.framebuffer = Image(*self.core.desired_video_dimensions()) + self.core.set_video_buffer(self.framebuffer) self.core.reset() skip = (skip or 0) + 1 while skip > 0: frame = self.core.frame_counter + self.framebuffer = Image(*self.core.desired_video_dimensions()) + self.core.set_video_buffer(self.framebuffer) self.core.run_frame() skip -= 1 while frame <= self.core.frame_counter and limit != 0:
@@ -3,7 +3,6 @@ import os.path
import mgba.core import mgba.image import cinema.movie -import itertools import glob import re from copy import deepcopy@@ -73,7 +72,7 @@ super(VideoTest, self).setup()
self.tracer = cinema.movie.Tracer(self.core) def generate_frames(self): - for i, frame in zip(itertools.count(), self.tracer.video(**self.output_settings())): + for i, frame in enumerate(self.tracer.video(**self.output_settings())): try: baseline = VideoFrame.load(os.path.join(self.path, self.BASELINE % i)) yield baseline, frame, VideoFrame.diff(baseline, frame)@@ -85,7 +84,7 @@ self.baseline, self.frames, self.diffs = zip(*self.generate_frames())
assert not any(any(diffs[0].image.convert("L").point(bool).getdata()) for diffs in self.diffs) def generate_baseline(self): - for i, frame in zip(itertools.count(), self.tracer.video(**self.output_settings())): + for i, frame in enumerate(self.tracer.video(**self.output_settings())): frame.save(os.path.join(self.path, self.BASELINE % i))
@@ -859,9 +859,8 @@ return;
} Interrupter interrupter(this); - mVideoLogContextDestroy(m_threadContext.core, m_vl); - if (m_vlVf && closeVf) { - m_vlVf->close(m_vlVf); + mVideoLogContextDestroy(m_threadContext.core, m_vl, closeVf); + if (closeVf) { m_vlVf = nullptr; } m_vl = nullptr;
@@ -126,7 +126,7 @@ <translation>0x00 (00)</translation>
</message> </context> <context> - <name>BattleChipView</name> + <name>BattleChipView</name> <message> <location filename="../BattleChipView.ui" line="14"/> <source>BattleChip Gate</source>@@ -531,7 +531,7 @@ <message>
<location filename="../LoadSaveState.ui" line="248"/> <source>2</source> <translation>2</translation> - </message> + </message> <message> <location filename="../LoadSaveState.ui" line="261"/> <source>Cancel</source>@@ -904,7 +904,7 @@ <location filename="../ObjView.ui" line="72"/>
<source>Address</source> <translation>地址</translation> </message> - <message> + <message> <location filename="../ObjView.ui" line="79"/> <source>0x07000000</source> <translation>0x07000000</translation>@@ -1513,11 +1513,6 @@ <source>Add CodeBreaker</source>
<translation>添加 CodeBreaker</translation> </message> <message> - <location filename="../CheatsView.cpp" line="74"/> - <source>Add GameShark</source> - <translation>添加 GameShark</translation> - </message> - <message> <location filename="../CheatsView.cpp" line="80"/> <source>Add GameGenie</source> <translation>添加 GameGenie</translation>@@ -1562,7 +1557,7 @@ </message>
<message> <location filename="../CoreManager.cpp" line="86"/> <source>Could not load game. Are you sure it's in the correct format?</source> - <translation>无法载入游戏。请确认游戏格式是否无误。</translation> + <translation>无法载入游戏。是否确认游戏格式无误?</translation> </message> </context> <context>@@ -1696,8 +1691,8 @@ <translation>选择输出文件</translation>
</message> <message> <location filename="../GIFView.cpp" line="81"/> - <source>Graphics Interchange Format (*.gif);;Animated Portable Network Graphics (*.png *.apng)"</source> - <translation>图形交换格式 (*.gif);;动画便携式网络图形 (*.png *.apng)"</translation> + <source>Graphics Interchange Format (*.gif);;Animated Portable Network Graphics (*.png *.apng)"</source> + <translation>图形交换格式 (*.gif);;动画便携式网络图形 (*.png *.apng)"</translation> </message> </context> <context>@@ -3821,7 +3816,7 @@ </message>
<message> <location filename="../Window.cpp" line="1060"/> <source>%1 - %2 - %3</source> - <translation>%1 - %2 - %3</translation> + <translation>%1 - %2 - %3</translation> </message> <message> <location filename="../Window.cpp" line="1062"/>@@ -3947,7 +3942,7 @@ <message>
<location filename="../Window.cpp" line="1191"/> <location filename="../Window.cpp" line="1196"/> <source>State &%1</source> - <translation>即时存档 1(&1)</translation> + <translation>即时存档 (&%1)</translation> </message> <message> <location filename="../Window.cpp" line="1203"/>@@ -4098,7 +4093,7 @@ <message>
<location filename="../Window.cpp" line="1342"/> <source>BattleChip Gate...</source> <translation>BattleChip Gate...</translation> - </message> + </message> <message> <location filename="../Window.cpp" line="1346"/> <source>Audio/&Video</source>@@ -5319,19 +5314,9 @@ <source>MKV</source>
<translation>MKV</translation> </message> <message> - <location filename="../VideoView.ui" line="251"/> - <source>WebM</source> - <translation>WebM</translation> - </message> - <message> <location filename="../VideoView.ui" line="256"/> <source>AVI</source> <translation>AVI</translation> - </message> - <message> - <location filename="../VideoView.ui" line="21"/> - <source>MP4</source> - <translation>MP4</translation> </message> <message> <location filename="../VideoView.ui" line="273"/>@@ -5407,7 +5392,7 @@ </message>
<message> <location filename="../VideoView.ui" line="361"/> <source> Bitrate (kbps)</source> - <translation>比特率 (kbps)</translation> + <translation> 比特率 (kbps)</translation> </message> <message> <location filename="../VideoView.ui" line="367"/>
@@ -36,4 +36,11 @@ target_link_libraries(test-${TEST_NAME} ${BINARY_NAME} ${PLATFORM_LIBRARY} cmocka)
set_target_properties(test-${TEST_NAME} PROPERTIES COMPILE_DEFINITIONS "${OS_DEFINES};${FEATURE_DEFINES};${FUNCTION_DEFINES}") add_test(${TEST_NAME} test-${TEST_NAME}) endforeach() -endif()+endif() + +if(BUILD_CINEMA) + enable_testing() + add_executable(${BINARY_NAME}-cinema ${CMAKE_CURRENT_SOURCE_DIR}/cinema-main.c) + target_link_libraries(${BINARY_NAME}-cinema ${BINARY_NAME} ${PLATFORM_LIBRARY}) + set_target_properties(${BINARY_NAME}-cinema PROPERTIES COMPILE_DEFINITIONS "${OS_DEFINES};${FEATURE_DEFINES};${FUNCTION_DEFINES}") +endif()
@@ -0,0 +1,850 @@
+/* Copyright (c) 2013-2020 Jeffrey Pfau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include <mgba/core/config.h> +#include <mgba/core/core.h> +#include <mgba/core/log.h> +#include <mgba/core/version.h> +#include <mgba/feature/commandline.h> +#include <mgba/feature/video-logger.h> + +#include <mgba-util/png-io.h> +#include <mgba-util/table.h> +#include <mgba-util/vector.h> +#include <mgba-util/vfs.h> + +#ifdef _MSC_VER +#include <mgba-util/platform/windows/getopt.h> +#else +#include <getopt.h> +#endif + +#include <stdlib.h> +#include <sys/stat.h> +#include <sys/types.h> + +#define MAX_TEST 200 + +static const struct option longOpts[] = { + { "base", required_argument, 0, 'b' }, + { "diffs", no_argument, 0, 'd' }, + { "help", no_argument, 0, 'h' }, + { "dry-run", no_argument, 0, 'n' }, + { "outdir", required_argument, 0, 'o' }, + { "quiet", no_argument, 0, 'q' }, + { "rebaseline", no_argument, 0, 'r' }, + { "verbose", no_argument, 0, 'v' }, + { "version", no_argument, 0, '\0' }, + { 0, 0, 0, 0 } +}; + +static const char shortOpts[] = "b:dhno:qrv"; + +enum CInemaStatus { + CI_PASS, + CI_FAIL, + CI_XPASS, + CI_XFAIL, + CI_ERROR, + CI_SKIP +}; + +struct CInemaTest { + char directory[MAX_TEST]; + char filename[MAX_TEST]; + char name[MAX_TEST]; + enum CInemaStatus status; + unsigned failedFrames; + uint64_t failedPixels; + unsigned totalFrames; + uint64_t totalDistance; + uint64_t totalPixels; +}; + +struct CInemaImage { + void* data; + unsigned width; + unsigned height; + unsigned stride; +}; + +DECLARE_VECTOR(CInemaTestList, struct CInemaTest) +DEFINE_VECTOR(CInemaTestList, struct CInemaTest) + +DECLARE_VECTOR(ImageList, void*) +DEFINE_VECTOR(ImageList, void*) + +static bool showVersion = false; +static bool showUsage = false; +static char base[PATH_MAX] = {0}; +static char outdir[PATH_MAX] = {'.'}; +static bool dryRun = false; +static bool diffs = false; +static bool rebaseline = false; +static int verbosity = 0; + +bool CInemaTestInit(struct CInemaTest*, const char* directory, const char* filename); +void CInemaTestRun(struct CInemaTest*, struct Table* configTree); + +bool CInemaConfigGetUInt(struct Table* configTree, const char* testName, const char* key, unsigned* value); +void CInemaConfigLoad(struct Table* configTree, const char* testName, struct mCore* core); + +static void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args); + +ATTRIBUTE_FORMAT(printf, 2, 3) void CIlog(int minlevel, const char* format, ...) { + if (verbosity < minlevel) { + return; + } + va_list args; + va_start(args, format); + vprintf(format, args); + va_end(args); +} + +ATTRIBUTE_FORMAT(printf, 2, 3) void CIerr(int minlevel, const char* format, ...) { + if (verbosity < minlevel) { + return; + } + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); +} + +static bool parseCInemaArgs(int argc, char* const* argv) { + int ch; + int index = 0; + while ((ch = getopt_long(argc, argv, shortOpts, longOpts, &index)) != -1) { + const struct option* opt = &longOpts[index]; + switch (ch) { + case '\0': + if (strcmp(opt->name, "version") == 0) { + showVersion = true; + } else { + return false; + } + break; + case 'b': + strncpy(base, optarg, sizeof(base)); + // TODO: Verify path exists + break; + case 'd': + diffs = true; + break; + case 'h': + showUsage = true; + break; + case 'n': + dryRun = true; + break; + case 'o': + strncpy(outdir, optarg, sizeof(outdir)); + // TODO: Make directory + break; + case 'q': + --verbosity; + break; + case 'r': + rebaseline = true; + break; + case 'v': + ++verbosity; + break; + default: + return false; + } + } + + return true; +} + +static void usageCInema(const char* arg0) { + printf("usage: %s [-dhnqv] [-b BASE] [-o DIR] [--version] [test...]\n", arg0); + puts(" -b, --base [BASE] Path to the CInema base directory"); + puts(" -d, --diffs Output image diffs from failures"); + puts(" -h, --help Print this usage and exit"); + puts(" -n, --dry-run List all collected tests instead of running them"); + puts(" -o, --output [DIR] Path to output applicable results"); + puts(" -q, --quiet Decrease log verbosity (can be repeated)"); + puts(" -r, --rebaseline Rewrite the baseline for failing tests"); + puts(" -v, --verbose Increase log verbosity (can be repeated)"); + puts(" --version Print version and exit"); +} + +static bool determineBase(int argc, char* const* argv) { + // TODO: Better dynamic detection + separatePath(__FILE__, base, NULL, NULL); + strncat(base, PATH_SEP ".." PATH_SEP ".." PATH_SEP ".." PATH_SEP "cinema", sizeof(base) - strlen(base) - 1); + return true; +} + +static bool collectTests(struct CInemaTestList* tests, const char* path) { + CIerr(2, "Considering path %s\n", path); + struct VDir* dir = VDirOpen(path); + if (!dir) { + return false; + } + struct VDirEntry* entry = dir->listNext(dir); + while (entry) { + char subpath[PATH_MAX]; + snprintf(subpath, sizeof(subpath), "%s" PATH_SEP "%s", path, entry->name(entry)); + if (entry->type(entry) == VFS_DIRECTORY && strncmp(entry->name(entry), ".", 2) != 0 && strncmp(entry->name(entry), "..", 3) != 0) { + if (!collectTests(tests, subpath)) { + dir->close(dir); + return false; + } + } else if (entry->type(entry) == VFS_FILE && strncmp(entry->name(entry), "test.", 5) == 0) { + CIerr(3, "Found potential test %s\n", subpath); + struct VFile* vf = dir->openFile(dir, entry->name(entry), O_RDONLY); + if (vf) { + if (mCoreIsCompatible(vf) != PLATFORM_NONE || mVideoLogIsCompatible(vf) != PLATFORM_NONE) { + struct CInemaTest* test = CInemaTestListAppend(tests); + if (!CInemaTestInit(test, path, entry->name(entry))) { + CIerr(3, "Failed to create test\n"); + CInemaTestListResize(tests, -1); + } else { + CIerr(2, "Found test %s\n", test->name); + } + } else { + CIerr(3, "Not a compatible file\n"); + } + vf->close(vf); + } else { + CIerr(3, "Failed to open file\n"); + } + } + entry = dir->listNext(dir); + } + dir->close(dir); + return true; +} + +static int _compareNames(const void* a, const void* b) { + const struct CInemaTest* ta = a; + const struct CInemaTest* tb = b; + + return strncmp(ta->name, tb->name, sizeof(ta->name)); +} + +static void reduceTestList(struct CInemaTestList* tests) { + qsort(CInemaTestListGetPointer(tests, 0), CInemaTestListSize(tests), sizeof(struct CInemaTest), _compareNames); + + size_t i; + for (i = 1; i < CInemaTestListSize(tests);) { + struct CInemaTest* cur = CInemaTestListGetPointer(tests, i); + struct CInemaTest* prev = CInemaTestListGetPointer(tests, i - 1); + if (strncmp(cur->name, prev->name, sizeof(cur->name)) != 0) { + ++i; + continue; + } + CInemaTestListShift(tests, i, 1); + } +} + +static void testToPath(const char* testName, char* path) { + strncpy(path, base, PATH_MAX); + + bool dotSeen = true; + size_t i; + for (i = strlen(path); testName[0] && i < PATH_MAX; ++testName) { + if (testName[0] == '.') { + dotSeen = true; + } else { + if (dotSeen) { + strncpy(&path[i], PATH_SEP, PATH_MAX - i); + i += strlen(PATH_SEP); + dotSeen = false; + if (!i) { + break; + } + } + path[i] = testName[0]; + ++i; + } + } +} + +static void _loadConfigTree(struct Table* configTree, const char* testName) { + char key[MAX_TEST]; + strncpy(key, testName, sizeof(key) - 1); + + struct mCoreConfig* config; + while (!(config = HashTableLookup(configTree, key))) { + char path[PATH_MAX]; + config = malloc(sizeof(*config)); + mCoreConfigInit(config, "cinema"); + testToPath(key, path); + strncat(path, PATH_SEP, sizeof(path) - 1); + strncat(path, "config.ini", sizeof(path) - 1); + mCoreConfigLoadPath(config, path); + HashTableInsert(configTree, key, config); + char* pos = strrchr(key, '.'); + if (pos) { + pos[0] = '\0'; + } else if (key[0]) { + key[0] = '\0'; + } else { + break; + } + } +} + +static void _unloadConfigTree(const char* key, void* value, void* user) { + UNUSED(key); + UNUSED(user); + mCoreConfigDeinit(value); +} + +static const char* _lookupValue(struct Table* configTree, const char* testName, const char* key) { + _loadConfigTree(configTree, testName); + + char testKey[MAX_TEST]; + strncpy(testKey, testName, sizeof(testKey) - 1); + + struct mCoreConfig* config; + while (true) { + config = HashTableLookup(configTree, testKey); + if (!config) { + continue; + } + const char* str = ConfigurationGetValue(&config->configTable, "testinfo", key); + if (str) { + return str; + } + char* pos = strrchr(testKey, '.'); + if (pos) { + pos[0] = '\0'; + } else if (testKey[0]) { + testKey[0] = '\0'; + } else { + break; + } + } + return NULL; +} + +bool CInemaConfigGetUInt(struct Table* configTree, const char* testName, const char* key, unsigned* out) { + const char* charValue = _lookupValue(configTree, testName, key); + if (!charValue) { + return false; + } + char* end; + unsigned long value = strtoul(charValue, &end, 10); + if (*end) { + return false; + } + *out = value; + return true; +} + +void CInemaConfigLoad(struct Table* configTree, const char* testName, struct mCore* core) { + _loadConfigTree(configTree, testName); + + char testKey[MAX_TEST] = {0}; + char* keyEnd = testKey; + const char* pos; + while (true) { + pos = strchr(testName, '.'); + size_t maxlen = sizeof(testKey) - (keyEnd - testKey) - 1; + size_t len; + if (pos) { + len = pos - testName; + } else { + len = strlen(testName); + } + if (len > maxlen) { + len = maxlen; + } + strncpy(keyEnd, testName, len); + keyEnd += len; + + struct mCoreConfig* config = HashTableLookup(configTree, testKey); + if (config) { + core->loadConfig(core, config); + } + if (!pos) { + break; + } + testName = pos + 1; + keyEnd[0] = '.'; + ++keyEnd; + } +} + +bool CInemaTestInit(struct CInemaTest* test, const char* directory, const char* filename) { + if (strncmp(base, directory, strlen(base)) != 0) { + return false; + } + memset(test, 0, sizeof(*test)); + strncpy(test->directory, directory, sizeof(test->directory) - 1); + strncpy(test->filename, filename, sizeof(test->filename) - 1); + directory += strlen(base) + 1; + strncpy(test->name, directory, sizeof(test->name) - 1); + char* str = strstr(test->name, PATH_SEP); + while (str) { + str[0] = '.'; + str = strstr(str, PATH_SEP); + } + return true; +} + +static bool _loadBaseline(struct VDir* dir, struct CInemaImage* image, size_t frame, enum CInemaStatus* status) { + char baselineName[32]; + snprintf(baselineName, sizeof(baselineName), "baseline_%04" PRIz "u.png", frame); + struct VFile* baselineVF = dir->openFile(dir, baselineName, O_RDONLY); + if (!baselineVF) { + if (*status == CI_PASS) { + *status = CI_FAIL; + } + return false; + } + + png_structp png = PNGReadOpen(baselineVF, 0); + png_infop info = png_create_info_struct(png); + png_infop end = png_create_info_struct(png); + if (!png || !info || !end || !PNGReadHeader(png, info)) { + PNGReadClose(png, info, end); + baselineVF->close(baselineVF); + CIerr(1, "Failed to load %s\n", baselineName); + *status = CI_ERROR; + return false; + } + + unsigned pwidth = png_get_image_width(png, info); + unsigned pheight = png_get_image_height(png, info); + if (pheight != image->height || pwidth != image->width) { + PNGReadClose(png, info, end); + baselineVF->close(baselineVF); + CIlog(1, "Size mismatch for %s, expected %ux%u, got %ux%u\n", baselineName, pwidth, pheight, image->width, image->height); + if (*status == CI_PASS) { + *status = CI_FAIL; + } + return false; + } + + image->data = malloc(pwidth * pheight * BYTES_PER_PIXEL); + if (!image->data) { + CIerr(1, "Failed to allocate baseline buffer\n"); + *status = CI_ERROR; + PNGReadClose(png, info, end); + baselineVF->close(baselineVF); + return false; + } + if (!PNGReadPixels(png, info, image->data, pwidth, pheight, pwidth) || !PNGReadFooter(png, end)) { + CIerr(1, "Failed to read %s\n", baselineName); + *status = CI_ERROR; + free(image->data); + return false; + } + PNGReadClose(png, info, end); + baselineVF->close(baselineVF); + image->stride = pwidth; + return true; +} + +static struct VDir* _makeOutDir(const char* testName) { + char path[PATH_MAX] = {0}; + strncpy(path, outdir, sizeof(path) - 1); + char* pathEnd = path + strlen(path); + const char* pos; + while (true) { + pathEnd[0] = PATH_SEP[0]; + ++pathEnd; + pos = strchr(testName, '.'); + size_t maxlen = sizeof(path) - (pathEnd - path) - 1; + size_t len; + if (pos) { + len = pos - testName; + } else { + len = strlen(testName); + } + if (len > maxlen) { + len = maxlen; + } + strncpy(pathEnd, testName, len); + pathEnd += len; + + mkdir(path, 0777); + + if (!pos) { + break; + } + testName = pos + 1; + } + return VDirOpen(path); +} + +static void _writeImage(struct VFile* vf, const struct CInemaImage* image) { + png_structp png = PNGWriteOpen(vf); + png_infop info = PNGWriteHeader(png, image->width, image->height); + if (!PNGWritePixels(png, image->width, image->height, image->stride, image->data)) { + CIerr(0, "Could not write output image\n"); + } + PNGWriteClose(png, info); + + vf->close(vf); +} + +static void _writeDiff(const char* testName, const struct CInemaImage* image, size_t frame, const char* type) { + struct VDir* dir = _makeOutDir(testName); + if (!dir) { + CIerr(0, "Could not open directory for %s\n", testName); + return; + } + char name[32]; + snprintf(name, sizeof(name), "%s_%04" PRIz "u.png", type, frame); + struct VFile* vf = dir->openFile(dir, name, O_CREAT | O_TRUNC | O_WRONLY); + if (!vf) { + CIerr(0, "Could not open output file %s\n", name); + dir->close(dir); + return; + } + _writeImage(vf, image); + dir->close(dir); +} + +static void _writeBaseline(struct VDir* dir, const struct CInemaImage* image, size_t frame) { + char baselineName[32]; + snprintf(baselineName, sizeof(baselineName), "baseline_%04" PRIz "u.png", frame); + struct VFile* baselineVF = dir->openFile(dir, baselineName, O_CREAT | O_TRUNC | O_WRONLY); + if (baselineVF) { + _writeImage(baselineVF, image); + } else { + CIerr(0, "Could not open output file %s\n", baselineName); + } +} + +void CInemaTestRun(struct CInemaTest* test, struct Table* configTree) { + unsigned ignore = 0; + CInemaConfigGetUInt(configTree, test->name, "ignore", &ignore); + if (ignore) { + test->status = CI_SKIP; + return; + } + + struct VDir* dir = VDirOpen(test->directory); + if (!dir) { + CIerr(0, "Failed to open test directory\n"); + test->status = CI_ERROR; + return; + } + struct VFile* rom = dir->openFile(dir, test->filename, O_RDONLY); + if (!rom) { + CIerr(0, "Failed to open test\n"); + test->status = CI_ERROR; + return; + } + struct mCore* core = mCoreFindVF(rom); + if (!core) { + CIerr(0, "Failed to load test\n"); + test->status = CI_ERROR; + rom->close(rom); + return; + } + if (!core->init(core)) { + CIerr(0, "Failed to init test\n"); + test->status = CI_ERROR; + core->deinit(core); + return; + } + struct CInemaImage image; + core->desiredVideoDimensions(core, &image.width, &image.height); + ssize_t bufferSize = image.width * image.height * BYTES_PER_PIXEL; + image.data = malloc(bufferSize); + image.stride = image.width; + if (!image.data) { + CIerr(0, "Failed to allocate video buffer\n"); + test->status = CI_ERROR; + core->deinit(core); + } + core->setVideoBuffer(core, image.data, image.stride); + mCoreConfigInit(&core->config, "cinema"); + + unsigned limit = 9999; + unsigned skip = 0; + unsigned fail = 0; + + CInemaConfigGetUInt(configTree, test->name, "frames", &limit); + CInemaConfigGetUInt(configTree, test->name, "skip", &skip); + CInemaConfigGetUInt(configTree, test->name, "fail", &fail); + CInemaConfigLoad(configTree, test->name, core); + + core->loadROM(core, rom); + core->rtc.override = RTC_FAKE_EPOCH; + core->rtc.value = 1200000000; + core->reset(core); + + test->status = CI_PASS; + + unsigned minFrame = core->frameCounter(core); + size_t frame; + for (frame = 0; frame < skip; ++frame) { + core->runFrame(core); + } + for (frame = 0; limit; ++frame, --limit) { + core->runFrame(core); + ++test->totalFrames; + unsigned frameCounter = core->frameCounter(core); + if (frameCounter <= minFrame) { + break; + } + CIlog(3, "Test frame: %u\n", frameCounter); + core->desiredVideoDimensions(core, &image.width, &image.height); + uint8_t* diff = NULL; + struct CInemaImage expected = { + .data = NULL, + .width = image.width, + .height = image.height, + .stride = image.width, + }; + if (_loadBaseline(dir, &expected, frame, &test->status)) { + uint8_t* testPixels = image.data; + uint8_t* expectPixels = expected.data; + size_t x; + size_t y; + int max = 0; + bool failed = false; + for (y = 0; y < image.height; ++y) { + for (x = 0; x < image.width; ++x) { + size_t pix = expected.stride * y + x; + size_t tpix = image.stride * y + x; + int testR = testPixels[tpix * 4 + 0]; + int testG = testPixels[tpix * 4 + 1]; + int testB = testPixels[tpix * 4 + 2]; + int expectR = expectPixels[pix * 4 + 0]; + int expectG = expectPixels[pix * 4 + 1]; + int expectB = expectPixels[pix * 4 + 2]; + int r = expectR - testR; + int g = expectG - testG; + int b = expectB - testB; + if (r | g | b) { + failed = true; + if (diffs && !diff) { + diff = calloc(expected.width * expected.height, BYTES_PER_PIXEL); + } + CIlog(3, "Frame %u failed at pixel %" PRIz "ux%" PRIz "u with diff %i,%i,%i (expected %02x%02x%02x, got %02x%02x%02x)\n", + frameCounter, x, y, r, g, b, + expectR, expectG, expectB, + testR, testG, testB); + test->status = CI_FAIL; + if (r < 0) { + r = -r; + } + if (g < 0) { + g = -g; + } + if (b < 0) { + b = -b; + } + + if (diff) { + if (r > max) { + max = r; + } + if (g > max) { + max = g; + } + if (b > max) { + max = b; + } + diff[pix * 4 + 0] = r; + diff[pix * 4 + 1] = g; + diff[pix * 4 + 2] = b; + } + + test->totalDistance += r + g + b; + ++test->failedPixels; + } + } + } + if (failed) { + ++test->failedFrames; + } + test->totalPixels += image.height * image.width; + if (rebaseline && failed) { + _writeBaseline(dir, &image, frame); + } + if (diff) { + if (failed) { + struct CInemaImage outdiff = { + .data = diff, + .width = image.width, + .height = image.height, + .stride = image.width, + }; + + _writeDiff(test->name, &image, frame, "result"); + _writeDiff(test->name, &expected, frame, "expected"); + _writeDiff(test->name, &outdiff, frame, "diff"); + + for (y = 0; y < outdiff.height; ++y) { + for (x = 0; x < outdiff.width; ++x) { + size_t pix = outdiff.stride * y + x; + diff[pix * 4 + 0] = diff[pix * 4 + 0] * 255 / max; + diff[pix * 4 + 1] = diff[pix * 4 + 1] * 255 / max; + diff[pix * 4 + 2] = diff[pix * 4 + 2] * 255 / max; + } + } + _writeDiff(test->name, &outdiff, frame, "normalized"); + } + free(diff); + } + free(expected.data); + } else if (test->status == CI_ERROR) { + break; + } else if (rebaseline) { + _writeBaseline(dir, &image, frame); + } + } + + if (fail) { + if (test->status == CI_FAIL) { + test->status = CI_XFAIL; + } else if (test->status == CI_PASS) { + test->status = CI_XPASS; + } + } + + free(image.data); + mCoreConfigDeinit(&core->config); + core->deinit(core); + dir->close(dir); +} + +void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args) { + UNUSED(log); + if (verbosity < 0) { + return; + } + int mask = mLOG_FATAL; + if (verbosity >= 1) { + mask |= mLOG_ERROR; + } + if (verbosity >= 2) { + mask |= mLOG_WARN; + } + if (verbosity >= 4) { + mask |= mLOG_INFO; + } + if (verbosity >= 5) { + mask |= mLOG_ALL; + } + if (!(mask & level)) { + return; + } + + char buffer[256]; + vsnprintf(buffer, sizeof(buffer), format, args); + CIerr(0, "[%s] %s\n", mLogCategoryName(category), buffer); +} + +int main(int argc, char** argv) { + int status = 0; + if (!parseCInemaArgs(argc, argv)) { + status = 1; + goto cleanup; + } + + if (showVersion) { + version(argv[0]); + goto cleanup; + } + + if (showUsage) { + usageCInema(argv[0]); + goto cleanup; + } + + argc -= optind; + argv += optind; + + if (!base[0] && !determineBase(argc, argv)) { + CIlog(0, "Could not determine CInema test base. Please specify manually."); + status = 1; + goto cleanup; + } +#ifndef _WIN32 + char* rbase = realpath(base, NULL); + strncpy(base, rbase, PATH_MAX); + free(rbase); +#endif + + struct CInemaTestList tests; + CInemaTestListInit(&tests, 0); + + struct mLogger logger = { .log = _log }; + mLogSetDefaultLogger(&logger); + + if (argc > 0) { + size_t i; + for (i = 0; i < (size_t) argc; ++i) { + char path[PATH_MAX + 1] = {0}; + testToPath(argv[i], path); + + if (!collectTests(&tests, path)) { + status = 1; + break; + } + } + } else if (!collectTests(&tests, base)) { + status = 1; + } + + if (CInemaTestListSize(&tests) == 0) { + CIlog(1, "No tests found."); + status = 1; + } else { + reduceTestList(&tests); + } + + struct Table configTree; + HashTableInit(&configTree, 0, free); + + size_t i; + for (i = 0; i < CInemaTestListSize(&tests); ++i) { + struct CInemaTest* test = CInemaTestListGetPointer(&tests, i); + if (dryRun) { + CIlog(-1, "%s\n", test->name); + } else { + CIlog(1, "%s: ", test->name); + fflush(stdout); + CInemaTestRun(test, &configTree); + switch (test->status) { + case CI_PASS: + CIlog(1, "pass\n"); + break; + case CI_FAIL: + status = 1; + CIlog(1, "fail\n"); + break; + case CI_XPASS: + CIlog(1, "xpass\n"); + break; + case CI_XFAIL: + CIlog(1, "xfail\n"); + break; + case CI_SKIP: + CIlog(1, "skip\n"); + break; + case CI_ERROR: + status = 1; + CIlog(1, "error"); + break; + } + if (test->failedFrames) { + CIlog(2, "\tfailed frames: %u/%u (%1.3g%%)\n", test->failedFrames, test->totalFrames, test->failedFrames / (test->totalFrames * 0.01)); + CIlog(2, "\tfailed pixels: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->failedPixels, test->totalPixels, test->failedPixels / (test->totalPixels * 0.01)); + CIlog(2, "\tdistance: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->totalDistance, test->totalPixels * 765, test->totalDistance / (test->totalPixels * 7.65)); + } + } + } + + HashTableEnumerate(&configTree, _unloadConfigTree, NULL); + HashTableDeinit(&configTree); + CInemaTestListDeinit(&tests); + +cleanup: + return status; +}