Skip to content

Commit f8d56c1

Browse files
committed
GS: Add regression tester and batch mode to GS runner.
1 parent f799631 commit f8d56c1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+5094
-231
lines changed

pcsx2-gsrunner/Main.cpp

Lines changed: 1705 additions & 137 deletions
Large diffs are not rendered by default.

pcsx2-gsrunner/test_run_dumps.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pathlib import Path
88
from functools import partial
99
import platform
10+
import time
1011

1112
def get_gs_name(path):
1213
lpath = path.lower()
@@ -113,7 +114,11 @@ def run_regression_tests(runner, gsdir, dumpdir, renderer, upscale, renderhacks,
113114

114115
args = parser.parse_args()
115116

116-
if not run_regression_tests(args.runner, os.path.realpath(args.gsdir), os.path.realpath(args.dumpdir), args.renderer, args.upscale, args.renderhacks, args.parallel):
117+
start = time.time()
118+
res = run_regression_tests(args.runner, os.path.realpath(args.gsdir), os.path.realpath(args.dumpdir), args.renderer, args.upscale, args.renderhacks, args.parallel)
119+
end = time.time()
120+
print("Regression test %.2f seconds" % (end - start))
121+
if not res:
117122
sys.exit(1)
118123
else:
119124
sys.exit(0)

pcsx2-gsrunner/test_run_dumps_2.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import argparse
2+
import sys
3+
import os
4+
import subprocess
5+
import multiprocessing
6+
from functools import partial
7+
import platform
8+
import time
9+
10+
def get_gs_name(path):
11+
lpath = path.lower()
12+
13+
for extension in [".gs", ".gs.xz", ".gs.zst"]:
14+
if lpath.endswith(extension):
15+
return os.path.basename(path)[:-len(extension)]
16+
17+
return None
18+
19+
20+
def run_regression_test(runner, dumpdir, renderer, upscale, renderhacks, gspath, gsfastreopen, startfrom, nbatches, batch_id):
21+
args = [runner]
22+
23+
args.extend(["-batch"])
24+
25+
args.extend(["-nbatches", str(nbatches)])
26+
27+
args.extend(["-batch-id", str(batch_id)])
28+
29+
if renderer is not None:
30+
args.extend(["-renderer", renderer])
31+
32+
if upscale != 1.0:
33+
args.extend(["-upscale", str(upscale)])
34+
35+
if renderhacks is not None:
36+
args.extend(["-renderhacks", renderhacks])
37+
38+
args.extend(["-dumpdir", dumpdir])
39+
args.extend(["-logfile", dumpdir])
40+
41+
if gsfastreopen:
42+
args.extend(["-batch-gs-fast-reopen"])
43+
44+
# loop a couple of times for those stubborn merge/interlace dumps that don't render anything
45+
# the first time around
46+
args.extend(["-loop", "2"])
47+
48+
# disable shader cache for parallel runs, otherwise it'll have sharing violations
49+
if nbatches > 1:
50+
args.append("-noshadercache")
51+
52+
# run surfaceless, we don't want tons of windows popping up
53+
args.append("-surfaceless")
54+
55+
if startfrom is not None:
56+
args.extend(["-batch-start-from", startfrom])
57+
58+
# disable output console entirely
59+
environ = os.environ.copy()
60+
environ["PCSX2_NOCONSOLE"] = "1"
61+
62+
creationflags = 0
63+
# Set low priority by default
64+
if platform.system() == "Windows":
65+
creationflags = 0x00004000 # BELOW_NORMAL_PRIORITY_CLASS
66+
elif platform.system() in ["Linux", "Darwin"]:
67+
try:
68+
os.nice(10) # lower priority
69+
except OSError:
70+
pass
71+
72+
args.append("--")
73+
args.append(gspath)
74+
75+
#print("Running '%s'" % (" ".join(args)))
76+
subprocess.run(args, env=environ, stdin=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, creationflags=creationflags)
77+
78+
79+
def run_regression_tests(runner, gsdir, dumpdir, renderer, upscale, renderhacks, gsfastreopen, startfrom, parallel):
80+
try:
81+
os.makedirs(dumpdir)
82+
except FileExistsError:
83+
pass
84+
85+
if parallel <= 1:
86+
run_regression_test(runner, dumpdir, renderer, upscale, renderhacks, gsdir, gsfastreopen, startfrom, 1, 0)
87+
else:
88+
print("Processing on %u processors" % (parallel))
89+
func = partial(run_regression_test, runner, dumpdir, renderer, upscale, renderhacks, gsdir, gsfastreopen, startfrom, parallel)
90+
pool = multiprocessing.Pool(parallel)
91+
for i, _ in enumerate(pool.imap_unordered(func, range(parallel), chunksize=1)):
92+
print("Process %u finished" % (i))
93+
pool.close()
94+
95+
return True
96+
97+
98+
if __name__ == "__main__":
99+
parser = argparse.ArgumentParser(description="Generate frame dump images for regression tests")
100+
parser.add_argument("-runner", action="store", required=True, help="Path to PCSX2 GS runner")
101+
parser.add_argument("-gsdir", action="store", required=True, help="Directory containing GS dumps")
102+
parser.add_argument("-dumpdir", action="store", required=True, help="Base directory to dump frames to")
103+
parser.add_argument("-renderer", action="store", required=False, help="Renderer to use")
104+
parser.add_argument("-upscale", action="store", type=float, default=1, help="Upscaling multiplier to use")
105+
parser.add_argument("-renderhacks", action="store", required=False, help="Enable HW Rendering hacks")
106+
parser.add_argument("-parallel", action="store", type=int, default=1, help="Number of processes to run")
107+
parser.add_argument("-gsfastreopen", action="store_true", required=False, help="Enable GS fast reopen")
108+
parser.add_argument("-startfrom", action="store", required=False, help="Dump name/prefix to start from")
109+
110+
args = parser.parse_args()
111+
112+
start = time.time()
113+
res = run_regression_tests(
114+
runner = args.runner,
115+
gsdir = os.path.realpath(args.gsdir),
116+
dumpdir = os.path.realpath(args.dumpdir),
117+
renderer = args.renderer,
118+
upscale = args.upscale,
119+
renderhacks = args.renderhacks,
120+
startfrom = args.startfrom,
121+
parallel = args.parallel,
122+
gsfastreopen = args.gsfastreopen,
123+
)
124+
end = time.time()
125+
print("Regression test %.2f seconds" % (end - start))
126+
if not res:
127+
sys.exit(1)
128+
else:
129+
sys.exit(0)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import argparse
2+
import sys
3+
import os
4+
import subprocess
5+
import platform
6+
import time
7+
8+
def get_gs_name(path):
9+
lpath = path.lower()
10+
11+
for extension in [".gs", ".gs.xz", ".gs.zst"]:
12+
if lpath.endswith(extension):
13+
return os.path.basename(path)[:-len(extension)]
14+
15+
return None
16+
17+
18+
def run_regression_test(tester, runner1, runner2, dumpdir, renderer, upscale, renderhacks, gspath, parallel, gsfastreopen, verbose):
19+
args = [tester]
20+
21+
args.append("tester")
22+
23+
args.extend(["-path", runner1, runner2])
24+
25+
args.extend(["-nthreads", str(parallel)])
26+
27+
if renderer is not None:
28+
args.extend(["-renderer", renderer])
29+
30+
if upscale != 1.0:
31+
args.extend(["-upscale", str(upscale)])
32+
33+
if renderhacks is not None:
34+
args.extend(["-renderhacks", str(renderhacks)])
35+
36+
args.extend(["-output", dumpdir])
37+
38+
args.extend(["-log"])
39+
40+
args.extend(["-verbose-level", str(verbose)])
41+
42+
# loop a couple of times for those stubborn merge/interlace dumps that don't render anything
43+
# the first time around
44+
args.extend(["-loop", "2"])
45+
46+
if gsfastreopen:
47+
args.extend(["-batch-gs-fast-reopen"])
48+
49+
# disable output console entirely
50+
environ = os.environ.copy()
51+
environ["PCSX2_NOCONSOLE"] = "1"
52+
53+
creationflags = 0
54+
# Set low priority by default
55+
if platform.system() == "Windows":
56+
creationflags = 0x00004000 # BELOW_NORMAL_PRIORITY_CLASS
57+
elif platform.system() in ["Linux", "Darwin"]:
58+
try:
59+
os.nice(10) # lower priority
60+
except OSError:
61+
pass
62+
63+
args.extend(["-input", gspath])
64+
65+
#print("Running '%s'" % (" ".join(args)))
66+
subprocess.run(args, env=environ, stdin=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, creationflags=creationflags)
67+
68+
return True
69+
70+
71+
if __name__ == "__main__":
72+
parser = argparse.ArgumentParser(description="Generate frame dump images for regression tests")
73+
parser.add_argument("-tester", action="store", required=True, help="Path to PCSX2 GS regression tester")
74+
parser.add_argument("-runner1", action="store", required=True, help="Path to PCSX2 GS runner (1)")
75+
parser.add_argument("-runner2", action="store", required=True, help="Path to PCSX2 GS runner (2)")
76+
parser.add_argument("-gsdir", action="store", required=True, help="Directory containing GS dumps")
77+
parser.add_argument("-dumpdir", action="store", required=True, help="Base directory to dump frames to")
78+
parser.add_argument("-renderer", action="store", required=False, help="Renderer to use")
79+
parser.add_argument("-upscale", action="store", type=float, default=1, help="Upscaling multiplier to use")
80+
parser.add_argument("-renderhacks", action="store", required=False, help="Enable HW Rendering hacks")
81+
parser.add_argument("-parallel", action="store", type=int, default=1, help="Number of processes to run")
82+
parser.add_argument("-gsfastreopen", action="store_true", required=False, help="Enable GS fast reopen")
83+
parser.add_argument("-verbose", action="store", type=int, default=0, required=False, help="Verbose logging level (0-3)")
84+
85+
args = parser.parse_args()
86+
87+
start = time.time()
88+
res = run_regression_test(
89+
tester = args.tester,
90+
runner1 = args.runner1,
91+
runner2 = args.runner2,
92+
dumpdir = os.path.realpath(args.dumpdir),
93+
gspath = os.path.realpath(args.gsdir),
94+
renderer = args.renderer,
95+
upscale = args.upscale,
96+
renderhacks = args.renderhacks,
97+
parallel = args.parallel,
98+
gsfastreopen = args.gsfastreopen,
99+
verbose = args.verbose
100+
)
101+
end = time.time()
102+
print("Regression test %.2f seconds" % (end - start))
103+
if not res:
104+
sys.exit(1)
105+
else:
106+
sys.exit(0)

pcsx2-qt/QtHost.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,14 @@ void Host::OnVMResumed()
982982
emit g_emu_thread->onVMResumed();
983983
}
984984

985+
void Host::OnBatchDumpStart(const std::string& dump_name)
986+
{
987+
}
988+
989+
void Host::OnBatchDumpEnd(const std::string& dump_name)
990+
{
991+
}
992+
985993
void Host::OnGameChanged(const std::string& title, const std::string& elf_override, const std::string& disc_path,
986994
const std::string& disc_serial, u32 disc_crc, u32 current_crc)
987995
{

pcsx2/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ set(pcsx2Sources
7171
Gif_Unit.cpp
7272
GS.cpp
7373
GSDumpReplayer.cpp
74+
GSRegressionTester.cpp
7475
Host.cpp
7576
Hotkeys.cpp
7677
Hw.cpp
@@ -156,6 +157,7 @@ set(pcsx2Headers
156157
Gif_Unit.h
157158
GS.h
158159
GSDumpReplayer.h
160+
GSRegressionTester.h
159161
Hardware.h
160162
Host.h
161163
Hw.h

pcsx2/GS/GS.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1289,4 +1289,4 @@ BEGIN_HOTKEY_LIST(g_gs_hotkeys){"Screenshot", TRANSLATE_NOOP("Hotkeys", "Graphic
12891289
}
12901290
}
12911291
}},
1292-
END_HOTKEY_LIST()
1292+
END_HOTKEY_LIST()

pcsx2/GS/GSLocalMemory.cpp

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
22
// SPDX-License-Identifier: GPL-3.0+
33

4+
#include "common/Console.h"
5+
#include "common/ScopedGuard.h"
6+
47
#include "GS/GS.h"
58
#include "GS/GSLocalMemory.h"
69
#include "GS/GSExtra.h"
710
#include "GS/GSPng.h"
11+
#include "GSDumpReplayer.h"
812
#include <unordered_set>
913

1014
template <typename Fn>
@@ -651,11 +655,41 @@ void GSLocalMemory::ReadTexture(const GSOffset& off, const GSVector4i& r, u8* ds
651655

652656
//
653657

654-
void GSLocalMemory::SaveBMP(const std::string& fn, u32 bp, u32 bw, u32 psm, int w, int h, int x, int y)
658+
void GSLocalMemory::SaveBMP(const std::string& fn, u32 bp, u32 bw, u32 psm, int w, int h, int x, int y,
659+
GSRegressionBuffer* rbp)
655660
{
656661
int pitch = w * 4;
657662
int size = pitch * h;
658-
void* bits = _aligned_malloc(size, VECTOR_ALIGNMENT);
663+
664+
GSRegressionPacket* packet = nullptr;
665+
666+
ScopedGuard sg([&]() {
667+
if (rbp)
668+
{
669+
rbp->SetStateRunner(GSRegressionBuffer::DEFAULT);
670+
if (packet)
671+
rbp->DonePacketWrite();
672+
}
673+
});
674+
675+
void* bits;
676+
if (rbp)
677+
{
678+
rbp->SetStateRunner(GSRegressionBuffer::WRITE_DATA);
679+
680+
packet = rbp->GetPacketWrite(std::bind(GSCheckTesterStatus, true, false));
681+
if (!packet)
682+
{
683+
Console.ErrorFmt("(GSRunner/{}) Failed to get regression packet.", GSDumpReplayer::GetRunnerName());
684+
return;
685+
}
686+
687+
bits = packet->GetData();
688+
}
689+
else
690+
{
691+
bits = _aligned_malloc(size, VECTOR_ALIGNMENT);
692+
}
659693

660694
GIFRegTEX0 TEX0;
661695

@@ -675,9 +709,23 @@ void GSLocalMemory::SaveBMP(const std::string& fn, u32 bp, u32 bw, u32 psm, int
675709
}
676710
}
677711

678-
GSPng::Save((IsDevBuild || GSConfig.SaveAlpha) ? GSPng::RGB_A_PNG : GSPng::RGB_PNG, fn, static_cast<u8*>(bits), w, h, pitch, GSConfig.PNGCompressionLevel, false);
679-
680-
_aligned_free(bits);
712+
if (packet)
713+
{
714+
packet->SetNameDump(rbp->GetNameDump());
715+
packet->SetNamePacket(fn.c_str());
716+
packet->SetImage(nullptr, w, h, pitch, 4); // Image data is already written so pass null.
717+
if (GSDumpReplayer::IsVerboseLogging())
718+
{
719+
Console.WriteLnFmt("(GSRunner/{}) New regression packet: {} / {}",
720+
GSDumpReplayer::GetRunnerName(), packet->GetNameDump(), packet->GetNamePacket());
721+
}
722+
}
723+
else
724+
{
725+
GSPng::Save((IsDevBuild || GSConfig.SaveAlpha) ? GSPng::RGB_A_PNG : GSPng::RGB_PNG, fn, static_cast<u8*>(bits), w, h, pitch, GSConfig.PNGCompressionLevel, false);
726+
727+
_aligned_free(bits);
728+
}
681729
}
682730

683731
// GSOffset

pcsx2/GS/GSLocalMemory.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "GSVector.h"
88
#include "GSClut.h"
99
#include "MultiISA.h"
10+
#include "GSRegressionTester.h"
1011

1112
#include "common/Assertions.h"
1213

@@ -1121,7 +1122,8 @@ class GSLocalMemory final : public GSAlignedClass<32>
11211122

11221123
//
11231124

1124-
void SaveBMP(const std::string& fn, u32 bp, u32 bw, u32 psm, int w, int h, int x = 0, int y = 0);
1125+
void SaveBMP(const std::string& fn, u32 bp, u32 bw, u32 psm, int w, int h, int x = 0, int y = 0,
1126+
GSRegressionBuffer* rbp = nullptr);
11251127
};
11261128

11271129
constexpr inline GSOffset GSOffset::fromKnownPSM(u32 bp, u32 bw, GS_PSM psm)

0 commit comments

Comments
 (0)