CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
hrydgard

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: hrydgard/ppsspp
Path: blob/master/headless/Compare.cpp
Views: 1401
1
// Copyright (c) 2012- PPSSPP Project.
2
3
// This program is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, version 2.0 or later versions.
6
7
// This program is distributed in the hope that it will be useful,
8
// but WITHOUT ANY WARRANTY; without even the implied warranty of
9
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
// GNU General Public License 2.0 for more details.
11
12
// A copy of the GPL 2.0 should have been included with the program.
13
// If not, see http://www.gnu.org/licenses/
14
15
// Official git repository and contact information can be found at
16
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
17
18
#include <algorithm>
19
#include <cmath>
20
#include <cstdarg>
21
#include <iostream>
22
#include <png.h>
23
#include <vector>
24
25
#include "headless/Compare.h"
26
#include "Common/Data/Convert/ColorConv.h"
27
#include "Common/Data/Format/PNGLoad.h"
28
#include "Common/File/FileUtil.h"
29
#include "Common/StringUtils.h"
30
#include "Core/Loaders.h"
31
32
#include "GPU/GPUState.h"
33
#include "GPU/Common/GPUDebugInterface.h"
34
#include "GPU/Common/TextureDecoder.h"
35
36
37
bool teamCityMode = false;
38
std::string currentTestName = "";
39
40
void TeamCityPrint(const char *fmt, ...)
41
{
42
if (!teamCityMode)
43
return;
44
45
const int TEMP_BUFFER_SIZE = 32768;
46
char temp[TEMP_BUFFER_SIZE];
47
48
va_list args;
49
va_start(args, fmt);
50
vsnprintf(temp, TEMP_BUFFER_SIZE - 1, fmt, args);
51
temp[TEMP_BUFFER_SIZE - 1] = '\0';
52
va_end(args);
53
54
printf("##teamcity[%s]\n", temp);
55
}
56
57
void GitHubActionsPrint(const char *type, const char *fmt, ...) {
58
if (!getenv("GITHUB_ACTIONS"))
59
return;
60
61
const int TEMP_BUFFER_SIZE = 32768;
62
char temp[TEMP_BUFFER_SIZE];
63
64
va_list args;
65
va_start(args, fmt);
66
vsnprintf(temp, TEMP_BUFFER_SIZE - 1, fmt, args);
67
temp[TEMP_BUFFER_SIZE - 1] = '\0';
68
va_end(args);
69
70
printf("::%s file=%s::%s\n", type, currentTestName.c_str(), temp);
71
}
72
73
struct BufferedLineReader {
74
const static int MAX_BUFFER = 5;
75
const static int TEMP_BUFFER_SIZE = 32768;
76
77
BufferedLineReader(const std::string &data) : data_(data) {
78
}
79
80
void Fill() {
81
while (valid_ < MAX_BUFFER && HasMoreLines()) {
82
buffer_[valid_++] = TrimNewlines(ReadLine());
83
}
84
}
85
86
const std::string Peek(int pos) {
87
if (pos >= valid_) {
88
Fill();
89
}
90
if (pos >= valid_) {
91
return "";
92
}
93
return buffer_[pos];
94
}
95
96
void Skip(int count) {
97
if (count > valid_) {
98
count = valid_;
99
}
100
valid_ -= count;
101
for (int i = 0; i < valid_; ++i) {
102
buffer_[i] = buffer_[i + count];
103
}
104
Fill();
105
}
106
107
const std::string Consume() {
108
const std::string result = Peek(0);
109
Skip(1);
110
return result;
111
}
112
113
bool HasLines() {
114
if (HasMoreLines()) {
115
return true;
116
}
117
// Don't say yes if it's a blank line.
118
for (int i = 0; i < valid_; ++i) {
119
if (!buffer_[i].empty()) {
120
return true;
121
}
122
}
123
return false;
124
}
125
126
bool Compare(BufferedLineReader &other) {
127
if (Peek(0) != other.Peek(0)) {
128
return false;
129
}
130
131
Skip(1);
132
other.Skip(1);
133
return true;
134
}
135
136
protected:
137
BufferedLineReader() {
138
}
139
140
bool HasMoreLines() {
141
return pos_ != data_.npos;
142
}
143
144
std::string ReadLine() {
145
size_t next = data_.find('\n', pos_);
146
if (next == data_.npos) {
147
std::string result = data_.substr(pos_);
148
pos_ = next;
149
return result;
150
} else {
151
std::string result = data_.substr(pos_, next - pos_);
152
pos_ = next + 1;
153
return result;
154
}
155
}
156
157
static std::string TrimNewlines(const std::string &s) {
158
size_t p = s.find_last_not_of("\r\n");
159
if (p == s.npos) {
160
return "";
161
}
162
return s.substr(0, p + 1);
163
}
164
165
int valid_ = 0;
166
std::string buffer_[MAX_BUFFER];
167
const std::string data_;
168
size_t pos_ = 0;
169
};
170
171
Path ExpectedScreenshotFromFilename(const Path &bootFilename) {
172
std::string extension = bootFilename.GetFileExtension();
173
if (extension.empty()) {
174
return bootFilename.WithExtraExtension(".bmp");
175
}
176
177
// Let's use pngs as the default for ppdmp tests.
178
if (extension == ".ppdmp") {
179
return bootFilename.WithReplacedExtension(".png");
180
}
181
return bootFilename.WithReplacedExtension(".expected.bmp");
182
}
183
184
static std::string ChopFront(std::string s, std::string front)
185
{
186
if (s.size() >= front.size())
187
{
188
if (s.substr(0, front.size()) == front)
189
return s.substr(front.size());
190
}
191
return s;
192
}
193
194
static std::string ChopEnd(std::string s, std::string end)
195
{
196
if (s.size() >= end.size())
197
{
198
size_t endpos = s.size() - end.size();
199
if (s.substr(endpos) == end)
200
return s.substr(0, endpos);
201
}
202
return s;
203
}
204
205
std::string GetTestName(const Path &bootFilename)
206
{
207
// Kinda ugly, trying to guesstimate the test name from filename...
208
return ChopEnd(ChopFront(ChopFront(bootFilename.ToString(), "tests/"), "pspautotests/tests/"), ".prx");
209
}
210
211
bool CompareOutput(const Path &bootFilename, const std::string &output, bool verbose) {
212
Path expect_filename = bootFilename.GetFileExtension() == ".prx" ? bootFilename.WithReplacedExtension(".prx", ".expected") : bootFilename.WithExtraExtension(".expected");
213
std::unique_ptr<FileLoader> expect_loader(ConstructFileLoader(expect_filename));
214
215
if (expect_loader->Exists()) {
216
std::string expect_results;
217
expect_results.resize(expect_loader->FileSize());
218
expect_results.resize(expect_loader->ReadAt(0, expect_loader->FileSize(), &expect_results[0]));
219
220
BufferedLineReader expected(expect_results);
221
BufferedLineReader actual(output);
222
223
bool failed = false;
224
while (expected.HasLines())
225
{
226
if (expected.Compare(actual))
227
continue;
228
229
if (!failed)
230
{
231
TeamCityPrint("testFailed name='%s' message='Output different from expected file'", currentTestName.c_str());
232
GitHubActionsPrint("error", "Incorrect output for %s", currentTestName.c_str());
233
failed = true;
234
}
235
236
// This is a really dirt simple comparing algorithm.
237
238
// Perhaps it was an extra line?
239
if (expected.Peek(0) == actual.Peek(1) || !expected.HasLines())
240
printf("+ %s\n", actual.Consume().c_str());
241
// A single missing line?
242
else if (expected.Peek(1) == actual.Peek(0) || !actual.HasLines())
243
printf("- %s\n", expected.Consume().c_str());
244
else
245
{
246
printf("O %s\n", actual.Consume().c_str());
247
printf("E %s\n", expected.Consume().c_str());
248
}
249
}
250
251
while (actual.HasLines())
252
{
253
// If it's a blank line, this will pass.
254
if (actual.Compare(expected))
255
continue;
256
257
printf("+ %s\n", actual.Consume().c_str());
258
}
259
260
if (verbose)
261
{
262
if (!failed)
263
{
264
printf("++++++++++++++ The Equal Output +++++++++++++\n");
265
printf("%s", output.c_str());
266
printf("+++++++++++++++++++++++++++++++++++++++++++++\n");
267
}
268
else
269
{
270
printf("============== output from failed %s:\n", GetTestName(bootFilename).c_str());
271
printf("%s", output.c_str());
272
printf("============== expected output:\n");
273
std::string fullExpected;
274
if (File::ReadTextFileToString(expect_filename, &fullExpected))
275
printf("%s", fullExpected.c_str());
276
printf("===============================\n");
277
}
278
}
279
280
return !failed;
281
} else {
282
std::unique_ptr<FileLoader> screenshot(ConstructFileLoader(ExpectedScreenshotFromFilename(bootFilename)));
283
bool failed = true;
284
if (screenshot->Exists()) {
285
// Okay, just a screenshot then. Allow a pass with no output (i.e. screenshot match.)
286
failed = output.find_first_not_of(" \r\n\t") != output.npos;
287
if (failed) {
288
TeamCityPrint("testFailed name='%s' message='Output different from expected file'", currentTestName.c_str());
289
GitHubActionsPrint("error", "Incorrect output for %s", currentTestName.c_str());
290
}
291
} else {
292
fprintf(stderr, "Expectation file %s not found\n", expect_filename.c_str());
293
TeamCityPrint("testIgnored name='%s' message='Expects file missing'", currentTestName.c_str());
294
GitHubActionsPrint("error", "Expected file missing for %s", currentTestName.c_str());
295
}
296
297
if (verbose || (screenshot->Exists() && failed)) {
298
BufferedLineReader actual(output);
299
while (actual.HasLines()) {
300
printf("+ %s\n", actual.Consume().c_str());
301
}
302
}
303
return !failed;
304
}
305
}
306
307
static inline double CompareChannel(int pix1, int pix2) {
308
double diff = pix1 - pix2;
309
return diff * diff;
310
}
311
312
static inline double ComparePixel(u32 pix1, u32 pix2) {
313
// Ignore alpha.
314
double r = CompareChannel(pix1 & 0xFF, pix2 & 0xFF);
315
double g = CompareChannel((pix1 >> 8) & 0xFF, (pix2 >> 8) & 0xFF);
316
double b = CompareChannel((pix1 >> 16) & 0xFF, (pix2 >> 16) & 0xFF);
317
318
return r + g + b;
319
}
320
321
std::vector<u32> TranslateDebugBufferToCompare(const GPUDebugBuffer *buffer, u32 stride, u32 h) {
322
// If the output was small, act like everything outside was 0.
323
// This can happen depending on viewport parameters.
324
u32 safeW = std::min(stride, buffer->GetStride());
325
u32 safeH = std::min(h, buffer->GetHeight());
326
327
std::vector<u32> data;
328
data.resize(stride * h, 0);
329
330
const u32 *pixels32 = (const u32 *)buffer->GetData();
331
const u16 *pixels16 = (const u16 *)buffer->GetData();
332
int outStride = buffer->GetStride();
333
if (!buffer->GetFlipped()) {
334
// Bitmaps are flipped, so we have to compare backwards in this case.
335
int toLastRow = outStride * (h > buffer->GetHeight() ? buffer->GetHeight() - 1 : h - 1);
336
pixels32 += toLastRow;
337
pixels16 += toLastRow;
338
outStride = -outStride;
339
}
340
341
// Skip the bottom of the image in the buffer was smaller. Remember, we're flipped.
342
u32 *dst = &data[0];
343
if (safeH < h) {
344
dst += (h - safeH) * stride;
345
}
346
347
for (u32 y = 0; y < safeH; ++y) {
348
switch (buffer->GetFormat()) {
349
case GPU_DBG_FORMAT_8888:
350
ConvertBGRA8888ToRGBA8888(&dst[y * stride], pixels32, safeW);
351
break;
352
case GPU_DBG_FORMAT_8888_BGRA:
353
memcpy(&dst[y * stride], pixels32, safeW * sizeof(u32));
354
break;
355
356
case GPU_DBG_FORMAT_565:
357
ConvertRGB565ToBGRA8888(&dst[y * stride], pixels16, safeW);
358
break;
359
case GPU_DBG_FORMAT_5551:
360
ConvertRGBA5551ToBGRA8888(&dst[y * stride], pixels16, safeW);
361
break;
362
case GPU_DBG_FORMAT_4444:
363
ConvertRGBA4444ToBGRA8888(&dst[y * stride], pixels16, safeW);
364
break;
365
366
default:
367
data.resize(0);
368
return data;
369
}
370
371
pixels32 += outStride;
372
pixels16 += outStride;
373
}
374
375
return data;
376
}
377
378
379
ScreenshotComparer::~ScreenshotComparer() {
380
if (reference_)
381
free(reference_);
382
}
383
384
double ScreenshotComparer::Compare(const Path &screenshotFilename) {
385
if (pixels_.size() < stride_ * h_) {
386
error_ = "Buffer format conversion error";
387
return -1.0f;
388
}
389
390
// We assume the bitmap is the specified size, not including whatever stride.
391
std::unique_ptr<FileLoader> loader(ConstructFileLoader(screenshotFilename));
392
if (loader->Exists()) {
393
uint8_t header[2];
394
if (loader->ReadAt(0, 2, header) != 2) {
395
error_ = "Unable to read screenshot data: " + screenshotFilename.ToVisualString();
396
return -1.0f;
397
}
398
399
if (header[0] == 'B' && header[1] == 'M') {
400
reference_ = (u32 *)calloc(stride_ * h_, sizeof(u32));
401
referenceStride_ = stride_;
402
asBitmap_ = true;
403
// The bitmap header is 14 + 40 bytes. We could validate it but the test would fail either way.
404
if (reference_ && loader->ReadAt(14 + 40, sizeof(u32), stride_ * h_, reference_) != stride_ * h_) {
405
error_ = "Unable to read screenshot data: " + screenshotFilename.ToVisualString();
406
free(reference_);
407
reference_ = nullptr;
408
return -1.0f;
409
}
410
} else {
411
// For now, assume a PNG otherwise.
412
std::vector<uint8_t> compressed;
413
compressed.resize(loader->FileSize());
414
if (loader->ReadAt(0, compressed.size(), &compressed[0]) != compressed.size()) {
415
error_ = "Unable to read screenshot data: " + screenshotFilename.ToVisualString();
416
return -1.0f;
417
}
418
419
int width, height;
420
if (!pngLoadPtr(&compressed[0], compressed.size(), &width, &height, (unsigned char **)&reference_)) {
421
error_ = "Unable to read screenshot data: " + screenshotFilename.ToVisualString();
422
if (reference_)
423
free(reference_);
424
reference_ = nullptr;
425
return -1.0f;
426
}
427
428
referenceStride_ = width;
429
}
430
} else {
431
error_ = "Unable to read screenshot: " + screenshotFilename.ToVisualString();
432
return -1.0f;
433
}
434
435
if (!reference_) {
436
error_ = "Unable to allocate screenshot data: " + screenshotFilename.ToVisualString();
437
return -1.0f;
438
}
439
440
double errors = 0;
441
if (asBitmap_) {
442
// The reference is flipped and BGRA by default for the common BMP compare case.
443
for (u32 y = 0; y < h_; ++y) {
444
u32 yoff = y * referenceStride_;
445
for (u32 x = 0; x < w_; ++x)
446
errors += ComparePixel(pixels_[y * stride_ + x], reference_[yoff + x]);
447
}
448
} else {
449
// Just convert to BGRA for simplicity.
450
ConvertRGBA8888ToBGRA8888(reference_, reference_, h_ * referenceStride_);
451
for (u32 y = 0; y < h_; ++y) {
452
u32 yoff = (h_ - y - 1) * referenceStride_;
453
for (u32 x = 0; x < w_; ++x)
454
errors += ComparePixel(pixels_[y * stride_ + x], reference_[yoff + x]);
455
}
456
}
457
458
// Convert to MSE, accounting for all three channels (RGB.)
459
return errors / (double)(w_ * h_ * 3);
460
}
461
462
bool ScreenshotComparer::SaveActualBitmap(const Path &resultFilename) {
463
static const u8 header[14 + 40] = {
464
0x42, 0x4D, 0x38, 0x80, 0x08, 0x00, 0x00, 0x00,
465
0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00,
466
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x10, 0x01,
467
0x00, 0x00, 0x01, 0x00, 0x20, 0x00, 0x00, 0x00,
468
0x00, 0x00, 0x02, 0x80, 0x08, 0x00, 0x12, 0x0B,
469
0x00, 0x00, 0x12, 0x0B, 0x00, 0x00, 0x00, 0x00,
470
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
471
};
472
473
FILE *saved = File::OpenCFile(resultFilename, "wb");
474
if (saved) {
475
fwrite(&header, sizeof(header), 1, saved);
476
fwrite(pixels_.data(), sizeof(u32), stride_ * h_, saved);
477
fclose(saved);
478
479
return true;
480
}
481
482
return false;
483
}
484
485
bool ScreenshotComparer::SaveVisualComparisonPNG(const Path &resultFilename) {
486
std::unique_ptr<u32[]> comparison(new u32[w_ * 2 * h_ * 2]);
487
488
if (asBitmap_) {
489
// The reference is flipped and BGRA by default for the common BMP compare case.
490
for (u32 y = 0; y < h_; ++y) {
491
u32 yoff = y * referenceStride_;
492
u32 comparisonRow = (h_ - y - 1) * 2 * w_ * 2;
493
for (u32 x = 0; x < w_; ++x) {
494
PlotVisualComparison(comparison.get(), comparisonRow + x * 2, pixels_[y * stride_ + x], reference_[yoff + x]);
495
}
496
}
497
} else {
498
// Reference is already in BGRA either way.
499
for (u32 y = 0; y < h_; ++y) {
500
u32 yoff = (h_ - y - 1) * referenceStride_;
501
u32 comparisonRow = (h_ - y - 1) * 2 * w_ * 2;
502
for (u32 x = 0; x < w_; ++x) {
503
PlotVisualComparison(comparison.get(), comparisonRow + x * 2, pixels_[y * stride_ + x], reference_[yoff + x]);
504
}
505
}
506
}
507
508
FILE *fp = File::OpenCFile(resultFilename, "wb");
509
if (!fp)
510
return false;
511
512
png_image png;
513
memset(&png, 0, sizeof(png));
514
png.version = PNG_IMAGE_VERSION;
515
png.format = PNG_FORMAT_BGRA;
516
png.width = w_ * 2;
517
png.height = h_ * 2;
518
519
bool success = png_image_write_to_stdio(&png, fp, 0, comparison.get(), w_ * 2 * 4, nullptr) != 0;
520
fclose(fp);
521
png_image_free(&png);
522
523
return success && png.warning_or_error < 2;
524
}
525
526
int ChannelDifference(u8 actual, u8 reference) {
527
int diff = actual > reference ? actual - reference : reference - actual;
528
if (diff == 0)
529
return 0;
530
if (diff < 4)
531
return 1;
532
if (diff < 8)
533
return 2;
534
if (diff < 16)
535
return 3;
536
if (diff < 32)
537
return 4;
538
return 5;
539
}
540
541
int PixelDifference(u32 actual, u32 reference) {
542
int b = ChannelDifference((actual >> 0) & 0xFF, (reference >> 0) & 0xFF);
543
int g = ChannelDifference((actual >> 8) & 0xFF, (reference >> 8) & 0xFF);
544
int r = ChannelDifference((actual >> 16) & 0xFF, (reference >> 16) & 0xFF);
545
return std::max(b, std::max(g, r));
546
}
547
548
void ScreenshotComparer::PlotVisualComparison(u32 *dst, u32 offset, u32 actual, u32 reference) {
549
int diff = PixelDifference(actual, reference);
550
dst[offset + 0] = actual | 0xFF000000;
551
dst[offset + 1] = actual | 0xFF000000;
552
dst[offset + w_ * 2 + 0] = reference | 0xFF000000;
553
554
int alpha = 0x00000000;
555
switch (diff) {
556
case 0: alpha = 0xFF000000; break;
557
case 1: alpha = 0xEF000000; break;
558
case 2: alpha = 0xCF000000; break;
559
case 3: alpha = 0xAF000000; break;
560
case 4: alpha = 0x7F000000; break;
561
default: break;
562
}
563
564
dst[offset + w_ * 2 + 1] = (reference & 0x00FFFFFF) | alpha;
565
}
566
567