all repos — mgba @ 5ceec845601788b9d9d3862555380793457fdc44

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/vector.h>
 15#include <mgba-util/vfs.h>
 16
 17#ifdef _MSC_VER
 18#include <mgba-util/platform/windows/getopt.h>
 19#else
 20#include <getopt.h>
 21#endif
 22
 23#include <stdlib.h>
 24
 25#define MAX_TEST 200
 26
 27static const struct option longOpts[] = {
 28	{ "base",      required_argument, 0, 'b' },
 29	{ "help",      no_argument, 0, 'h' },
 30	{ "quiet",     no_argument, 0, 'q' },
 31	{ "dry-run",   no_argument, 0, 'n' },
 32	{ "verbose",   no_argument, 0, 'v' },
 33	{ "version",   no_argument, 0, '\0' },
 34	{ 0, 0, 0, 0 }
 35};
 36
 37static const char shortOpts[] = "b:hnqv";
 38
 39enum CInemaStatus {
 40	CI_PASS,
 41	CI_FAIL,
 42	CI_XPASS,
 43	CI_XFAIL,
 44	CI_ERROR,
 45	CI_SKIP
 46};
 47
 48struct CInemaTest {
 49	char directory[MAX_TEST];
 50	char filename[MAX_TEST];
 51	char name[MAX_TEST];
 52	enum CInemaStatus status;
 53	int failedFrames;
 54};
 55
 56DECLARE_VECTOR(CInemaTestList, struct CInemaTest)
 57DEFINE_VECTOR(CInemaTestList, struct CInemaTest)
 58
 59DECLARE_VECTOR(ImageList, void*)
 60DEFINE_VECTOR(ImageList, void*)
 61
 62static bool showVersion = false;
 63static bool showUsage = false;
 64static char base[PATH_MAX] = {0};
 65static bool dryRun = false;
 66static int verbosity = 0;
 67
 68bool CInemaTestInit(struct CInemaTest*, const char* directory, const char* filename);
 69void CInemaTestRun(struct CInemaTest*);
 70
 71static void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args);
 72
 73ATTRIBUTE_FORMAT(printf, 2, 3) void CIlog(int minlevel, const char* format, ...) {
 74	if (verbosity < minlevel) {
 75		return;
 76	}
 77	va_list args;
 78	va_start(args, format);
 79	vprintf(format, args);
 80	va_end(args);
 81}
 82
 83ATTRIBUTE_FORMAT(printf, 2, 3) void CIerr(int minlevel, const char* format, ...) {
 84	if (verbosity < minlevel) {
 85		return;
 86	}
 87	va_list args;
 88	va_start(args, format);
 89	vfprintf(stderr, format, args);
 90	va_end(args);
 91}
 92
 93static bool parseCInemaArgs(int argc, char* const* argv) {
 94	int ch;
 95	int index = 0;
 96	while ((ch = getopt_long(argc, argv, shortOpts, longOpts, &index)) != -1) {
 97		const struct option* opt = &longOpts[index];
 98		switch (ch) {
 99		case '\0':
100			if (strcmp(opt->name, "version") == 0) {
101				showVersion = true;
102			} else {
103				return false;
104			}
105			break;
106		case 'b':
107			strncpy(base, optarg, sizeof(base));
108			// TODO: Verify path exists
109			break;
110		case 'h':
111			showUsage = true;
112			break;
113		case 'n':
114			dryRun = true;
115			break;
116		case 'q':
117			--verbosity;
118			break;
119		case 'v':
120			++verbosity;
121			break;
122		default:
123			return false;
124		}
125	}
126
127	return true;
128}
129
130static void usageCInema(const char* arg0) {
131	printf("usage: %s [-h] [-b BASE] [--version] [test...]\n", arg0);
132	puts("  -b, --base                 Path to the CInema base directory");
133	puts("  -h, --help                 Print this usage and exit");
134	puts("  -n, --dry-run              List all collected tests instead of running them");
135	puts("  -q, --quiet                Decrease log verbosity (can be repeated)");
136	puts("  -v, --verbose              Increase log verbosity (can be repeated)");
137	puts("  --version                  Print version and exit");
138}
139
140static bool determineBase(int argc, char* const* argv) {
141	// TODO: Better dynamic detection
142	separatePath(__FILE__, base, NULL, NULL);
143	strncat(base, PATH_SEP ".." PATH_SEP ".." PATH_SEP ".." PATH_SEP "cinema", sizeof(base) - strlen(base) - 1);
144	return true;
145}
146
147static bool collectTests(struct CInemaTestList* tests, const char* path) {
148	CIerr(2, "Considering path %s\n", path);
149	struct VDir* dir = VDirOpen(path);
150	if (!dir) {
151		return false;
152	}
153	struct VDirEntry* entry = dir->listNext(dir);
154	while (entry) {
155		char subpath[PATH_MAX];
156		snprintf(subpath, sizeof(subpath), "%s" PATH_SEP "%s", path, entry->name(entry));
157		if (entry->type(entry) == VFS_DIRECTORY && strncmp(entry->name(entry), ".", 2) != 0 && strncmp(entry->name(entry), "..", 3) != 0) {
158			if (!collectTests(tests, subpath)) {
159				dir->close(dir);
160				return false;
161			}
162		} else if (entry->type(entry) == VFS_FILE && strncmp(entry->name(entry), "test.", 5) == 0) {
163			CIerr(2, "Found potential test %s\n", subpath);
164			struct VFile* vf = dir->openFile(dir, entry->name(entry), O_RDONLY);
165			if (vf) {
166				if (mCoreIsCompatible(vf) != PLATFORM_NONE || mVideoLogIsCompatible(vf) != PLATFORM_NONE) {
167					struct CInemaTest* test = CInemaTestListAppend(tests);
168					if (!CInemaTestInit(test, path, entry->name(entry))) {
169						CIerr(2, "Failed to create test\n");
170						CInemaTestListResize(tests, -1);
171					} else {
172						CIerr(1, "Found test %s\n", test->name);
173					}
174				} else {
175					CIerr(2, "Not a compatible file\n");
176				}
177				vf->close(vf);
178			} else {
179				CIerr(2, "Failed to open file\n");
180			}
181		}
182		entry = dir->listNext(dir);
183	}
184	dir->close(dir);
185	return true;
186}
187
188static int _compareNames(const void* a, const void* b) {
189	const struct CInemaTest* ta = a;
190	const struct CInemaTest* tb = b;
191
192	return strncmp(ta->name, tb->name, sizeof(ta->name));
193}
194
195static void reduceTestList(struct CInemaTestList* tests) {
196	qsort(CInemaTestListGetPointer(tests, 0), CInemaTestListSize(tests), sizeof(struct CInemaTest), _compareNames);
197
198	size_t i;
199	for (i = 1; i < CInemaTestListSize(tests);) {
200		struct CInemaTest* cur = CInemaTestListGetPointer(tests, i);
201		struct CInemaTest* prev = CInemaTestListGetPointer(tests, i - 1);
202		if (strncmp(cur->name, prev->name, sizeof(cur->name)) != 0) {
203			++i;
204			continue;
205		}
206		CInemaTestListShift(tests, i, 1);
207	}
208}
209
210bool CInemaTestInit(struct CInemaTest* test, const char* directory, const char* filename) {
211	if (strncmp(base, directory, strlen(base)) != 0) {
212		return false;
213	}
214	strncpy(test->directory, directory, sizeof(test->directory));
215	strncpy(test->filename, filename, sizeof(test->filename));
216	directory += strlen(base) + 1;
217	strncpy(test->name, directory, sizeof(test->name));
218	char* str = strstr(test->name, PATH_SEP);
219	while (str) {
220		str[0] = '.';
221		str = strstr(str, PATH_SEP);
222	}
223	return true;
224}
225
226void CInemaTestRun(struct CInemaTest* test) {
227	struct VDir* dir = VDirOpen(test->directory);
228	if (!dir) {
229		CIerr(0, "Failed to open test directory\n");
230		test->status = CI_ERROR;
231		return;
232	}
233	struct VFile* rom = dir->openFile(dir, test->filename, O_RDONLY);
234	if (!rom) {
235		CIerr(0, "Failed to open test\n");
236		test->status = CI_ERROR;
237		return;
238	}
239	struct mCore* core = mCoreFindVF(rom);
240	if (!core) {
241		CIerr(0, "Failed to load test\n");
242		test->status = CI_ERROR;
243		rom->close(rom);
244		return;
245	}
246	if (!core->init(core)) {
247		CIerr(0, "Failed to init test\n");
248		test->status = CI_ERROR;
249		core->deinit(core);
250		return;
251	}
252	unsigned width, height;
253	core->desiredVideoDimensions(core, &width, &height);
254	ssize_t bufferSize = width * height * BYTES_PER_PIXEL;
255	void* buffer = malloc(bufferSize);
256	if (!buffer) {
257		CIerr(0, "Failed to allocate video buffer\n");
258		test->status = CI_ERROR;
259		core->deinit(core);
260	}
261	core->setVideoBuffer(core, buffer, width);
262	mCoreConfigInit(&core->config, "cinema");
263
264	core->loadROM(core, rom);
265	core->reset(core);
266
267	test->status = CI_PASS;
268
269	unsigned minFrame = core->frameCounter(core);
270	unsigned limit = 9999;
271	size_t frame;
272	for (frame = 0; limit; ++frame, --limit) {
273		char baselineName[32];
274		snprintf(baselineName, sizeof(baselineName), "baseline_%04" PRIz "u.png", frame);
275		core->runFrame(core);
276		unsigned frameCounter = core->frameCounter(core);
277		if (frameCounter <= minFrame) {
278			break;
279		}
280		CIerr(2, "Test frame: %u\n", frameCounter);
281
282		struct VFile* baselineVF = dir->openFile(dir, baselineName, O_RDONLY);
283		if (!baselineVF) {
284			test->status = CI_FAIL;
285		} else {
286			png_structp png = PNGReadOpen(baselineVF, 0);
287			png_infop info = png_create_info_struct(png);
288			png_infop end = png_create_info_struct(png);
289			if (!png || !info || !end || !PNGReadHeader(png, info)) {
290				PNGReadClose(png, info, end);
291				CIerr(1, "Failed to load %s\n", baselineName);
292				test->status = CI_ERROR;
293			} else {
294				unsigned pwidth = png_get_image_width(png, info);
295				unsigned pheight = png_get_image_height(png, info);
296				unsigned twidth, theight;
297				core->desiredVideoDimensions(core, &twidth, &theight);
298				if (pheight != theight || pwidth != twidth) {
299					PNGReadClose(png, info, end);
300					CIerr(1, "Size mismatch for %s, expected %ux%u, got %ux%u\n", baselineName, pwidth, pheight, twidth, theight);
301					test->status = CI_FAIL;
302				} else {
303					uint8_t* pixels = malloc(pwidth * pheight * BYTES_PER_PIXEL);
304					if (!pixels) {
305						CIerr(1, "Failed to allocate baseline buffer\n");
306						test->status = CI_ERROR;
307					} else {
308						if (!PNGReadPixels(png, info, pixels, pwidth, pheight, pwidth) || !PNGReadFooter(png, end)) {
309							CIerr(1, "Failed to read %s\n", baselineName);
310							test->status = CI_ERROR;
311						} else {
312							uint8_t* testPixels = buffer;
313							size_t x;
314							size_t y;
315							for (y = 0; y < theight; ++y) {
316								for (x = 0; x < twidth; ++x) {
317									size_t pix = pwidth * y + x;
318									size_t tpix = width * y + x;
319									int testR = testPixels[tpix * 4 + 0];
320									int testG = testPixels[tpix * 4 + 1];
321									int testB = testPixels[tpix * 4 + 2];
322									int expectR = pixels[pix * 4 + 0];
323									int expectG = pixels[pix * 4 + 1];
324									int expectB = pixels[pix * 4 + 2];
325									int r = expectR - testR;
326									int g = expectG - testG;
327									int b = expectB - testB;
328									if (r | g | b) {
329										CIerr(2, "Frame %u failed at pixel %" PRIz "ux%" PRIz "u with diff %i,%i,%i (expected %02x%02x%02x, got %02x%02x%02x)\n",
330										    frameCounter, x, y, r, g, b,
331										    expectR, expectG, expectB,
332										    testR, testG, testB);
333										test->status = CI_FAIL;
334										++test->failedFrames;
335									}
336								}
337							}
338						}
339					}
340					PNGReadClose(png, info, end);
341					free(pixels);
342				}
343			}
344			baselineVF->close(baselineVF);
345		}
346	}
347
348	free(buffer);
349	core->deinit(core);
350	dir->close(dir);
351}
352
353void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args) {
354	// TODO: Write
355}
356
357int main(int argc, char** argv) {
358	int status = 0;
359	if (!parseCInemaArgs(argc, argv)) {
360		status = 1;
361		goto cleanup;
362	}
363
364	if (showVersion) {
365		version(argv[0]);
366		goto cleanup;
367	}
368
369	if (showUsage) {
370		usageCInema(argv[0]);
371		goto cleanup;
372	}
373
374	argc -= optind;
375	argv += optind;
376
377	if (!base[0] && !determineBase(argc, argv)) {
378		CIerr(0, "Could not determine CInema test base. Please specify manually.");
379		status = 1;
380		goto cleanup;
381	}
382#ifndef _WIN32
383	char* rbase = realpath(base, NULL);
384	strncpy(base, rbase, PATH_MAX);
385	free(rbase);
386#endif
387
388	struct CInemaTestList tests;
389	CInemaTestListInit(&tests, 0);
390
391	struct mLogger logger = { .log = _log };
392	mLogSetDefaultLogger(&logger);
393
394	if (argc > 0) {
395		size_t i;
396		for (i = 0; i < (size_t) argc; ++i) {
397			char path[PATH_MAX + 1] = {0};
398			char* arg = argv[i];
399			strncpy(path, base, sizeof(path));
400
401			bool dotSeen = true;
402			size_t j;
403			for (arg = argv[i], j = strlen(path); arg[0] && j < sizeof(path); ++arg) {
404				if (arg[0] == '.') {
405					dotSeen = true;
406				} else {
407					if (dotSeen) {
408						strncpy(&path[j], PATH_SEP, sizeof(path) - j);
409						j += strlen(PATH_SEP);
410						dotSeen = false;
411						if (!j) {
412							break;
413						}
414					}
415					path[j] = arg[0];
416					++j;
417				}
418			}
419
420			if (!collectTests(&tests, path)) {
421				status = 1;
422				break;
423			}
424		}
425	} else if (!collectTests(&tests, base)) {
426		status = 1;
427	}
428
429	if (CInemaTestListSize(&tests) == 0) {
430		CIerr(1, "No tests found.");
431		status = 1;
432	} else {
433		reduceTestList(&tests);
434	}
435
436	size_t i;
437	for (i = 0; i < CInemaTestListSize(&tests); ++i) {
438		struct CInemaTest* test = CInemaTestListGetPointer(&tests, i);
439		if (dryRun) {
440			CIlog(-1, "%s\n", test->name);
441		} else {
442			CIerr(1, "%s: ", test->name);
443			CInemaTestRun(test);
444			switch (test->status) {
445			case CI_PASS:
446				CIerr(1, "pass");
447				break;
448			case CI_FAIL:
449				status = 1;
450				CIerr(1, "fail");
451				break;
452			case CI_XPASS:
453				CIerr(1, "xpass");
454				break;
455			case CI_XFAIL:
456				CIerr(1, "xfail");
457				break;
458			case CI_SKIP:
459				CIerr(1, "skip");
460				break;
461			case CI_ERROR:
462				status = 1;
463				CIerr(1, "error");
464				break;
465			}
466			CIerr(1, "\n");
467		}
468	}
469
470	CInemaTestListDeinit(&tests);
471
472cleanup:
473	return status;
474}