Merge branch 'master' into medusa

This commit is contained in:
Vicki Pfau 2020-07-16 22:29:18 -07:00
commit d13041f4e9
60 changed files with 922 additions and 46 deletions

View File

@ -35,6 +35,7 @@ Emulation fixes:
- 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 @@ Emulation fixes:
- 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 @@ Other fixes:
- 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)

View File

@ -60,6 +60,7 @@ if(NOT LIBMGBA_ONLY)
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 @@ if(NOT QUIET AND NOT LIBMGBA_ONLY)
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:")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 B

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -10,6 +10,8 @@
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 addre
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

View File

@ -56,7 +56,7 @@ enum {
GBA_COLOR_WHITE = 0x7FFF,
#endif
#else
GBA_COLOR_WHITE = 0x00F8F8F8,
GBA_COLOR_WHITE = 0x00FFFFFF,
#endif
OFFSET_PRIORITY = 30,
OFFSET_INDEX = 28,

View File

@ -369,6 +369,8 @@ bool FFmpegEncoderOpen(struct FFmpegEncoder* encoder, const char* outfile) {
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;

View File

@ -5,7 +5,6 @@
* 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 @@ static void _flushBuffer(struct mVideoLogContext* context) {
}
}
void mVideoLogContextDestroy(struct mCore* core, struct mVideoLogContext* context) {
void mVideoLogContextDestroy(struct mCore* core, struct mVideoLogContext* context, bool closeVF) {
if (context->write) {
_flushBuffer(context);
@ -752,6 +751,10 @@ void mVideoLogContextDestroy(struct mCore* core, struct mVideoLogContext* contex
#endif
}
if (closeVF && context->backing) {
context->backing->close(context->backing);
}
free(context);
}
@ -1033,7 +1036,7 @@ static ssize_t mVideoLoggerWriteChannel(struct mVideoLogChannel* channel, const
return read;
}
struct mCore* mVideoLogCoreFind(struct VFile* vf) {
static const struct mVLDescriptor* _mVideoLogDescriptor(struct VFile* vf) {
if (!vf) {
return NULL;
}
@ -1055,6 +1058,25 @@ struct mCore* mVideoLogCoreFind(struct VFile* vf) {
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) {
core = descriptor->open();

View File

@ -1091,7 +1091,7 @@ static bool _GBVLPInit(struct mCore* core) {
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;
}

View File

@ -213,6 +213,8 @@ static void GBVideoSoftwareRendererInit(struct GBVideoRenderer* renderer, enum G
softwareRenderer->lookup[i] = i;
softwareRenderer->lookup[i] = i;
}
memset(softwareRenderer->palette, 0, sizeof(softwareRenderer->palette));
}
static void GBVideoSoftwareRendererDeinit(struct GBVideoRenderer* renderer) {

View File

@ -894,6 +894,9 @@ void GBVideoDeserialize(struct GBVideo* video, const struct GBSerializedState* s
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 @@ void GBVideoDeserialize(struct GBVideo* video, const struct GBSerializedState* s
_cleanOAM(video, video->ly);
GBVideoSwitchBank(video, video->vramCurrentBank);
video->renderer->deinit(video->renderer);
video->renderer->init(video->renderer, video->p->model, video->sgbBorders);
}

View File

@ -1248,7 +1248,7 @@ static bool _GBAVLPInit(struct mCore* core) {
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;
}

View File

@ -8,15 +8,17 @@ Output = namedtuple('Output', ['video'])
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:

View File

@ -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 @@ class VideoTest(CinemaTest):
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 @@ class VideoTest(CinemaTest):
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))

View File

@ -859,9 +859,8 @@ void CoreController::endVideoLog(bool closeVf) {
}
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;

View File

@ -1512,11 +1512,6 @@ Game Boy Advance 是任天堂有限公司Nintendo Co., Ltd.)的注册商标
<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>
@ -1562,7 +1557,7 @@ Game Boy Advance 是任天堂有限公司Nintendo Co., Ltd.)的注册商标
<message>
<location filename="../CoreManager.cpp" line="86"/>
<source>Could not load game. Are you sure it&apos;s in the correct format?</source>
<translation></translation>
<translation></translation>
</message>
</context>
<context>
@ -1696,8 +1691,8 @@ Game Boy Advance 是任天堂有限公司Nintendo Co., Ltd.)的注册商标
</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)&quot;</source>
<translation> (*.gif);;便 (*.png *.apng)&quot;</translation>
</message>
</context>
<context>
@ -3947,7 +3942,7 @@ Game Boy Advance 是任天堂有限公司Nintendo Co., Ltd.)的注册商标
<location filename="../Window.cpp" line="1191"/>
<location filename="../Window.cpp" line="1196"/>
<source>State &amp;%1</source>
<translation> 1(&amp;1)</translation>
<translation> (&amp;%1)</translation>
</message>
<message>
<location filename="../Window.cpp" line="1203"/>
@ -5318,21 +5313,11 @@ Game Boy Advance 是任天堂有限公司Nintendo Co., Ltd.)的注册商标
<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"/>
<source>h.264</source>
@ -5407,7 +5392,7 @@ Game Boy Advance 是任天堂有限公司Nintendo Co., Ltd.)的注册商标
<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"/>

View File

@ -37,3 +37,10 @@ if(BUILD_SUITE)
add_test(${TEST_NAME} test-${TEST_NAME})
endforeach()
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()

View File

@ -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;
}