Merge pull request #15363 from unknownbrackets/softjit-unittest

Add unit test to verify compilation of sampler/pixel jits
This commit is contained in:
Henrik Rydgård 2022-01-30 10:47:30 +01:00 committed by GitHub
commit 77502db4c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 304 additions and 61 deletions

View file

@ -2318,6 +2318,7 @@ if(UNITTEST)
unittest/TestArm64Emitter.cpp
unittest/TestX64Emitter.cpp
unittest/TestVertexJit.cpp
unittest/TestSoftwareGPUJit.cpp
unittest/TestThreadManager.cpp
unittest/JitHarness.cpp
Core/MIPS/ARM/ArmRegCache.cpp

View file

@ -32,6 +32,8 @@
#define LOG_BUF_SIZE 2048
static bool hitAnyAsserts = false;
bool HandleAssert(const char *function, const char *file, int line, const char *expression, const char* format, ...) {
// Read message and write it to the log
char text[LOG_BUF_SIZE];
@ -50,16 +52,22 @@ bool HandleAssert(const char *function, const char *file, int line, const char *
// Also do a simple printf for good measure, in case logging of SYSTEM is disabled (should we disallow that?)
fprintf(stderr, "%s\n", formatted);
hitAnyAsserts = true;
#if defined(USING_WIN_UI)
int msgBoxStyle = MB_ICONINFORMATION | MB_YESNO;
std::wstring wtext = ConvertUTF8ToWString(formatted) + L"\n\nTry to continue?";
std::wstring wcaption = ConvertUTF8ToWString(caption);
OutputDebugString(wtext.c_str());
if (IDYES != MessageBox(0, wtext.c_str(), wcaption.c_str(), msgBoxStyle)) {
return false;
} else {
return true;
// Avoid hanging on CI.
if (!getenv("CI")) {
int msgBoxStyle = MB_ICONINFORMATION | MB_YESNO;
std::wstring wtext = ConvertUTF8ToWString(formatted) + L"\n\nTry to continue?";
std::wstring wcaption = ConvertUTF8ToWString(caption);
OutputDebugString(wtext.c_str());
if (IDYES != MessageBox(0, wtext.c_str(), wcaption.c_str(), msgBoxStyle)) {
return false;
} else {
return true;
}
}
return false;
#elif PPSSPP_PLATFORM(ANDROID)
__android_log_assert(expression, "PPSSPP", "%s", formatted);
// Doesn't matter what we return here.
@ -69,3 +77,11 @@ bool HandleAssert(const char *function, const char *file, int line, const char *
return false;
#endif
}
// These are mainly used for unit testing.
bool HitAnyAsserts() {
return hitAnyAsserts;
}
void ResetHitAnyAsserts() {
hitAnyAsserts = false;
}

View file

@ -116,6 +116,9 @@ __attribute__((format(printf, 5, 6)))
#endif
;
bool HitAnyAsserts();
void ResetHitAnyAsserts();
#if defined(__ANDROID__)
// Tricky macro to get the basename, that also works if *built* on Win32.
// Doesn't mean this macro can be used on Win32 though.

View file

@ -59,15 +59,21 @@ SingleFunc PixelJitCache::CompileSingle(const PixelFuncID &id) {
Describe("Init");
WriteConstantPool(id);
const u8 *start = AlignCode16();
const u8 *resetPos = AlignCode16();
bool success = true;
#if PPSSPP_PLATFORM(WINDOWS)
// RET + Windows reserves space to save args, half of 1 xmm + 4 ints before the id.
_assert_(!regCache_.Has(RegCache::GEN_ARG_ID));
stackIDOffset_ = 8 + 8 + 4 * PTRBITS / 8;
int stackSpace = 0;
if (id.hasStencilTestMask)
stackSpace = WriteProlog(0, { XMM6, XMM7, XMM8, XMM9, XMM10, XMM11, XMM12, XMM13, XMM14, XMM15 }, { R12, R13, R14, R15 });
else
stackSpace = WriteProlog(0, {}, {});
stackIDOffset_ = stackSpace + 8 + 8 + 4 * PTRBITS / 8;
#else
_assert_(regCache_.Has(RegCache::GEN_ARG_ID));
WriteProlog(0, {}, {});
stackIDOffset_ = -1;
#endif
@ -105,19 +111,18 @@ SingleFunc PixelJitCache::CompileSingle(const PixelFuncID &id) {
if (regCache_.Has(RegCache::GEN_ARG_ID))
regCache_.ForceRelease(RegCache::GEN_ARG_ID);
regCache_.Reset(success);
if (!success) {
ERROR_LOG_REPORT(G3D, "Could not compile pixel func: %s", DescribePixelFuncID(id).c_str());
regCache_.Reset(false);
EndWrite();
ResetCodePtr(GetOffset(start));
ResetCodePtr(GetOffset(resetPos));
return nullptr;
}
RET();
EndWrite();
const u8 *start = WriteFinalizedEpilog();
regCache_.Reset(true);
return (SingleFunc)start;
}
@ -595,7 +600,7 @@ bool PixelJitCache::Jit_StencilAndDepthTest(const PixelFuncID &id) {
X64Reg stencilReg = GetDestStencil(id);
Describe("StencilAndDepth");
X64Reg maskedReg = stencilReg;
if (id.hasStencilTestMask) {
if (id.hasStencilTestMask && stencilReg != INVALID_REG) {
X64Reg idReg = GetPixelID();
maskedReg = regCache_.Alloc(RegCache::GEN_TEMP0);
MOV(32, R(maskedReg), R(stencilReg));

View file

@ -253,6 +253,8 @@ std::string DescribePixelFuncID(const PixelFuncID &id) {
}
if (id.AlphaTestFunc() != GE_COMP_ALWAYS) {
if (id.clearMode)
desc = "INVALID:" + desc;
switch (id.AlphaTestFunc()) {
case GE_COMP_NEVER: desc += "ANever"; break;
case GE_COMP_ALWAYS: break;
@ -266,9 +268,13 @@ std::string DescribePixelFuncID(const PixelFuncID &id) {
if (id.hasAlphaTestMask)
desc += "Msk";
desc += StringFromFormat("%02X:", id.alphaTestRef);
} else if (id.hasAlphaTestMask || id.alphaTestRef != 0) {
desc = "INVALID:" + desc;
}
if (id.DepthTestFunc() != GE_COMP_ALWAYS) {
if (id.clearMode)
desc = "INVALID:" + desc;
switch (id.DepthTestFunc()) {
case GE_COMP_NEVER: desc += "ZNever:"; break;
case GE_COMP_ALWAYS: break;
@ -300,6 +306,8 @@ std::string DescribePixelFuncID(const PixelFuncID &id) {
if (id.hasStencilTestMask)
desc += "Msk";
desc += StringFromFormat("%02X:", id.stencilTestRef);
} else if (id.hasStencilTestMask || id.stencilTestRef != 0 || id.stencilTestFunc != 0) {
desc = "INVALID:" + desc;
}
switch (id.SFail()) {
@ -326,8 +334,14 @@ std::string DescribePixelFuncID(const PixelFuncID &id) {
case GE_STENCILOP_INCR: desc += "ZTstTInc:"; break;
case GE_STENCILOP_DECR: desc += "ZTstTDec:"; break;
}
if (!id.stencilTest || id.clearMode) {
if (id.sFail != 0 || id.zFail != 0 || id.zPass != 0)
desc = "INVALID:" + desc;
}
if (id.alphaBlend) {
if (id.clearMode)
desc = "INVALID:" + desc;
switch (id.AlphaBlendEq()) {
case GE_BLENDMODE_MUL_AND_ADD: desc += "BlendAdd<"; break;
case GE_BLENDMODE_MUL_AND_SUBTRACT: desc += "BlendSub<"; break;
@ -366,15 +380,21 @@ std::string DescribePixelFuncID(const PixelFuncID &id) {
case PixelBlendFactor::ZERO: desc += "0>:"; break;
case PixelBlendFactor::ONE: desc += "1>:"; break;
}
} else if (id.alphaBlendEq != 0 || id.alphaBlendSrc != 0 || id.alphaBlendDst != 0) {
desc = "INVALID:" + desc;
}
if (id.applyLogicOp)
desc += "Logic:";
else if (id.clearMode)
desc = "INVALID:" + desc;
if (id.applyFog)
desc += "Fog:";
else if (id.clearMode)
desc = "INVALID:" + desc;
if (desc.empty())
return desc;
return "INVALID";
desc.resize(desc.size() - 1);
return desc;
}
@ -405,6 +425,7 @@ void ComputeSamplerID(SamplerID *id_out) {
id.cached.sizes[i].w = w;
id.cached.sizes[i].h = h;
}
// TODO: What specifically happens if these are above 11?
id.width0Shift = gstate.texsize[0] & 0xF;
id.height0Shift = (gstate.texsize[0] >> 8) & 0xF;
id.hasAnyMips = maxLevel != 0;
@ -450,6 +471,7 @@ std::string DescribeSamplerID(const SamplerID &id) {
case GE_TFMT_DXT1: name = "DXT1"; break;
case GE_TFMT_DXT3: name = "DXT3"; break;
case GE_TFMT_DXT5: name = "DXT5"; break;
default: name = "INVALID"; break;
}
switch (id.ClutFmt()) {
case GE_CMODE_16BIT_BGR5650:
@ -529,6 +551,8 @@ std::string DescribeSamplerID(const SamplerID &id) {
break;
}
name += StringFromFormat(":W%dH%d", 1 << id.width0Shift, 1 << id.height0Shift);
if (id.width0Shift > 10 || id.height0Shift > 10)
name = "INVALID:" + name;
return name;
}

View file

@ -141,23 +141,15 @@ NearestFunc SamplerJitCache::GetNearest(const SamplerID &id) {
std::lock_guard<std::mutex> guard(jitCacheLock);
auto it = cache_.find(id);
if (it != cache_.end()) {
if (it != cache_.end())
return (NearestFunc)it->second;
}
// TODO: What should be the min size? Can we even hit this?
if (GetSpaceLeft() < 16384) {
Clear();
}
Compile(id);
#if PPSSPP_ARCH(AMD64) && !PPSSPP_PLATFORM(UWP)
if (g_Config.bSoftwareRenderingJit) {
addresses_[id] = GetCodePointer();
NearestFunc func = CompileNearest(id);
cache_[id] = (NearestFunc)func;
return func;
}
#endif
// Okay, should be there now.
it = cache_.find(id);
if (it != cache_.end())
return (NearestFunc)it->second;
return nullptr;
}
@ -165,23 +157,15 @@ LinearFunc SamplerJitCache::GetLinear(const SamplerID &id) {
std::lock_guard<std::mutex> guard(jitCacheLock);
auto it = cache_.find(id);
if (it != cache_.end()) {
if (it != cache_.end())
return (LinearFunc)it->second;
}
// TODO: What should be the min size? Can we even hit this?
if (GetSpaceLeft() < 16384) {
Clear();
}
Compile(id);
#if PPSSPP_ARCH(AMD64) && !PPSSPP_PLATFORM(UWP)
if (g_Config.bSoftwareRenderingJit) {
addresses_[id] = GetCodePointer();
LinearFunc func = CompileLinear(id);
cache_[id] = (NearestFunc)func;
return func;
}
#endif
// Okay, should be there now.
it = cache_.find(id);
if (it != cache_.end())
return (LinearFunc)it->second;
return nullptr;
}
@ -189,24 +173,47 @@ FetchFunc SamplerJitCache::GetFetch(const SamplerID &id) {
std::lock_guard<std::mutex> guard(jitCacheLock);
auto it = cache_.find(id);
if (it != cache_.end()) {
if (it != cache_.end())
return (FetchFunc)it->second;
}
// TODO: What should be the min size? Can we even hit this?
Compile(id);
// Okay, should be there now.
it = cache_.find(id);
if (it != cache_.end())
return (FetchFunc)it->second;
return nullptr;
}
void SamplerJitCache::Compile(const SamplerID &id) {
// This should be sufficient.
if (GetSpaceLeft() < 16384) {
Clear();
}
// We compile them together so the cache can't possibly be cleared in between.
// We might vary between nearest and linear, so we can't clear between.
#if PPSSPP_ARCH(AMD64) && !PPSSPP_PLATFORM(UWP)
if (g_Config.bSoftwareRenderingJit) {
addresses_[id] = GetCodePointer();
FetchFunc func = CompileFetch(id);
cache_[id] = (NearestFunc)func;
return func;
SamplerID fetchID = id;
fetchID.linear = false;
fetchID.fetch = true;
addresses_[fetchID] = GetCodePointer();
cache_[fetchID] = (NearestFunc)CompileFetch(fetchID);
SamplerID nearestID = id;
nearestID.linear = false;
nearestID.fetch = false;
addresses_[nearestID] = GetCodePointer();
cache_[nearestID] = (NearestFunc)CompileNearest(nearestID);
SamplerID linearID = id;
linearID.linear = true;
linearID.fetch = false;
addresses_[linearID] = GetCodePointer();
cache_[linearID] = (NearestFunc)CompileLinear(linearID);
}
#endif
return nullptr;
}
template <uint32_t texel_size_bits>

View file

@ -60,6 +60,7 @@ public:
std::string DescribeCodePtr(const u8 *ptr) override;
private:
void Compile(const SamplerID &id);
FetchFunc CompileFetch(const SamplerID &id);
NearestFunc CompileNearest(const SamplerID &id);
LinearFunc CompileLinear(const SamplerID &id);
@ -101,6 +102,7 @@ private:
#if PPSSPP_ARCH(AMD64) || PPSSPP_ARCH(X86)
int stackArgPos_ = 0;
int stackIDOffset_ = -1;
int stackLevelOffset_ = -1;
int stackUV1Offset_ = 0;
#endif

View file

@ -53,9 +53,11 @@ FetchFunc SamplerJitCache::CompileFetch(const SamplerID &id) {
// RET and shadow space.
stackArgPos_ = 8 + 32;
stackIDOffset_ = 8;
stackLevelOffset_ = 0;
#else
stackArgPos_ = 0;
stackIDOffset_ = -1;
stackLevelOffset_ = -1;
#endif
// Early exit on !srcPtr.
@ -147,10 +149,12 @@ NearestFunc SamplerJitCache::CompileNearest(const SamplerID &id) {
// Positions: stackArgPos_+0=src, stackArgPos_+8=bufw, stackArgPos_+16=level, stackArgPos_+24=levelFrac
stackIDOffset_ = 32;
stackLevelOffset_ = 16;
#else
stackArgPos_ = 0;
// This is the only arg that went to the stack, it's after the RET.
stackIDOffset_ = 8;
stackLevelOffset_ = -1;
#endif
// Start out by saving some registers, since we'll need more.
@ -173,7 +177,7 @@ NearestFunc SamplerJitCache::CompileNearest(const SamplerID &id) {
#endif
// We can throw these away right off if there are no mips.
if (!id.hasAnyMips && regCache_.Has(RegCache::GEN_ARG_LEVEL))
if (!id.hasAnyMips && regCache_.Has(RegCache::GEN_ARG_LEVEL) && id.useSharedClut)
regCache_.ForceRelease(RegCache::GEN_ARG_LEVEL);
if (!id.hasAnyMips && regCache_.Has(RegCache::GEN_ARG_LEVELFRAC))
regCache_.ForceRelease(RegCache::GEN_ARG_LEVELFRAC);
@ -528,6 +532,7 @@ LinearFunc SamplerJitCache::CompileLinear(const SamplerID &id) {
// Positions: stackArgPos_+0=src, stackArgPos_+8=bufw, stackArgPos_+16=level, stackArgPos_+24=levelFrac
stackIDOffset_ = 32;
stackLevelOffset_ = 16;
// If needed, we could store UV1 data in shadow space, but we no longer due.
stackUV1Offset_ = -8;
@ -536,6 +541,7 @@ LinearFunc SamplerJitCache::CompileLinear(const SamplerID &id) {
stackArgPos_ += WriteProlog(0, {}, { R15, R14, R13, R12 });
// Just after the RET.
stackIDOffset_ = 8;
stackLevelOffset_ = -1;
// Use the red zone.
stackUV1Offset_ = -stackArgPos_ - 8;
@ -709,6 +715,12 @@ LinearFunc SamplerJitCache::CompileLinear(const SamplerID &id) {
doNearestCall(4, false);
doNearestCall(8, false);
doNearestCall(12, false);
// After doing the calls, certain cached things aren't safe.
if (regCache_.Has(RegCache::GEN_ID))
regCache_.ForceRelease(RegCache::GEN_ID);
if (regCache_.Has(RegCache::VEC_ZERO))
regCache_.ForceRelease(RegCache::VEC_ZERO);
} else {
success = success && Jit_FetchQuad(id, false);
}
@ -3328,14 +3340,12 @@ bool SamplerJitCache::Jit_ReadClutColor(const SamplerID &id) {
// We need to multiply by 16 and add, LEA allows us to copy too.
LEA(32, temp2Reg, MScaled(levelReg, SCALE_4, 0));
regCache_.Unlock(levelReg, RegCache::GEN_ARG_LEVEL);
regCache_.ForceRelease(RegCache::GEN_ARG_LEVEL);
if (id.fetch)
regCache_.ForceRelease(RegCache::GEN_ARG_LEVEL);
} else {
#if PPSSPP_PLATFORM(WINDOWS)
_assert_(stackLevelOffset_ != -1);
// The argument was saved on the stack.
MOV(32, R(temp2Reg), MDisp(RSP, stackArgPos_));
#else
_assert_(false);
#endif
MOV(32, R(temp2Reg), MDisp(RSP, stackArgPos_ + stackLevelOffset_));
LEA(32, temp2Reg, MScaled(temp2Reg, SCALE_4, 0));
}

View file

@ -738,8 +738,9 @@ ifeq ($(UNITTEST),1)
LOCAL_SRC_FILES := \
$(SRC)/unittest/JitHarness.cpp \
$(SRC)/unittest/TestShaderGenerators.cpp \
$(SRC)/unittest/TestVertexJit.cpp \
$(SRC)/unittest/TestSoftwareGPUJit.cpp \
$(SRC)/unittest/TestThreadManager.cpp \
$(SRC)/unittest/TestVertexJit.cpp \
$(TESTARMEMITTER_FILE) \
$(SRC)/unittest/UnitTest.cpp

View file

@ -0,0 +1,170 @@
// Copyright (c) 2022- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include "Common/Data/Random/Rng.h"
#include "Common/StringUtils.h"
#include "Core/Config.h"
#include "GPU/Software/DrawPixel.h"
#include "GPU/Software/Sampler.h"
#include "GPU/Software/SoftGpu.h"
static bool TestSamplerJit() {
using namespace Sampler;
SamplerJitCache *cache = new SamplerJitCache();
auto GetLinear = [&](SamplerID &id) {
id.linear = true;
id.fetch = false;
return cache->GetLinear(id);
};
auto GetNearest = [&](SamplerID &id) {
id.linear = false;
id.fetch = false;
return cache->GetNearest(id);
};
auto GetFetch = [&](SamplerID &id) {
id.linear = false;
id.fetch = true;
return cache->GetFetch(id);
};
GMRng rng;
int successes = 0;
int count = 3000;
bool header = false;
u8 **tptr = new u8 *[8];
int *bufw = new int[8];
u8 *clut = new u8[1024];
for (int i = 0; i < 8; ++i) {
tptr[i] = new u8[1024 * 1024 * 4];
memset(tptr[i], 0, 1024 * 1024 * 4);
bufw[i] = 1;
}
for (int i = 0; i < count; ) {
SamplerID id;
memset(&id, 0, sizeof(id));
id.fullKey = rng.R32();
id.cached.clut = clut;
for (int i = 0; i < 8; ++i) {
id.cached.sizes[i].w = 1;
id.cached.sizes[i].h = 1;
}
std::string desc = DescribeSamplerID(id);
if (startsWith(desc, "INVALID"))
continue;
i++;
LinearFunc linearFunc = GetLinear(id);
NearestFunc nearestFunc = GetNearest(id);
FetchFunc fetchFunc = GetFetch(id);
if (linearFunc != nullptr && nearestFunc != nullptr && fetchFunc != nullptr) {
successes++;
} else {
if (!header)
printf("Failed sampler funcs:\n");
header = true;
printf(" * %s (L:%d, N:%d, F:%d)\n", desc.c_str(), linearFunc != nullptr, nearestFunc != nullptr, fetchFunc != nullptr);
continue;
}
// Try running each to make sure they don't trivially crash.
const auto primArg = Rasterizer::ToVec4IntArg(Math3D::Vec4<int>(127, 127, 127, 127));
linearFunc(0.0f, 0.0f, 0, 0, primArg, tptr, bufw, 1, 7, id);
nearestFunc(0.0f, 0.0f, 0, 0, primArg, tptr, bufw, 1, 7, id);
fetchFunc(0, 0, tptr[0], bufw[0], 1, id);
}
if (successes < count)
printf("SamplerFunc success: %d / %d\n", successes, count);
for (int i = 0; i < 8; ++i) {
delete [] tptr[i];
}
delete [] tptr;
delete [] bufw;
delete [] clut;
delete cache;
return successes == count && !HitAnyAsserts();
}
static bool TestPixelJit() {
using namespace Rasterizer;
PixelJitCache *cache = new PixelJitCache();
GMRng rng;
int successes = 0;
int count = 3000;
bool header = false;
u32 *fb_data = new u32[512 * 2];
u16 *zb_data = new u16[512 * 2];
fb.as32 = fb_data;
depthbuf.as16 = zb_data;
for (int i = 0; i < count; ) {
PixelFuncID id;
memset(&id, 0, sizeof(id));
id.fullKey = (uint64_t)rng.R32() | ((uint64_t)rng.R32() << 32);
std::string desc = DescribePixelFuncID(id);
if (startsWith(desc, "INVALID"))
continue;
i++;
SingleFunc func = cache->GetSingle(id);
SingleFunc genericFunc = cache->GenericSingle(id);
if (func != genericFunc) {
successes++;
} else {
if (!header)
printf("Failed pixel funcs:\n");
header = true;
printf(" * %s\n", desc.c_str());
}
// Try running it to make sure it doesn't trivially crash.
func(0, 0, 1000, 255, ToVec4IntArg(Math3D::Vec4<int>(127, 127, 127, 127)), id);
}
if (successes < count)
printf("PixelFunc success: %d / %d\n", successes, count);
delete [] fb_data;
delete [] zb_data;
delete cache;
return successes == count && !HitAnyAsserts();
}
bool TestSoftwareGPUJit() {
g_Config.bSoftwareRenderingJit = true;
ResetHitAnyAsserts();
if (!TestSamplerJit()) {
return false;
}
if (!TestPixelJit()) {
return false;
}
return true;
}

View file

@ -753,6 +753,7 @@ bool TestArmEmitter();
bool TestArm64Emitter();
bool TestX64Emitter();
bool TestShaderGenerators();
bool TestSoftwareGPUJit();
bool TestThreadManager();
TestItem availableTests[] = {
@ -778,6 +779,7 @@ TestItem availableTests[] = {
TEST_ITEM(CLZ),
TEST_ITEM(MemMap),
TEST_ITEM(ShaderGenerators),
TEST_ITEM(SoftwareGPUJit),
TEST_ITEM(Path),
TEST_ITEM(AndroidContentURI),
TEST_ITEM(ThreadManager),

View file

@ -383,6 +383,7 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="TestShaderGenerators.cpp" />
<ClCompile Include="TestSoftwareGPUJit.cpp" />
<ClCompile Include="TestThreadManager.cpp" />
<ClCompile Include="TestVertexJit.cpp" />
<ClCompile Include="UnitTest.cpp" />

View file

@ -13,6 +13,7 @@
</ClCompile>
<ClCompile Include="TestShaderGenerators.cpp" />
<ClCompile Include="TestThreadManager.cpp" />
<ClCompile Include="TestSoftwareGPUJit.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="JitHarness.h" />