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\"> </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\"> </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