all repos — mgba @ 5135756ec42d6109158e360a4f1ef8d3e0a5a27f

mGBA Game Boy Advance Emulator

Merge branch 'master' into medusa
Vicki Pfau vi@endrift.com
Thu, 30 Jul 2020 19:09:52 -0700
commit

5135756ec42d6109158e360a4f1ef8d3e0a5a27f

parent

c62d913e233e7ea3bb23a3f52fcb7b481f2faed5

M CHANGESCHANGES

@@ -88,6 +88,8 @@ - Util: Fix crash if PNG header fails to write

- SM83: Simplify register pair access on big endian - Wii: Fix pixelated filtering on interframe blending (fixes mgba.io/i/1830) Misc: + - GB: Allow pausing event loop while CPU is blocked + - GBA: Allow pausing event loop while CPU is blocked - Debugger: Keep track of global cycle count - FFmpeg: Add looping option for GIF/APNG - FFmpeg: Use range coder for FFV1 to reduce output size
M CMakeLists.txtCMakeLists.txt

@@ -13,7 +13,7 @@ set(CMAKE_C_STANDARD 99)

if(NOT MSVC) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_EXTENSIONS OFF) - if(SWITCH) + if(SWITCH OR 3DS) set(CMAKE_C_STANDARD 11) set(CMAKE_C_EXTENSIONS ON) elseif(CMAKE_C_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_VERSION VERSION_LESS "4.3")

@@ -297,6 +297,7 @@ include(CheckIncludeFiles)

check_function_exists(strdup HAVE_STRDUP) check_function_exists(strndup HAVE_STRNDUP) check_function_exists(strlcpy HAVE_STRLCPY) +check_function_exists(vasprintf HAVE_VASPRINTF) if(NOT DEFINED PSP2) check_function_exists(localtime_r HAVE_LOCALTIME_R) endif()

@@ -377,6 +378,10 @@ endif()

if(HAVE_STRLCPY) list(APPEND FUNCTION_DEFINES HAVE_STRLCPY) +endif() + +if(HAVE_VASPRINTF) + list(APPEND FUNCTION_DEFINES HAVE_VASPRINTF) endif() if(HAVE_LOCALTIME_R)

@@ -987,6 +992,9 @@ endif()

if(NOT USE_CMOCKA) set(BUILD_SUITE OFF) +endif() +if(BUILD_TEST OR BUILD_SUITE OR BUILD_CINEMA) + enable_testing() endif() add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/platform/test ${CMAKE_CURRENT_BINARY_DIR}/test)
M include/mgba-util/platform/posix/threading.hinclude/mgba-util/platform/posix/threading.h

@@ -24,6 +24,7 @@

typedef pthread_t Thread; typedef pthread_mutex_t Mutex; typedef pthread_cond_t Condition; +typedef pthread_key_t ThreadLocal; static inline int MutexInit(Mutex* mutex) { return pthread_mutex_init(mutex, 0);

@@ -99,6 +100,18 @@ #else

UNUSED(name); return 0; #endif +} + +static inline void ThreadLocalInitKey(ThreadLocal* key) { + pthread_key_create(key, 0); +} + +static inline void ThreadLocalSetKey(ThreadLocal key, void* value) { + pthread_setspecific(key, value); +} + +static inline void* ThreadLocalGetValue(ThreadLocal key) { + return pthread_getspecific(key); } CXX_GUARD_END
M include/mgba-util/platform/psp2/threading.hinclude/mgba-util/platform/psp2/threading.h

@@ -17,6 +17,7 @@ int waiting;

} Condition; #define THREAD_ENTRY int typedef THREAD_ENTRY (*ThreadEntry)(void*); +typedef int ThreadLocal; static inline int MutexInit(Mutex* mutex) { Mutex id = sceKernelCreateMutex("mutex", 0, 0, 0);

@@ -142,5 +143,20 @@

static inline int ThreadSetName(const char* name) { UNUSED(name); return -1; +} + +static inline void ThreadLocalInitKey(ThreadLocal* key) { + static int base = 0x90; + *key = __atomic_fetch_add(&base, 1, __ATOMIC_SEQ_CST); +} + +static inline void ThreadLocalSetKey(ThreadLocal key, void* value) { + void** tls = sceKernelGetTLSAddr(key); + *tls = value; +} + +static inline void* ThreadLocalGetValue(ThreadLocal key) { + void** tls = sceKernelGetTLSAddr(key); + return *tls; } #endif
M include/mgba-util/platform/windows/threading.hinclude/mgba-util/platform/windows/threading.h

@@ -16,6 +16,7 @@

typedef HANDLE Thread; typedef CRITICAL_SECTION Mutex; typedef CONDITION_VARIABLE Condition; +typedef DWORD ThreadLocal; static inline int MutexInit(Mutex* mutex) { InitializeCriticalSection(mutex);

@@ -86,6 +87,18 @@

static inline int ThreadSetName(const char* name) { UNUSED(name); return -1; +} + +static inline void ThreadLocalInitKey(ThreadLocal* key) { + *key = TlsAlloc(); +} + +static inline void ThreadLocalSetKey(ThreadLocal key, void* value) { + TlsSetValue(key, value); +} + +static inline void* ThreadLocalGetValue(ThreadLocal key) { + return TlsGetValue(key); } #endif
M include/mgba-util/string.hinclude/mgba-util/string.h

@@ -45,6 +45,7 @@

void rtrim(char* string); ssize_t parseQuotedString(const char* unparsed, ssize_t unparsedLen, char* parsed, ssize_t parsedLen); +bool wildcard(const char* search, const char* string); CXX_GUARD_END
M include/mgba-util/threading.hinclude/mgba-util/threading.h

@@ -11,6 +11,12 @@

CXX_GUARD_START #ifndef DISABLE_THREADING +#if __STDC_VERSION__ >= 201112L +#define ThreadLocal _Thread_local void* +#define ThreadLocalInitKey(X) +#define ThreadLocalSetKey(K, V) K = V +#define ThreadLocalGetValue(K) K +#endif #ifdef USE_PTHREADS #include <mgba-util/platform/posix/threading.h> #elif defined(_WIN32)

@@ -40,6 +46,7 @@ #else

typedef void* Mutex; #endif typedef void* Condition; +typedef int ThreadLocal; static inline int MutexInit(Mutex* mutex) { UNUSED(mutex);

@@ -92,6 +99,20 @@

static inline int ConditionWake(Condition* cond) { UNUSED(cond); return 0; +} + +static inline void ThreadLocalInitKey(ThreadLocal* key) { + UNUSED(key); +} + +static inline void ThreadLocalSetKey(ThreadLocal key, void* value) { + UNUSED(key); + UNUSED(value); +} + +static inline void* ThreadLocalGetValue(ThreadLocal key) { + UNUSED(key); + return NULL; } #endif
M include/mgba/internal/gb/serialize.hinclude/mgba/internal/gb/serialize.h

@@ -235,6 +235,8 @@ DECL_BIT(GBSerializedCpuFlags, Condition, 0);

DECL_BIT(GBSerializedCpuFlags, IrqPending, 1); DECL_BIT(GBSerializedCpuFlags, DoubleSpeed, 2); DECL_BIT(GBSerializedCpuFlags, EiPending, 3); +DECL_BIT(GBSerializedCpuFlags, Halted, 4); +DECL_BIT(GBSerializedCpuFlags, Blocked, 5); DECL_BITFIELD(GBSerializedTimerFlags, uint8_t); DECL_BIT(GBSerializedTimerFlags, IrqPending, 0);
M include/mgba/internal/gba/serialize.hinclude/mgba/internal/gba/serialize.h

@@ -236,6 +236,7 @@ DECL_BITFIELD(GBASerializedMiscFlags, uint32_t);

DECL_BIT(GBASerializedMiscFlags, Halted, 0); DECL_BIT(GBASerializedMiscFlags, POSTFLG, 1); DECL_BIT(GBASerializedMiscFlags, IrqPending, 2); +DECL_BIT(GBASerializedMiscFlags, Blocked, 3); struct GBASerializedState { uint32_t versionMagic;
M src/core/thread.csrc/core/thread.c

@@ -16,23 +16,22 @@

#ifndef DISABLE_THREADING static const float _defaultFPSTarget = 60.f; +static ThreadLocal _contextKey; #ifdef USE_PTHREADS -static pthread_key_t _contextKey; static pthread_once_t _contextOnce = PTHREAD_ONCE_INIT; static void _createTLS(void) { - pthread_key_create(&_contextKey, 0); + ThreadLocalInitKey(&_contextKey); } #elif _WIN32 -static DWORD _contextKey; static INIT_ONCE _contextOnce = INIT_ONCE_STATIC_INIT; static BOOL CALLBACK _createTLS(PINIT_ONCE once, PVOID param, PVOID* context) { UNUSED(once); UNUSED(param); UNUSED(context); - _contextKey = TlsAlloc(); + ThreadLocalInitKey(&_contextKey); return TRUE; } #endif

@@ -144,12 +143,11 @@ static THREAD_ENTRY _mCoreThreadRun(void* context) {

struct mCoreThread* threadContext = context; #ifdef USE_PTHREADS pthread_once(&_contextOnce, _createTLS); - pthread_setspecific(_contextKey, threadContext); #elif _WIN32 InitOnceExecuteOnce(&_contextOnce, _createTLS, NULL, 0); - TlsSetValue(_contextKey, threadContext); #endif + ThreadLocalSetKey(_contextKey, threadContext); ThreadSetName("CPU Thread"); #if !defined(_WIN32) && defined(USE_PTHREADS)

@@ -620,21 +618,14 @@ }

MutexUnlock(&threadContext->impl->stateMutex); } -#ifdef USE_PTHREADS struct mCoreThread* mCoreThreadGet(void) { +#ifdef USE_PTHREADS pthread_once(&_contextOnce, _createTLS); - return pthread_getspecific(_contextKey); -} #elif _WIN32 -struct mCoreThread* mCoreThreadGet(void) { InitOnceExecuteOnce(&_contextOnce, _createTLS, NULL, 0); - return TlsGetValue(_contextKey); +#endif + return ThreadLocalGetValue(_contextKey); } -#else -struct mCoreThread* mCoreThreadGet(void) { - return NULL; -} -#endif static void _mCoreLog(struct mLogger* logger, int category, enum mLogLevel level, const char* format, va_list args) { UNUSED(logger);
M src/gb/gb.csrc/gb/gb.c

@@ -681,7 +681,7 @@

nextEvent = cycles; do { nextEvent = mTimingTick(&gb->timing, nextEvent); - } while (gb->cpuBlocked); + } while (gb->cpuBlocked && !gb->earlyExit); cpu->nextEvent = nextEvent; if (cpu->halted) {

@@ -695,6 +695,9 @@ break;

} } while (cpu->cycles >= cpu->nextEvent); gb->earlyExit = false; + if (gb->cpuBlocked) { + cpu->cycles = cpu->nextEvent; + } } void GBSetInterrupts(struct SM83Core* cpu, bool enable) {
M src/gb/serialize.csrc/gb/serialize.c

@@ -56,6 +56,8 @@ flags = GBSerializedCpuFlagsSetCondition(flags, gb->cpu->condition);

flags = GBSerializedCpuFlagsSetIrqPending(flags, gb->cpu->irqPending); flags = GBSerializedCpuFlagsSetDoubleSpeed(flags, gb->doubleSpeed); flags = GBSerializedCpuFlagsSetEiPending(flags, mTimingIsScheduled(&gb->timing, &gb->eiPending)); + flags = GBSerializedCpuFlagsSetHalted(flags, gb->cpu->halted); + flags = GBSerializedCpuFlagsSetBlocked(flags, gb->cpuBlocked); STORE_32LE(flags, 0, &state->cpu.flags); STORE_32LE(gb->eiPending.when - mTimingCurrentTime(&gb->timing), 0, &state->cpu.eiPending);

@@ -173,6 +175,9 @@ LOAD_32LE(flags, 0, &state->cpu.flags);

gb->cpu->condition = GBSerializedCpuFlagsGetCondition(flags); gb->cpu->irqPending = GBSerializedCpuFlagsGetIrqPending(flags); gb->doubleSpeed = GBSerializedCpuFlagsGetDoubleSpeed(flags); + gb->cpu->halted = GBSerializedCpuFlagsGetHalted(flags); + gb->cpuBlocked = GBSerializedCpuFlagsGetBlocked(flags); + gb->audio.timingFactor = gb->doubleSpeed + 1; LOAD_32LE(gb->cpu->cycles, 0, &state->cpu.cycles);
M src/gb/video.csrc/gb/video.c

@@ -379,6 +379,7 @@ }

GBFrameEnded(video->p); mCoreSyncPostFrame(video->p->sync); ++video->frameCounter; + video->p->earlyExit = true; GBFrameStarted(video->p); }
M src/gba/gba.csrc/gba/gba.c

@@ -286,7 +286,7 @@ mLOG(GBA, FATAL, "Negative cycles passed: %i", cycles);

} #endif nextEvent = mTimingTick(&gba->timing, cycles < nextEvent ? nextEvent : cycles); - } while (gba->cpuBlocked); + } while (gba->cpuBlocked && !gba->earlyExit); cpu->nextEvent = nextEvent; if (cpu->halted) {

@@ -305,11 +305,9 @@ break;

} } gba->earlyExit = false; -#ifndef NDEBUG if (gba->cpuBlocked) { - mLOG(GBA, FATAL, "CPU is blocked!"); + cpu->cycles = cpu->nextEvent; } -#endif } #ifdef USE_DEBUGGERS
M src/gba/serialize.csrc/gba/serialize.c

@@ -67,6 +67,7 @@ if (mTimingIsScheduled(&gba->timing, &gba->irqEvent)) {

miscFlags = GBASerializedMiscFlagsFillIrqPending(miscFlags); STORE_32(gba->irqEvent.when - mTimingCurrentTime(&gba->timing), 0, &state->nextIrq); } + miscFlags = GBASerializedMiscFlagsSetBlocked(miscFlags, gba->cpuBlocked); STORE_32(miscFlags, 0, &state->miscFlags); GBAMemorySerialize(&gba->memory, state);

@@ -185,6 +186,7 @@ int32_t when;

LOAD_32(when, 0, &state->nextIrq); mTimingSchedule(&gba->timing, &gba->irqEvent, when); } + gba->cpuBlocked = GBASerializedMiscFlagsGetBlocked(miscFlags); GBAVideoDeserialize(&gba->video, state); GBAMemoryDeserialize(&gba->memory, state);
M src/gba/video.csrc/gba/video.c

@@ -196,6 +196,7 @@ if (video->frameskipCounter < 0) {

video->frameskipCounter = video->frameskip; } ++video->frameCounter; + video->p->earlyExit = true; break; case VIDEO_VERTICAL_TOTAL_PIXELS - 1: video->p->memory.io[REG_DISPSTAT >> 1] = GBARegisterDISPSTATClearInVblank(dispstat);
M src/platform/python/CMakeLists.txtsrc/platform/python/CMakeLists.txt

@@ -54,9 +54,8 @@ COMMAND BINDIR=${CMAKE_CURRENT_BINARY_DIR}/.. LIBDIR=${CMAKE_CURRENT_BINARY_DIR}/.. CPPFLAGS="${INCLUDE_FLAGS}" ${PYTHON_EXECUTABLE} setup.py bdist_wheel -b ${CMAKE_CURRENT_BINARY_DIR}

WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS ${BINARY_NAME}-py) -file(GLOB BASE_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/test_*.py) -file(GLOB SUBTESTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/*/test_*.py) -foreach(TEST IN LISTS BASE_TESTS SUBTESTS) +file(GLOB TESTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/*/test_*.py) +foreach(TEST IN LISTS TESTS) if(APPLE) set(PATH DYLD_LIBRARY_PATH) elseif(WIN32)

@@ -64,7 +63,7 @@ set(PATH PATH)

else() set(PATH LD_LIBRARY_PATH) endif() - string(REGEX REPLACE "${CMAKE_CURRENT_SOURCE_DIR}/(tests/.*/)?test_" "" TEST_NAME "${TEST}") + string(REGEX REPLACE "${CMAKE_CURRENT_SOURCE_DIR}/tests/(.*/)?test_" "" TEST_NAME "${TEST}") string(REPLACE ".py" "" TEST_NAME "${TEST_NAME}") add_test(NAME python-${TEST_NAME} COMMAND ${PYTHON_EXECUTABLE} setup.py build -b ${CMAKE_CURRENT_BINARY_DIR} pytest --extras --addopts ${TEST}
M src/platform/test/CMakeLists.txtsrc/platform/test/CMakeLists.txt

@@ -43,4 +43,5 @@ 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}") + add_test(cinema ${BINARY_NAME}-cinema -v) endif()
M src/platform/test/cinema-main.csrc/platform/test/cinema-main.c

@@ -13,6 +13,7 @@

#include <mgba-util/png-io.h> #include <mgba-util/string.h> #include <mgba-util/table.h> +#include <mgba-util/threading.h> #include <mgba-util/vector.h> #include <mgba-util/vfs.h>

@@ -32,21 +33,26 @@ #include <sys/stat.h>

#include <sys/types.h> #define MAX_TEST 200 +#define MAX_JOBS 128 +#define LOG_THRESHOLD 1000000 static const struct option longOpts[] = { { "base", required_argument, 0, 'b' }, { "diffs", no_argument, 0, 'd' }, { "help", no_argument, 0, 'h' }, + { "jobs", required_argument, 0, 'j' }, { "dry-run", no_argument, 0, 'n' }, { "outdir", required_argument, 0, 'o' }, { "quiet", no_argument, 0, 'q' }, { "rebaseline", no_argument, 0, 'r' }, + { "rebaseline-missing", no_argument, 0, 'R' }, { "verbose", no_argument, 0, 'v' }, + { "xbaseline", no_argument, 0, 'x' }, { "version", no_argument, 0, '\0' }, { 0, 0, 0, 0 } }; -static const char shortOpts[] = "b:dhno:qrv"; +static const char shortOpts[] = "b:dhj:no:qRrvx"; enum CInemaStatus { CI_PASS,

@@ -57,6 +63,12 @@ CI_ERROR,

CI_SKIP }; +enum CInemaRebaseline { + CI_R_NONE = 0, + CI_R_FAILING, + CI_R_MISSING, +}; + struct CInemaTest { char directory[MAX_TEST]; char filename[MAX_TEST];

@@ -82,30 +94,111 @@

DECLARE_VECTOR(ImageList, void*) DEFINE_VECTOR(ImageList, void*) +struct StringBuilder { + struct StringList lines; + struct StringList partial; + unsigned repeat; + +}; + +struct CInemaLogStream { + struct StringBuilder err; + struct StringBuilder out; +}; + 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 enum CInemaRebaseline rebaseline = CI_R_NONE; +static enum CInemaRebaseline xbaseline = CI_R_NONE; static int verbosity = 0; +static struct Table configTree; +static Mutex configMutex; + +static int jobs = 1; +static size_t jobIndex = 0; +static Mutex jobMutex; +static Thread jobThreads[MAX_JOBS]; +static int jobStatus; +static ThreadLocal logStream; +static ThreadLocal currentTest; + bool CInemaTestInit(struct CInemaTest*, const char* directory, const char* filename); -void CInemaTestRun(struct CInemaTest*, struct Table* configTree); +void CInemaTestRun(struct CInemaTest*); 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); +void CIflush(struct StringBuilder* list, FILE* file); + +static char* _compileStringList(struct StringList* list) { + size_t len = 0; + size_t i; + for (i = 0; i < StringListSize(list); ++i) { + len += strlen(*StringListGetPointer(list, i)); + } + char* string = calloc(len + 1, sizeof(char)); + char* cur = string; + for (i = 0; i < StringListSize(list); ++i) { + char* brick = *StringListGetPointer(list, i); + size_t portion = strlen(brick); + memcpy(cur, brick, portion); + free(brick); + cur += portion; + } + StringListClear(list); + return string; +} + +static void _logToStream(FILE* file, const char* format, va_list args) { +#ifdef HAVE_VASPRINTF + struct CInemaLogStream* stream = ThreadLocalGetValue(logStream); + if (!stream) { + vfprintf(file, format, args); + } else { + struct StringBuilder* builder = &stream->out; + if (file == stderr) { + builder = &stream->err; + } + if (StringListSize(&builder->lines) > LOG_THRESHOLD) { + CIflush(builder, file); + } + char** line = StringListAppend(&builder->partial); + vasprintf(line, format, args); + size_t len = strlen(*line); + if (len && (*line)[len - 1] == '\n') { + char* string = _compileStringList(&builder->partial); + size_t linecount = StringListSize(&builder->lines); + if (linecount && strcmp(string, *StringListGetPointer(&builder->lines, linecount - 1)) == 0) { + ++builder->repeat; + free(string); + } else { + if (builder->repeat > 1) { + asprintf(StringListAppend(&builder->lines), "The previous message was repeated %u times.\n", builder->repeat); + } + *StringListAppend(&builder->lines) = string; + builder->repeat = 1; + } + } + } +#else + vfprintf(file, format, args); +#endif +} + 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); + _logToStream(stdout, format, args); va_end(args); }

@@ -115,10 +208,26 @@ return;

} va_list args; va_start(args, format); - vfprintf(stderr, format, args); + _logToStream(stderr, format, args); va_end(args); } +void CIflush(struct StringBuilder* builder, FILE* out) { + if (StringListSize(&builder->partial)) { + *StringListAppend(&builder->lines) = _compileStringList(&builder->partial); + } +#ifdef HAVE_VASPRINTF + if (builder->repeat > 1) { + asprintf(StringListAppend(&builder->lines), "The previous message was repeated %u times.\n", builder->repeat); + } +#endif + + char* string = _compileStringList(&builder->lines); + builder->repeat = 0; + fputs(string, out); + free(string); +} + static bool parseCInemaArgs(int argc, char* const* argv) { int ch; int index = 0;

@@ -142,6 +251,15 @@ break;

case 'h': showUsage = true; break; + case 'j': + jobs = atoi(optarg); + if (jobs > MAX_JOBS) { + jobs = MAX_JOBS; + } + if (jobs < 1) { + jobs = 1; + } + break; case 'n': dryRun = true; break;

@@ -153,11 +271,17 @@ case 'q':

--verbosity; break; case 'r': - rebaseline = true; + rebaseline = CI_R_FAILING; + break; + case 'R': + rebaseline = CI_R_MISSING; break; case 'v': ++verbosity; break; + case 'x': + xbaseline = CI_R_FAILING; + break; default: return false; }

@@ -167,15 +291,18 @@ 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"); + printf("usage: %s [-dhnqrRv] [-j JOBS] [-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(" -j, --jobs JOBS Run a number of jobs in parallel"); puts(" -n, --dry-run List all collected tests instead of running them"); - puts(" -o, --output [DIR] Path to output applicable results"); + 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(" -R, --rebaseline-missing Write missing baselines tests only"); puts(" -v, --verbose Increase log verbosity (can be repeated)"); + puts(" -x, --xbaseline Rewrite the xfail baselines for failing tests"); puts(" --version Print version and exit"); }

@@ -276,6 +403,84 @@ }

path[i] = '\0'; } +static bool globTests(struct CInemaTestList* tests, const char* glob, const char* ancestors) { + bool success = true; + const char* next = strpbrk(glob, "*."); + + char path[PATH_MAX]; + if (!next) { + testToPath(glob, path); + return collectTests(tests, path); + } else if (next[0] == '.') { + char subtest[MAX_TEST]; + if (!ancestors) { + strncpy(subtest, glob, next - glob); + } else { + size_t len = strlen(ancestors) + (next - glob) + 2; + if (len > sizeof(subtest)) { + len = sizeof(subtest); + } + snprintf(subtest, len, "%s.%s", ancestors, glob); + } + return globTests(tests, next + 1, subtest); + } else if (next[0] == '*') { + char globBuffer[MAX_TEST]; + const char* subglob; + + next = strchr(next, '.'); + if (!next) { + subglob = glob; + } else { + size_t len = next - glob + 1; + if (len > sizeof(globBuffer)) { + len = sizeof(globBuffer); + } + strncpy(globBuffer, glob, len - 1); + subglob = globBuffer; + } + bool hasMoreGlobs = next && strchr(next, '*'); + + struct VDir* dir; + if (ancestors) { + testToPath(ancestors, path); + dir = VDirOpen(path); + } else { + dir = VDirOpen(base); + } + if (!dir) { + return false; + } + + struct VDirEntry* dirent = dir->listNext(dir); + while (dirent) { + const char* name = dirent->name(dirent); + if (dirent->type(dirent) != VFS_DIRECTORY || strncmp(name, ".", 2) == 0 || strncmp(name, "..", 3) == 0) { + dirent = dir->listNext(dir); + continue; + } + if (wildcard(subglob, name)) { + char newgen[MAX_TEST]; + if (ancestors) { + snprintf(newgen, sizeof(newgen), "%s.%s", ancestors, name); + } else { + strlcpy(newgen, name, sizeof(newgen)); + } + if (next && hasMoreGlobs) { + globTests(tests, next + 1, newgen); + } else { + testToPath(newgen, path); + collectTests(tests, path); + } + } + dirent = dir->listNext(dir); + } + + return true; + } else { + abort(); + } +} + static void _loadConfigTree(struct Table* configTree, const char* testName) { char key[MAX_TEST]; strlcpy(key, testName, sizeof(key));

@@ -307,7 +512,7 @@ UNUSED(user);

mCoreConfigDeinit(value); } -static const char* _lookupValue(struct Table* configTree, const char* testName, const char* key) { +static const char* CInemaConfigGet(struct Table* configTree, const char* testName, const char* key) { _loadConfigTree(configTree, testName); char testKey[MAX_TEST];

@@ -336,7 +541,7 @@ return NULL;

} bool CInemaConfigGetUInt(struct Table* configTree, const char* testName, const char* key, unsigned* out) { - const char* charValue = _lookupValue(configTree, testName, key); + const char* charValue = CInemaConfigGet(configTree, testName, key); if (!charValue) { return false; }

@@ -400,9 +605,9 @@ }

return true; } -static bool _loadBaselinePNG(struct VDir* dir, struct CInemaImage* image, size_t frame, enum CInemaStatus* status) { +static bool _loadBaselinePNG(struct VDir* dir, const char* type, struct CInemaImage* image, size_t frame, enum CInemaStatus* status) { char baselineName[32]; - snprintf(baselineName, sizeof(baselineName), "baseline_%04" PRIz "u.png", frame); + snprintf(baselineName, sizeof(baselineName), "%s_%04" PRIz "u.png", type, frame); struct VFile* baselineVF = dir->openFile(dir, baselineName, O_RDONLY); if (!baselineVF) { if (*status == CI_PASS) {

@@ -541,9 +746,9 @@ _writeImage(vf, image);

dir->close(dir); } -static void _writeBaseline(struct VDir* dir, const struct CInemaImage* image, size_t frame) { +static void _writeBaseline(struct VDir* dir, const char* type, const struct CInemaImage* image, size_t frame) { char baselineName[32]; - snprintf(baselineName, sizeof(baselineName), "baseline_%04" PRIz "u.png", frame); + snprintf(baselineName, sizeof(baselineName), "%s_%04" PRIz "u.png", type, frame); struct VFile* baselineVF = dir->openFile(dir, baselineName, O_CREAT | O_TRUNC | O_WRONLY); if (baselineVF) { _writeImage(baselineVF, image);

@@ -552,9 +757,180 @@ CIerr(0, "Could not open output file %s\n", baselineName);

} } -void CInemaTestRun(struct CInemaTest* test, struct Table* configTree) { +static bool _updateInput(struct mCore* core, size_t frame, const char** input) { + if (!*input || !*input[0]) { + return false; + } + char* end; + uint32_t start = strtoul(*input, &end, 10); + if (end[0] != ':') { + return false; + } + if (start != frame) { + return true; + } + ++end; + *input = end; + uint32_t keys = strtoul(*input, &end, 16); + if (end[0] == ',') { + ++end; + } + *input = end; + core->setKeys(core, keys); + return true; +} + +static bool _compareImages(struct CInemaTest* restrict test, const struct CInemaImage* restrict image, const struct CInemaImage* restrict expected, int* restrict max, uint8_t** restrict outdiff) { + const uint8_t* testPixels = image->data; + const uint8_t* expectPixels = expected->data; + uint8_t* diff = NULL; + size_t x; + size_t y; + 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 (outdiff && !diff) { + diff = calloc(expected->stride * expected->height, BYTES_PER_PIXEL); + *outdiff = diff; + } + 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; + } + + if (test) { + test->totalDistance += r + g + b; + ++test->failedPixels; + } + } + } + } + return !failed; +} + +void _writeDiffSet(struct CInemaImage* expected, const char* name, uint8_t* diff, int frame, int max, bool xfail) { + struct CInemaImage outdiff = { + .data = diff, + .width = expected->width, + .height = expected->height, + .stride = expected->stride, + }; + + if (xfail) { + _writeDiff(name, expected, frame, "xexpected"); + _writeDiff(name, &outdiff, frame, "xdiff"); + } else { + _writeDiff(name, expected, frame, "expected"); + _writeDiff(name, &outdiff, frame, "diff"); + } + + size_t x; + size_t y; + 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; + } + } + if (xfail) { + _writeDiff(name, &outdiff, frame, "xnormalized"); + } +} + +#ifdef USE_FFMPEG +static void _replayBaseline(struct CInemaTest* test, struct FFmpegEncoder* encoder, const struct CInemaImage* image, int frame) { + char baselineName[PATH_MAX]; + snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP ".baseline.avi", test->directory); + + if (!FFmpegEncoderOpen(encoder, baselineName)) { + CIerr(1, "Failed to save baseline video\n"); + test->status = CI_ERROR; + return; + } + + snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP "baseline.avi", test->directory); + + + struct CInemaImage buffer = { + .data = NULL, + .width = image->width, + .height = image->height, + .stride = image->width, + }; + struct FFmpegDecoder decoder; + struct CInemaStream stream = {0}; + stream.d.postVideoFrame = _cinemaVideoFrame; + stream.d.videoDimensionsChanged = _cinemaDimensionsChanged; + stream.status = &test->status; + stream.image = &buffer; + + FFmpegDecoderInit(&decoder); + decoder.out = &stream.d; + + if (!FFmpegDecoderOpen(&decoder, baselineName)) { + CIerr(1, "Failed to load baseline video\n"); + test->status = CI_ERROR; + return; + } + + int i; + for (i = 0; i < frame; ++i) { + while (!buffer.data) { + if (!FFmpegDecoderRead(&decoder)) { + CIlog(1, "Failed to read more frames. EOF?\n"); + test->status = CI_FAIL; + break; + } + } + encoder->d.postVideoFrame(&encoder->d, buffer.data, buffer.stride); + free(buffer.data); + buffer.data = NULL; + } + FFmpegDecoderClose(&decoder); +} +#endif + +void CInemaTestRun(struct CInemaTest* test) { unsigned ignore = 0; - CInemaConfigGetUInt(configTree, test->name, "ignore", &ignore); + MutexLock(&configMutex); + CInemaConfigGetUInt(&configTree, test->name, "ignore", &ignore); + MutexUnlock(&configMutex); if (ignore) { test->status = CI_SKIP; return;

@@ -602,12 +978,16 @@ unsigned limit = 9999;

unsigned skip = 0; unsigned fail = 0; unsigned video = 0; + const char* input = NULL; - CInemaConfigGetUInt(configTree, test->name, "frames", &limit); - CInemaConfigGetUInt(configTree, test->name, "skip", &skip); - CInemaConfigGetUInt(configTree, test->name, "fail", &fail); - CInemaConfigGetUInt(configTree, test->name, "video", &video); - CInemaConfigLoad(configTree, test->name, core); + MutexLock(&configMutex); + CInemaConfigGetUInt(&configTree, test->name, "frames", &limit); + CInemaConfigGetUInt(&configTree, test->name, "skip", &skip); + CInemaConfigGetUInt(&configTree, test->name, "fail", &fail); + CInemaConfigGetUInt(&configTree, test->name, "video", &video); + input = CInemaConfigGet(&configTree, test->name, "input"); + CInemaConfigLoad(&configTree, test->name, core); + MutexUnlock(&configMutex); struct VFile* save = VFileMemChunk(NULL, 0); core->loadROM(core, rom);

@@ -631,30 +1011,39 @@ #ifdef USE_FFMPEG

struct FFmpegDecoder decoder; struct FFmpegEncoder encoder; struct CInemaStream stream = {0}; + + char baselineName[PATH_MAX]; + snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP "baseline.avi", test->directory); + bool exists = access(baselineName, 0) == 0; + if (video) { - char fname[PATH_MAX]; - snprintf(fname, sizeof(fname), "%s" PATH_SEP "baseline.mkv", test->directory); - if (rebaseline) { - FFmpegEncoderInit(&encoder); - FFmpegEncoderSetAudio(&encoder, NULL, 0); - FFmpegEncoderSetVideo(&encoder, "png", 0, 0); - FFmpegEncoderSetContainer(&encoder, "mkv"); - FFmpegEncoderSetDimensions(&encoder, image.width, image.height); - if (!FFmpegEncoderOpen(&encoder, fname)) { + FFmpegEncoderInit(&encoder); + FFmpegDecoderInit(&decoder); + + FFmpegEncoderSetAudio(&encoder, NULL, 0); + FFmpegEncoderSetVideo(&encoder, "zmbv", 0, 0); + FFmpegEncoderSetContainer(&encoder, "avi"); + FFmpegEncoderSetDimensions(&encoder, image.width, image.height); + + if (rebaseline && !exists) { + if (!FFmpegEncoderOpen(&encoder, baselineName)) { CIerr(1, "Failed to save baseline video\n"); } else { core->setAVStream(core, &encoder.d); } - } else { - FFmpegDecoderInit(&decoder); + } + + if (exists) { stream.d.postVideoFrame = _cinemaVideoFrame; stream.d.videoDimensionsChanged = _cinemaDimensionsChanged; stream.status = &test->status; decoder.out = &stream.d; - if (!FFmpegDecoderOpen(&decoder, fname)) { + if (!FFmpegDecoderOpen(&decoder, baselineName)) { CIerr(1, "Failed to load baseline video\n"); } + } else if (!rebaseline) { + test->status = CI_FAIL; } } #else

@@ -664,11 +1053,16 @@ test->status = CI_ERROR;

} #endif + bool xdiff = false; for (frame = 0; limit; ++frame, --limit) { + _updateInput(core, frame, &input); core->runFrame(core); ++test->totalFrames; unsigned frameCounter = core->frameCounter(core); if (frameCounter <= minFrame) { + break; + } + if (test->status == CI_ERROR) { break; } CIlog(3, "Test frame: %u\n", frameCounter);

@@ -684,7 +1078,7 @@ bool baselineFound;

if (video) { baselineFound = false; #ifdef USE_FFMPEG - if (!rebaseline && FFmpegDecoderIsOpen(&decoder)) { + if (FFmpegDecoderIsOpen(&decoder)) { stream.image = &expected; while (!expected.data) { if (!FFmpegDecoderRead(&decoder)) {

@@ -697,136 +1091,203 @@ baselineFound = expected.data;

} #endif } else { - baselineFound = _loadBaselinePNG(dir, &expected, frame, &test->status); + baselineFound = _loadBaselinePNG(dir, "baseline", &expected, frame, &test->status); } + if (test->status == CI_ERROR) { + break; + } + bool failed = false; if (baselineFound) { - 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.stride * 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; + failed = !_compareImages(test, &image, &expected, &max, diffs ? &diff : NULL); + if (failed) { + ++test->failedFrames; +#ifdef USE_FFMPEG + if (video && exists && rebaseline && !FFmpegEncoderIsOpen(&encoder)) { + _replayBaseline(test, &encoder, &image, frame); + if (test->status == CI_ERROR) { + break; } + encoder.d.postVideoFrame(&encoder.d, image.data, image.stride); + core->setAVStream(core, &encoder.d); } - } - if (failed) { - ++test->failedFrames; +#endif } test->totalPixels += image.height * image.width; - if (rebaseline && failed) { - _writeBaseline(dir, &image, frame); + if (rebaseline == CI_R_FAILING && !video && failed) { + _writeBaseline(dir, "baseline", &image, frame); } if (diff) { if (failed) { - struct CInemaImage outdiff = { - .data = diff, - .width = expected.width, - .height = expected.height, - .stride = expected.stride, - }; - _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"); + _writeDiffSet(&expected, test->name, diff, frame, max, false); } free(diff); + diff = NULL; } free(expected.data); - } else if (test->status == CI_ERROR) { - break; } else if (rebaseline && !video) { - _writeBaseline(dir, &image, frame); + _writeBaseline(dir, "baseline", &image, frame); } else if (!rebaseline) { test->status = CI_FAIL; } - } - if (fail) { - if (test->status == CI_FAIL) { - test->status = CI_XFAIL; - } else if (test->status == CI_PASS) { - test->status = CI_XPASS; + if (fail && failed) { + if (video) { + // TODO + baselineFound = false; + } else { + baselineFound = _loadBaselinePNG(dir, "xbaseline", &expected, frame, &test->status); + } + + if (baselineFound) { + int max = 0; + failed = !_compareImages(test, &image, &expected, &max, diffs ? &diff : NULL); + if (diff) { + if (failed) { + _writeDiffSet(&expected, test->name, diff, frame, max, true); + } + free(diff); + diff = NULL; + } + if (failed) { + if (xbaseline == CI_R_FAILING && !video) { + _writeBaseline(dir, "xbaseline", &image, frame); + } + xdiff = true; + } + free(expected.data); + } else if (xbaseline && !video) { + _writeBaseline(dir, "xbaseline", &image, frame); + } } } #ifdef USE_FFMPEG if (video) { - if (rebaseline) { + if (FFmpegEncoderIsOpen(&encoder)) { FFmpegEncoderClose(&encoder); - } else { + if (exists && rebaseline) { + char tmpBaselineName[PATH_MAX]; + snprintf(tmpBaselineName, sizeof(tmpBaselineName), "%s" PATH_SEP ".baseline.avi", test->directory); +#ifdef _WIN32 + MoveFileEx(tmpBaselineName, baselineName, MOVEFILE_REPLACE_EXISTING); +#else + rename(tmpBaselineName, baselineName); +#endif + } + } + if (FFmpegDecoderIsOpen(&decoder)) { FFmpegDecoderClose(&decoder); } } #endif + if (fail) { + if (test->status == CI_FAIL && !xdiff) { + 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); } +static bool CInemaTask(struct CInemaTestList* tests, size_t i) { + bool success = true; + struct CInemaTest* test = CInemaTestListGetPointer(tests, i); + if (dryRun) { + CIlog(-1, "%s\n", test->name); + } else { + CIlog(1, "%s: ", test->name); + fflush(stdout); + ThreadLocalSetKey(currentTest, test); + CInemaTestRun(test); + ThreadLocalSetKey(currentTest, NULL); + + switch (test->status) { + case CI_PASS: + CIlog(1, "pass\n"); + break; + case CI_FAIL: + success = false; + 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: + success = false; + CIlog(1, "error\n"); + 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)); + } + } + return success; +} + +static THREAD_ENTRY CInemaJob(void* context) { + struct CInemaTestList* tests = context; + struct CInemaLogStream stream; + StringListInit(&stream.out.lines, 0); + StringListInit(&stream.out.partial, 0); + stream.out.repeat = 0; + StringListInit(&stream.err.lines, 0); + StringListInit(&stream.err.partial, 0); + stream.err.repeat = 0; + ThreadLocalSetKey(logStream, &stream); + + bool success = true; + while (true) { + size_t i; + MutexLock(&jobMutex); + i = jobIndex; + ++jobIndex; + MutexUnlock(&jobMutex); + if (i >= CInemaTestListSize(tests)) { + break; + } + if (!CInemaTask(tests, i)) { + success = false; + } + CIflush(&stream.out, stdout); + CIflush(&stream.err, stderr); + } + MutexLock(&jobMutex); + if (!success) { + jobStatus = 1; + } + MutexUnlock(&jobMutex); + + CIflush(&stream.out, stdout); + StringListDeinit(&stream.out.lines); + StringListDeinit(&stream.out.partial); + + CIflush(&stream.err, stderr); + StringListDeinit(&stream.err.lines); + StringListDeinit(&stream.err.partial); +} + void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args) { UNUSED(log); + if (level == mLOG_FATAL) { + struct CInemaTest* test = ThreadLocalGetValue(currentTest); + test->status = CI_ERROR; + } if (verbosity < 0) { return; }

@@ -853,6 +1314,9 @@ CIerr(0, "[%s] %s\n", mLogCategoryName(category), buffer);

} int main(int argc, char** argv) { + ThreadLocalInitKey(&logStream); + ThreadLocalSetKey(logStream, NULL); + int status = 0; if (!parseCInemaArgs(argc, argv)) { status = 1;

@@ -899,6 +1363,13 @@

if (argc > 0) { size_t i; for (i = 0; i < (size_t) argc; ++i) { + if (strchr(argv[i], '*')) { + if (!globTests(&tests, argv[i], NULL)) { + status = 1; + break; + } + continue; + } char path[PATH_MAX + 1] = {0}; testToPath(argv[i], path);

@@ -918,48 +1389,33 @@ } else {

reduceTestList(&tests); } - struct Table configTree; HashTableInit(&configTree, 0, free); + MutexInit(&configMutex); + ThreadLocalInitKey(&currentTest); + ThreadLocalSetKey(currentTest, NULL); - 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: + if (jobs == 1) { + size_t i; + for (i = 0; i < CInemaTestListSize(&tests); ++i) { + bool success = CInemaTask(&tests, i); + if (!success) { status = 1; - CIlog(1, "error\n"); - 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)); } } + } else { + MutexInit(&jobMutex); + int i; + for (i = 0; i < jobs; ++i) { + ThreadCreate(&jobThreads[i], CInemaJob, &tests); + } + for (i = 0; i < jobs; ++i) { + ThreadJoin(&jobThreads[i]); + } + MutexDeinit(&jobMutex); + status = jobStatus; } + MutexDeinit(&configMutex); HashTableEnumerate(&configTree, _unloadConfigTree, NULL); HashTableDeinit(&configTree); CInemaTestListDeinit(&tests);
M src/util/string.csrc/util/string.c

@@ -518,4 +518,34 @@ break;

} } return -1; +} + +bool wildcard(const char* search, const char* string) { + while (true) { + if (search[0] == '*') { + while (search[0] == '*') { + ++search; + } + if (!search[0]) { + return true; + } + while (string[0]) { + if (string[0] == search[0] && wildcard(search, string)) { + return true; + } + ++string; + } + return false; + } else if (!search[0]) { + return !string[0]; + } else if (!string[0]) { + return false; + } else if (string[0] != search[0]) { + return false; + } else { + ++search; + ++string; + } + } + return false; }