Files
ppsspp/unittest/TestShaderGenerators.cpp
T
Henrik RydgårdandClaude Opus 5.5 3cd6bf9d27 Tantalus depth fog: read depth at each render pixel's own sub-position
The Tantalus games (Cars Race-O-Rama, MX vs ATV Reflex and Untamed, SpongeBob)
turn depth into fog with a CLUT16 read of the swizzled depth mirror, stretched
2x into a 565 view of an 8888 buffer. A following 8888 copy writes the scene's
RGB and keeps the alpha, so each pixel's fog factor is the upper 565 half.

At a higher render scale, the depth X swizzle snapped every render pixel to its
PSP pixel's left edge, so the fog came out in PSP-pixel blocks. It now keeps the
position within the PSP pixel. That leaves the upper half sampling the right
half of the depth texel, half a pixel off the scene, which drew magenta halos on
the right of objects in Cars. With the new DepthCLUTIntoHalves compat flag, the
depal pass outputs twice as wide so each render pixel reads its own 8888 pixel's
depth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:48:05 +02:00

547 lines
15 KiB
C++

#include "ppsspp_config.h"
#include <algorithm>
#include "Common/StringUtils.h"
#include "GPU/Common/ShaderId.h"
#include "GPU/Common/ShaderCommon.h"
#include "GPU/Common/GPUStateUtils.h"
#include "Common/Data/Random/Rng.h"
#include "GPU/Vulkan/VulkanContext.h"
#include "GPU/Common/FragmentShaderGenerator.h"
#include "GPU/Common/VertexShaderGenerator.h"
#include "GPU/Common/ReinterpretFramebuffer.h"
#include "GPU/Common/StencilCommon.h"
#include "GPU/Common/DepalettizeShaderCommon.h"
#include "UnitTest.h"
#if PPSSPP_PLATFORM(WINDOWS)
#include <wrl/client.h>
#include "GPU/D3D11/D3D11Util.h"
#include "GPU/D3D11/D3D11Loader.h"
#endif
static constexpr size_t CODE_BUFFER_SIZE = 32768;
bool GenerateFShader(FShaderID id, char *buffer, ShaderLanguage lang, Draw::Bugs bugs, std::string *errorString) {
buffer[0] = '\0';
FragmentShaderFlags flags;
uint64_t uniformMask;
switch (lang) {
case ShaderLanguage::GLSL_VULKAN:
{
ShaderLanguageDesc compat(ShaderLanguage::GLSL_VULKAN);
return GenerateFragmentShader(id, buffer, compat, bugs, &uniformMask, &flags, errorString);
}
case ShaderLanguage::GLSL_1xx:
{
ShaderLanguageDesc compat(ShaderLanguage::GLSL_1xx);
return GenerateFragmentShader(id, buffer, compat, bugs, &uniformMask, &flags, errorString);
}
case ShaderLanguage::GLSL_3xx:
{
ShaderLanguageDesc compat(ShaderLanguage::GLSL_3xx);
return GenerateFragmentShader(id, buffer, compat, bugs, &uniformMask, &flags, errorString);
}
case ShaderLanguage::HLSL_D3D11:
{
ShaderLanguageDesc compat(ShaderLanguage::HLSL_D3D11);
return GenerateFragmentShader(id, buffer, compat, bugs, &uniformMask, &flags, errorString);
}
default:
return false;
}
}
bool GenerateVShader(VShaderID id, char *buffer, ShaderLanguage lang, Draw::Bugs bugs, std::string *errorString) {
buffer[0] = '\0';
VertexShaderFlags flags;
uint32_t attrMask;
uint64_t uniformMask;
switch (lang) {
case ShaderLanguage::GLSL_VULKAN:
{
ShaderLanguageDesc compat(ShaderLanguage::GLSL_VULKAN);
return GenerateVertexShader(id, buffer, compat, bugs, &attrMask, &uniformMask, &flags, errorString);
}
case ShaderLanguage::GLSL_1xx:
{
ShaderLanguageDesc compat(ShaderLanguage::GLSL_1xx);
return GenerateVertexShader(id, buffer, compat, bugs, &attrMask, &uniformMask, &flags, errorString);
}
case ShaderLanguage::GLSL_3xx:
{
ShaderLanguageDesc compat(ShaderLanguage::GLSL_3xx);
return GenerateVertexShader(id, buffer, compat, bugs, &attrMask, &uniformMask, &flags, errorString);
}
case ShaderLanguage::HLSL_D3D11:
{
ShaderLanguageDesc compat(ShaderLanguage::HLSL_D3D11);
return GenerateVertexShader(id, buffer, compat, bugs, &attrMask, &uniformMask, &flags, errorString);
}
default:
return false;
}
}
static VkShaderStageFlagBits StageToVulkan(ShaderStage stage) {
switch (stage) {
case ShaderStage::Vertex: return VK_SHADER_STAGE_VERTEX_BIT;
case ShaderStage::Compute: return VK_SHADER_STAGE_COMPUTE_BIT;
case ShaderStage::Fragment: return VK_SHADER_STAGE_FRAGMENT_BIT;
}
return VK_SHADER_STAGE_FRAGMENT_BIT;
}
bool TestCompileShader(const char *buffer, ShaderLanguage lang, ShaderStage stage, std::string *errorMessage) {
std::vector<uint32_t> spirv;
switch (lang) {
#if PPSSPP_PLATFORM(WINDOWS)
case ShaderLanguage::HLSL_D3D11:
{
const char *programType = nullptr;
switch (stage) {
case ShaderStage::Vertex: programType = "vs_4_0"; break;
case ShaderStage::Fragment: programType = "ps_4_0"; break;
default:
*errorMessage = "Unknown shader stage";
return false;
}
auto output = CompileShaderToBytecodeD3D11(buffer, strlen(buffer), programType, 0, errorMessage);
if (output.empty() && errorMessage->empty()) {
*errorMessage = "Error compiling HLSL shader: bytecode empty";
}
return !output.empty();
}
#endif
case ShaderLanguage::GLSL_VULKAN:
return GLSLtoSPV(StageToVulkan(stage), buffer, GLSLVariant::VULKAN, spirv, errorMessage);
case ShaderLanguage::GLSL_1xx:
return GLSLtoSPV(StageToVulkan(stage), buffer, GLSLVariant::GL140, spirv, errorMessage);
case ShaderLanguage::GLSL_3xx:
return GLSLtoSPV(StageToVulkan(stage), buffer, GLSLVariant::GLES300, spirv, errorMessage);
default:
*errorMessage = "Unknown shader language";
return false;
}
}
void PrintDiff(const char *a, const char *b) {
// Stupidest diff ever: Just print both lines, and a few around it, when we find a mismatch.
std::vector<std::string> a_lines;
std::vector<std::string> b_lines;
SplitString(a, '\n', a_lines);
SplitString(b, '\n', b_lines);
for (size_t i = 0; i < a_lines.size() && i < b_lines.size(); i++) {
if (a_lines[i] != b_lines[i]) {
// Print some context
for (size_t j = std::max((int)i - 4, 0); j < i; j++) {
printf("%s\n", a_lines[j].c_str());
}
printf("DIFF found at line %d:\n", (int)i);
printf("a: %s\n", a_lines[i].c_str());
printf("b: %s\n", b_lines[i].c_str());
printf("...continues...\n");
for (size_t j = i + 1; j < i + 5 && j < a_lines.size() && j < b_lines.size(); j++) {
printf("a: %s\n", a_lines[j].c_str());
printf("b: %s\n", b_lines[j].c_str());
}
printf("==================\n");
return;
}
}
}
const char *ShaderLanguageToString(ShaderLanguage lang) {
switch (lang) {
case HLSL_D3D11: return "HLSL_D3D11";
case GLSL_VULKAN: return "GLSL_VULKAN";
case GLSL_1xx: return "GLSL_1xx";
case GLSL_3xx: return "GLSL_3xx";
default: return "N/A";
}
}
bool TestReinterpretShaders() {
Draw::Bugs bugs;
ShaderLanguage languages[] = {
#if PPSSPP_PLATFORM(WINDOWS)
ShaderLanguage::HLSL_D3D11,
#endif
ShaderLanguage::GLSL_VULKAN,
ShaderLanguage::GLSL_3xx,
};
GEBufferFormat fmts[3] = {
GE_FORMAT_565,
GE_FORMAT_5551,
GE_FORMAT_4444,
};
char *buffer = new char[65536];
// Generate all despite failures - it's only 6.
bool failed = false;
for (int k = 0; k < ARRAY_SIZE(languages); k++) {
if (g_testLog) {
printf("=== %s ===\n\n", ShaderLanguageToString(languages[k]));
}
ShaderLanguageDesc desc(languages[k]);
std::string errorMessage;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == j)
continue; // useless shader!
ShaderWriter writer(buffer, desc, ShaderStage::Fragment);
GenerateReinterpretFragmentShader(writer, fmts[i], fmts[j]);
if (strlen(buffer) >= 8192) {
printf("Reinterpret fragment shader %d exceeded buffer:\n\n%s\n", (int)j, LineNumberString(buffer).c_str());
failed = true;
}
if (!TestCompileShader(buffer, languages[k], ShaderStage::Fragment, &errorMessage)) {
printf("Error compiling reinterpret fragment shader %d:\n\n%s\n\n%s\n", (int)j, LineNumberString(buffer).c_str(), errorMessage.c_str());
failed = true;
} else {
if (g_testLog) {
printf("===\n%s\n===\n", buffer);
}
}
}
}
}
delete[] buffer;
return !failed;
}
bool TestStencilShaders() {
Draw::Bugs bugs;
ShaderLanguage languages[] = {
#if PPSSPP_PLATFORM(WINDOWS)
ShaderLanguage::HLSL_D3D11,
#endif
ShaderLanguage::GLSL_VULKAN,
ShaderLanguage::GLSL_3xx,
};
char *buffer = new char[65536];
bool failed = false;
for (int k = 0; k < ARRAY_SIZE(languages); k++) {
if (g_testLog) {
printf("=== %s ===\n\n", ShaderLanguageToString(languages[k]));
}
ShaderLanguageDesc desc(languages[k]);
std::string errorMessage;
// Generate all despite failures - it's only a few.
// Only use export on Vulkan, because GLSL_3xx is ES which doesn't support stencil export.
bool allowUseExport = languages[k] == ShaderLanguage::GLSL_VULKAN;
for (int useExport = 0; useExport <= (allowUseExport ? 1 : 0); ++useExport) {
GenerateStencilFs(buffer, desc, bugs, useExport == 1);
if (strlen(buffer) >= 8192) {
printf("Stencil fragment shader (useExport=%d) exceeded buffer:\n\n%s\n", useExport, LineNumberString(buffer).c_str());
failed = true;
}
if (!TestCompileShader(buffer, languages[k], ShaderStage::Fragment, &errorMessage)) {
printf("Error compiling stencil shader (useExport=%d):\n\n%s\n\n%s\n", useExport, LineNumberString(buffer).c_str(), errorMessage.c_str());
failed = true;
} else {
if (g_testLog) {
printf("===\n%s\n===\n", buffer);
}
}
}
GenerateStencilVs(buffer, desc);
if (strlen(buffer) >= 8192) {
printf("Stencil vertex shader exceeded buffer:\n\n%s\n", LineNumberString(buffer).c_str());
failed = true;
}
if (!TestCompileShader(buffer, languages[k], ShaderStage::Vertex, &errorMessage)) {
printf("Error compiling stencil shader:\n\n%s\n\n%s\n", LineNumberString(buffer).c_str(), errorMessage.c_str());
failed = true;
} else {
if (g_testLog) {
printf("===\n%s\n===\n", buffer);
}
}
}
delete[] buffer;
return !failed;
}
bool TestDepalShaders() {
Draw::Bugs bugs;
ShaderLanguage languages[] = {
#if PPSSPP_PLATFORM(WINDOWS)
ShaderLanguage::HLSL_D3D11,
#endif
ShaderLanguage::GLSL_VULKAN,
ShaderLanguage::GLSL_3xx,
ShaderLanguage::GLSL_1xx,
};
char *buffer = new char[65536];
for (int k = 0; k < ARRAY_SIZE(languages); k++) {
if (g_testLog) {
printf("=== %s ===\n\n", ShaderLanguageToString(languages[k]));
}
ShaderLanguageDesc desc(languages[k]);
std::string errorMessage;
// TODO: Try more configurations of the fragment shader.
DepalConfig configs[2]{};
configs[0].clutFormat = GE_CMODE_16BIT_ABGR4444;
configs[0].shift = 8;
configs[0].startPos = 64;
configs[0].mask = 0xFF;
configs[0].bufferFormat = GE_FORMAT_8888;
configs[0].textureFormat = GE_TFMT_CLUT32;
configs[0].depthUpperBits = 0;
// A depth read through the X swizzle, stretched into 16-bit halves (Cars Race-O-Rama).
configs[1].clutFormat = GE_CMODE_16BIT_BGR5650;
configs[1].shift = 1;
configs[1].mask = 0xFF;
configs[1].bufferFormat = GE_FORMAT_DEPTH16;
configs[1].textureFormat = GE_TFMT_CLUT16;
configs[1].depthUpperBits = 2;
configs[1].depthIntoHalves = true;
for (const DepalConfig &config : configs) {
ShaderWriter writer(buffer, desc, ShaderStage::Fragment);
GenerateDepalFs(writer, config);
if (strlen(buffer) >= 8192) {
printf("Depal shader exceeded buffer:\n\n%s\n", LineNumberString(buffer).c_str());
delete[] buffer;
return false;
}
if (!TestCompileShader(buffer, languages[k], ShaderStage::Fragment, &errorMessage)) {
printf("Error compiling depal shader:\n\n%s\n\n%s\n", LineNumberString(buffer).c_str(), errorMessage.c_str());
delete[] buffer;
return false;
} else {
if (g_testLog) {
printf("===\n%s\n===\n", buffer);
}
}
}
}
delete[] buffer;
return true;
}
const ShaderLanguage languages[] = {
#if PPSSPP_PLATFORM(WINDOWS)
ShaderLanguage::HLSL_D3D11,
#endif
ShaderLanguage::GLSL_VULKAN,
ShaderLanguage::GLSL_1xx,
ShaderLanguage::GLSL_3xx,
};
const int numLanguages = ARRAY_SIZE(languages);
bool TestVertexShaders() {
char *buffer[numLanguages];
for (int i = 0; i < numLanguages; i++) {
buffer[i] = new char[65536];
}
GMRng rng;
int successes = 0;
int count = 700;
Draw::Bugs bugs;
// Generate a bunch of random vertex shader IDs, try to generate shader source.
// Then compile it and check that it's ok.
for (int i = 0; i < count; i++) {
uint64_t id64 = rng.R64();
VShaderID id;
id.FromUint64(id64);
// The generated bits need some adjustment:
// If mode is through, we won't do hardware transform.
if (id.Bit(VS_BIT_IS_THROUGH)) {
id.SetBit(VS_BIT_USE_HW_TRANSFORM, 0);
}
if (id.Bit(VS_BIT_VERTEX_RANGE_CULLING)) {
continue;
}
bool generateSuccess[numLanguages]{};
std::string genErrorString[numLanguages];
for (int j = 0; j < numLanguages; j++) {
generateSuccess[j] = GenerateVShader(id, buffer[j], languages[j], bugs, &genErrorString[j]);
if (!genErrorString[j].empty()) {
if (g_testLog) {
printf("%s\n", genErrorString[j].c_str());
}
}
}
for (int j = 0; j < numLanguages; j++) {
if (strlen(buffer[j]) >= CODE_BUFFER_SIZE) {
printf("Vertex shader exceeded buffer:\n\n%s\n", LineNumberString(buffer[j]).c_str());
for (int i = 0; i < numLanguages; i++) {
delete[] buffer[i];
}
return false;
}
}
// Now that we have the strings ready for easy comparison (buffer,4 in the watch window),
// let's try to compile them.
for (int j = 0; j < numLanguages; j++) {
if (generateSuccess[j]) {
std::string errorMessage;
if (!TestCompileShader(buffer[j], languages[j], ShaderStage::Vertex, &errorMessage)) {
printf("Error compiling vertex shader %d:\n\n%s\n\nERROR: %s\n\n(end of error)\n\n", (int)j, LineNumberString(buffer[j]).c_str(), errorMessage.c_str());
for (int i = 0; i < numLanguages; i++) {
delete[] buffer[i];
}
return false;
}
successes++;
}
}
}
if (g_testLog) {
printf("%d/%d vertex shaders generated (it's normal that it's not all, there are invalid bit combos)\n", successes, count * numLanguages);
}
for (int i = 0; i < numLanguages; i++) {
delete[] buffer[i];
}
return true;
}
bool TestFragmentShaders() {
char *buffer[numLanguages];
for (int i = 0; i < numLanguages; i++) {
buffer[i] = new char[65536];
}
GMRng rng;
int successes = 0;
int count = 300;
Draw::Bugs bugs;
// Generate a bunch of random fragment shader IDs, try to generate shader source.
// Then compile it and check that it's ok.
for (int i = 0; i < count; i++) {
uint64_t id64 = rng.R64();
FShaderID id;
id.FromUint64(id64);
// bits we don't need to test because they are irrelevant on d3d11
id.SetBit(FS_BIT_NO_DEPTH_CANNOT_DISCARD_STENCIL, false);
// DX9 disabling:
if (static_cast<ReplaceAlphaType>(id.Bits(FS_BIT_STENCIL_TO_ALPHA, 2)) == ReplaceAlphaType::REPLACE_ALPHA_DUALSOURCE)
continue;
bool generateSuccess[numLanguages]{};
std::string genErrorString[numLanguages];
for (int j = 0; j < numLanguages; j++) {
generateSuccess[j] = GenerateFShader(id, buffer[j], languages[j], bugs, &genErrorString[j]);
if (!genErrorString[j].empty()) {
if (g_testLog) {
printf("%s\n", genErrorString[j].c_str());
}
}
// We ignore the contents of the error string here, not even gonna try to compile if it errors.
}
for (int j = 0; j < numLanguages; j++) {
if (strlen(buffer[j]) >= CODE_BUFFER_SIZE) {
printf("Fragment shader exceeded buffer:\n\n%s\n", LineNumberString(buffer[j]).c_str());
for (int i = 0; i < numLanguages; i++) {
delete[] buffer[i];
}
return false;
}
}
// Now that we have the strings ready for easy comparison (buffer,4 in the watch window),
// let's try to compile them.
for (int j = 0; j < numLanguages; j++) {
if (generateSuccess[j]) {
std::string errorMessage;
if (!TestCompileShader(buffer[j], languages[j], ShaderStage::Fragment, &errorMessage)) {
printf("Error compiling fragment shader %d:\n\n%s\n\nERROR: %s\n\n(end of error)\n\n", (int)j, LineNumberString(buffer[j]).c_str(), errorMessage.c_str());
for (int i = 0; i < numLanguages; i++) {
delete[] buffer[i];
}
return false;
}
successes++;
}
}
}
if (g_testLog) {
printf("%d/%d fragment shaders generated (it's normal that it's not all, there are invalid bit combos)\n", successes, count * numLanguages);
}
for (int i = 0; i < numLanguages; i++) {
delete[] buffer[i];
}
return true;
}
bool TestShaderGenerators() {
#if PPSSPP_PLATFORM(WINDOWS)
LoadD3D11();
init_glslang();
#else
init_glslang();
#endif
if (!TestStencilShaders()) {
return false;
}
if (!TestReinterpretShaders()) {
return false;
}
if (!TestDepalShaders()) {
return false;
}
if (!TestFragmentShaders()) {
return false;
}
if (!TestVertexShaders()) {
return false;
}
return true;
}