all repos — mgba @ db4f1ecb2deb6dcc0c17057770f84240f7fe60be

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/vector.h>
 17#include <mgba-util/vfs.h>
 18
 19#ifdef USE_FFMPEG
 20#include "feature/ffmpeg/ffmpeg-decoder.h"
 21#include "feature/ffmpeg/ffmpeg-encoder.h"
 22#endif
 23
 24#ifdef _MSC_VER
 25#include <mgba-util/platform/windows/getopt.h>
 26#else
 27#include <getopt.h>
 28#endif
 29
 30#include <stdlib.h>
 31#include <sys/stat.h>
 32#include <sys/types.h>
 33
 34#define MAX_TEST 200
 35
 36static const struct option longOpts[] = {
 37	{ "base",       required_argument, 0, 'b' },
 38	{ "diffs",      no_argument, 0, 'd' },
 39	{ "help",       no_argument, 0, 'h' },
 40	{ "dry-run",    no_argument, 0, 'n' },
 41	{ "outdir",     required_argument, 0, 'o' },
 42	{ "quiet",      no_argument, 0, 'q' },
 43	{ "rebaseline", no_argument, 0, 'r' },
 44	{ "verbose",    no_argument, 0, 'v' },
 45	{ "version",    no_argument, 0, '\0' },
 46	{ 0, 0, 0, 0 }
 47};
 48
 49static const char shortOpts[] = "b:dhno:qrv";
 50
 51enum CInemaStatus {
 52	CI_PASS,
 53	CI_FAIL,
 54	CI_XPASS,
 55	CI_XFAIL,
 56	CI_ERROR,
 57	CI_SKIP
 58};
 59
 60struct CInemaTest {
 61	char directory[MAX_TEST];
 62	char filename[MAX_TEST];
 63	char name[MAX_TEST];
 64	enum CInemaStatus status;
 65	unsigned failedFrames;
 66	uint64_t failedPixels;
 67	unsigned totalFrames;
 68	uint64_t totalDistance;
 69	uint64_t totalPixels;
 70};
 71
 72struct CInemaImage {
 73	void* data;
 74	unsigned width;
 75	unsigned height;
 76	unsigned stride;
 77};
 78
 79DECLARE_VECTOR(CInemaTestList, struct CInemaTest)
 80DEFINE_VECTOR(CInemaTestList, struct CInemaTest)
 81
 82DECLARE_VECTOR(ImageList, void*)
 83DEFINE_VECTOR(ImageList, void*)
 84
 85static bool showVersion = false;
 86static bool showUsage = false;
 87static char base[PATH_MAX] = {0};
 88static char outdir[PATH_MAX] = {'.'};
 89static bool dryRun = false;
 90static bool diffs = false;
 91static bool rebaseline = false;
 92static int verbosity = 0;
 93
 94bool CInemaTestInit(struct CInemaTest*, const char* directory, const char* filename);
 95void CInemaTestRun(struct CInemaTest*, struct Table* configTree);
 96
 97bool CInemaConfigGetUInt(struct Table* configTree, const char* testName, const char* key, unsigned* value);
 98void CInemaConfigLoad(struct Table* configTree, const char* testName, struct mCore* core);
 99
100static void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args);
101
102ATTRIBUTE_FORMAT(printf, 2, 3) void CIlog(int minlevel, const char* format, ...) {
103	if (verbosity < minlevel) {
104		return;
105	}
106	va_list args;
107	va_start(args, format);
108	vprintf(format, args);
109	va_end(args);
110}
111
112ATTRIBUTE_FORMAT(printf, 2, 3) void CIerr(int minlevel, const char* format, ...) {
113	if (verbosity < minlevel) {
114		return;
115	}
116	va_list args;
117	va_start(args, format);
118	vfprintf(stderr, format, args);
119	va_end(args);
120}
121
122static bool parseCInemaArgs(int argc, char* const* argv) {
123	int ch;
124	int index = 0;
125	while ((ch = getopt_long(argc, argv, shortOpts, longOpts, &index)) != -1) {
126		const struct option* opt = &longOpts[index];
127		switch (ch) {
128		case '\0':
129			if (strcmp(opt->name, "version") == 0) {
130				showVersion = true;
131			} else {
132				return false;
133			}
134			break;
135		case 'b':
136			strlcpy(base, optarg, sizeof(base));
137			// TODO: Verify path exists
138			break;
139		case 'd':
140			diffs = true;
141			break;
142		case 'h':
143			showUsage = true;
144			break;
145		case 'n':
146			dryRun = true;
147			break;
148		case 'o':
149			strlcpy(outdir, optarg, sizeof(outdir));
150			// TODO: Make directory
151			break;
152		case 'q':
153			--verbosity;
154			break;
155		case 'r':
156			rebaseline = true;
157			break;
158		case 'v':
159			++verbosity;
160			break;
161		default:
162			return false;
163		}
164	}
165
166	return true;
167}
168
169static void usageCInema(const char* arg0) {
170	printf("usage: %s [-dhnqv] [-b BASE] [-o DIR] [--version] [test...]\n", arg0);
171	puts("  -b, --base [BASE]          Path to the CInema base directory");
172	puts("  -d, --diffs                Output image diffs from failures");
173	puts("  -h, --help                 Print this usage and exit");
174	puts("  -n, --dry-run              List all collected tests instead of running them");
175	puts("  -o, --output [DIR]         Path to output applicable results");
176	puts("  -q, --quiet                Decrease log verbosity (can be repeated)");
177	puts("  -r, --rebaseline           Rewrite the baseline for failing tests");
178	puts("  -v, --verbose              Increase log verbosity (can be repeated)");
179	puts("  --version                  Print version and exit");
180}
181
182static bool determineBase(int argc, char* const* argv) {
183	// TODO: Better dynamic detection
184	separatePath(__FILE__, base, NULL, NULL);
185	strncat(base, PATH_SEP ".." PATH_SEP ".." PATH_SEP ".." PATH_SEP "cinema", sizeof(base) - strlen(base) - 1);
186	return true;
187}
188
189static bool collectTests(struct CInemaTestList* tests, const char* path) {
190	CIerr(2, "Considering path %s\n", path);
191	struct VDir* dir = VDirOpen(path);
192	if (!dir) {
193		return false;
194	}
195	struct VDirEntry* entry = dir->listNext(dir);
196	while (entry) {
197		char subpath[PATH_MAX];
198		snprintf(subpath, sizeof(subpath), "%s" PATH_SEP "%s", path, entry->name(entry));
199		if (entry->type(entry) == VFS_DIRECTORY && strncmp(entry->name(entry), ".", 2) != 0 && strncmp(entry->name(entry), "..", 3) != 0) {
200			if (!collectTests(tests, subpath)) {
201				dir->close(dir);
202				return false;
203			}
204		} else if (entry->type(entry) == VFS_FILE && strncmp(entry->name(entry), "test.", 5) == 0) {
205			CIerr(3, "Found potential test %s\n", subpath);
206			struct VFile* vf = dir->openFile(dir, entry->name(entry), O_RDONLY);
207			if (vf) {
208				if (mCoreIsCompatible(vf) != PLATFORM_NONE || mVideoLogIsCompatible(vf) != PLATFORM_NONE) {
209					struct CInemaTest* test = CInemaTestListAppend(tests);
210					if (!CInemaTestInit(test, path, entry->name(entry))) {
211						CIerr(3, "Failed to create test\n");
212						CInemaTestListResize(tests, -1);
213					} else {
214						CIerr(2, "Found test %s\n", test->name);
215					}
216				} else {
217					CIerr(3, "Not a compatible file\n");
218				}
219				vf->close(vf);
220			} else {
221				CIerr(3, "Failed to open file\n");
222			}
223		}
224		entry = dir->listNext(dir);
225	}
226	dir->close(dir);
227	return true;
228}
229
230static int _compareNames(const void* a, const void* b) {
231	const struct CInemaTest* ta = a;
232	const struct CInemaTest* tb = b;
233
234	return strncmp(ta->name, tb->name, sizeof(ta->name));
235}
236
237static void reduceTestList(struct CInemaTestList* tests) {
238	qsort(CInemaTestListGetPointer(tests, 0), CInemaTestListSize(tests), sizeof(struct CInemaTest), _compareNames);
239
240	size_t i;
241	for (i = 1; i < CInemaTestListSize(tests);) {
242		struct CInemaTest* cur = CInemaTestListGetPointer(tests, i);
243		struct CInemaTest* prev = CInemaTestListGetPointer(tests, i - 1);
244		if (strncmp(cur->name, prev->name, sizeof(cur->name)) != 0) {
245			++i;
246			continue;
247		}
248		CInemaTestListShift(tests, i, 1);
249	}
250}
251
252static void testToPath(const char* testName, char* path) {
253	strlcpy(path, base, PATH_MAX);
254
255	bool dotSeen = true;
256	size_t i;
257	for (i = strlen(path); testName[0] && i < PATH_MAX; ++testName) {
258		if (testName[0] == '.') {
259			dotSeen = true;
260		} else {
261			if (dotSeen) {
262				strlcpy(&path[i], PATH_SEP, PATH_MAX - i);
263				i += strlen(PATH_SEP);
264				dotSeen = false;
265				if (!i) {
266					break;
267				}
268			}
269			path[i] = testName[0];
270			++i;
271		}
272	}
273}
274
275static void _loadConfigTree(struct Table* configTree, const char* testName) {
276	char key[MAX_TEST];
277	strlcpy(key, testName, sizeof(key));
278
279	struct mCoreConfig* config;
280	while (!(config = HashTableLookup(configTree, key))) {
281		char path[PATH_MAX];
282		config = malloc(sizeof(*config));
283		mCoreConfigInit(config, "cinema");
284		testToPath(key, path);
285		strncat(path, PATH_SEP, sizeof(path) - 1);
286		strncat(path, "config.ini", sizeof(path) - 1);
287		mCoreConfigLoadPath(config, path);
288		HashTableInsert(configTree, key, config);
289		char* pos = strrchr(key, '.');
290		if (pos) {
291			pos[0] = '\0';
292		} else if (key[0]) {
293			key[0] = '\0';
294		} else {
295			break;
296		}
297	}
298}
299
300static void _unloadConfigTree(const char* key, void* value, void* user) {
301	UNUSED(key);
302	UNUSED(user);
303	mCoreConfigDeinit(value);
304}
305
306static const char* _lookupValue(struct Table* configTree, const char* testName, const char* key) {
307	_loadConfigTree(configTree, testName);
308
309	char testKey[MAX_TEST];
310	strlcpy(testKey, testName, sizeof(testKey));
311
312	struct mCoreConfig* config;
313	while (true) {
314		config = HashTableLookup(configTree, testKey);
315		if (!config) {
316			continue;
317		}
318		const char* str = ConfigurationGetValue(&config->configTable, "testinfo", key);
319		if (str) {
320			return str;
321		}
322		char* pos = strrchr(testKey, '.');
323		if (pos) {
324			pos[0] = '\0';
325		} else if (testKey[0]) {
326			testKey[0] = '\0';
327		} else {
328			break;
329		}
330	}
331	return NULL;
332}
333
334bool CInemaConfigGetUInt(struct Table* configTree, const char* testName, const char* key, unsigned* out) {
335	const char* charValue = _lookupValue(configTree, testName, key);
336	if (!charValue) {
337		return false;
338	}
339	char* end;
340	unsigned long value = strtoul(charValue, &end, 10);
341	if (*end) {
342		return false;
343	}
344	*out = value;
345	return true;
346}
347
348void CInemaConfigLoad(struct Table* configTree, const char* testName, struct mCore* core) {
349	_loadConfigTree(configTree, testName);
350
351	char testKey[MAX_TEST] = {0};
352	char* keyEnd = testKey;
353	const char* pos;
354	while (true) {
355		pos = strchr(testName, '.');
356		size_t maxlen = sizeof(testKey) - (keyEnd - testKey) - 1;
357		size_t len;
358		if (pos) {
359			len = pos - testName;
360		} else {
361			len = strlen(testName);
362		}
363		if (len > maxlen) {
364			len = maxlen;
365		}
366		strncpy(keyEnd, testName, len);
367		keyEnd += len;
368
369		struct mCoreConfig* config = HashTableLookup(configTree, testKey);
370		if (config) {
371			core->loadConfig(core, config);
372		}
373		if (!pos) {
374			break;
375		}
376		testName = pos + 1;
377		keyEnd[0] = '.';
378		++keyEnd;
379	}
380}
381
382bool CInemaTestInit(struct CInemaTest* test, const char* directory, const char* filename) {
383	if (strncmp(base, directory, strlen(base)) != 0) {
384		return false;
385	}
386	memset(test, 0, sizeof(*test));
387	strlcpy(test->directory, directory, sizeof(test->directory));
388	strlcpy(test->filename, filename, sizeof(test->filename));
389	directory += strlen(base) + 1;
390	strlcpy(test->name, directory, sizeof(test->name));
391	char* str = strstr(test->name, PATH_SEP);
392	while (str) {
393		str[0] = '.';
394		str = strstr(str, PATH_SEP);
395	}
396	return true;
397}
398
399static bool _loadBaselinePNG(struct VDir* dir, struct CInemaImage* image, size_t frame, enum CInemaStatus* status) {
400	char baselineName[32];
401	snprintf(baselineName, sizeof(baselineName), "baseline_%04" PRIz "u.png", frame);
402	struct VFile* baselineVF = dir->openFile(dir, baselineName, O_RDONLY);
403	if (!baselineVF) {
404		if (*status == CI_PASS) {
405			*status = CI_FAIL;
406		}
407		return false;
408	}
409
410	png_structp png = PNGReadOpen(baselineVF, 0);
411	png_infop info = png_create_info_struct(png);
412	png_infop end = png_create_info_struct(png);
413	if (!png || !info || !end || !PNGReadHeader(png, info)) {
414		PNGReadClose(png, info, end);
415		baselineVF->close(baselineVF);
416		CIerr(1, "Failed to load %s\n", baselineName);
417		*status = CI_ERROR;
418		return false;
419	}
420
421	unsigned pwidth = png_get_image_width(png, info);
422	unsigned pheight = png_get_image_height(png, info);
423	if (pheight != image->height || pwidth != image->width) {
424		PNGReadClose(png, info, end);
425		baselineVF->close(baselineVF);
426		CIlog(1, "Size mismatch for %s, expected %ux%u, got %ux%u\n", baselineName, pwidth, pheight, image->width, image->height);
427		if (*status == CI_PASS) {
428			*status = CI_FAIL;
429		}
430		return false;
431	}
432
433	image->data = malloc(pwidth * pheight * BYTES_PER_PIXEL);
434	if (!image->data) {
435		CIerr(1, "Failed to allocate baseline buffer\n");
436		*status = CI_ERROR;
437		PNGReadClose(png, info, end);
438		baselineVF->close(baselineVF);
439		return false;
440	}
441	if (!PNGReadPixels(png, info, image->data, pwidth, pheight, pwidth) || !PNGReadFooter(png, end)) {
442		CIerr(1, "Failed to read %s\n", baselineName);
443		*status = CI_ERROR;
444		free(image->data);
445		return false;
446	}
447	PNGReadClose(png, info, end);
448	baselineVF->close(baselineVF);
449	image->stride = pwidth;
450	return true;
451}
452
453#ifdef USE_FFMPEG
454struct CInemaStream {
455	struct mAVStream d;
456	struct CInemaImage* image;
457	enum CInemaStatus* status;
458};
459
460static void _cinemaDimensionsChanged(struct mAVStream* stream, unsigned width, unsigned height) {
461	struct CInemaStream* cistream = (struct CInemaStream*) stream;
462	if (height != cistream->image->height || width != cistream->image->width) {
463		CIerr(1, "Size mismatch for video, expected %ux%u, got %ux%u\n", width, height, cistream->image->width, cistream->image->height);
464		if (*cistream->status == CI_PASS) {
465			*cistream->status = CI_FAIL;
466		}
467	}
468}
469
470static void _cinemaVideoFrame(struct mAVStream* stream, const color_t* pixels, size_t stride) {
471	struct CInemaStream* cistream = (struct CInemaStream*) stream;
472	cistream->image->stride = stride;
473	size_t bufferSize = cistream->image->stride * cistream->image->height * BYTES_PER_PIXEL;
474	cistream->image->data = malloc(bufferSize);
475	memcpy(cistream->image->data, pixels, bufferSize);
476}
477#endif
478
479static struct VDir* _makeOutDir(const char* testName) {
480	char path[PATH_MAX] = {0};
481	strlcpy(path, outdir, sizeof(path));
482	char* pathEnd = path + strlen(path);
483	const char* pos;
484	while (true) {
485		pathEnd[0] = PATH_SEP[0];
486		++pathEnd;
487		pos = strchr(testName, '.');
488		size_t maxlen = sizeof(path) - (pathEnd - path) - 1;
489		size_t len;
490		if (pos) {
491			len = pos - testName;
492		} else {
493			len = strlen(testName);
494		}
495		if (len > maxlen) {
496			len = maxlen;
497		}
498		strncpy(pathEnd, testName, len);
499		pathEnd += len;
500
501		mkdir(path, 0777);
502
503		if (!pos) {
504			break;
505		}
506		testName = pos + 1;
507	}
508	return VDirOpen(path);
509}
510
511static void _writeImage(struct VFile* vf, const struct CInemaImage* image) {
512	png_structp png = PNGWriteOpen(vf);
513	png_infop info = PNGWriteHeader(png, image->width, image->height);
514	if (!PNGWritePixels(png, image->width, image->height, image->stride, image->data)) {
515		CIerr(0, "Could not write output image\n");
516	}
517	PNGWriteClose(png, info);
518
519	vf->close(vf);
520}
521
522static void _writeDiff(const char* testName, const struct CInemaImage* image, size_t frame, const char* type) {
523	struct VDir* dir = _makeOutDir(testName);
524	if (!dir) {
525		CIerr(0, "Could not open directory for %s\n", testName);
526		return;
527	}
528	char name[32];
529	snprintf(name, sizeof(name), "%s_%04" PRIz "u.png", type, frame);
530	struct VFile* vf = dir->openFile(dir, name, O_CREAT | O_TRUNC | O_WRONLY);
531	if (!vf) {
532		CIerr(0, "Could not open output file %s\n", name);
533		dir->close(dir);
534		return;
535	}
536	_writeImage(vf, image);
537	dir->close(dir);
538}
539
540static void _writeBaseline(struct VDir* dir, const struct CInemaImage* image, size_t frame) {
541	char baselineName[32];
542	snprintf(baselineName, sizeof(baselineName), "baseline_%04" PRIz "u.png", frame);
543	struct VFile* baselineVF = dir->openFile(dir, baselineName, O_CREAT | O_TRUNC | O_WRONLY);
544	if (baselineVF) {
545		_writeImage(baselineVF, image);
546	} else {
547		CIerr(0, "Could not open output file %s\n", baselineName);
548	}
549}
550
551void CInemaTestRun(struct CInemaTest* test, struct Table* configTree) {
552	unsigned ignore = 0;
553	CInemaConfigGetUInt(configTree, test->name, "ignore", &ignore);
554	if (ignore) {
555		test->status = CI_SKIP;
556		return;
557	}
558
559	struct VDir* dir = VDirOpen(test->directory);
560	if (!dir) {
561		CIerr(0, "Failed to open test directory\n");
562		test->status = CI_ERROR;
563		return;
564	}
565	struct VFile* rom = dir->openFile(dir, test->filename, O_RDONLY);
566	if (!rom) {
567		CIerr(0, "Failed to open test\n");
568		test->status = CI_ERROR;
569		return;
570	}
571	struct mCore* core = mCoreFindVF(rom);
572	if (!core) {
573		CIerr(0, "Failed to load test\n");
574		test->status = CI_ERROR;
575		rom->close(rom);
576		return;
577	}
578	if (!core->init(core)) {
579		CIerr(0, "Failed to init test\n");
580		test->status = CI_ERROR;
581		core->deinit(core);
582		return;
583	}
584	struct CInemaImage image;
585	core->desiredVideoDimensions(core, &image.width, &image.height);
586	ssize_t bufferSize = image.width * image.height * BYTES_PER_PIXEL;
587	image.data = malloc(bufferSize);
588	image.stride = image.width;
589	if (!image.data) {
590		CIerr(0, "Failed to allocate video buffer\n");
591		test->status = CI_ERROR;
592		core->deinit(core);
593	}
594	core->setVideoBuffer(core, image.data, image.stride);
595	mCoreConfigInit(&core->config, "cinema");
596
597	unsigned limit = 9999;
598	unsigned skip = 0;
599	unsigned fail = 0;
600	unsigned video = 0;
601
602	CInemaConfigGetUInt(configTree, test->name, "frames", &limit);
603	CInemaConfigGetUInt(configTree, test->name, "skip", &skip);
604	CInemaConfigGetUInt(configTree, test->name, "fail", &fail);
605	CInemaConfigGetUInt(configTree, test->name, "video", &video);
606	CInemaConfigLoad(configTree, test->name, core);
607
608	core->loadROM(core, rom);
609	core->rtc.override = RTC_FAKE_EPOCH;
610	core->rtc.value = 1200000000;
611	core->reset(core);
612
613	test->status = CI_PASS;
614
615	unsigned minFrame = core->frameCounter(core);
616	size_t frame;
617	for (frame = 0; frame < skip; ++frame) {
618		core->runFrame(core);
619	}
620
621#ifdef USE_FFMPEG
622	struct FFmpegDecoder decoder;
623	struct FFmpegEncoder encoder;
624	struct CInemaStream stream = {0};
625	if (video) {
626		char fname[PATH_MAX];
627		snprintf(fname, sizeof(fname), "%s" PATH_SEP "baseline.mkv", test->directory);
628		if (rebaseline) {
629			FFmpegEncoderInit(&encoder);
630			FFmpegEncoderSetAudio(&encoder, NULL, 0);
631			FFmpegEncoderSetVideo(&encoder, "png", 0, 0);
632			FFmpegEncoderSetContainer(&encoder, "mkv");
633			FFmpegEncoderSetDimensions(&encoder, image.width, image.height);
634			if (!FFmpegEncoderOpen(&encoder, fname)) {
635				CIerr(1, "Failed to save baseline video\n");
636			} else {
637				core->setAVStream(core, &encoder.d);
638			}
639		} else {
640			FFmpegDecoderInit(&decoder);
641			stream.d.postVideoFrame = _cinemaVideoFrame;
642			stream.d.videoDimensionsChanged = _cinemaDimensionsChanged;
643			stream.status = &test->status;
644			decoder.out = &stream.d;
645			stream.image = &image;
646
647			if (!FFmpegDecoderOpen(&decoder, fname)) {
648				CIerr(1, "Failed to load baseline video\n");
649			}
650		}
651	}
652#else
653	if (video) {
654		CIerr(0, "Failed to run video test without ffmpeg linked in\n");
655		test->status = CI_ERROR;
656	}
657#endif
658
659	for (frame = 0; limit; ++frame, --limit) {
660		core->runFrame(core);
661		++test->totalFrames;
662		unsigned frameCounter = core->frameCounter(core);
663		if (frameCounter <= minFrame) {
664			break;
665		}
666		CIlog(3, "Test frame: %u\n", frameCounter);
667		core->desiredVideoDimensions(core, &image.width, &image.height);
668		uint8_t* diff = NULL;
669		struct CInemaImage expected = {
670			.data = NULL,
671			.width = image.width,
672			.height = image.height,
673			.stride = image.width,
674		};
675		bool baselineFound;
676		if (video) {
677			baselineFound = false;
678#ifdef USE_FFMPEG
679			if (!rebaseline && FFmpegDecoderIsOpen(&decoder)) {
680				stream.image = &expected;
681				while (!expected.data) {
682					if (!FFmpegDecoderRead(&decoder)) {
683						CIerr(1, "Failed to read more frames. EOF?\n");
684						test->status = CI_FAIL;
685						break;
686					}
687				}
688				baselineFound = expected.data;
689			}
690#endif
691		} else {
692			baselineFound = _loadBaselinePNG(dir, &expected, frame, &test->status);
693		}
694		if (baselineFound) {
695			uint8_t* testPixels = image.data;
696			uint8_t* expectPixels = expected.data;
697			size_t x;
698			size_t y;
699			int max = 0;
700			bool failed = false;
701			for (y = 0; y < image.height; ++y) {
702				for (x = 0; x < image.width; ++x) {
703					size_t pix = expected.stride * y + x;
704					size_t tpix = image.stride * y + x;
705					int testR = testPixels[tpix * 4 + 0];
706					int testG = testPixels[tpix * 4 + 1];
707					int testB = testPixels[tpix * 4 + 2];
708					int expectR = expectPixels[pix * 4 + 0];
709					int expectG = expectPixels[pix * 4 + 1];
710					int expectB = expectPixels[pix * 4 + 2];
711					int r = expectR - testR;
712					int g = expectG - testG;
713					int b = expectB - testB;
714					if (r | g | b) {
715						failed = true;
716						if (diffs && !diff) {
717							diff = calloc(expected.width * expected.height, BYTES_PER_PIXEL);
718						}
719						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",
720						    frameCounter, x, y, r, g, b,
721						    expectR, expectG, expectB,
722						    testR, testG, testB);
723						test->status = CI_FAIL;
724						if (r < 0) {
725							r = -r;
726						}
727						if (g < 0) {
728							g = -g;
729						}
730						if (b < 0) {
731							b = -b;
732						}
733
734						if (diff) {
735							if (r > max) {
736								max = r;
737							}
738							if (g > max) {
739								max = g;
740							}
741							if (b > max) {
742								max = b;
743							}
744							diff[pix * 4 + 0] = r;
745							diff[pix * 4 + 1] = g;
746							diff[pix * 4 + 2] = b;
747						}
748
749						test->totalDistance += r + g + b;
750						++test->failedPixels;
751					}
752				}
753			}
754			if (failed) {
755				++test->failedFrames;
756			}
757			test->totalPixels += image.height * image.width;
758			if (rebaseline && failed) {
759				_writeBaseline(dir, &image, frame);
760			}
761			if (diff) {
762				if (failed) {
763					struct CInemaImage outdiff = {
764						.data = diff,
765						.width = image.width,
766						.height = image.height,
767						.stride = image.width,
768					};
769
770					_writeDiff(test->name, &image, frame, "result");
771					_writeDiff(test->name, &expected, frame, "expected");
772					_writeDiff(test->name, &outdiff, frame, "diff");
773
774					for (y = 0; y < outdiff.height; ++y) {
775						for (x = 0; x < outdiff.width; ++x) {
776							size_t pix = outdiff.stride * y + x;
777							diff[pix * 4 + 0] = diff[pix * 4 + 0] * 255 / max;
778							diff[pix * 4 + 1] = diff[pix * 4 + 1] * 255 / max;
779							diff[pix * 4 + 2] = diff[pix * 4 + 2] * 255 / max;
780						}
781					}
782					_writeDiff(test->name, &outdiff, frame, "normalized");
783				}
784				free(diff);
785			}
786			free(expected.data);
787		} else if (test->status == CI_ERROR) {
788			break;
789		} else if (rebaseline && !video) {
790			_writeBaseline(dir, &image, frame);
791		} else if (!rebaseline) {
792			test->status = CI_FAIL;
793		}
794	}
795
796	if (fail) {
797		if (test->status == CI_FAIL) {
798			test->status = CI_XFAIL;
799		} else if (test->status == CI_PASS) {
800			test->status = CI_XPASS;
801		}
802	}
803
804#ifdef USE_FFMPEG
805	if (video) {
806		if (rebaseline) {
807			FFmpegEncoderClose(&encoder);
808		} else {
809			FFmpegDecoderClose(&decoder);
810		}
811	}
812#endif
813
814	free(image.data);
815	mCoreConfigDeinit(&core->config);
816	core->deinit(core);
817	dir->close(dir);
818}
819
820void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args) {
821	UNUSED(log);
822	if (verbosity < 0) {
823		return;
824	}
825	int mask = mLOG_FATAL;
826	if (verbosity >= 1) {
827		mask |= mLOG_ERROR;
828	}
829	if (verbosity >= 2) {
830		mask |= mLOG_WARN;
831	}
832	if (verbosity >= 4) {
833		mask |= mLOG_INFO;
834	}
835	if (verbosity >= 5) {
836		mask |= mLOG_ALL;
837	}
838	if (!(mask & level)) {
839		return;
840	}
841
842	char buffer[256];
843	vsnprintf(buffer, sizeof(buffer), format, args);
844	CIerr(0, "[%s] %s\n", mLogCategoryName(category), buffer);
845}
846
847int main(int argc, char** argv) {
848	int status = 0;
849	if (!parseCInemaArgs(argc, argv)) {
850		status = 1;
851		goto cleanup;
852	}
853
854	if (showVersion) {
855		version(argv[0]);
856		goto cleanup;
857	}
858
859	if (showUsage) {
860		usageCInema(argv[0]);
861		goto cleanup;
862	}
863
864	argc -= optind;
865	argv += optind;
866
867	if (!base[0] && !determineBase(argc, argv)) {
868		CIlog(0, "Could not determine CInema test base. Please specify manually.");
869		status = 1;
870		goto cleanup;
871	}
872#ifndef _WIN32
873	char* rbase = realpath(base, NULL);
874	if (rbase) {
875		strlcpy(base, rbase, sizeof(base));
876		free(rbase);
877	}
878#endif
879
880	struct CInemaTestList tests;
881	CInemaTestListInit(&tests, 0);
882
883	struct mLogger logger = { .log = _log };
884	mLogSetDefaultLogger(&logger);
885#ifdef USE_FFMPEG
886	if (verbosity < 2) {
887		av_log_set_level(AV_LOG_ERROR);
888	}
889#endif
890
891	if (argc > 0) {
892		size_t i;
893		for (i = 0; i < (size_t) argc; ++i) {
894			char path[PATH_MAX + 1] = {0};
895			testToPath(argv[i], path);
896
897			if (!collectTests(&tests, path)) {
898				status = 1;
899				break;
900			}
901		}
902	} else if (!collectTests(&tests, base)) {
903		status = 1;
904	}
905
906	if (CInemaTestListSize(&tests) == 0) {
907		CIlog(1, "No tests found.");
908		status = 1;
909	} else {
910		reduceTestList(&tests);
911	}
912
913	struct Table configTree;
914	HashTableInit(&configTree, 0, free);
915
916	size_t i;
917	for (i = 0; i < CInemaTestListSize(&tests); ++i) {
918		struct CInemaTest* test = CInemaTestListGetPointer(&tests, i);
919		if (dryRun) {
920			CIlog(-1, "%s\n", test->name);
921		} else {
922			CIlog(1, "%s: ", test->name);
923			fflush(stdout);
924			CInemaTestRun(test, &configTree);
925			switch (test->status) {
926			case CI_PASS:
927				CIlog(1, "pass\n");
928				break;
929			case CI_FAIL:
930				status = 1;
931				CIlog(1, "fail\n");
932				break;
933			case CI_XPASS:
934				CIlog(1, "xpass\n");
935				break;
936			case CI_XFAIL:
937				CIlog(1, "xfail\n");
938				break;
939			case CI_SKIP:
940				CIlog(1, "skip\n");
941				break;
942			case CI_ERROR:
943				status = 1;
944				CIlog(1, "error\n");
945				break;
946			}
947			if (test->failedFrames) {
948				CIlog(2, "\tfailed frames: %u/%u (%1.3g%%)\n", test->failedFrames, test->totalFrames, test->failedFrames / (test->totalFrames * 0.01));
949				CIlog(2, "\tfailed pixels: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->failedPixels, test->totalPixels, test->failedPixels / (test->totalPixels * 0.01));
950				CIlog(2, "\tdistance: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->totalDistance, test->totalPixels * 765, test->totalDistance / (test->totalPixels * 7.65));
951			}
952		}
953	}
954
955	HashTableEnumerate(&configTree, _unloadConfigTree, NULL);
956	HashTableDeinit(&configTree);
957	CInemaTestListDeinit(&tests);
958
959cleanup:
960	return status;
961}