1 // license:BSD-3-Clause
2 // copyright-holders:Aaron Giles
3 /***************************************************************************
4 
5     Regression test report generator
6 
7 ****************************************************************************/
8 
9 #include <cstdio>
10 #include <cstdlib>
11 #include <cstring>
12 #include <cctype>
13 #include <new>
14 #include <cassert>
15 #include "osdcore.h"
16 #include "localpng.h"
17 
18 
19 /***************************************************************************
20     CONSTANTS & DEFINES
21 ***************************************************************************/
22 
23 #define MAX_COMPARES            16
24 #define BITMAP_SPACE            4
25 
26 enum
27 {
28 	STATUS_NOT_PRESENT = 0,
29 	STATUS_SUCCESS,
30 	STATUS_SUCCESS_DIFFERENT,
31 	STATUS_MISSING_FILES,
32 	STATUS_EXCEPTION,
33 	STATUS_FATAL_ERROR,
34 	STATUS_FAILED_VALIDITY,
35 	STATUS_OTHER,
36 	STATUS_COUNT
37 };
38 
39 enum
40 {
41 	BUCKET_UNKNOWN = 0,
42 	BUCKET_IMPROVED,
43 	BUCKET_REGRESSED,
44 	BUCKET_CHANGED,
45 	BUCKET_MULTI_ERROR,
46 	BUCKET_CONSISTENT_ERROR,
47 	BUCKET_GOOD,
48 	BUCKET_GOOD_BUT_CHANGED,
49 	BUCKET_GOOD_BUT_CHANGED_SCREENSHOTS,
50 	BUCKET_COUNT
51 };
52 
53 
54 
55 /***************************************************************************
56     TYPE DEFINITIONS
57 ***************************************************************************/
58 
59 struct summary_file
60 {
61 	summary_file *  next;
62 	char            name[20];
63 	char            source[100];
64 	uint8_t           status[MAX_COMPARES];
65 	uint8_t           matchbitmap[MAX_COMPARES];
66 	std::string     text[MAX_COMPARES];
67 };
68 
69 
70 struct summary_list
71 {
72 	summary_list *  next;
73 	summary_file *  files;
74 	char *          dir;
75 	char            version[40];
76 };
77 
78 
79 
80 /***************************************************************************
81     GLOBAL VARIABLES
82 ***************************************************************************/
83 
84 static summary_file *filehash[128][128];
85 static summary_list lists[MAX_COMPARES];
86 static int list_count;
87 
88 static const char *const bucket_name[] =
89 {
90 	"Unknown",
91 	"Games That Have Improved",
92 	"Games That Have Regressed",
93 	"Games With Changed Screenshots",
94 	"Games With Multiple Errors",
95 	"Games With Consistent Errors",
96 	"Games That Are Consistently Good",
97 	"Games That Regressed But Improved",
98 	"Games With Changed Screenshots",
99 };
100 
101 static const int bucket_output_order[] =
102 {
103 	BUCKET_REGRESSED,
104 	BUCKET_IMPROVED,
105 	BUCKET_CHANGED,
106 	BUCKET_GOOD_BUT_CHANGED_SCREENSHOTS,
107 	BUCKET_GOOD_BUT_CHANGED,
108 	BUCKET_MULTI_ERROR,
109 	BUCKET_CONSISTENT_ERROR
110 };
111 
112 static const char *const status_text[] =
113 {
114 	"",
115 	"Success",
116 	"Changed",
117 	"Missing Files",
118 	"Exception",
119 	"Fatal Error",
120 	"Failed Validity Check",
121 	"Other Unknown Error"
122 };
123 
124 static const char *const status_color[] =
125 {
126 	"",
127 	"background:#00A000",
128 	"background:#E0E000",
129 	"background:#8000C0",
130 	"background:#C00000",
131 	"background:#C00000",
132 	"background:#C06000",
133 	"background:#C00000",
134 	"background:#C00000",
135 };
136 
137 
138 
139 /***************************************************************************
140     PROTOTYPES
141 ***************************************************************************/
142 
143 /* summary parsing */
144 static int read_summary_log(const char *filename, int index);
145 static summary_file *parse_driver_tag(char *linestart, int index);
146 static summary_file *get_file(const char *filename);
147 static int CLIB_DECL compare_file(const void *file0ptr, const void *file1ptr);
148 static summary_file *sort_file_list(void);
149 
150 /* HTML helpers */
151 static util::core_file::ptr create_file_and_output_header(std::string &filename, std::string &templatefile, std::string &title);
152 static void output_footer_and_close_file(util::core_file::ptr &&file, std::string &templatefile, std::string &title);
153 
154 /* report generators */
155 static void output_report(std::string &dirname, std::string &tempheader, std::string &tempfooter, summary_file *filelist);
156 static int compare_screenshots(summary_file *curfile);
157 static int generate_png_diff(const summary_file *curfile, std::string &destdir, const char *destname);
158 static void create_linked_file(std::string &dirname, const summary_file *curfile, const summary_file *prevfile, const summary_file *nextfile, const char *pngfile, std::string &tempheader, std::string &tempfooter);
159 static void append_driver_list_table(const char *header, std::string &dirname, util::core_file &indexfile, const summary_file *listhead, std::string &tempheader, std::string &tempfooter);
160 
161 
162 
163 /***************************************************************************
164     INLINE FUNCTIONS
165 ***************************************************************************/
166 
167 /*-------------------------------------------------
168     trim_string - trim leading/trailing spaces
169     from a string
170 -------------------------------------------------*/
171 
trim_string(char * string)172 static inline char *trim_string(char *string)
173 {
174 	int length;
175 
176 	/* trim leading spaces */
177 	while (*string != 0 && isspace((uint8_t)*string))
178 		string++;
179 
180 	/* trim trailing spaces */
181 	length = strlen(string);
182 	while (length > 0 && isspace((uint8_t)string[length - 1]))
183 		string[--length] = 0;
184 
185 	return string;
186 }
187 
188 
189 /*-------------------------------------------------
190     get_unique_index - get the unique bitmap
191     index for a given entry
192 -------------------------------------------------*/
193 
get_unique_index(const summary_file * curfile,int index)194 static inline int get_unique_index(const summary_file *curfile, int index)
195 {
196 	int listnum, curindex = 0;
197 
198 	/* if we're invalid, just return that */
199 	if (curfile->matchbitmap[index] == 0xff)
200 		return -1;
201 
202 	/* count unique elements up to us */
203 	for (listnum = 0; listnum < curfile->matchbitmap[index]; listnum++)
204 		if (curfile->matchbitmap[listnum] == listnum)
205 			curindex++;
206 	return curindex;
207 }
208 
209 
210 
211 /***************************************************************************
212     MAIN
213 ***************************************************************************/
214 
215 /*-------------------------------------------------
216     main - main entry point
217 -------------------------------------------------*/
218 
main(int argc,char * argv[])219 int main(int argc, char *argv[])
220 {
221 	uint32_t bufsize;
222 	void *buffer;
223 	int listnum;
224 	int result;
225 
226 	/* first argument is the directory */
227 	if (argc < 4)
228 	{
229 		fprintf(stderr, "Usage:\nregrep <template> <outputdir> <summary1> [<summary2> [<summary3> ...]]\n");
230 		return 1;
231 	}
232 	std::string tempfilename(argv[1]);
233 	std::string dirname(argv[2]);
234 	list_count = argc - 3;
235 
236 	/* read the template file into an astring */
237 	std::string tempheader;
238 	if (util::core_file::load(tempfilename.c_str(), &buffer, bufsize) == osd_file::error::NONE)
239 	{
240 		tempheader.assign((const char *)buffer, bufsize);
241 		free(buffer);
242 	}
243 
244 	/* verify the template */
245 	if (tempheader.length() == 0)
246 	{
247 		fprintf(stderr, "Unable to read template file\n");
248 		return 1;
249 	}
250 	result = tempheader.find("<!--CONTENT-->");
251 	if (result == -1)
252 	{
253 		fprintf(stderr, "Template is missing a <!--CONTENT--> marker\n");
254 		return 1;
255 	}
256 	std::string tempfooter(tempheader);
257 	tempfooter = tempfooter.substr(result + 14);
258 	tempfooter = tempheader.substr(0, result);
259 
260 	/* loop over arguments and read the files */
261 	for (listnum = 0; listnum < list_count; listnum++)
262 	{
263 		result = read_summary_log(argv[listnum + 3], listnum);
264 		if (result != 0)
265 			return result;
266 	}
267 
268 	/* output the summary */
269 	output_report(dirname, tempheader, tempfooter, sort_file_list());
270 	return 0;
271 }
272 
273 
274 
275 /***************************************************************************
276     SUMMARY PARSING
277 ***************************************************************************/
278 
279 /*-------------------------------------------------
280     get_file - lookup a driver name in the hash
281     table and return a pointer to it; if none
282     found, allocate a new entry
283 -------------------------------------------------*/
284 
get_file(const char * filename)285 static summary_file *get_file(const char *filename)
286 {
287 	summary_file *file;
288 
289 	/* use the first two characters as a lookup */
290 	for (file = filehash[filename[0] & 0x7f][filename[1] & 0x7f]; file != nullptr; file = file->next)
291 		if (strcmp(filename, file->name) == 0)
292 			return file;
293 
294 	/* didn't find one -- allocate */
295 	file = (summary_file *)malloc(sizeof(*file));
296 	if (file == nullptr)
297 		return nullptr;
298 	memset(file, 0, sizeof(*file));
299 
300 	/* set the name so we find it in the future */
301 	strcpy(file->name, filename);
302 
303 	/* add to the head of the list */
304 	file->next = filehash[filename[0] & 0x7f][filename[1] & 0x7f];
305 	filehash[filename[0] & 0x7f][filename[1] & 0x7f] = file;
306 	return file;
307 }
308 
309 
310 /*-------------------------------------------------
311     read_summary_log - read a summary.log file
312     and build entries for its data
313 -------------------------------------------------*/
314 
read_summary_log(const char * filename,int index)315 static int read_summary_log(const char *filename, int index)
316 {
317 	summary_file *curfile = nullptr;
318 	char linebuffer[1024];
319 	char *linestart;
320 	int drivers = 0;
321 	FILE *file;
322 
323 	/* open the logfile */
324 	file = fopen(filename, "r");
325 	if (file == nullptr)
326 	{
327 		fprintf(stderr, "Error: file '%s' not found\n", filename);
328 		return 1;
329 	}
330 
331 	/* parse it */
332 	while (fgets(linebuffer, sizeof(linebuffer), file) != nullptr)
333 	{
334 		/* trim the leading/trailing spaces */
335 		linestart = trim_string(linebuffer);
336 
337 		/* is this one of our specials? */
338 		if (strncmp(linestart, "@@@@@", 5) == 0)
339 		{
340 			/* advance past the signature */
341 			linestart += 5;
342 
343 			/* look for the driver= tag */
344 			if (strncmp(linestart, "driver=", 7) == 0)
345 			{
346 				curfile = parse_driver_tag(linestart + 7, index);
347 				if (curfile == nullptr)
348 					goto error;
349 				drivers++;
350 			}
351 
352 			/* look for the source= tag */
353 			else if (strncmp(linestart, "source=", 7) == 0)
354 			{
355 				/* error if no driver yet */
356 				if (curfile == nullptr)
357 				{
358 					fprintf(stderr, "Unexpected @@@@@source= tag\n");
359 					goto error;
360 				}
361 
362 				/* copy the string */
363 				strcpy(curfile->source, trim_string(linestart + 7));
364 			}
365 
366 			/* look for the dir= tag */
367 			else if (strncmp(linestart, "dir=", 4) == 0)
368 			{
369 				char *dirname = trim_string(linestart + 4);
370 
371 				/* allocate a copy of the string */
372 				lists[index].dir = (char *)malloc(strlen(dirname) + 1);
373 				if (lists[index].dir == nullptr)
374 					goto error;
375 				strcpy(lists[index].dir, dirname);
376 				fprintf(stderr, "Directory %s\n", lists[index].dir);
377 			}
378 		}
379 
380 		/* if not, consider other options */
381 		else if (curfile != nullptr)
382 		{
383 			int foundchars = 0;
384 			char *curptr;
385 
386 			/* look for the pngcrc= tag */
387 			if (strncmp(linestart, "pngcrc: ", 7) == 0)
388 			{
389 			}
390 
391 			/* otherwise, accumulate the text */
392 			else
393 			{
394 				/* find the end of the line and normalize it with a CR */
395 				for (curptr = linestart; *curptr != 0 && *curptr != '\n' && *curptr != '\r'; curptr++)
396 					if (!isspace((uint8_t)*curptr))
397 						foundchars = 1;
398 				*curptr++ = '\n';
399 				*curptr = 0;
400 
401 				/* ignore blank lines */
402 				if (!foundchars)
403 					continue;
404 
405 				/* append our text */
406 				curfile->text[index].append(linestart);
407 			}
408 		}
409 
410 		/* look for the MAME header */
411 		else if (strncmp(linestart, "MAME v", 6) == 0)
412 		{
413 			char *start = linestart + 6;
414 			char *end;
415 
416 			/* find the end */
417 			for (end = start; !isspace((uint8_t)*end); end++) ;
418 			*end = 0;
419 			strcpy(lists[index].version, start);
420 			fprintf(stderr, "Parsing results from version %s\n", lists[index].version);
421 		}
422 	}
423 
424 	fclose(file);
425 	fprintf(stderr, "Parsed %d drivers\n", drivers);
426 	return 0;
427 
428 error:
429 	fclose(file);
430 	return 1;
431 }
432 
433 
434 /*-------------------------------------------------
435     parse_driver_tag - parse the status info
436     from a driver tag
437 -------------------------------------------------*/
438 
parse_driver_tag(char * linestart,int index)439 static summary_file *parse_driver_tag(char *linestart, int index)
440 {
441 	summary_file *curfile;
442 	char *colon;
443 
444 	/* find the colon separating name from status */
445 	colon = strchr(linestart, ':');
446 	if (colon == nullptr)
447 	{
448 		fprintf(stderr, "Unexpected text after @@@@@driver=\n");
449 		return nullptr;
450 	}
451 
452 	/* NULL terminate at the colon and look up the file */
453 	*colon = 0;
454 	curfile = get_file(trim_string(linestart));
455 	if (curfile == nullptr)
456 	{
457 		fprintf(stderr, "Unable to allocate memory for driver\n");
458 		return nullptr;
459 	}
460 
461 	/* clear out any old status for this file */
462 	curfile->status[index] = STATUS_NOT_PRESENT;
463 	curfile->text[index].clear();
464 
465 	/* strip leading/trailing spaces from the status */
466 	colon = trim_string(colon + 1);
467 
468 	/* convert status into statistics */
469 	if (strcmp(colon, "Success") == 0)
470 		curfile->status[index] = STATUS_SUCCESS;
471 	else if (strcmp(colon, "Missing files") == 0)
472 		curfile->status[index] = STATUS_MISSING_FILES;
473 	else if (strcmp(colon, "Exception") == 0)
474 		curfile->status[index] = STATUS_EXCEPTION;
475 	else if (strcmp(colon, "Fatal error") == 0)
476 		curfile->status[index] = STATUS_FATAL_ERROR;
477 	else if (strcmp(colon, "Failed validity check") == 0)
478 		curfile->status[index] = STATUS_FAILED_VALIDITY;
479 	else
480 		curfile->status[index] = STATUS_OTHER;
481 
482 	return curfile;
483 }
484 
485 
486 /*-------------------------------------------------
487     compare_file - compare two files, sorting
488     first by source filename, then by driver name
489 -------------------------------------------------*/
490 
compare_file(const void * file0ptr,const void * file1ptr)491 static int CLIB_DECL compare_file(const void *file0ptr, const void *file1ptr)
492 {
493 	summary_file *file0 = *(summary_file **)file0ptr;
494 	summary_file *file1 = *(summary_file **)file1ptr;
495 	int result = strcmp(file0->source, file1->source);
496 	if (result == 0)
497 		result = strcmp(file0->name, file1->name);
498 	return result;
499 }
500 
501 
502 /*-------------------------------------------------
503     sort_file_list - convert the hashed lists
504     into a single, sorted list
505 -------------------------------------------------*/
506 
sort_file_list(void)507 static summary_file *sort_file_list(void)
508 {
509 	summary_file *listhead, **tailptr, *curfile, **filearray;
510 	int numfiles, filenum;
511 	int c0, c1;
512 
513 	/* count the total number of files */
514 	numfiles = 0;
515 	for (c0 = 0; c0 < 128; c0++)
516 		for (c1 = 0; c1 < 128; c1++)
517 			for (curfile = filehash[c0][c1]; curfile != nullptr; curfile = curfile->next)
518 				numfiles++;
519 
520 	/* allocate an array of files */
521 	filearray = (summary_file **)malloc(numfiles * sizeof(*filearray));
522 	if (filearray == nullptr)
523 	{
524 		fprintf(stderr, "Out of memory!\n");
525 		return nullptr;
526 	}
527 
528 	/* populate the array */
529 	numfiles = 0;
530 	for (c0 = 0; c0 < 128; c0++)
531 		for (c1 = 0; c1 < 128; c1++)
532 			for (curfile = filehash[c0][c1]; curfile != nullptr; curfile = curfile->next)
533 				filearray[numfiles++] = curfile;
534 
535 	/* sort the array */
536 	qsort(filearray, numfiles, sizeof(filearray[0]), compare_file);
537 
538 	/* now regenerate a single list */
539 	listhead = nullptr;
540 	tailptr = &listhead;
541 	for (filenum = 0; filenum < numfiles; filenum++)
542 	{
543 		*tailptr = filearray[filenum];
544 		tailptr = &(*tailptr)->next;
545 	}
546 	*tailptr = nullptr;
547 	free(filearray);
548 
549 	return listhead;
550 }
551 
552 
553 
554 /***************************************************************************
555     HTML OUTPUT HELPERS
556 ***************************************************************************/
557 
558 /*-------------------------------------------------
559     create_file_and_output_header - create a new
560     HTML file with a standard header
561 -------------------------------------------------*/
562 
create_file_and_output_header(std::string & filename,std::string & templatefile,std::string & title)563 static util::core_file::ptr create_file_and_output_header(std::string &filename, std::string &templatefile, std::string &title)
564 {
565 	util::core_file::ptr file;
566 
567 	/* create the indexfile */
568 	if (util::core_file::open(filename, OPEN_FLAG_WRITE | OPEN_FLAG_CREATE | OPEN_FLAG_CREATE_PATHS | OPEN_FLAG_NO_BOM, file) != osd_file::error::NONE)
569 		return util::core_file::ptr();
570 
571 	/* print a header */
572 	std::string modified(templatefile);
573 	strreplace(modified, "<!--TITLE-->", title.c_str());
574 	file->write(modified.c_str(), modified.length());
575 
576 	/* return the file */
577 	return file;
578 }
579 
580 
581 /*-------------------------------------------------
582     output_footer_and_close_file - write a
583     standard footer to an HTML file and close it
584 -------------------------------------------------*/
585 
output_footer_and_close_file(util::core_file::ptr && file,std::string & templatefile,std::string & title)586 static void output_footer_and_close_file(util::core_file::ptr &&file, std::string &templatefile, std::string &title)
587 {
588 	std::string modified(templatefile);
589 	strreplace(modified, "<!--TITLE-->", title.c_str());
590 	file->write(modified.c_str(), modified.length());
591 	file.reset();
592 }
593 
594 
595 
596 /***************************************************************************
597     REPORT GENERATORS
598 ***************************************************************************/
599 
600 /*-------------------------------------------------
601     output_report - generate the summary
602     report HTML files
603 -------------------------------------------------*/
604 
output_report(std::string & dirname,std::string & tempheader,std::string & tempfooter,summary_file * filelist)605 static void output_report(std::string &dirname, std::string &tempheader, std::string &tempfooter, summary_file *filelist)
606 {
607 	summary_file *buckethead[BUCKET_COUNT], **buckettailptr[BUCKET_COUNT];
608 	summary_file *curfile;
609 	std::string title("MAME Regressions");
610 	int listnum, bucknum;
611 	util::core_file::ptr indexfile;
612 	int count = 0, total;
613 
614 	/* initialize the lists */
615 	for (bucknum = 0; bucknum < BUCKET_COUNT; bucknum++)
616 	{
617 		buckethead[bucknum] = nullptr;
618 		buckettailptr[bucknum] = &buckethead[bucknum];
619 	}
620 
621 	/* compute the total number of files */
622 	total = 0;
623 	for (curfile = filelist; curfile != nullptr; curfile = curfile->next)
624 		total++;
625 
626 	/* first bucketize the games */
627 	for (curfile = filelist; curfile != nullptr; curfile = curfile->next)
628 	{
629 		int statcount[STATUS_COUNT] = { 0 };
630 		int bucket = BUCKET_UNKNOWN;
631 		int unique_codes = 0;
632 		int first_valid;
633 
634 		/* print status */
635 		if (++count % 100 == 0)
636 			fprintf(stderr, "Processing file %d/%d\n", count, total);
637 
638 		/* find the first valid entry */
639 		for (first_valid = 0; curfile->status[first_valid] == STATUS_NOT_PRESENT; first_valid++) ;
640 
641 		/* do we need to output anything? */
642 		for (listnum = first_valid; listnum < list_count; listnum++)
643 			if (statcount[curfile->status[listnum]]++ == 0)
644 				unique_codes++;
645 
646 		/* were we consistent? */
647 		if (unique_codes == 1)
648 		{
649 			/* were we consistently ok? */
650 			if (curfile->status[first_valid] == STATUS_SUCCESS)
651 				bucket = compare_screenshots(curfile);
652 
653 			/* must have been consistently erroring */
654 			else
655 				bucket = BUCKET_CONSISTENT_ERROR;
656 		}
657 
658 		/* ok, we're not consistent; could be a number of things */
659 		else
660 		{
661 			/* were we ok at the start and end but not in the middle? */
662 			if (curfile->status[first_valid] == STATUS_SUCCESS && curfile->status[list_count - 1] == STATUS_SUCCESS)
663 				bucket = BUCKET_GOOD_BUT_CHANGED;
664 
665 			/* did we go from good to bad? */
666 			else if (curfile->status[first_valid] == STATUS_SUCCESS)
667 				bucket = BUCKET_REGRESSED;
668 
669 			/* did we go from bad to good? */
670 			else if (curfile->status[list_count - 1] == STATUS_SUCCESS)
671 				bucket = BUCKET_IMPROVED;
672 
673 			/* must have had multiple errors */
674 			else
675 				bucket = BUCKET_MULTI_ERROR;
676 		}
677 
678 		/* add us to the appropriate list */
679 		*buckettailptr[bucket] = curfile;
680 		buckettailptr[bucket] = &curfile->next;
681 	}
682 
683 	/* terminate all the lists */
684 	for (bucknum = 0; bucknum < BUCKET_COUNT; bucknum++)
685 		*buckettailptr[bucknum] = nullptr;
686 
687 	/* output header */
688 	std::string tempname = string_format("%s" PATH_SEPARATOR "%s", dirname.c_str(), "index.html");
689 	indexfile = create_file_and_output_header(tempname, tempheader, title);
690 	if (!indexfile)
691 	{
692 		fprintf(stderr, "Error creating file '%s'\n", tempname.c_str());
693 		return;
694 	}
695 
696 	/* iterate over buckets and output them */
697 	for (bucknum = 0; bucknum < ARRAY_LENGTH(bucket_output_order); bucknum++)
698 	{
699 		int curbucket = bucket_output_order[bucknum];
700 
701 		if (buckethead[curbucket] != nullptr)
702 		{
703 			fprintf(stderr, "Outputting bucket: %s\n", bucket_name[curbucket]);
704 			append_driver_list_table(bucket_name[curbucket], dirname, *indexfile, buckethead[curbucket], tempheader, tempfooter);
705 		}
706 	}
707 
708 	/* output footer */
709 	output_footer_and_close_file(std::move(indexfile), tempfooter, title);
710 }
711 
712 
713 /*-------------------------------------------------
714     compare_screenshots - compare the screenshots
715     for all the games in a file
716 -------------------------------------------------*/
717 
compare_screenshots(summary_file * curfile)718 static int compare_screenshots(summary_file *curfile)
719 {
720 	bitmap_argb32 bitmaps[MAX_COMPARES];
721 	int unique[MAX_COMPARES];
722 	int numunique = 0;
723 
724 	/* iterate over all files and load their bitmaps */
725 	for (int listnum = 0; listnum < list_count; listnum++)
726 		if (curfile->status[listnum] == STATUS_SUCCESS)
727 		{
728 			std::string fullname;
729 			osd_file::error filerr;
730 			util::core_file::ptr file;
731 
732 			/* get the filename for the image */
733 			fullname = string_format("%s" PATH_SEPARATOR "snap" PATH_SEPARATOR "%s" PATH_SEPARATOR "final.png", lists[listnum].dir, curfile->name);
734 
735 			/* open the file */
736 			filerr = util::core_file::open(fullname, OPEN_FLAG_READ, file);
737 
738 			/* if that failed, look in the old location */
739 			if (filerr != osd_file::error::NONE)
740 			{
741 				/* get the filename for the image */
742 				fullname = string_format("%s" PATH_SEPARATOR "snap" PATH_SEPARATOR "_%s.png", lists[listnum].dir, curfile->name);
743 
744 				/* open the file */
745 				filerr = util::core_file::open(fullname, OPEN_FLAG_READ, file);
746 			}
747 
748 			/* if that worked, load the file */
749 			if (filerr == osd_file::error::NONE)
750 			{
751 				util::png_read_bitmap(*file, bitmaps[listnum]);
752 				file.reset();
753 			}
754 		}
755 
756 	/* now find all the different bitmap types */
757 	int listnum;
758 	for (listnum = 0; listnum < list_count; listnum++)
759 	{
760 		curfile->matchbitmap[listnum] = 0xff;
761 		if (bitmaps[listnum].valid())
762 		{
763 			bitmap_argb32 &this_bitmap = bitmaps[listnum];
764 
765 			/* compare against all unique bitmaps */
766 			int compnum;
767 			for (compnum = 0; compnum < numunique; compnum++)
768 			{
769 				/* if the sizes are different, we differ; otherwise start off assuming we are the same */
770 				bitmap_argb32 &base_bitmap = bitmaps[unique[compnum]];
771 				bool bitmaps_differ = (this_bitmap.width() != base_bitmap.width() || this_bitmap.height() != base_bitmap.height());
772 
773 				/* compare scanline by scanline */
774 				for (int y = 0; y < this_bitmap.height() && !bitmaps_differ; y++)
775 				{
776 					uint32_t const *base = &base_bitmap.pix(y);
777 					uint32_t const *curr = &this_bitmap.pix(y);
778 
779 					/* scan the scanline */
780 					int x;
781 					for (x = 0; x < this_bitmap.width(); x++)
782 						if (*base++ != *curr++)
783 							break;
784 					bitmaps_differ = (x != this_bitmap.width());
785 				}
786 
787 				/* if we matched, remember which listnum index we matched, and stop */
788 				if (!bitmaps_differ)
789 				{
790 					curfile->matchbitmap[listnum] = unique[compnum];
791 					break;
792 				}
793 
794 				/* if different from the first unique entry, adjust the status */
795 				if (bitmaps_differ && compnum == 0)
796 					curfile->status[listnum] = STATUS_SUCCESS_DIFFERENT;
797 			}
798 
799 			/* if we're unique, add ourselves to the list */
800 			if (compnum >= numunique)
801 			{
802 				unique[numunique++] = listnum;
803 				curfile->matchbitmap[listnum] = listnum;
804 				continue;
805 			}
806 		}
807 	}
808 
809 	/* if all screenshots matched, we're good */
810 	if (numunique == 1)
811 		return BUCKET_GOOD;
812 
813 	/* if the last screenshot matched the first unique one, we're good but changed */
814 	if (curfile->matchbitmap[listnum - 1] == unique[0])
815 		return BUCKET_GOOD_BUT_CHANGED_SCREENSHOTS;
816 
817 	/* otherwise we're just changed */
818 	return BUCKET_CHANGED;
819 }
820 
821 
822 /*-------------------------------------------------
823     generate_png_diff - create a new PNG file
824     that shows multiple differing PNGs side by
825     side with a third set of differences
826 -------------------------------------------------*/
827 
generate_png_diff(const summary_file * curfile,std::string & destdir,const char * destname)828 static int generate_png_diff(const summary_file *curfile, std::string &destdir, const char *destname)
829 {
830 	bitmap_argb32 bitmaps[MAX_COMPARES];
831 	std::string srcimgname;
832 	std::string dstfilename;
833 	bitmap_argb32 finalbitmap;
834 	int width, height, maxwidth;
835 	int bitmapcount = 0;
836 	util::core_file::ptr file;
837 	osd_file::error filerr;
838 	util::png_error pngerr;
839 	int error = -1;
840 	int starty;
841 
842 	/* generate the common source filename */
843 	dstfilename = string_format("%s" PATH_SEPARATOR "%s", destdir.c_str(), destname);
844 	srcimgname = string_format("snap" PATH_SEPARATOR "%s" PATH_SEPARATOR "final.png", curfile->name);
845 
846 	/* open and load all unique bitmaps */
847 	for (int listnum = 0; listnum < list_count; listnum++)
848 		if (curfile->matchbitmap[listnum] == listnum)
849 		{
850 			std::string tempname = string_format("%s" PATH_SEPARATOR "%s", lists[listnum].dir, srcimgname.c_str());
851 
852 			/* open the source image */
853 			filerr = util::core_file::open(tempname, OPEN_FLAG_READ, file);
854 			if (filerr != osd_file::error::NONE)
855 				goto error;
856 
857 			/* load the source image */
858 			pngerr = util::png_read_bitmap(*file, bitmaps[bitmapcount++]);
859 			file.reset();
860 			if (pngerr != util::png_error::NONE)
861 				goto error;
862 		}
863 
864 	/* if there's only one unique bitmap, skip it */
865 	if (bitmapcount <= 1)
866 		goto error;
867 
868 	/* determine the size of the final bitmap */
869 	height = width = 0;
870 	maxwidth = bitmaps[0].width();
871 	for (int bmnum = 1; bmnum < bitmapcount; bmnum++)
872 	{
873 		int curwidth;
874 
875 		/* determine the maximal width */
876 		maxwidth = std::max(maxwidth, bitmaps[bmnum].width());
877 		curwidth = bitmaps[0].width() + BITMAP_SPACE + maxwidth + BITMAP_SPACE + maxwidth;
878 		width = std::max(width, curwidth);
879 
880 		/* add to the height */
881 		height += std::max(bitmaps[0].height(), bitmaps[bmnum].height());
882 		if (bmnum != 1)
883 			height += BITMAP_SPACE;
884 	}
885 
886 	/* allocate the final bitmap */
887 	finalbitmap.allocate(width, height);
888 
889 	/* now copy and compare each set of bitmaps */
890 	starty = 0;
891 	for (int bmnum = 1; bmnum < bitmapcount; bmnum++)
892 	{
893 		bitmap_argb32 const &bitmap1 = bitmaps[0];
894 		bitmap_argb32 const &bitmap2 = bitmaps[bmnum];
895 		int curheight = std::max(bitmap1.height(), bitmap2.height());
896 
897 		/* iterate over rows in these bitmaps */
898 		for (int y = 0; y < curheight; y++)
899 		{
900 			uint32_t const *src1 = (y < bitmap1.height()) ? &bitmap1.pix(y) : nullptr;
901 			uint32_t const *src2 = (y < bitmap2.height()) ? &bitmap2.pix(y) : nullptr;
902 			uint32_t *dst1 = &finalbitmap.pix(starty + y, 0);
903 			uint32_t *dst2 = &finalbitmap.pix(starty + y, bitmap1.width() + BITMAP_SPACE);
904 			uint32_t *dstdiff = &finalbitmap.pix(starty + y, bitmap1.width() + BITMAP_SPACE + maxwidth + BITMAP_SPACE);
905 
906 			/* now iterate over columns */
907 			for (int x = 0; x < maxwidth; x++)
908 			{
909 				int pix1 = -1, pix2 = -2;
910 
911 				if (src1 != nullptr && x < bitmap1.width())
912 					pix1 = dst1[x] = src1[x];
913 				if (src2 != nullptr && x < bitmap2.width())
914 					pix2 = dst2[x] = src2[x];
915 				dstdiff[x] = (pix1 != pix2) ? 0xffffffff : 0xff000000;
916 			}
917 		}
918 
919 		/* update the starting Y position */
920 		starty += BITMAP_SPACE + std::max(bitmap1.height(), bitmap2.height());
921 	}
922 
923 	/* write the final PNG */
924 	filerr = util::core_file::open(dstfilename, OPEN_FLAG_WRITE | OPEN_FLAG_CREATE, file);
925 	if (filerr != osd_file::error::NONE)
926 		goto error;
927 	pngerr = util::png_write_bitmap(*file, nullptr, finalbitmap, 0, nullptr);
928 	file.reset();
929 	if (pngerr != util::png_error::NONE)
930 		goto error;
931 
932 	/* if we get here, we are error free */
933 	error = 0;
934 
935 error:
936 	if (error)
937 		osd_file::remove(dstfilename);
938 	return error;
939 }
940 
941 
942 /*-------------------------------------------------
943     create_linked_file - create a comparison
944     file between differing versions
945 -------------------------------------------------*/
946 
create_linked_file(std::string & dirname,const summary_file * curfile,const summary_file * prevfile,const summary_file * nextfile,const char * pngfile,std::string & tempheader,std::string & tempfooter)947 static void create_linked_file(std::string &dirname, const summary_file *curfile, const summary_file *prevfile, const summary_file *nextfile, const char *pngfile, std::string &tempheader, std::string &tempfooter)
948 {
949 	std::string linkname;
950 	std::string filename;
951 	std::string title;
952 	util::core_file::ptr linkfile;
953 	int listnum;
954 
955 	/* create the filename */
956 	filename = string_format("%s.html", curfile->name);
957 
958 	/* output header */
959 	title = string_format("%s Regressions (%s)", curfile->name, curfile->source);
960 	linkname = string_format("%s" PATH_SEPARATOR "%s", dirname.c_str(), filename.c_str());
961 	linkfile = create_file_and_output_header(linkname, tempheader, title);
962 	if (linkfile == nullptr)
963 	{
964 		fprintf(stderr, "Error creating file '%s'\n", filename.c_str());
965 		return;
966 	}
967 
968 	/* link to the previous/next entries */
969 	linkfile->printf("\t<p>\n");
970 	linkfile->printf("\t<table width=\"100%%\">\n");
971 	linkfile->printf("\t\t<td align=\"left\" width=\"40%%\" style=\"border:none\">");
972 	if (prevfile != nullptr)
973 		linkfile->printf("<a href=\"%s.html\"><< %s (%s)</a>", prevfile->name, prevfile->name, prevfile->source);
974 	linkfile->printf("</td>\n");
975 	linkfile->printf("\t\t<td align=\"center\" width=\"20%%\" style=\"border:none\"><a href=\"index.html\">Home</a></td>\n");
976 	linkfile->printf("\t\t<td align=\"right\" width=\"40%%\" style=\"border:none\">");
977 	if (nextfile != nullptr)
978 		linkfile->printf("<a href=\"%s.html\">%s (%s) >></a>", nextfile->name, nextfile->name, nextfile->source);
979 	linkfile->printf("</td>\n");
980 	linkfile->printf("\t</table>\n");
981 	linkfile->printf("\t</p>\n");
982 
983 	/* output data for each one */
984 	for (listnum = 0; listnum < list_count; listnum++)
985 	{
986 		int imageindex = -1;
987 
988 		/* generate the HTML */
989 		linkfile->printf("\n\t<h2>%s</h2>\n", lists[listnum].version);
990 		linkfile->printf("\t<p>\n");
991 		linkfile->printf("\t<b>Status:</b> %s\n", status_text[curfile->status[listnum]]);
992 		if (pngfile != nullptr)
993 			imageindex = get_unique_index(curfile, listnum);
994 		if (imageindex != -1)
995 			linkfile->printf(" [%d]", imageindex);
996 		linkfile->printf("\t</p>\n");
997 		if (curfile->text[listnum].length() != 0)
998 		{
999 			linkfile->printf("\t<p>\n");
1000 			linkfile->printf("\t<b>Errors:</b>\n");
1001 			linkfile->printf("\t<pre>%s</pre>\n", curfile->text[listnum].c_str());
1002 			linkfile->printf("\t</p>\n");
1003 		}
1004 	}
1005 
1006 	/* output link to the image */
1007 	if (pngfile != nullptr)
1008 	{
1009 		linkfile->printf("\n\t<h2>Screenshot Comparisons</h2>\n");
1010 		linkfile->printf("\t<p>\n");
1011 		linkfile->printf("\t<img src=\"%s\" />\n", pngfile);
1012 		linkfile->printf("\t</p>\n");
1013 	}
1014 
1015 	/* output footer */
1016 	output_footer_and_close_file(std::move(linkfile), tempfooter, title);
1017 }
1018 
1019 
1020 /*-------------------------------------------------
1021     append_driver_list_table - append a table
1022     of drivers from a list to an HTML file
1023 -------------------------------------------------*/
1024 
append_driver_list_table(const char * header,std::string & dirname,util::core_file & indexfile,const summary_file * listhead,std::string & tempheader,std::string & tempfooter)1025 static void append_driver_list_table(const char *header, std::string &dirname, util::core_file &indexfile, const summary_file *listhead, std::string &tempheader, std::string &tempfooter)
1026 {
1027 	const summary_file *curfile, *prevfile;
1028 	int width = 100 / (2 + list_count);
1029 	int listnum;
1030 
1031 	/* output a header */
1032 	indexfile.printf("\t<h2>%s</h2>\n", header);
1033 
1034 	/* start the table */
1035 	indexfile.printf("\t<p><table width=\"90%%\">\n");
1036 	indexfile.printf("\t\t<tr>\n\t\t\t<th width=\"%d%%\">Source</th><th width=\"%d%%\">Driver</th>", width, width);
1037 	for (listnum = 0; listnum < list_count; listnum++)
1038 		indexfile.printf("<th width=\"%d%%\">%s</th>", width, lists[listnum].version);
1039 	indexfile.printf("\n\t\t</tr>\n");
1040 
1041 	/* if nothing, print a default message */
1042 	if (listhead == nullptr)
1043 	{
1044 		indexfile.printf("\t\t<tr>\n\t\t\t");
1045 		indexfile.printf("<td colspan=\"%d\" align=\"center\">(No regressions detected)</td>", list_count + 2);
1046 		indexfile.printf("\n\t\t</tr>\n");
1047 	}
1048 
1049 	/* iterate over files */
1050 	for (prevfile = nullptr, curfile = listhead; curfile != nullptr; prevfile = curfile, curfile = curfile->next)
1051 	{
1052 		int rowspan = 0, uniqueshots = 0;
1053 		char pngdiffname[40];
1054 
1055 		/* if this is the first entry in this source file, count how many rows we need to span */
1056 		if (prevfile == nullptr || strcmp(prevfile->source, curfile->source) != 0)
1057 		{
1058 			const summary_file *cur;
1059 			for (cur = curfile; cur != nullptr; cur = cur->next)
1060 				if (strcmp(cur->source, curfile->source) == 0)
1061 					rowspan++;
1062 				else
1063 					break;
1064 		}
1065 
1066 		/* create screenshots if necessary */
1067 		pngdiffname[0] = 0;
1068 		for (listnum = 0; listnum < list_count; listnum++)
1069 			if (curfile->matchbitmap[listnum] == listnum)
1070 				uniqueshots++;
1071 		if (uniqueshots > 1)
1072 		{
1073 			sprintf(pngdiffname, "compare_%s.png", curfile->name);
1074 			if (generate_png_diff(curfile, dirname, pngdiffname) != 0)
1075 				pngdiffname[0] = 0;
1076 		}
1077 
1078 		/* create a linked file */
1079 		create_linked_file(dirname, curfile, prevfile, curfile->next, (pngdiffname[0] == 0) ? nullptr : pngdiffname, tempheader, tempfooter);
1080 
1081 		/* create a row */
1082 		indexfile.printf("\t\t<tr>\n\t\t\t");
1083 		if (rowspan > 0)
1084 			indexfile.printf("<td rowspan=\"%d\">%s</td>", rowspan, curfile->source);
1085 		indexfile.printf("<td><a href=\"%s.html\">%s</a></td>", curfile->name, curfile->name);
1086 		for (listnum = 0; listnum < list_count; listnum++)
1087 		{
1088 			int unique_index = -1;
1089 
1090 			if (pngdiffname[0] != 0)
1091 				unique_index = get_unique_index(curfile, listnum);
1092 			if (unique_index != -1)
1093 				indexfile.printf("<td><span style=\"%s\">&nbsp;&nbsp;&nbsp;</span> %s [<a href=\"%s\" target=\"blank\">%d</a>]</td>", status_color[curfile->status[listnum]], status_text[curfile->status[listnum]], pngdiffname, unique_index);
1094 			else
1095 				indexfile.printf("<td><span style=\"%s\">&nbsp;&nbsp;&nbsp;</span> %s</td>", status_color[curfile->status[listnum]], status_text[curfile->status[listnum]]);
1096 		}
1097 		indexfile.printf("\n\t\t</tr>\n");
1098 
1099 		/* also print the name and source file */
1100 		printf("%s %s\n", curfile->name, curfile->source);
1101 	}
1102 
1103 	/* end of table */
1104 	indexfile.printf("</table></p>\n");
1105 }
1106