all repos — mgba @ f00afe0758a14db76f847aa6fa9108c0291dc4ab

mGBA Game Boy Advance Emulator

src/platform/test/cinema-main.c (view raw)

   1/* Copyright (c) 2013-2020 Jeffrey Pfau
   2 *
   3 * This Source Code Form is subject to the terms of the Mozilla Public
   4 * License, v. 2.0. If a copy of the MPL was not distributed with this
   5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
   6#include <mgba/core/config.h>
   7#include <mgba/core/core.h>
   8#include <mgba/core/log.h>
   9#include <mgba/core/version.h>
  10#include <mgba/feature/commandline.h>
  11#include <mgba/feature/video-logger.h>
  12
  13#include <mgba-util/png-io.h>
  14#include <mgba-util/string.h>
  15#include <mgba-util/table.h>
  16#include <mgba-util/threading.h>
  17#include <mgba-util/vector.h>
  18#include <mgba-util/vfs.h>
  19
  20#ifdef USE_FFMPEG
  21#include "feature/ffmpeg/ffmpeg-decoder.h"
  22#include "feature/ffmpeg/ffmpeg-encoder.h"
  23#endif
  24
  25#ifdef _MSC_VER
  26#include <mgba-util/platform/windows/getopt.h>
  27#else
  28#include <getopt.h>
  29#endif
  30
  31#include <stdlib.h>
  32#include <sys/stat.h>
  33#include <sys/types.h>
  34
  35#define MAX_TEST 200
  36#define MAX_JOBS 128
  37#define LOG_THRESHOLD 1000000
  38
  39static const struct option longOpts[] = {
  40	{ "base",       required_argument, 0, 'b' },
  41	{ "diffs",      no_argument, 0, 'd' },
  42	{ "help",       no_argument, 0, 'h' },
  43	{ "jobs",       required_argument, 0, 'j' },
  44	{ "dry-run",    no_argument, 0, 'n' },
  45	{ "outdir",     required_argument, 0, 'o' },
  46	{ "quiet",      no_argument, 0, 'q' },
  47	{ "rebaseline", no_argument, 0, 'r' },
  48	{ "rebaseline-missing", no_argument, 0, 'R' },
  49	{ "verbose",    no_argument, 0, 'v' },
  50	{ "xbaseline",  no_argument, 0, 'x' },
  51	{ "version",    no_argument, 0, '\0' },
  52	{ 0, 0, 0, 0 }
  53};
  54
  55static const char shortOpts[] = "b:dhj:no:qRrvx";
  56
  57enum CInemaStatus {
  58	CI_PASS,
  59	CI_FAIL,
  60	CI_XPASS,
  61	CI_XFAIL,
  62	CI_ERROR,
  63	CI_SKIP
  64};
  65
  66enum CInemaRebaseline {
  67	CI_R_NONE = 0,
  68	CI_R_FAILING,
  69	CI_R_MISSING,
  70};
  71
  72struct CInemaTest {
  73	char directory[PATH_MAX];
  74	char filename[MAX_TEST];
  75	char name[MAX_TEST];
  76	enum CInemaStatus status;
  77	unsigned failedFrames;
  78	uint64_t failedPixels;
  79	unsigned totalFrames;
  80	uint64_t totalDistance;
  81	uint64_t totalPixels;
  82};
  83
  84struct CInemaImage {
  85	void* data;
  86	unsigned width;
  87	unsigned height;
  88	unsigned stride;
  89};
  90
  91DECLARE_VECTOR(CInemaTestList, struct CInemaTest)
  92DEFINE_VECTOR(CInemaTestList, struct CInemaTest)
  93
  94DECLARE_VECTOR(ImageList, void*)
  95DEFINE_VECTOR(ImageList, void*)
  96
  97struct StringBuilder {
  98	struct StringList lines;
  99	struct StringList partial;
 100	unsigned repeat;
 101
 102};
 103
 104struct CInemaLogStream {
 105	struct StringBuilder err;
 106	struct StringBuilder out;
 107};
 108
 109static bool showVersion = false;
 110static bool showUsage = false;
 111static char base[PATH_MAX] = {0};
 112static char outdir[PATH_MAX] = {'.'};
 113static bool dryRun = false;
 114static bool diffs = false;
 115static enum CInemaRebaseline rebaseline = CI_R_NONE;
 116static enum CInemaRebaseline xbaseline = CI_R_NONE;
 117static int verbosity = 0;
 118
 119static struct Table configTree;
 120static Mutex configMutex;
 121
 122static int jobs = 1;
 123static size_t jobIndex = 0;
 124static Mutex jobMutex;
 125static Thread jobThreads[MAX_JOBS];
 126static int jobStatus;
 127static ThreadLocal logStream;
 128static ThreadLocal currentTest;
 129
 130bool CInemaTestInit(struct CInemaTest*, const char* directory, const char* filename);
 131void CInemaTestRun(struct CInemaTest*);
 132
 133bool CInemaConfigGetUInt(struct Table* configTree, const char* testName, const char* key, unsigned* value);
 134void CInemaConfigLoad(struct Table* configTree, const char* testName, struct mCore* core);
 135
 136static void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args);
 137
 138void CIflush(struct StringBuilder* list, FILE* file);
 139
 140static char* _compileStringList(struct StringList* list) {
 141	size_t len = 0;
 142	size_t i;
 143	for (i = 0; i < StringListSize(list); ++i) {
 144		len += strlen(*StringListGetPointer(list, i));
 145	}
 146	char* string = calloc(len + 1, sizeof(char));
 147	char* cur = string;
 148	for (i = 0; i < StringListSize(list); ++i) {
 149		char* brick = *StringListGetPointer(list, i);
 150		size_t portion = strlen(brick);
 151		memcpy(cur, brick, portion);
 152		free(brick);
 153		cur += portion;
 154	}
 155	StringListClear(list);
 156	return string;
 157}
 158
 159static void _logToStream(FILE* file, const char* format, va_list args) {
 160#ifdef HAVE_VASPRINTF
 161	struct CInemaLogStream* stream = ThreadLocalGetValue(logStream);
 162	if (!stream) {
 163		vfprintf(file, format, args);
 164	} else {
 165		struct StringBuilder* builder = &stream->out;
 166		if (file == stderr) {
 167			builder = &stream->err;
 168		}
 169		if (StringListSize(&builder->lines) > LOG_THRESHOLD) {
 170			CIflush(builder, file);
 171		}
 172		char** line = StringListAppend(&builder->partial);
 173		vasprintf(line, format, args);
 174		size_t len = strlen(*line);
 175		if (len && (*line)[len - 1] == '\n') {
 176			char* string = _compileStringList(&builder->partial);
 177			size_t linecount = StringListSize(&builder->lines);
 178			if (linecount && strcmp(string, *StringListGetPointer(&builder->lines, linecount - 1)) == 0) {
 179				++builder->repeat;
 180				free(string);
 181			} else {
 182				if (builder->repeat > 1) {
 183					asprintf(StringListAppend(&builder->lines), "The previous message was repeated %u times.\n", builder->repeat);
 184				}
 185				*StringListAppend(&builder->lines) = string;
 186				builder->repeat = 1;
 187			}
 188		}
 189	}
 190#else
 191	vfprintf(file, format, args);
 192#endif
 193}
 194
 195ATTRIBUTE_FORMAT(printf, 2, 3) void CIlog(int minlevel, const char* format, ...) {
 196	if (verbosity < minlevel) {
 197		return;
 198	}
 199	va_list args;
 200	va_start(args, format);
 201	_logToStream(stdout, format, args);
 202	va_end(args);
 203}
 204
 205ATTRIBUTE_FORMAT(printf, 2, 3) void CIerr(int minlevel, const char* format, ...) {
 206	if (verbosity < minlevel) {
 207		return;
 208	}
 209	va_list args;
 210	va_start(args, format);
 211	_logToStream(stderr, format, args);
 212	va_end(args);
 213}
 214
 215void CIflush(struct StringBuilder* builder, FILE* out) {
 216	if (StringListSize(&builder->partial)) {
 217		*StringListAppend(&builder->lines) = _compileStringList(&builder->partial);
 218	}
 219#ifdef HAVE_VASPRINTF
 220	if (builder->repeat > 1) {
 221		asprintf(StringListAppend(&builder->lines), "The previous message was repeated %u times.\n", builder->repeat);
 222	}
 223#endif
 224
 225	char* string = _compileStringList(&builder->lines);
 226	builder->repeat = 0;
 227	fputs(string, out);
 228	free(string);
 229	fflush(out);
 230}
 231
 232static bool parseCInemaArgs(int argc, char* const* argv) {
 233	int ch;
 234	int index = 0;
 235	while ((ch = getopt_long(argc, argv, shortOpts, longOpts, &index)) != -1) {
 236		const struct option* opt = &longOpts[index];
 237		switch (ch) {
 238		case '\0':
 239			if (strcmp(opt->name, "version") == 0) {
 240				showVersion = true;
 241			} else {
 242				return false;
 243			}
 244			break;
 245		case 'b':
 246			strlcpy(base, optarg, sizeof(base));
 247			// TODO: Verify path exists
 248			break;
 249		case 'd':
 250			diffs = true;
 251			break;
 252		case 'h':
 253			showUsage = true;
 254			break;
 255		case 'j':
 256			jobs = atoi(optarg);
 257			if (jobs > MAX_JOBS) {
 258				jobs = MAX_JOBS;
 259			}
 260			if (jobs < 1) {
 261				jobs = 1;
 262			}
 263			break;
 264		case 'n':
 265			dryRun = true;
 266			break;
 267		case 'o':
 268			strlcpy(outdir, optarg, sizeof(outdir));
 269			// TODO: Make directory
 270			break;
 271		case 'q':
 272			--verbosity;
 273			break;
 274		case 'r':
 275			rebaseline = CI_R_FAILING;
 276			break;
 277		case 'R':
 278			rebaseline = CI_R_MISSING;
 279			break;
 280		case 'v':
 281			++verbosity;
 282			break;
 283		case 'x':
 284			xbaseline = CI_R_FAILING;
 285			break;
 286		default:
 287			return false;
 288		}
 289	}
 290
 291	return true;
 292}
 293
 294static void usageCInema(const char* arg0) {
 295	printf("usage: %s [-dhnqrRv] [-j JOBS] [-b BASE] [-o DIR] [--version] [test...]\n", arg0);
 296	puts("  -b, --base BASE            Path to the CInema base directory");
 297	puts("  -d, --diffs                Output image diffs from failures");
 298	puts("  -h, --help                 Print this usage and exit");
 299	puts("  -j, --jobs JOBS            Run a number of jobs in parallel");
 300	puts("  -n, --dry-run              List all collected tests instead of running them");
 301	puts("  -o, --output DIR           Path to output applicable results");
 302	puts("  -q, --quiet                Decrease log verbosity (can be repeated)");
 303	puts("  -r, --rebaseline           Rewrite the baseline for failing tests");
 304	puts("  -R, --rebaseline-missing   Write missing baselines tests only");
 305	puts("  -v, --verbose              Increase log verbosity (can be repeated)");
 306	puts("  -x, --xbaseline            Rewrite the xfail baselines for failing tests");
 307	puts("  --version                  Print version and exit");
 308}
 309
 310static bool determineBase(int argc, char* const* argv) {
 311	// TODO: Better dynamic detection
 312	separatePath(__FILE__, base, NULL, NULL);
 313	strncat(base, PATH_SEP ".." PATH_SEP ".." PATH_SEP ".." PATH_SEP "cinema", sizeof(base) - strlen(base) - 1);
 314	return true;
 315}
 316
 317static bool collectTests(struct CInemaTestList* tests, const char* path) {
 318	CIerr(2, "Considering path %s\n", path);
 319	struct VDir* dir = VDirOpen(path);
 320	if (!dir) {
 321		return false;
 322	}
 323	struct VDirEntry* entry = dir->listNext(dir);
 324	while (entry) {
 325		char subpath[PATH_MAX];
 326		snprintf(subpath, sizeof(subpath), "%s" PATH_SEP "%s", path, entry->name(entry));
 327		if (entry->type(entry) == VFS_DIRECTORY && strncmp(entry->name(entry), ".", 2) != 0 && strncmp(entry->name(entry), "..", 3) != 0) {
 328			if (!collectTests(tests, subpath)) {
 329				dir->close(dir);
 330				return false;
 331			}
 332		} else if (entry->type(entry) == VFS_FILE && strncmp(entry->name(entry), "test.", 5) == 0) {
 333			CIerr(3, "Found potential test %s\n", subpath);
 334			struct VFile* vf = dir->openFile(dir, entry->name(entry), O_RDONLY);
 335			if (vf) {
 336				if (mCoreIsCompatible(vf) != PLATFORM_NONE || mVideoLogIsCompatible(vf) != PLATFORM_NONE) {
 337					struct CInemaTest* test = CInemaTestListAppend(tests);
 338					if (!CInemaTestInit(test, path, entry->name(entry))) {
 339						CIerr(3, "Failed to create test\n");
 340						CInemaTestListResize(tests, -1);
 341					} else {
 342						CIerr(2, "Found test %s\n", test->name);
 343					}
 344				} else {
 345					CIerr(3, "Not a compatible file\n");
 346				}
 347				vf->close(vf);
 348			} else {
 349				CIerr(3, "Failed to open file\n");
 350			}
 351		}
 352		entry = dir->listNext(dir);
 353	}
 354	dir->close(dir);
 355	return true;
 356}
 357
 358static int _compareNames(const void* a, const void* b) {
 359	const struct CInemaTest* ta = a;
 360	const struct CInemaTest* tb = b;
 361
 362	return strncmp(ta->name, tb->name, sizeof(ta->name));
 363}
 364
 365static void reduceTestList(struct CInemaTestList* tests) {
 366	qsort(CInemaTestListGetPointer(tests, 0), CInemaTestListSize(tests), sizeof(struct CInemaTest), _compareNames);
 367
 368	size_t i;
 369	for (i = 1; i < CInemaTestListSize(tests);) {
 370		struct CInemaTest* cur = CInemaTestListGetPointer(tests, i);
 371		struct CInemaTest* prev = CInemaTestListGetPointer(tests, i - 1);
 372		if (strncmp(cur->name, prev->name, sizeof(cur->name)) != 0) {
 373			++i;
 374			continue;
 375		}
 376		CInemaTestListShift(tests, i, 1);
 377	}
 378}
 379
 380static void testToPath(const char* testName, char* path) {
 381	strlcpy(path, base, PATH_MAX);
 382
 383	bool dotSeen = true;
 384	size_t i;
 385	for (i = strlen(path); testName[0] && i < PATH_MAX; ++testName) {
 386		if (testName[0] == '.') {
 387			dotSeen = true;
 388		} else {
 389			if (dotSeen) {
 390				strlcpy(&path[i], PATH_SEP, PATH_MAX - i);
 391				i += strlen(PATH_SEP);
 392				dotSeen = false;
 393				if (!i) {
 394					break;
 395				}
 396			}
 397			path[i] = testName[0];
 398			++i;
 399		}
 400	}
 401	if (i == PATH_MAX) {
 402		--i;
 403	}
 404	path[i] = '\0';
 405}
 406
 407static bool globTests(struct CInemaTestList* tests, const char* glob, const char* ancestors) {
 408	bool success = true;
 409	const char* next = strpbrk(glob, "*.");
 410
 411	char path[PATH_MAX];
 412	if (!next) {
 413		testToPath(glob, path);
 414		return collectTests(tests, path);
 415	} else if (next[0] == '.') {
 416		char subtest[MAX_TEST];
 417		if (!ancestors) {
 418			strncpy(subtest, glob, next - glob);
 419		} else {
 420			size_t len = strlen(ancestors) + (next - glob) + 2;
 421			if (len > sizeof(subtest)) {
 422				len = sizeof(subtest);
 423			}
 424			snprintf(subtest, len, "%s.%s", ancestors, glob);
 425		}
 426		return globTests(tests, next + 1, subtest);
 427	} else if (next[0] == '*') {
 428		char globBuffer[MAX_TEST];
 429		const char* subglob;
 430
 431		next = strchr(next, '.');
 432		if (!next) {
 433			subglob = glob;
 434		} else {
 435			size_t len = next - glob + 1;
 436			if (len > sizeof(globBuffer)) {
 437				len = sizeof(globBuffer);
 438			}
 439			strncpy(globBuffer, glob, len - 1);
 440			subglob = globBuffer;
 441		}
 442		bool hasMoreGlobs = next && strchr(next, '*');
 443
 444		struct VDir* dir;
 445		if (ancestors) {
 446			testToPath(ancestors, path);
 447			dir = VDirOpen(path);
 448		} else {
 449			dir = VDirOpen(base);
 450		}
 451		if (!dir) {
 452			return false;
 453		}
 454
 455		struct VDirEntry* dirent = dir->listNext(dir);
 456		while (dirent) {
 457			const char* name = dirent->name(dirent);
 458			if (dirent->type(dirent) != VFS_DIRECTORY || strncmp(name, ".", 2) == 0 || strncmp(name, "..", 3) == 0) {
 459				dirent = dir->listNext(dir);
 460				continue;
 461			}
 462			if (wildcard(subglob, name)) {
 463				char newgen[MAX_TEST];
 464				if (ancestors) {
 465					snprintf(newgen, sizeof(newgen), "%s.%s", ancestors, name);
 466				} else {
 467					strlcpy(newgen, name, sizeof(newgen));
 468				}
 469				if (next && hasMoreGlobs) {
 470					globTests(tests, next + 1, newgen);
 471				} else {
 472					testToPath(newgen, path);
 473					collectTests(tests, path);
 474				}
 475			}
 476			dirent = dir->listNext(dir);
 477		}
 478
 479		return true;
 480	} else {
 481		abort();
 482	}
 483}
 484
 485static void _loadConfigTree(struct Table* configTree, const char* testName) {
 486	char key[MAX_TEST];
 487	strlcpy(key, testName, sizeof(key));
 488
 489	struct mCoreConfig* config;
 490	while (!(config = HashTableLookup(configTree, key))) {
 491		char path[PATH_MAX];
 492		config = malloc(sizeof(*config));
 493		mCoreConfigInit(config, "cinema");
 494		testToPath(key, path);
 495		strncat(path, PATH_SEP, sizeof(path) - 1);
 496		strncat(path, "config.ini", sizeof(path) - 1);
 497		mCoreConfigLoadPath(config, path);
 498		HashTableInsert(configTree, key, config);
 499		char* pos = strrchr(key, '.');
 500		if (pos) {
 501			pos[0] = '\0';
 502		} else if (key[0]) {
 503			key[0] = '\0';
 504		} else {
 505			break;
 506		}
 507	}
 508}
 509
 510static void _unloadConfigTree(const char* key, void* value, void* user) {
 511	UNUSED(key);
 512	UNUSED(user);
 513	mCoreConfigDeinit(value);
 514}
 515
 516static const char* CInemaConfigGet(struct Table* configTree, const char* testName, const char* key) {
 517	_loadConfigTree(configTree, testName);
 518
 519	char testKey[MAX_TEST];
 520	strlcpy(testKey, testName, sizeof(testKey));
 521
 522	struct mCoreConfig* config;
 523	while (true) {
 524		config = HashTableLookup(configTree, testKey);
 525		if (!config) {
 526			continue;
 527		}
 528		const char* str = ConfigurationGetValue(&config->configTable, "testinfo", key);
 529		if (str) {
 530			return str;
 531		}
 532		char* pos = strrchr(testKey, '.');
 533		if (pos) {
 534			pos[0] = '\0';
 535		} else if (testKey[0]) {
 536			testKey[0] = '\0';
 537		} else {
 538			break;
 539		}
 540	}
 541	return NULL;
 542}
 543
 544bool CInemaConfigGetUInt(struct Table* configTree, const char* testName, const char* key, unsigned* out) {
 545	const char* charValue = CInemaConfigGet(configTree, testName, key);
 546	if (!charValue) {
 547		return false;
 548	}
 549	char* end;
 550	unsigned long value = strtoul(charValue, &end, 10);
 551	if (*end) {
 552		return false;
 553	}
 554	*out = value;
 555	return true;
 556}
 557
 558void CInemaConfigLoad(struct Table* configTree, const char* testName, struct mCore* core) {
 559	_loadConfigTree(configTree, testName);
 560
 561	char testKey[MAX_TEST] = {0};
 562	char* keyEnd = testKey;
 563	const char* pos;
 564	while (true) {
 565		pos = strchr(testName, '.');
 566		size_t maxlen = sizeof(testKey) - (keyEnd - testKey) - 1;
 567		size_t len;
 568		if (pos) {
 569			len = pos - testName;
 570		} else {
 571			len = strlen(testName);
 572		}
 573		if (len > maxlen) {
 574			len = maxlen;
 575		}
 576		strncpy(keyEnd, testName, len);
 577		keyEnd += len;
 578
 579		struct mCoreConfig* config = HashTableLookup(configTree, testKey);
 580		if (config) {
 581			core->loadConfig(core, config);
 582		}
 583		if (!pos) {
 584			break;
 585		}
 586		testName = pos + 1;
 587		keyEnd[0] = '.';
 588		++keyEnd;
 589	}
 590}
 591
 592bool CInemaTestInit(struct CInemaTest* test, const char* directory, const char* filename) {
 593	if (strncmp(base, directory, strlen(base)) != 0) {
 594		return false;
 595	}
 596	memset(test, 0, sizeof(*test));
 597	strlcpy(test->directory, directory, sizeof(test->directory));
 598	strlcpy(test->filename, filename, sizeof(test->filename));
 599	directory += strlen(base) + 1;
 600	strlcpy(test->name, directory, sizeof(test->name));
 601	char* str = strstr(test->name, PATH_SEP);
 602	while (str) {
 603		str[0] = '.';
 604		str = strstr(str, PATH_SEP);
 605	}
 606	return true;
 607}
 608
 609static bool _loadBaselinePNG(struct VDir* dir, const char* type, struct CInemaImage* image, size_t frame, enum CInemaStatus* status) {
 610	char baselineName[32];
 611	snprintf(baselineName, sizeof(baselineName), "%s_%04" PRIz "u.png", type, frame);
 612	struct VFile* baselineVF = dir->openFile(dir, baselineName, O_RDONLY);
 613	if (!baselineVF) {
 614		if (*status == CI_PASS) {
 615			*status = CI_FAIL;
 616		}
 617		return false;
 618	}
 619
 620	png_structp png = PNGReadOpen(baselineVF, 0);
 621	png_infop info = png_create_info_struct(png);
 622	png_infop end = png_create_info_struct(png);
 623	if (!png || !info || !end || !PNGReadHeader(png, info)) {
 624		PNGReadClose(png, info, end);
 625		baselineVF->close(baselineVF);
 626		CIerr(1, "Failed to load %s\n", baselineName);
 627		*status = CI_ERROR;
 628		return false;
 629	}
 630
 631	unsigned pwidth = png_get_image_width(png, info);
 632	unsigned pheight = png_get_image_height(png, info);
 633	if (pheight != image->height || pwidth != image->width) {
 634		PNGReadClose(png, info, end);
 635		baselineVF->close(baselineVF);
 636		CIlog(1, "Size mismatch for %s, expected %ux%u, got %ux%u\n", baselineName, pwidth, pheight, image->width, image->height);
 637		if (*status == CI_PASS) {
 638			*status = CI_FAIL;
 639		}
 640		return false;
 641	}
 642
 643	image->data = malloc(pwidth * pheight * BYTES_PER_PIXEL);
 644	if (!image->data) {
 645		CIerr(1, "Failed to allocate baseline buffer\n");
 646		*status = CI_ERROR;
 647		PNGReadClose(png, info, end);
 648		baselineVF->close(baselineVF);
 649		return false;
 650	}
 651	if (!PNGReadPixels(png, info, image->data, pwidth, pheight, pwidth) || !PNGReadFooter(png, end)) {
 652		CIerr(1, "Failed to read %s\n", baselineName);
 653		*status = CI_ERROR;
 654		free(image->data);
 655		return false;
 656	}
 657	PNGReadClose(png, info, end);
 658	baselineVF->close(baselineVF);
 659	image->stride = pwidth;
 660	return true;
 661}
 662
 663#ifdef USE_FFMPEG
 664struct CInemaStream {
 665	struct mAVStream d;
 666	struct CInemaImage* image;
 667	enum CInemaStatus* status;
 668};
 669
 670static void _cinemaDimensionsChanged(struct mAVStream* stream, unsigned width, unsigned height) {
 671	struct CInemaStream* cistream = (struct CInemaStream*) stream;
 672	if (height != cistream->image->height || width != cistream->image->width) {
 673		CIlog(1, "Size mismatch for video, expected %ux%u, got %ux%u\n", width, height, cistream->image->width, cistream->image->height);
 674		if (*cistream->status == CI_PASS) {
 675			*cistream->status = CI_FAIL;
 676		}
 677	}
 678}
 679
 680static void _cinemaVideoFrame(struct mAVStream* stream, const color_t* pixels, size_t stride) {
 681	struct CInemaStream* cistream = (struct CInemaStream*) stream;
 682	cistream->image->stride = stride;
 683	size_t bufferSize = cistream->image->stride * cistream->image->height * BYTES_PER_PIXEL;
 684	cistream->image->data = malloc(bufferSize);
 685	memcpy(cistream->image->data, pixels, bufferSize);
 686}
 687#endif
 688
 689static struct VDir* _makeOutDir(const char* testName) {
 690	char path[PATH_MAX] = {0};
 691	strlcpy(path, outdir, sizeof(path));
 692	char* pathEnd = path + strlen(path);
 693	const char* pos;
 694	while (true) {
 695		pathEnd[0] = PATH_SEP[0];
 696		++pathEnd;
 697		pos = strchr(testName, '.');
 698		size_t maxlen = sizeof(path) - (pathEnd - path) - 1;
 699		size_t len;
 700		if (pos) {
 701			len = pos - testName;
 702		} else {
 703			len = strlen(testName);
 704		}
 705		if (len > maxlen) {
 706			len = maxlen;
 707		}
 708		strncpy(pathEnd, testName, len);
 709		pathEnd += len;
 710
 711		mkdir(path, 0777);
 712
 713		if (!pos) {
 714			break;
 715		}
 716		testName = pos + 1;
 717	}
 718	return VDirOpen(path);
 719}
 720
 721static void _writeImage(struct VFile* vf, const struct CInemaImage* image) {
 722	png_structp png = PNGWriteOpen(vf);
 723	png_infop info = PNGWriteHeader(png, image->width, image->height);
 724	if (!PNGWritePixels(png, image->width, image->height, image->stride, image->data)) {
 725		CIerr(0, "Could not write output image\n");
 726	}
 727	PNGWriteClose(png, info);
 728
 729	vf->close(vf);
 730}
 731
 732static void _writeDiff(const char* testName, const struct CInemaImage* image, size_t frame, const char* type) {
 733	struct VDir* dir = _makeOutDir(testName);
 734	if (!dir) {
 735		CIerr(0, "Could not open directory for %s\n", testName);
 736		return;
 737	}
 738	char name[32];
 739	snprintf(name, sizeof(name), "%s_%05" PRIz "u.png", type, frame);
 740	struct VFile* vf = dir->openFile(dir, name, O_CREAT | O_TRUNC | O_WRONLY);
 741	if (!vf) {
 742		CIerr(0, "Could not open output file %s\n", name);
 743		dir->close(dir);
 744		return;
 745	}
 746	_writeImage(vf, image);
 747	dir->close(dir);
 748}
 749
 750static void _writeBaseline(struct VDir* dir, const char* type, const struct CInemaImage* image, size_t frame) {
 751	char baselineName[32];
 752	snprintf(baselineName, sizeof(baselineName), "%s_%04" PRIz "u.png", type, frame);
 753	struct VFile* baselineVF = dir->openFile(dir, baselineName, O_CREAT | O_TRUNC | O_WRONLY);
 754	if (baselineVF) {
 755		_writeImage(baselineVF, image);
 756	} else {
 757		CIerr(0, "Could not open output file %s\n", baselineName);
 758	}
 759}
 760
 761static bool _updateInput(struct mCore* core, size_t frame, const char** input) {
 762	if (!*input || !*input[0]) {
 763		return false;
 764	}
 765	char* end;
 766	uint32_t start = strtoul(*input, &end, 10);
 767	if (end[0] != ':') {
 768		return false;
 769	}
 770	if (start != frame) {
 771		return true;
 772	}
 773	++end;
 774	*input = end;
 775	uint32_t keys = strtoul(*input, &end, 16);
 776	if (end[0] == ',') {
 777		++end;
 778	}
 779	*input = end;
 780	core->setKeys(core, keys);
 781	return true;
 782}
 783
 784static bool _compareImages(struct CInemaTest* restrict test, const struct CInemaImage* restrict image, const struct CInemaImage* restrict expected, int* restrict max, uint8_t** restrict outdiff) {
 785	const uint8_t* testPixels = image->data;
 786	const uint8_t* expectPixels = expected->data;
 787	uint8_t* diff = NULL;
 788	size_t x;
 789	size_t y;
 790	bool failed = false;
 791	for (y = 0; y < image->height; ++y) {
 792		for (x = 0; x < image->width; ++x) {
 793			size_t pix = expected->stride * y + x;
 794			size_t tpix = image->stride * y + x;
 795#ifndef __BIG_ENDIAN__
 796			int testR = testPixels[tpix * 4 + 0];
 797			int testG = testPixels[tpix * 4 + 1];
 798			int testB = testPixels[tpix * 4 + 2];
 799			int expectR = expectPixels[pix * 4 + 0];
 800			int expectG = expectPixels[pix * 4 + 1];
 801			int expectB = expectPixels[pix * 4 + 2];
 802#else
 803			int testB = testPixels[tpix * 4 + 1];
 804			int testG = testPixels[tpix * 4 + 2];
 805			int testR = testPixels[tpix * 4 + 3];
 806			int expectB = expectPixels[pix * 4 + 1];
 807			int expectG = expectPixels[pix * 4 + 2];
 808			int expectR = expectPixels[pix * 4 + 3];
 809#endif
 810			int r = expectR - testR;
 811			int g = expectG - testG;
 812			int b = expectB - testB;
 813			if (r | g | b) {
 814				failed = true;
 815				if (outdiff && !diff) {
 816					diff = calloc(expected->stride * expected->height, BYTES_PER_PIXEL);
 817					*outdiff = diff;
 818				}
 819				test->status = CI_FAIL;
 820				if (r < 0) {
 821					r = -r;
 822				}
 823				if (g < 0) {
 824					g = -g;
 825				}
 826				if (b < 0) {
 827					b = -b;
 828				}
 829
 830				if (diff) {
 831					if (r > *max) {
 832						*max = r;
 833					}
 834					if (g > *max) {
 835						*max = g;
 836					}
 837					if (b > *max) {
 838						*max = b;
 839					}
 840#ifndef __BIG_ENDIAN__
 841					diff[pix * 4 + 0] = r;
 842					diff[pix * 4 + 1] = g;
 843					diff[pix * 4 + 2] = b;
 844#else
 845					diff[pix * 4 + 1] = b;
 846					diff[pix * 4 + 2] = g;
 847					diff[pix * 4 + 3] = r;
 848#endif
 849				}
 850
 851				if (test) {
 852					test->totalDistance += r + g + b;
 853					++test->failedPixels;
 854				}
 855			}
 856		}
 857	}
 858	return !failed;
 859}
 860
 861void _writeDiffSet(struct CInemaImage* expected, const char* name, uint8_t* diff, int frame, int max, bool xfail) {
 862	struct CInemaImage outdiff = {
 863		.data = diff,
 864		.width = expected->width,
 865		.height = expected->height,
 866		.stride = expected->stride,
 867	};
 868
 869	if (xfail) {
 870		_writeDiff(name, expected, frame, "xexpected");
 871		_writeDiff(name, &outdiff, frame, "xdiff");
 872	} else {
 873		_writeDiff(name, expected, frame, "expected");
 874		_writeDiff(name, &outdiff, frame, "diff");
 875	}
 876
 877	size_t x;
 878	size_t y;
 879	for (y = 0; y < outdiff.height; ++y) {
 880		for (x = 0; x < outdiff.width; ++x) {
 881			size_t pix = outdiff.stride * y + x;
 882#ifndef __BIG_ENDIAN__
 883			diff[pix * 4 + 0] = diff[pix * 4 + 0] * 255 / max;
 884			diff[pix * 4 + 1] = diff[pix * 4 + 1] * 255 / max;
 885			diff[pix * 4 + 2] = diff[pix * 4 + 2] * 255 / max;
 886#else
 887			diff[pix * 4 + 1] = diff[pix * 4 + 1] * 255 / max;
 888			diff[pix * 4 + 2] = diff[pix * 4 + 2] * 255 / max;
 889			diff[pix * 4 + 3] = diff[pix * 4 + 3] * 255 / max;
 890#endif
 891		}
 892	}
 893	if (xfail) {
 894		_writeDiff(name, &outdiff, frame, "xnormalized");
 895	} else {
 896		_writeDiff(name, &outdiff, frame, "normalized");
 897	}
 898}
 899
 900#ifdef USE_FFMPEG
 901static void _replayBaseline(struct CInemaTest* test, struct FFmpegEncoder* encoder, const struct CInemaImage* image, int frame) {
 902	char baselineName[PATH_MAX];
 903	snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP ".baseline.avi", test->directory);
 904
 905	if (!FFmpegEncoderOpen(encoder, baselineName)) {
 906		CIerr(1, "Failed to save baseline video\n");
 907		test->status = CI_ERROR;
 908		return;
 909	}
 910	encoder->d.videoDimensionsChanged(&encoder->d, image->width, image->height);
 911
 912	snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP "baseline.avi", test->directory);
 913
 914	struct CInemaImage buffer = {
 915		.data = NULL,
 916		.width = image->width,
 917		.height = image->height,
 918		.stride = image->width,
 919	};
 920	struct FFmpegDecoder decoder;
 921	struct CInemaStream stream = {0};
 922	stream.d.postVideoFrame = _cinemaVideoFrame;
 923	stream.d.videoDimensionsChanged = _cinemaDimensionsChanged;
 924	stream.status = &test->status;
 925	stream.image = &buffer;
 926
 927	FFmpegDecoderInit(&decoder);
 928	decoder.out = &stream.d;
 929
 930	if (!FFmpegDecoderOpen(&decoder, baselineName)) {
 931		CIerr(1, "Failed to load baseline video\n");
 932		test->status = CI_ERROR;
 933		return;
 934	}
 935
 936	int i;
 937	for (i = 0; i < frame; ++i) {
 938		while (!buffer.data) {
 939			if (!FFmpegDecoderRead(&decoder)) {
 940				CIlog(1, "Failed to read more frames. EOF?\n");
 941				test->status = CI_FAIL;
 942				break;
 943			}
 944		}
 945		encoder->d.postVideoFrame(&encoder->d, buffer.data, buffer.stride);
 946		free(buffer.data);
 947		buffer.data = NULL;
 948	}
 949	FFmpegDecoderClose(&decoder);
 950}
 951#endif
 952
 953void CInemaTestRun(struct CInemaTest* test) {
 954	unsigned ignore = 0;
 955	MutexLock(&configMutex);
 956	CInemaConfigGetUInt(&configTree, test->name, "ignore", &ignore);
 957	MutexUnlock(&configMutex);
 958	if (ignore) {
 959		test->status = CI_SKIP;
 960		return;
 961	}
 962
 963	struct VDir* dir = VDirOpen(test->directory);
 964	if (!dir) {
 965		CIerr(0, "Failed to open test directory\n");
 966		test->status = CI_ERROR;
 967		return;
 968	}
 969	struct VFile* rom = dir->openFile(dir, test->filename, O_RDONLY);
 970	if (!rom) {
 971		CIerr(0, "Failed to open test\n");
 972		test->status = CI_ERROR;
 973		return;
 974	}
 975	struct mCore* core = mCoreFindVF(rom);
 976	if (!core) {
 977		CIerr(0, "Failed to load test\n");
 978		test->status = CI_ERROR;
 979		rom->close(rom);
 980		return;
 981	}
 982	if (!core->init(core)) {
 983		CIerr(0, "Failed to init test\n");
 984		test->status = CI_ERROR;
 985		core->deinit(core);
 986		return;
 987	}
 988	struct CInemaImage image;
 989	core->desiredVideoDimensions(core, &image.width, &image.height);
 990	ssize_t bufferSize = image.width * image.height * BYTES_PER_PIXEL;
 991	image.data = malloc(bufferSize);
 992	image.stride = image.width;
 993	if (!image.data) {
 994		CIerr(0, "Failed to allocate video buffer\n");
 995		test->status = CI_ERROR;
 996		core->deinit(core);
 997	}
 998	core->setVideoBuffer(core, image.data, image.stride);
 999	mCoreConfigInit(&core->config, "cinema");
1000
1001	unsigned limit = 3600;
1002	unsigned skip = 0;
1003	unsigned fail = 0;
1004	unsigned video = 0;
1005	const char* input = NULL;
1006
1007	MutexLock(&configMutex);
1008	CInemaConfigGetUInt(&configTree, test->name, "frames", &limit);
1009	CInemaConfigGetUInt(&configTree, test->name, "skip", &skip);
1010	CInemaConfigGetUInt(&configTree, test->name, "fail", &fail);
1011	CInemaConfigGetUInt(&configTree, test->name, "video", &video);
1012	input = CInemaConfigGet(&configTree, test->name, "input");
1013	CInemaConfigLoad(&configTree, test->name, core);
1014	MutexUnlock(&configMutex);
1015
1016	struct VFile* save = VFileMemChunk(NULL, 0);
1017	core->loadROM(core, rom);
1018	if (!core->loadSave(core, save)) {
1019		save->close(save);
1020	}
1021	core->rtc.override = RTC_FAKE_EPOCH;
1022	core->rtc.value = 1200000000;
1023	core->reset(core);
1024
1025	test->status = CI_PASS;
1026
1027	unsigned minFrame = core->frameCounter(core);
1028	size_t frame;
1029	for (frame = 0; frame < skip; ++frame) {
1030		core->runFrame(core);
1031	}
1032	core->desiredVideoDimensions(core, &image.width, &image.height);
1033
1034#ifdef USE_FFMPEG
1035	struct FFmpegDecoder decoder;
1036	struct FFmpegEncoder encoder;
1037	struct CInemaStream stream = {0};
1038
1039	char baselineName[PATH_MAX];
1040	snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP "baseline.avi", test->directory);
1041	bool exists = access(baselineName, 0) == 0;
1042
1043	if (video) {
1044		FFmpegEncoderInit(&encoder);
1045		FFmpegDecoderInit(&decoder);
1046
1047		FFmpegEncoderSetAudio(&encoder, NULL, 0);
1048		FFmpegEncoderSetVideo(&encoder, "zmbv", 0, 0);
1049		FFmpegEncoderSetContainer(&encoder, "avi");
1050		FFmpegEncoderSetDimensions(&encoder, image.width, image.height);
1051
1052		if (rebaseline && !exists) {
1053			if (!FFmpegEncoderOpen(&encoder, baselineName)) {
1054				CIerr(1, "Failed to save baseline video\n");
1055			} else {
1056				core->setAVStream(core, &encoder.d);
1057			}
1058		}
1059
1060		if (exists) {
1061			stream.d.postVideoFrame = _cinemaVideoFrame;
1062			stream.d.videoDimensionsChanged = _cinemaDimensionsChanged;
1063			stream.status = &test->status;
1064			decoder.out = &stream.d;
1065
1066			if (!FFmpegDecoderOpen(&decoder, baselineName)) {
1067				CIerr(1, "Failed to load baseline video\n");
1068			}
1069		} else if (!rebaseline) {
1070			test->status = CI_FAIL;
1071		}
1072	}
1073#else
1074	if (video) {
1075		CIerr(0, "Failed to run video test without ffmpeg linked in\n");
1076		test->status = CI_ERROR;
1077	}
1078#endif
1079
1080	bool xdiff = false;
1081	for (frame = 0; limit; ++frame, --limit) {
1082		_updateInput(core, frame, &input);
1083		core->runFrame(core);
1084		++test->totalFrames;
1085		unsigned frameCounter = core->frameCounter(core);
1086		if (frameCounter <= minFrame) {
1087			break;
1088		}
1089		if (test->status == CI_ERROR) {
1090			break;
1091		}
1092		CIlog(3, "Test frame: %u\n", frameCounter);
1093		core->desiredVideoDimensions(core, &image.width, &image.height);
1094		uint8_t* diff = NULL;
1095		struct CInemaImage expected = {
1096			.data = NULL,
1097			.width = image.width,
1098			.height = image.height,
1099			.stride = image.width,
1100		};
1101		bool baselineFound;
1102		if (video) {
1103			baselineFound = false;
1104#ifdef USE_FFMPEG
1105			if (FFmpegDecoderIsOpen(&decoder)) {
1106				stream.image = &expected;
1107				while (!expected.data) {
1108					if (!FFmpegDecoderRead(&decoder)) {
1109						CIlog(1, "Failed to read more frames. EOF?\n");
1110						test->status = CI_FAIL;
1111						if (rebaseline && !FFmpegEncoderIsOpen(&encoder)) {
1112							_replayBaseline(test, &encoder, &image, frame);
1113							if (test->status == CI_ERROR) {
1114								break;
1115							}
1116							encoder.d.postVideoFrame(&encoder.d, image.data, image.stride);
1117							core->setAVStream(core, &encoder.d);
1118						}
1119						break;
1120					}
1121				}
1122				baselineFound = expected.data;
1123			}
1124#endif
1125		} else {
1126			baselineFound = _loadBaselinePNG(dir, "baseline", &expected, frame, &test->status);
1127		}
1128		if (test->status == CI_ERROR) {
1129			break;
1130		}
1131		bool failed = false;
1132		if (baselineFound) {
1133			int max = 0;
1134			failed = !_compareImages(test, &image, &expected, &max, diffs ? &diff : NULL);
1135			if (failed) {
1136				++test->failedFrames;
1137#ifdef USE_FFMPEG
1138				if (video && exists && rebaseline && !FFmpegEncoderIsOpen(&encoder)) {
1139					_replayBaseline(test, &encoder, &image, frame);
1140					if (test->status == CI_ERROR) {
1141						break;
1142					}
1143					encoder.d.postVideoFrame(&encoder.d, image.data, image.stride);
1144					core->setAVStream(core, &encoder.d);
1145				}
1146#endif
1147			}
1148			test->totalPixels += image.height * image.width;
1149			if (rebaseline == CI_R_FAILING && !video && failed) {
1150				_writeBaseline(dir, "baseline", &image, frame);
1151			}
1152			if (diff) {
1153				if (failed) {
1154					_writeDiff(test->name, &image, frame, "result");
1155					_writeDiffSet(&expected, test->name, diff, frame, max, false);
1156				}
1157				free(diff);
1158				diff = NULL;
1159			}
1160			free(expected.data);
1161		} else if (rebaseline && !video) {
1162			_writeBaseline(dir, "baseline", &image, frame);
1163		} else if (!rebaseline) {
1164			test->status = CI_FAIL;
1165		}
1166
1167		if (fail && failed) {
1168			if (video) {
1169				// TODO
1170				baselineFound = false;
1171			} else {
1172				baselineFound = _loadBaselinePNG(dir, "xbaseline", &expected, frame, &test->status);
1173			}
1174
1175			if (baselineFound) {
1176				int max = 0;
1177				failed = !_compareImages(test, &image, &expected, &max, diffs ? &diff : NULL);
1178				if (diff) {
1179					if (failed) {
1180						_writeDiffSet(&expected, test->name, diff, frame, max, true);
1181					}
1182					free(diff);
1183					diff = NULL;
1184				}
1185				if (failed) {
1186					if (xbaseline == CI_R_FAILING && !video) {
1187						_writeBaseline(dir, "xbaseline", &image, frame);
1188					}
1189					xdiff = true;
1190				}
1191				free(expected.data);
1192			} else if (xbaseline && !video) {
1193				_writeBaseline(dir, "xbaseline", &image, frame);
1194			}
1195		}
1196	}
1197
1198#ifdef USE_FFMPEG
1199	if (video) {
1200		if (FFmpegEncoderIsOpen(&encoder)) {
1201			FFmpegEncoderClose(&encoder);
1202			if (exists && rebaseline) {
1203				char tmpBaselineName[PATH_MAX];
1204				snprintf(tmpBaselineName, sizeof(tmpBaselineName), "%s" PATH_SEP ".baseline.avi", test->directory);
1205#ifdef _WIN32
1206				MoveFileEx(tmpBaselineName, baselineName, MOVEFILE_REPLACE_EXISTING);
1207#else
1208				rename(tmpBaselineName, baselineName);
1209#endif
1210			}
1211		}
1212		if (FFmpegDecoderIsOpen(&decoder)) {
1213			FFmpegDecoderClose(&decoder);
1214		}
1215	}
1216#endif
1217
1218	if (fail) {
1219		if (test->status == CI_FAIL && !xdiff) {
1220			test->status = CI_XFAIL;
1221		} else if (test->status == CI_PASS) {
1222			test->status = CI_XPASS;
1223		}
1224	}
1225
1226	free(image.data);
1227	mCoreConfigDeinit(&core->config);
1228	core->deinit(core);
1229	dir->close(dir);
1230}
1231
1232static bool CInemaTask(struct CInemaTestList* tests, size_t i) {
1233	bool success = true;
1234	struct CInemaTest* test = CInemaTestListGetPointer(tests, i);
1235	if (dryRun) {
1236		CIlog(-1, "%s\n", test->name);
1237	} else {
1238		CIlog(1, "%s: ", test->name);
1239		fflush(stdout);
1240		ThreadLocalSetKey(currentTest, test);
1241		CInemaTestRun(test);
1242		ThreadLocalSetKey(currentTest, NULL);
1243
1244		switch (test->status) {
1245		case CI_PASS:
1246			CIlog(1, "pass\n");
1247			break;
1248		case CI_FAIL:
1249			success = false;
1250			CIlog(1, "fail\n");
1251			break;
1252		case CI_XPASS:
1253			CIlog(1, "xpass\n");
1254			break;
1255		case CI_XFAIL:
1256			CIlog(1, "xfail\n");
1257			break;
1258		case CI_SKIP:
1259			CIlog(1, "skip\n");
1260			break;
1261		case CI_ERROR:
1262			success = false;
1263			CIlog(1, "error\n");
1264			break;
1265		}
1266		if (test->failedFrames) {
1267			CIlog(2, "\tfailed frames: %u/%u (%1.3g%%)\n", test->failedFrames, test->totalFrames, test->failedFrames / (test->totalFrames * 0.01));
1268			CIlog(2, "\tfailed pixels: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->failedPixels, test->totalPixels, test->failedPixels / (test->totalPixels * 0.01));
1269			CIlog(2, "\tdistance: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->totalDistance, test->totalPixels * 765, test->totalDistance / (test->totalPixels * 7.65));
1270		}
1271	}
1272	return success;
1273}
1274
1275static THREAD_ENTRY CInemaJob(void* context) {
1276	struct CInemaTestList* tests = context;
1277	struct CInemaLogStream stream;
1278	StringListInit(&stream.out.lines, 0);
1279	StringListInit(&stream.out.partial, 0);
1280	stream.out.repeat = 0;
1281	StringListInit(&stream.err.lines, 0);
1282	StringListInit(&stream.err.partial, 0);
1283	stream.err.repeat = 0;
1284	ThreadLocalSetKey(logStream, &stream);
1285
1286	bool success = true;
1287	while (true) {
1288		size_t i;
1289		MutexLock(&jobMutex);
1290		i = jobIndex;
1291		++jobIndex;
1292		MutexUnlock(&jobMutex);
1293		if (i >= CInemaTestListSize(tests)) {
1294			break;
1295		}
1296		if (!CInemaTask(tests, i)) {
1297			success = false;
1298		}
1299		CIflush(&stream.out, stdout);
1300		CIflush(&stream.err, stderr);
1301	}
1302	MutexLock(&jobMutex);
1303	if (!success) {
1304		jobStatus = 1;
1305	}
1306	MutexUnlock(&jobMutex);
1307
1308	CIflush(&stream.out, stdout);
1309	StringListDeinit(&stream.out.lines);
1310	StringListDeinit(&stream.out.partial);
1311
1312	CIflush(&stream.err, stderr);
1313	StringListDeinit(&stream.err.lines);
1314	StringListDeinit(&stream.err.partial);
1315}
1316
1317void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args) {
1318	UNUSED(log);
1319	if (level == mLOG_FATAL) {
1320		struct CInemaTest* test = ThreadLocalGetValue(currentTest);
1321		test->status = CI_ERROR;
1322	}
1323	if (verbosity < 0) {
1324		return;
1325	}
1326	int mask = mLOG_FATAL;
1327	if (verbosity >= 1) {
1328		mask |= mLOG_ERROR;
1329	}
1330	if (verbosity >= 2) {
1331		mask |= mLOG_WARN;
1332	}
1333	if (verbosity >= 4) {
1334		mask |= mLOG_INFO;
1335	}
1336	if (verbosity >= 5) {
1337		mask |= mLOG_ALL;
1338	}
1339	if (!(mask & level)) {
1340		return;
1341	}
1342
1343	char buffer[256];
1344	vsnprintf(buffer, sizeof(buffer), format, args);
1345	CIerr(0, "[%s] %s\n", mLogCategoryName(category), buffer);
1346}
1347
1348int main(int argc, char** argv) {
1349	ThreadLocalInitKey(&logStream);
1350	ThreadLocalSetKey(logStream, NULL);
1351
1352	int status = 0;
1353	if (!parseCInemaArgs(argc, argv)) {
1354		status = 1;
1355		goto cleanup;
1356	}
1357
1358	if (showVersion) {
1359		version(argv[0]);
1360		goto cleanup;
1361	}
1362
1363	if (showUsage) {
1364		usageCInema(argv[0]);
1365		goto cleanup;
1366	}
1367
1368	argc -= optind;
1369	argv += optind;
1370
1371	if (!base[0] && !determineBase(argc, argv)) {
1372		CIlog(0, "Could not determine CInema test base. Please specify manually.");
1373		status = 1;
1374		goto cleanup;
1375	}
1376#ifndef _WIN32
1377	char* rbase = realpath(base, NULL);
1378	if (rbase) {
1379		strlcpy(base, rbase, sizeof(base));
1380		free(rbase);
1381	}
1382#endif
1383
1384	struct CInemaTestList tests;
1385	CInemaTestListInit(&tests, 0);
1386
1387	struct mLogger logger = { .log = _log };
1388	mLogSetDefaultLogger(&logger);
1389#ifdef USE_FFMPEG
1390	if (verbosity < 2) {
1391		av_log_set_level(AV_LOG_ERROR);
1392	}
1393#endif
1394
1395	if (argc > 0) {
1396		size_t i;
1397		for (i = 0; i < (size_t) argc; ++i) {
1398			if (strchr(argv[i], '*')) {
1399				if (!globTests(&tests, argv[i], NULL)) {
1400					status = 1;
1401					break;
1402				}
1403				continue;
1404			}
1405			char path[PATH_MAX + 1] = {0};
1406			testToPath(argv[i], path);
1407
1408			if (!collectTests(&tests, path)) {
1409				status = 1;
1410				break;
1411			}
1412		}
1413	} else if (!collectTests(&tests, base)) {
1414		status = 1;
1415	}
1416
1417	if (CInemaTestListSize(&tests) == 0) {
1418		CIlog(1, "No tests found.");
1419		status = 1;
1420	} else {
1421		reduceTestList(&tests);
1422	}
1423
1424	HashTableInit(&configTree, 0, free);
1425	MutexInit(&configMutex);
1426	ThreadLocalInitKey(&currentTest);
1427	ThreadLocalSetKey(currentTest, NULL);
1428
1429	if (jobs == 1) {
1430		size_t i;
1431		for (i = 0; i < CInemaTestListSize(&tests); ++i) {
1432			bool success = CInemaTask(&tests, i);
1433			if (!success) {
1434				status = 1;
1435			}
1436		}
1437	} else {
1438		MutexInit(&jobMutex);
1439		int i;
1440		for (i = 0; i < jobs; ++i) {
1441			ThreadCreate(&jobThreads[i], CInemaJob, &tests);
1442		}
1443		for (i = 0; i < jobs; ++i) {
1444			ThreadJoin(&jobThreads[i]);
1445		}
1446		MutexDeinit(&jobMutex);
1447		status = jobStatus;
1448	}
1449
1450	MutexDeinit(&configMutex);
1451	HashTableEnumerate(&configTree, _unloadConfigTree, NULL);
1452	HashTableDeinit(&configTree);
1453	CInemaTestListDeinit(&tests);
1454
1455cleanup:
1456	return status;
1457}