Files
ppsspp/GPU/Common/VertexShaderGenerator.cpp
T
Henrik RydgårdandClaude Opus 5.5 b25344ab7b Discard against one-sided depth ranges in the fragment fallback
Without clip distances, the fragment shader's min/max Z discard only came on
when both minz and maxz were set, while the clip planes take each side on
its own. Pursuit Force's sky has minz 0 and maxz 300 and lies past 300, so
the PSP drops it (its replays of 11399 UCES00019 and 11216 UCUS98703 show
that), but GPUs without clip distances drew it over the scene: 13.9k px off
the PSP on a Mali-G71, now 8 as with clip distances. The discard can now
come with the fragment depth clamp, for the side that's at the limit.

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

1029 lines
42 KiB
C++

// Copyright (c) 2012- 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/StringUtils.h"
#include "Common/GPU/OpenGL/GLFeatures.h"
#include "Common/GPU/ShaderWriter.h"
#include "Common/GPU/thin3d.h"
#include "Core/Config.h"
#include "Core/System.h"
#include "GPU/ge_constants.h"
#include "GPU/GPUState.h"
#include "GPU/Common/ShaderId.h"
#include "GPU/Common/ShaderUniforms.h"
#include "GPU/Common/VertexShaderGenerator.h"
#include "GPU/Vulkan/DrawEngineVulkan.h"
#undef WRITE
#define WRITE(p, ...) p.F(__VA_ARGS__)
// Depth range and viewport
//
// After the multiplication with the projection matrix, we have a 4D vector in clip space.
// In OpenGL, Z is from -1 to 1, while in D3D, Z is from 0 to 1.
// PSP appears to use the OpenGL convention. As Z is from -1 to 1, and the viewport is represented
// by a center and a scale, to find the final Z value, all we need to do is to multiply by ZScale and
// add ZCenter - these are properly scaled to directly give a Z value in [0, 65535].
//
// z = vec.z * ViewportZScale + ViewportZCenter;
//
// That will give us the final value between 0 and 65535, which we can simply floor to simulate
// the limited precision of the PSP's depth buffer. Then we convert it back:
// z = floor(z);
//
// vec.z = (z - ViewportZCenter) / ViewportZScale;
//
// Now, the regular machinery will take over and do the calculation again.
//
// Depth is not clipped to the viewport, but does clip to "minz" and "maxz". It may also be clamped
// to 0 and 65535 if a depth clamping/clipping flag is set (x/y clipping is performed only if depth
// needs to be clamped.)
//
// Additionally, depth is clipped to negative z based on vec.z (before viewport), at -1.
//
// All this above is for full transform mode.
// In through mode, the Z coordinate just goes straight through and there is no perspective division.
// We simulate this of course with pretty much an identity matrix. Rounding Z becomes very easy.
//
// TODO: Skip all this if we can actually get a 16-bit depth buffer along with stencil, which
// is a bit of a rare configuration, although quite common on mobile.
bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguageDesc &compat, Draw::Bugs bugs, uint32_t *attrMask, uint64_t *uniformMask, VertexShaderFlags *vertexShaderFlags, std::string *errorString) {
*attrMask = 0;
*uniformMask = 0;
*vertexShaderFlags = (VertexShaderFlags)0;
bool highpFog = false;
// Highp costs nothing here, and the fragment shader can then pick it up where it needs it (CLUT8 depal).
bool highpTexcoord = gstate_c.Use(GPU_USE_FULL_PRECISION_IN_FRAGMENT);
const bool isModeThrough = id.Bit(VS_BIT_IS_THROUGH);
const bool useHWTransform = id.Bit(VS_BIT_USE_HW_TRANSFORM);
const bool clipNearPlane = gstate_c.Use(GPU_USE_CLIP_DISTANCE) && useHWTransform;
const bool clipMinMax = gstate_c.Use(GPU_USE_CLIP_DISTANCE) && !isModeThrough; // If clip planes are available, we want to use them for min/max. We skip the min/max culling in software transform (not yet implemented).
const bool rangeCulling = id.Bit(VS_BIT_VERTEX_RANGE_CULLING);
const bool depthCullEnable = gstate_c.Use(GPU_USE_CULL_DISTANCE) && !isModeThrough && rangeCulling && useHWTransform; // Range culling is gated on draw type, we don't want to do this culling for splines apparently.
std::vector<const char*> extensions;
extensions.reserve(6);
if (ShaderLanguageIsOpenGL(compat.shaderLanguage)) {
if (gl_extensions.EXT_gpu_shader4) {
extensions.push_back("#extension GL_EXT_gpu_shader4 : enable");
}
if (gl_extensions.EXT_clip_cull_distance && (depthCullEnable || clipMinMax || clipNearPlane)) {
extensions.push_back("#extension GL_EXT_clip_cull_distance : enable");
}
if (gl_extensions.APPLE_clip_distance && (clipMinMax || clipNearPlane)) {
extensions.push_back("#extension GL_APPLE_clip_distance : enable");
}
if (gl_extensions.ARB_cull_distance && depthCullEnable) {
extensions.push_back("#extension GL_ARB_cull_distance : enable");
}
}
bool useSimpleStereo = id.Bit(VS_BIT_SIMPLE_STEREO);
if (useSimpleStereo) {
if (compat.shaderLanguage != ShaderLanguage::GLSL_VULKAN) {
*errorString = "Multiview only supported with Vulkan for now";
return false;
}
extensions.push_back("#extension GL_EXT_multiview : enable");
}
ShaderWriter p(buffer, compat, ShaderStage::Vertex, extensions);
p.C("// %").W(id.Description()).endl();
bool lmode = id.Bit(VS_BIT_LMODE);
GETexMapMode uvGenMode = static_cast<GETexMapMode>(id.Bits(VS_BIT_UVGEN_MODE, 2));
bool doTextureTransform = uvGenMode == GE_TEXMAP_TEXTURE_MATRIX;
// this is only valid for some settings of uvGenMode
GETexProjMapMode uvProjMode = static_cast<GETexProjMapMode>(id.Bits(VS_BIT_UVPROJ_MODE, 2));
bool doShadeMapping = uvGenMode == GE_TEXMAP_ENVIRONMENT_MAP;
bool flatBug = bugs.Has(Draw::Bugs::BROKEN_FLAT_IN_SHADER) && g_Config.bVendorBugChecksEnabled;
bool nanBug = bugs.Has(Draw::Bugs::BROKEN_NAN_IN_CONDITIONAL) && g_Config.bVendorBugChecksEnabled;
bool doFlatShading = id.Bit(VS_BIT_FLATSHADE) && !flatBug;
bool hasColor = id.Bit(VS_BIT_HAS_COLOR);
bool hasNormal = id.Bit(VS_BIT_HAS_NORMAL) && useHWTransform;
bool hasTexcoord = id.Bit(VS_BIT_HAS_TEXCOORD) || !useHWTransform;
bool flipNormal = id.Bit(VS_BIT_NORM_REVERSE);
int ls0 = id.Bits(VS_BIT_LS0, 2);
int ls1 = id.Bits(VS_BIT_LS1, 2);
bool enableLighting = id.Bit(VS_BIT_LIGHTING_ENABLE);
int matUpdate = id.Bits(VS_BIT_MATERIAL_UPDATE, 3);
bool lightUberShader = id.Bit(VS_BIT_LIGHT_UBERSHADER) && enableLighting; // checking lighting here for the shader test's benefit, in reality if ubershader is set, lighting is set.
// With the ubershader, shade mapping reads its lights' type and computation from u_lightControl.
bool shadeUberShader = id.Bit(VS_BIT_LIGHT_UBERSHADER) && doShadeMapping;
if ((lightUberShader || shadeUberShader) && !compat.bitwiseOps) {
*errorString = "Light ubershader requires bitwise ops in shader language";
return false;
}
// Should we do the min/max discard in the shader and/or or use depth clamping?
// In both cases we need to just forward
const bool fsMinmaxDiscard = id.Bit(VS_BIT_FS_MINMAX_DISCARD);
const bool fsDepthClamp = id.Bit(VS_BIT_FS_DEPTH_CLAMP);
const char *shading = "";
if (compat.glslES30 || compat.shaderLanguage == GLSL_VULKAN)
shading = doFlatShading ? "flat " : "";
DoLightComputation doLight[4] = { LIGHT_OFF, LIGHT_OFF, LIGHT_OFF, LIGHT_OFF };
if (useHWTransform) {
int shadeLight0 = doShadeMapping ? ls0 : -1;
int shadeLight1 = doShadeMapping ? ls1 : -1;
for (int i = 0; i < 4; i++) {
if (i == shadeLight0 || i == shadeLight1)
doLight[i] = LIGHT_SHADE;
if (enableLighting && id.Bit(VS_BIT_LIGHT0_ENABLE + i))
doLight[i] = LIGHT_FULL;
}
}
bool texCoordInVec3 = false;
const char *minZClipPlaneSuffix = "[0]";
const char *maxZClipPlaneSuffix = "[1]";
const char *zClipPlaneSuffix = "[2]";
const char *cullDistanceNearSuffix = "[0]";
const char *cullDistanceFarSuffix = "[1]";
if (compat.shaderLanguage == GLSL_VULKAN) {
WRITE(p, "\n");
WRITE(p, "layout (std140, set = 0, binding = %d) uniform baseVars {\n%s};\n", DRAW_BINDING_DYNUBO_BASE, ub_baseStr);
if (enableLighting || doShadeMapping)
WRITE(p, "layout (std140, set = 0, binding = %d) uniform lightVars {\n%s};\n", DRAW_BINDING_DYNUBO_LIGHT, ub_vs_lightsStr);
if (useHWTransform)
WRITE(p, "layout (location = %d) in vec3 position;\n", (int)PspAttributeLocation::POSITION);
else
WRITE(p, "layout (location = %d) in vec4 position;\n", (int)PspAttributeLocation::POSITION);
if (useHWTransform && hasNormal)
WRITE(p, "layout (location = %d) in vec3 normal;\n", (int)PspAttributeLocation::NORMAL);
if (!useHWTransform)
WRITE(p, "layout (location = %d) in float fog;\n", (int)PspAttributeLocation::NORMAL);
if (hasTexcoord) {
if (!useHWTransform && doTextureTransform && !isModeThrough) {
WRITE(p, "layout (location = %d) in vec3 texcoord;\n", (int)PspAttributeLocation::TEXCOORD);
texCoordInVec3 = true;
} else {
WRITE(p, "layout (location = %d) in vec2 texcoord;\n", (int)PspAttributeLocation::TEXCOORD);
}
}
if (hasColor || !useHWTransform) {
WRITE(p, "layout (location = %d) in vec4 color0;\n", (int)PspAttributeLocation::COLOR0);
if (lmode && !useHWTransform) // only software transform supplies color1 as vertex data
WRITE(p, "layout (location = %d) in vec3 color1;\n", (int)PspAttributeLocation::COLOR1);
}
// Centroid keeps them inside the primitive on pixels it only partly covers with MSAA, so that
// textures aren't sampled from outside the UV range. Without MSAA, it's the same as center.
const char *colorQualifier = *shading ? shading : "centroid ";
WRITE(p, "layout (location = 1) %sout lowp vec4 v_color0;\n", colorQualifier);
if (lmode) {
WRITE(p, "layout (location = 2) %sout lowp vec3 v_color1;\n", colorQualifier);
}
WRITE(p, "layout (location = 0) centroid out highp vec3 v_texcoord;\n");
WRITE(p, "layout (location = 3) out highp float v_fogdepth;\n");
if (fsMinmaxDiscard || fsDepthClamp) {
WRITE(p, "layout (location = 4) out highp vec2 v_zw;\n");
}
WRITE(p, "invariant gl_Position;\n");
} else if (compat.shaderLanguage == HLSL_D3D11) {
// Note: These two share some code after this hellishly large if/else.
WRITE(p, "cbuffer base : register(b0) {\n%s};\n", ub_baseStr);
WRITE(p, "cbuffer lights: register(b1) {\n%s};\n", ub_vs_lightsStr);
// And the "varyings".
if (useHWTransform) {
WRITE(p, "struct VS_IN {\n");
if (hasTexcoord) {
WRITE(p, " vec2 texcoord : TEXCOORD0;\n");
}
if (hasColor || !useHWTransform) {
WRITE(p, " vec4 color0 : COLOR0;\n");
}
if (hasNormal) {
WRITE(p, " vec3 normal : NORMAL;\n");
}
WRITE(p, " vec3 position : POSITION;\n");
WRITE(p, "};\n");
} else {
WRITE(p, "struct VS_IN {\n");
WRITE(p, " vec4 position : POSITION;\n");
if (hasTexcoord) {
if (doTextureTransform && !isModeThrough) {
texCoordInVec3 = true;
WRITE(p, " vec3 texcoord : TEXCOORD0;\n");
} else {
WRITE(p, " vec2 texcoord : TEXCOORD0;\n");
}
}
if (hasColor || !useHWTransform) {
WRITE(p, " vec4 color0 : COLOR0;\n");
}
// only software transform supplies color1 as vertex data
if (lmode) {
WRITE(p, " vec3 color1 : COLOR1;\n");
}
WRITE(p, " float fog : NORMAL;\n");
WRITE(p, "};\n");
}
WRITE(p, "struct VS_OUT {\n");
WRITE(p, " vec3 v_texcoord : TEXCOORD0;\n");
const char *colorInterpolation = (doFlatShading && compat.shaderLanguage == HLSL_D3D11) ? "nointerpolation " : "";
WRITE(p, " %svec4 v_color0 : COLOR0;\n", colorInterpolation);
if (lmode) {
WRITE(p, " %svec3 v_color1 : COLOR1;\n", colorInterpolation);
}
WRITE(p, " float v_fogdepth : TEXCOORD1;\n");
if (fsMinmaxDiscard || fsDepthClamp) {
WRITE(p, " highp vec2 v_zw : TEXCOORD2;\n");
}
// gl_Position must be last for D3D11.
WRITE(p, " vec4 gl_Position : SV_Position;\n");
if (clipMinMax && clipNearPlane) {
WRITE(p, " float3 gl_ClipDistance : SV_ClipDistance;\n");
} else if (clipMinMax) {
WRITE(p, " float2 gl_ClipDistance : SV_ClipDistance;\n");
} else if (clipNearPlane) {
_dbg_assert_(false); // not an allowed combination
}
if (depthCullEnable) {
WRITE(p, " float2 gl_CullDistance : SV_CullDistance0;\n");
}
WRITE(p, "};\n");
} else {
// Non-Vulkan GLSL
if (useHWTransform)
WRITE(p, "%s vec3 position;\n", compat.attribute);
else
WRITE(p, "%s vec4 position;\n", compat.attribute); // XYZW clip space coordinate.
*attrMask |= 1 << ATTR_POSITION;
if (useHWTransform && hasNormal) {
WRITE(p, "%s mediump vec3 normal;\n", compat.attribute);
*attrMask |= 1 << ATTR_NORMAL;
}
if (!useHWTransform) {
WRITE(p, "%s highp float fog;\n", compat.attribute);
*attrMask |= 1 << ATTR_NORMAL;
}
if (hasTexcoord) {
if (!useHWTransform && doTextureTransform && !isModeThrough) {
WRITE(p, "%s vec3 texcoord;\n", compat.attribute);
texCoordInVec3 = true;
} else {
WRITE(p, "%s vec2 texcoord;\n", compat.attribute);
}
*attrMask |= 1 << ATTR_TEXCOORD;
}
if (hasColor || !useHWTransform) {
WRITE(p, "%s lowp vec4 color0;\n", compat.attribute);
*attrMask |= 1 << ATTR_COLOR0;
if (lmode && !useHWTransform) { // only software transform supplies color1 as vertex data
WRITE(p, "%s lowp vec3 color1;\n", compat.attribute);
*attrMask |= 1 << ATTR_COLOR1;
}
}
WRITE(p, "uniform vec4 u_xywh;\n");
WRITE(p, "uniform float u_NaN;\n");
*uniformMask |= DIRTY_FRAMEBUFFER_DIM;
// highp to match the fragment shader's (see there).
WRITE(p, "uniform highp vec2 u_minZmaxZ;\n");
*uniformMask |= DIRTY_RASTER_OFFSET; // this flag is shared with raster offset.
if (!isModeThrough) {
WRITE(p, "uniform vec2 u_rasterOffset;\n");
*uniformMask |= DIRTY_RASTER_OFFSET;
}
if (useHWTransform) {
WRITE(p, "uniform vec3 u_vpScale;\n");
WRITE(p, "uniform vec3 u_vpOffset;\n");
if (gstate_c.Use(GPU_USE_VIRTUAL_REALITY)) {
WRITE(p, "uniform mat4 u_proj_lens;\n");
}
WRITE(p, "uniform mat4 u_proj;\n");
*uniformMask |= DIRTY_VIEWPORT_UNIFORMS | DIRTY_PROJMATRIX;
// TODO: Use 4x3 matrices where possible (world and view and maybe tex, not proj).
WRITE(p, "uniform mat4 u_world;\n");
WRITE(p, "uniform mat4 u_view;\n");
*uniformMask |= DIRTY_WORLDMATRIX | DIRTY_VIEWMATRIX;
if (doTextureTransform) {
WRITE(p, "uniform mediump mat4 u_texmtx;\n");
*uniformMask |= DIRTY_TEXMATRIX;
}
WRITE(p, "uniform vec4 u_uvscaleoffset;\n");
*uniformMask |= DIRTY_UVSCALEOFFSET;
if (lightUberShader || shadeUberShader) {
p.C("uniform uint u_lightControl;\n");
*uniformMask |= DIRTY_LIGHT_CONTROL;
}
for (int i = 0; i < 4; i++) {
if (lightUberShader || doLight[i] != LIGHT_OFF) {
// This is needed for shade mapping
WRITE(p, "uniform vec3 u_lightpos%i;\n", i);
*uniformMask |= DIRTY_LIGHT0 << i;
}
if (lightUberShader || doLight[i] == LIGHT_FULL) {
*uniformMask |= DIRTY_LIGHT0 << i;
GELightType type = static_cast<GELightType>(id.Bits(VS_BIT_LIGHT0_TYPE + 4 * i, 2));
GELightComputation comp = static_cast<GELightComputation>(id.Bits(VS_BIT_LIGHT0_COMP + 4 * i, 2));
if (lightUberShader || type != GE_LIGHTTYPE_DIRECTIONAL)
WRITE(p, "uniform mediump vec3 u_lightatt%i;\n", i);
if (lightUberShader || type == GE_LIGHTTYPE_SPOT || type == GE_LIGHTTYPE_UNKNOWN) {
WRITE(p, "uniform mediump vec3 u_lightdir%i;\n", i);
WRITE(p, "uniform mediump vec2 u_lightangle_spotCoef%i;\n", i);
}
WRITE(p, "uniform lowp vec3 u_lightambient%i;\n", i);
WRITE(p, "uniform lowp vec3 u_lightdiffuse%i;\n", i);
if (lightUberShader || comp == GE_LIGHTCOMP_BOTH) {
WRITE(p, "uniform lowp vec3 u_lightspecular%i;\n", i);
}
}
}
if (enableLighting) {
WRITE(p, "uniform lowp vec4 u_ambient;\n");
*uniformMask |= DIRTY_AMBIENT;
if (lightUberShader || (matUpdate & 2) == 0 || !hasColor) {
WRITE(p, "uniform lowp vec3 u_matdiffuse;\n");
*uniformMask |= DIRTY_MATDIFFUSE;
}
WRITE(p, "uniform lowp vec4 u_matspecular;\n"); // Specular coef is contained in alpha
WRITE(p, "uniform lowp vec3 u_matemissive;\n");
*uniformMask |= DIRTY_MATSPECULAR | DIRTY_MATEMISSIVE;
}
}
if (gstate_c.Use(GPU_USE_VIRTUAL_REALITY)) {
WRITE(p, "uniform lowp float u_scaleX;\n");
WRITE(p, "uniform lowp float u_scaleY;\n");
}
if (useHWTransform) {
WRITE(p, "uniform lowp vec4 u_matambientalpha;\n"); // matambient + matalpha
*uniformMask |= DIRTY_MATAMBIENTALPHA;
}
WRITE(p, "uniform highp vec2 u_fogcoef;\n");
*uniformMask |= DIRTY_FOGCOEF;
WRITE(p, "%s%s lowp vec4 v_color0;\n", shading, compat.varying_vs);
if (lmode) {
WRITE(p, "%s%s lowp vec3 v_color1;\n", shading, compat.varying_vs);
}
WRITE(p, "%s %s vec3 v_texcoord;\n", compat.varying_vs, highpTexcoord ? "highp" : "mediump");
// See the fragment shader generator
if (highpFog) {
WRITE(p, "%s highp float v_fogdepth;\n", compat.varying_vs);
} else {
WRITE(p, "%s mediump float v_fogdepth;\n", compat.varying_vs);
}
if (fsMinmaxDiscard || fsDepthClamp) {
WRITE(p, "%s highp vec2 v_zw;\n", compat.varying_vs);
}
}
if (useHWTransform) {
WRITE(p, "vec3 normalizeOr001(vec3 v) {\n");
WRITE(p, " float len2 = dot(v, v);\n");
WRITE(p, " return len2 == 0.0 ? vec3(0.0, 0.0, 1.0) : (v * inversesqrt(len2));\n");
WRITE(p, "}\n");
WRITE(p, "vec3 normalizeOr000(vec3 v) {\n");
WRITE(p, " float len2 = dot(v, v);\n");
WRITE(p, " return len2 == 0.0 ? splat3(0.0) : (v * inversesqrt(len2));\n");
WRITE(p, "}\n");
// The GE's pow for lighting: 1 for e <= 0, else 0 for x <= 0. Otherwise exp2(e * log2(x)) with
// log2 and exp2 each a straight line between powers of two, which is what reading a float's
// bits as an integer gives: exponent plus mantissa, scaled by 2^23. Without integers, a true
// pow is close enough.
WRITE(p, "float pspPow(float x, float e) {\n");
if (compat.bitwiseOps) {
// 1065353216 is 1.0's bits. Not written in hex: HLSL makes 0x3F800000 a uint, and the subtraction wraps.
WRITE(p, " float t = max(e, 0.0) * float(floatBitsToInt(max(x, 1e-30)) - 1065353216) + 1065353216.0;\n");
WRITE(p, " return x > 0.0 || e <= 0.0 ? intBitsToFloat(int(max(t, 0.0))) : 0.0;\n");
} else {
WRITE(p, " return e <= 0.0 ? 1.0 : pow(max(x, 0.0), e);\n");
}
WRITE(p, "}\n");
}
if (ShaderLanguageIsOpenGL(compat.shaderLanguage) || compat.shaderLanguage == GLSL_VULKAN) {
WRITE(p, "void main() {\n");
} else if (compat.shaderLanguage == HLSL_D3D11) {
WRITE(p, "VS_OUT main(VS_IN In) {\n");
WRITE(p, " VS_OUT Out;\n");
if (hasTexcoord) {
if (texCoordInVec3) {
WRITE(p, " vec3 texcoord = In.texcoord;\n");
} else {
WRITE(p, " vec2 texcoord = In.texcoord;\n");
}
}
if (hasColor || !useHWTransform) {
WRITE(p, " vec4 color0 = In.color0;\n");
if (lmode && !useHWTransform) {
WRITE(p, " vec3 color1 = In.color1;\n");
}
}
if (hasNormal) {
WRITE(p, " vec3 normal = In.normal;\n");
}
if (useHWTransform) {
WRITE(p, " vec3 position = In.position;\n");
} else {
WRITE(p, " vec4 position = In.position;\n");
}
if (!useHWTransform) {
WRITE(p, " float fog = In.fog;\n");
}
}
WRITE(p, " bool zClipped = false;\n");
if (!useHWTransform) {
// Simple pass-through of vertex data to fragment shader
if (texCoordInVec3) {
WRITE(p, " %sv_texcoord = texcoord;\n", compat.vsOutPrefix);
} else {
WRITE(p, " %sv_texcoord = vec3(texcoord, 1.0);\n", compat.vsOutPrefix);
}
WRITE(p, " %sv_color0 = color0;\n", compat.vsOutPrefix);
if (lmode) {
WRITE(p, " %sv_color1 = color1;\n", compat.vsOutPrefix);
}
WRITE(p, " %sv_fogdepth = fog;\n", compat.vsOutPrefix);
// If non-through, the viewport has already been applied here.
WRITE(p, " vec4 outPos = position;\n");
if (fsMinmaxDiscard) {
WRITE(p, " %sv_zw = vec2(outPos.z * outPos.w, outPos.w);\n", compat.vsOutPrefix);
}
} else {
// Step 1: World Transform (bones/skinning is now always handled in decode)
WRITE(p, " vec3 worldpos = mul(vec4(position, 1.0), u_world).xyz;\n");
if (hasNormal) {
WRITE(p, " mediump vec3 worldnormal = normalizeOr001(mul(vec4(%snormal, 0.0), u_world).xyz);\n", flipNormal ? "-" : "");
} else {
WRITE(p, " mediump vec3 worldnormal = normalizeOr001(mul(vec4(0.0, 0.0, %s1.0, 0.0), u_world).xyz);\n", flipNormal ? "-" : "");
}
if (enableLighting || doShadeMapping) {
// The viewer is at infinity along view space +z: in world space, the view matrix's third column.
if (compat.shaderLanguage == HLSL_D3D11) {
WRITE(p, " mediump vec3 viewDir = normalizeOr001(vec3(u_view[0].z, u_view[1].z, u_view[2].z));\n");
} else {
WRITE(p, " mediump vec3 viewDir = normalizeOr001(u_view[2].xyz);\n");
}
}
WRITE(p, " vec4 viewPos = vec4(mul(vec4(worldpos, 1.0), u_view).xyz, 1.0);\n");
if (useSimpleStereo) {
float ipd = 0.065f;
float scale = 1.0f;
if (PSP_CoreParameter().compat.vrCompat().UnitsPerMeter > 0) {
scale = PSP_CoreParameter().compat.vrCompat().UnitsPerMeter;
}
WRITE(p, " viewPos.x += %f * float(gl_ViewIndex * 2 - 1);\n", scale * ipd * 0.5);
}
// Final view and projection transforms.
if (gstate_c.Use(GPU_USE_VIRTUAL_REALITY)) {
WRITE(p, " vec4 outPos = mul(u_proj_lens, viewPos);\n");
WRITE(p, " vec4 orgPos = mul(u_proj, viewPos);\n");
} else {
WRITE(p, " vec4 outPos = mul(u_proj, viewPos);\n");
}
// We're in clip space, so here we check for clipping. We check if the actual hardware will clip.
// If so, we skip the "range culling" (x and y out-of-bounds checks) since they wouldn't have happened, most likely.
// NOTE: There are some games that depend on clipping already having been done when checking the range culling.
// We can't handle that here, we use the software transform pipeline for that.
WRITE(p, " if (outPos.z < -outPos.w) {\n");
WRITE(p, " zClipped = true;\n");
WRITE(p, " }\n");
if (fsDepthClamp) {
// For the rasterizer's Z below: 0 at the near plane, so it still clips there, and past W only beyond
// 65535 * W, so nothing else is clipped. The fragment shader writes the real depth.
WRITE(p, " float nearPlaneZ = (outPos.z + outPos.w) * (1.0 / 65536.0);\n");
}
// Then we actually add the clip plane.
if (clipNearPlane) {
WRITE(p, " %sgl_ClipDistance%s = outPos.z + outPos.w;\n", compat.vsOutPrefix, zClipPlaneSuffix);
}
if (depthCullEnable) {
// Before the viewport, discard any primitives that are fully outside the clipping volume in Z.
// NOTE: We add a small offset to the clip distance to allow hex 0x3F8000XX (up to 1.0000304) where XX are arbitrary.
WRITE(p, " %sgl_CullDistance%s = outPos.z + outPos.w + 0.0000304 / outPos.w;\n", compat.vsOutPrefix, cullDistanceNearSuffix);
WRITE(p, " %sgl_CullDistance%s = outPos.w - outPos.z + 0.0000304 / outPos.w;\n", compat.vsOutPrefix, cullDistanceFarSuffix);
}
// Perform the perspective projection and viewport transform. (We'll have to undo the division before passing the coordinate along).
// In software transform mode, this is performed in on the CPU.
WRITE(p, " float recip = 1.0 / outPos.w;\n");
WRITE(p, " outPos.xyz = (outPos.xyz * u_vpScale.xyz) * recip + u_vpOffset.xyz;\n");
if (fsMinmaxDiscard) {
WRITE(p, " %sv_zw = vec2(outPos.z * outPos.w, outPos.w);\n", compat.vsOutPrefix);
}
// TODO: Declare variables for dots for shade mapping if needed.
const char *srcCol = "color0";
if (lightUberShader && hasColor) {
p.F(" vec4 ambientColor = ((u_lightControl & (1u << 0x14u)) != 0x0u) ? %s : u_matambientalpha;\n", srcCol);
if (enableLighting) {
p.F(" vec3 diffuseColor = ((u_lightControl & (1u << 0x15u)) != 0x0u) ? %s.rgb : u_matdiffuse;\n", srcCol);
p.F(" vec3 specularColor = ((u_lightControl & (1u << 0x16u)) != 0x0u) ? %s.rgb : u_matspecular.rgb;\n", srcCol);
}
} else {
// This path also takes care of the lightUberShader && !hasColor path, because all comparisons fail.
p.F(" vec4 ambientColor = %s;\n", (matUpdate & 1) && hasColor ? srcCol : "u_matambientalpha");
if (enableLighting) {
p.F(" vec3 diffuseColor = %s.rgb;\n", (matUpdate & 2) && hasColor ? srcCol : "u_matdiffuse");
p.F(" vec3 specularColor = %s.rgb;\n", (matUpdate & 4) && hasColor ? srcCol : "u_matspecular");
}
}
bool diffuseIsZero = true;
bool specularIsZero = true;
bool distanceNeeded = false;
bool anySpots = false;
if (enableLighting) {
p.C(" lowp vec4 lightSum0 = u_ambient * ambientColor + vec4(u_matemissive, 0.0);\n");
for (int i = 0; i < 4; i++) {
GELightType type = static_cast<GELightType>(id.Bits(VS_BIT_LIGHT0_TYPE + 4*i, 2));
GELightComputation comp = static_cast<GELightComputation>(id.Bits(VS_BIT_LIGHT0_COMP + 4*i, 2));
if (doLight[i] != LIGHT_FULL)
continue;
diffuseIsZero = false;
if (comp == GE_LIGHTCOMP_BOTH)
specularIsZero = false;
if (type != GE_LIGHTTYPE_DIRECTIONAL)
distanceNeeded = true;
if (type == GE_LIGHTTYPE_SPOT || type == GE_LIGHTTYPE_UNKNOWN)
anySpots = true;
}
if (lightUberShader) {
anySpots = true;
diffuseIsZero = false;
specularIsZero = false;
distanceNeeded = true;
}
if (!specularIsZero) {
WRITE(p, " lowp vec3 lightSum1 = splat3(0.0);\n");
}
if (!diffuseIsZero) {
WRITE(p, " vec3 toLight;\n");
WRITE(p, " lowp vec3 diffuse;\n");
}
if (distanceNeeded) {
WRITE(p, " float distance;\n");
WRITE(p, " float distSq;\n");
WRITE(p, " float invDist;\n");
WRITE(p, " lowp float lightScale;\n");
}
WRITE(p, " mediump float ldot;\n");
if (anySpots) {
WRITE(p, " lowp float angle;\n");
}
}
// NOTE: Can't change this without updating uniform buffer declarations (for D3D11 and VK, the one in ShaderUniforms.h).
bool useIndexing = compat.shaderLanguage == HLSL_D3D11 || compat.shaderLanguage == GLSL_VULKAN;
char iStr[4];
if (lightUberShader) {
// We generate generic code that can calculate any combination of lights specified
// in u_lightControl. u_lightControl is computed in PackLightControlBits().
p.C(" uint comp; uint type; float attenuation;\n");
if (useIndexing) {
p.C(" for (uint i = 0; i < 4; i++) {\n");
}
// If we can use indexing, we actually loop in the shader now, using the loop emitted
// above. In that case, we only need to emit the code once, so the for loop here will
// only run for a single pass.
int count = useIndexing ? 1 : 4;
for (int i = 0; i < count; i++) {
snprintf(iStr, sizeof(iStr), useIndexing ? "[i]" : "%d", i);
if (useIndexing) {
p.C(" if ((u_lightControl & (0x1u << i)) != 0x0u) { \n");
p.C(" comp = (u_lightControl >> uint(0x4u + 0x4u * i)) & 0x3u;\n");
p.C(" type = (u_lightControl >> uint(0x4u + 0x4u * i + 0x2u)) & 0x3u;\n");
} else {
p.F(" if ((u_lightControl & 0x%xu) != 0x0u) { \n", 1 << i);
p.F(" comp = (u_lightControl >> 0x%02xu) & 0x3u;\n", 4 + 4 * i);
p.F(" type = (u_lightControl >> 0x%02xu) & 0x3u;\n", 4 + 4 * i + 2);
}
p.F(" toLight = u_lightpos%s;\n", iStr);
p.C(" if (type != 0x0u) {\n"); // GE_LIGHTTYPE_DIRECTIONAL
p.F(" toLight -= worldpos;\n");
p.F(" float distSq = dot(toLight, toLight);\n");
p.F(" float invDist = inversesqrt(distSq);\n");
p.F(" distance = distSq * invDist;\n");
p.F(" toLight *= invDist;\n");
p.F(" attenuation = clamp(1.0 / dot(u_lightatt%s, vec3(1.0, distance, distSq)), 0.0, 1.0);\n", iStr);
p.C(" if (type == 0x01u) {\n"); // GE_LIGHTTYPE_POINT
p.C(" lightScale = attenuation;\n");
p.C(" } else {\n"); // type must be 0x02 - GE_LIGHTTYPE_SPOT
p.F(" angle = dot(u_lightdir%s, toLight);\n", iStr);
p.F(" if (angle >= u_lightangle_spotCoef%s.x) {\n", iStr);
p.F(" lightScale = attenuation * pspPow(angle, u_lightangle_spotCoef%s.y);\n", iStr, iStr);
p.C(" } else {\n");
p.C(" lightScale = 0.0;\n");
p.C(" }\n");
p.C(" }\n");
p.C(" } else {\n");
p.C(" lightScale = 1.0;\n"); // GE_LIGHTTYPE_DIRECTIONAL
p.C(" }\n");
p.C(" ldot = dot(toLight, worldnormal);\n");
p.C(" if (comp == 0x2u) {\n"); // GE_LIGHTCOMP_ONLYPOWDIFFUSE
p.C(" ldot = pspPow(ldot, u_matspecular.a);\n");
p.C(" }\n");
p.F(" diffuse = (u_lightdiffuse%s * diffuseColor) * max(ldot, 0.0);\n", iStr);
p.C(" if (comp == 0x1u && ldot >= 0.0) {\n"); // do specular. note - must allow for the >= case, since the u_matspecular.a <= 0.0 case relies on it.
p.C(" vec3 halfVec = toLight + viewDir;\n");
p.C(" float halfInvLen = inversesqrt(dot(halfVec, halfVec));\n");
p.C(" ldot = pspPow(dot(halfVec, worldnormal) * halfInvLen, u_matspecular.a);\n");
p.F(" lightSum1 += u_lightspecular%s * specularColor * ldot * lightScale;\n", iStr);
p.C(" }\n");
p.F(" lightSum0.rgb += (u_lightambient%s * ambientColor.rgb + diffuse) * lightScale;\n", iStr);
p.C(" }\n");
}
if (useIndexing) {
p.F(" }");
}
} else {
// Generate specific code for calculating the enabled lights only.
for (int i = 0; i < 4; i++) {
if (doLight[i] != LIGHT_FULL)
continue;
snprintf(iStr, sizeof(iStr), useIndexing ? "[%d]" : "%d", i);
GELightType type = static_cast<GELightType>(id.Bits(VS_BIT_LIGHT0_TYPE + 4 * i, 2));
GELightComputation comp = static_cast<GELightComputation>(id.Bits(VS_BIT_LIGHT0_COMP + 4 * i, 2));
if (type == GE_LIGHTTYPE_DIRECTIONAL) {
// We prenormalize light positions for directional lights.
p.F(" toLight = u_lightpos%s;\n", iStr);
} else {
p.F(" toLight = u_lightpos%s - worldpos;\n", iStr);
// Use inversesqrt() for better performance on mobile GPUs.
// distance = sqrt(distSq), and toLight * inversesqrt(distSq) normalizes it.
p.C(" distSq = dot(toLight, toLight);\n");
p.C(" invDist = inversesqrt(distSq);\n");
p.C(" distance = distSq * invDist;\n");
p.C(" toLight *= invDist;\n");
}
bool doSpecular = comp == GE_LIGHTCOMP_BOTH;
bool poweredDiffuse = comp == GE_LIGHTCOMP_ONLYPOWDIFFUSE;
p.C(" ldot = dot(toLight, worldnormal);\n");
if (poweredDiffuse) {
// pow(0.0, 0.0) may be undefined, but the PSP seems to treat it as 1.0.
// Seen in Tales of the World: Radiant Mythology (#2424.)
p.C(" ldot = pspPow(ldot, u_matspecular.a);\n");
}
const char *timesLightScale = " * lightScale";
// Attenuation
switch (type) {
case GE_LIGHTTYPE_DIRECTIONAL:
timesLightScale = "";
break;
case GE_LIGHTTYPE_POINT:
p.F(" lightScale = clamp(1.0 / dot(u_lightatt%s, vec3(1.0, distance, distSq)), 0.0, 1.0);\n", iStr);
break;
case GE_LIGHTTYPE_SPOT:
case GE_LIGHTTYPE_UNKNOWN:
p.F(" angle = dot(u_lightdir%s, toLight);\n", iStr, iStr);
p.F(" if (angle >= u_lightangle_spotCoef%s.x) {\n", iStr);
p.F(" lightScale = clamp(1.0 / dot(u_lightatt%s, vec3(1.0, distance, distSq)), 0.0, 1.0) * pspPow(angle, u_lightangle_spotCoef%s.y);\n", iStr, iStr);
p.C(" } else {\n");
p.C(" lightScale = 0.0;\n");
p.C(" }\n");
break;
default:
// ILLEGAL
break;
}
p.F(" diffuse = (u_lightdiffuse%s * diffuseColor) * max(ldot, 0.0);\n", iStr);
if (doSpecular) {
p.C(" if (ldot >= 0.0) {\n");
p.C(" vec3 halfVec = toLight + viewDir;\n");
p.C(" float halfInvLen = inversesqrt(dot(halfVec, halfVec));\n");
p.C(" ldot = pspPow(dot(halfVec, worldnormal) * halfInvLen, u_matspecular.a);\n");
p.C(" if (ldot > 0.0)\n");
p.F(" lightSum1 += u_lightspecular%s * specularColor * ldot %s;\n", iStr, timesLightScale);
p.C(" }\n");
}
p.F(" lightSum0.rgb += (u_lightambient%s * ambientColor.rgb + diffuse)%s;\n", iStr, timesLightScale);
}
}
if (enableLighting) {
// Sum up ambient, emissive here.
if (lmode) {
WRITE(p, " %sv_color0 = clamp(lightSum0, 0.0, 1.0);\n", compat.vsOutPrefix);
// v_color1 only exists when lmode = 1.
if (specularIsZero) {
WRITE(p, " %sv_color1 = splat3(0.0);\n", compat.vsOutPrefix);
} else {
WRITE(p, " %sv_color1 = clamp(lightSum1, 0.0, 1.0);\n", compat.vsOutPrefix);
}
} else {
if (specularIsZero) {
WRITE(p, " %sv_color0 = clamp(lightSum0, 0.0, 1.0);\n", compat.vsOutPrefix);
} else {
WRITE(p, " %sv_color0 = clamp(vec4(lightSum0.rgb + lightSum1, lightSum0.a), 0.0, 1.0);\n", compat.vsOutPrefix);
}
}
} else {
// Lighting doesn't affect color.
if (hasColor) {
WRITE(p, " %sv_color0 = color0;\n", compat.vsOutPrefix);
} else {
WRITE(p, " %sv_color0 = u_matambientalpha;\n", compat.vsOutPrefix);
if (bugs.Has(Draw::Bugs::MALI_CONSTANT_LOAD_BUG) && g_Config.bVendorBugChecksEnabled) {
WRITE(p, " %sv_color0.r += 0.000001;\n", compat.vsOutPrefix);
}
}
if (lmode) {
WRITE(p, " %sv_color1 = splat3(0.0);\n", compat.vsOutPrefix);
}
}
bool scaleUV = !isModeThrough && (uvGenMode == GE_TEXMAP_TEXTURE_COORDS || uvGenMode == GE_TEXMAP_UNKNOWN);
// Step 3: UV generation
{
switch (uvGenMode) {
case GE_TEXMAP_TEXTURE_COORDS: // Scale-offset. Easy.
case GE_TEXMAP_UNKNOWN: // Not sure what this is, but Riviera uses it. Treating as coords works.
if (scaleUV) {
if (hasTexcoord) {
WRITE(p, " %sv_texcoord = vec3(texcoord.xy * u_uvscaleoffset.xy, 0.0);\n", compat.vsOutPrefix);
} else {
WRITE(p, " %sv_texcoord = splat3(0.0);\n", compat.vsOutPrefix);
}
} else {
if (hasTexcoord) {
WRITE(p, " %sv_texcoord = vec3(texcoord.xy * u_uvscaleoffset.xy + u_uvscaleoffset.zw, 0.0);\n", compat.vsOutPrefix);
} else {
WRITE(p, " %sv_texcoord = vec3(u_uvscaleoffset.zw, 0.0);\n", compat.vsOutPrefix);
}
}
break;
case GE_TEXMAP_TEXTURE_MATRIX: // Projection mapping.
{
std::string temp_tc;
switch (uvProjMode) {
case GE_PROJMAP_POSITION: // Use model space XYZ as source
temp_tc = "vec4(position, 1.0)";
break;
case GE_PROJMAP_UV: // Use unscaled UV as source
if (hasTexcoord) {
temp_tc = "vec4(texcoord.xy, 0.0, 1.0)";
} else {
temp_tc = "vec4(0.0, 0.0, 0.0, 1.0)";
}
break;
case GE_PROJMAP_NORMALIZED_NORMAL: // Use normalized transformed normal as source
if (hasNormal) {
temp_tc = StringFromFormat("length(normal) == 0.0 ? vec4(0.0, 0.0, 0.0, 1.0) : vec4(normalize(%snormal), 1.0)", flipNormal ? "-" : "");
} else {
temp_tc = "vec4(0.0, 0.0, 1.0, 1.0)";
}
break;
case GE_PROJMAP_NORMAL: // Use non-normalized transformed normal as source
if (hasNormal) {
temp_tc = flipNormal ? "vec4(-normal, 1.0)" : "vec4(normal, 1.0)";
} else {
temp_tc = "vec4(0.0, 0.0, 1.0, 1.0)";
}
break;
}
// Transform by texture matrix. XYZ as we are doing projection mapping.
WRITE(p, " %sv_texcoord = mul(%s, u_texmtx).xyz * vec3(u_uvscaleoffset.xy, 1.0);\n", compat.vsOutPrefix, temp_tc.c_str());
}
break;
case GE_TEXMAP_ENVIRONMENT_MAP: // Shade mapping - use dots from light sources.
{
char ls0Str[4];
char ls1Str[4];
if (useIndexing) {
snprintf(ls0Str, sizeof(ls0Str), "[%d]", ls0);
snprintf(ls1Str, sizeof(ls1Str), "[%d]", ls1);
} else {
snprintf(ls0Str, sizeof(ls0Str), "%d", ls0);
snprintf(ls1Str, sizeof(ls1Str), "%d", ls1);
}
// N.L with L the light vector as lighting sees it (zero stays zero), or the half vector
// if the light does specular. Whether lighting or the light is on doesn't matter.
auto shadeLight = [&](int ls, const char *lsStr, const char *name) {
if (shadeUberShader) {
p.F(" vec3 %s = u_lightpos%s;\n", name, lsStr);
p.F(" if (((u_lightControl >> 0x%02xu) & 0x3u) != 0x0u) %s = u_lightpos%s - worldpos;\n", 4 + 4 * ls + 2, name, lsStr);
p.F(" %s = normalizeOr000(%s);\n", name, name);
p.F(" if (((u_lightControl >> 0x%02xu) & 0x3u) == 0x1u) %s = normalizeOr000(%s + viewDir);\n", 4 + 4 * ls, name, name);
return;
}
GELightType type = static_cast<GELightType>(id.Bits(VS_BIT_LIGHT0_TYPE + 4 * ls, 2));
GELightComputation comp = static_cast<GELightComputation>(id.Bits(VS_BIT_LIGHT0_COMP + 4 * ls, 2));
if (type == GE_LIGHTTYPE_DIRECTIONAL) {
// Prenormalized.
p.F(" vec3 %s = u_lightpos%s;\n", name, lsStr);
} else {
p.F(" vec3 %s = normalizeOr000(u_lightpos%s - worldpos);\n", name, lsStr);
}
if (comp == GE_LIGHTCOMP_BOTH) {
p.F(" %s = normalizeOr000(%s + viewDir);\n", name, name);
}
};
shadeLight(ls0, ls0Str, "shadeL0");
shadeLight(ls1, ls1Str, "shadeL1");
WRITE(p, " %sv_texcoord = vec3(u_uvscaleoffset.xy * vec2(1.0 + dot(shadeL0, worldnormal), 1.0 + dot(shadeL1, worldnormal)) * 0.5, 1.0);\n", compat.vsOutPrefix);
}
break;
default:
// ILLEGAL
break;
}
}
// Compute fogdepth
WRITE(p, " %sv_fogdepth = (viewPos.z + u_fogcoef.x) * u_fogcoef.y;\n", compat.vsOutPrefix);
}
if (!isModeThrough) {
// Cull against X and Y limits (unless the GPU has a certain driver bug).
// It's not clear what the limits should be in through mode though, although I'm sure they exist.
if (!nanBug && rangeCulling) {
WRITE(p, " if (!zClipped && (outPos.x < 0.0 || outPos.y < 0.0 || outPos.x >= 4096.0 || outPos.y >= 4096.0 || outPos.w < -1.0)) {\n");
// Discard the whole triangle by setting one vertex to NaN.
WRITE(p, " outPos = vec4(u_NaN, u_NaN, u_NaN, u_NaN);\n");
// TODO: We could just return here. But we can also just keep going to avoid branching, the NaNs will propagate.
WRITE(p, " }\n");
}
// Apply raster offset after the range culling.
WRITE(p, " outPos.xy -= u_rasterOffset.xy;\n");
}
// I think we should use min/max clipping for through-mode as well, right?
if (clipMinMax) {
// NOTE: When changing this test, don't forget to change the test in the fragment shader fallback too.
// We use clipping, where available, to implement min/max Z.
// 1.0 is used to disable the clip plane (should we generate more shaders instead? how costly are they?)
// Note: outPos.z need to be multiplied by outPos.w to undo the division, which shouldn't be in effect here.
// We should probably store the undivided outPos in a variable.
// We round to nearest 15-bit value for the check - this seems to match some of [Unknown]'s test, and PSP GPU floats
// often have a 15-bit mantissa. TODO: should we truncate or nearest?
// Due to us having a bit too much precision, we round the value up for the min check and down for the max check.
// Will test this on hardware more carefully soon.
// The min check rounded down fixes the Test Drive map problem, and the max check rounded down fixes Taiko no Tatsujin.
WRITE(p, " float clipZNear = floor(outPos.z * 0.5 + 0.5) * 2.0;\n");
WRITE(p, " float clipZFar = floor(outPos.z * 0.5) * 2.0;\n");
WRITE(p, " %sgl_ClipDistance%s = u_minZmaxZ.x > 0.0 ? (clipZNear - u_minZmaxZ.x) * outPos.w : 1.0;\n", compat.vsOutPrefix, minZClipPlaneSuffix);
WRITE(p, " %sgl_ClipDistance%s = u_minZmaxZ.y < 65535.0 ? (u_minZmaxZ.y - clipZFar) * outPos.w : 1.0;\n", compat.vsOutPrefix, maxZClipPlaneSuffix);
}
// Convert to NDC space, using the framebuffer offset and (inverse size * 2) stored in u_xywh.
WRITE(p, " outPos.xy = (((outPos.xy + u_xywh.xy) * u_xywh.zw) - vec2(1.0, 1.0)) * outPos.w;\n");
if (gstate_c.Use(GPU_ROUND_DEPTH_TO_16BIT)) {
// Actually 15-bit. Truncate here fixes Afterburner (similarly to the min/max clipping above).
// Possibly this should only be 15-bit in transformed mode? Full 16 in through? needs hardware testing.
WRITE(p, " outPos.z = floor(outPos.z * 0.5) * 2.0;\n");
}
if (fsDepthClamp) {
// After the rounding, so the fragment shader clamps the Z that hardware depth clamp would. With fsMinmaxDiscard
// too, this replaces its unrounded Z: the same for the max side, which rounds down anyway, and up to 2 off for
// the min one, which only meets the clamp when max is 65535.
WRITE(p, " %sv_zw = vec2(outPos.z * outPos.w, outPos.w);\n", compat.vsOutPrefix);
}
// It seems that depth values of 65535.6 should survive. Seen in NBA2K12 for example.
// So even if it seems like 65535 would make more sense, we divide by 65536 here.
WRITE(p, " outPos.z *= outPos.w * (1.0 / 65536.0);\n");
if (compat.shaderLanguage == GLSL_VULKAN && gstate_c.Use(GPU_USE_PRE_ROTATION)) {
// Apply rotation from the uniform.
WRITE(p, " mat2 displayRotation = mat2(\n");
WRITE(p, " u_rotation == 0.0 ? 1.0 : (u_rotation == 2.0 ? -1.0 : 0.0), u_rotation == 1.0 ? 1.0 : (u_rotation == 3.0 ? -1.0 : 0.0),\n");
WRITE(p, " u_rotation == 3.0 ? 1.0 : (u_rotation == 1.0 ? -1.0 : 0.0), u_rotation == 0.0 ? 1.0 : (u_rotation == 2.0 ? -1.0 : 0.0)\n");
WRITE(p, " );\n");
WRITE(p, " outPos.xy = mul(displayRotation, outPos.xy);\n");
}
bool flipY = strlen(compat.viewportYSign) > 0;
if (gstate_c.Use(GPU_USE_NONBUFFERED_FLIP)) {
flipY = !flipY;
}
if (flipY) {
WRITE(p, " outPos.y *= -1.0;\n");
}
// We've named the output gl_Position in HLSL as well.
WRITE(p, " %sgl_Position = outPos;\n", compat.vsOutPrefix);
if (gstate_c.Use(GPU_USE_VIRTUAL_REALITY)) {
// Z correction for the depth buffer
if (useHWTransform) {
WRITE(p, " %sgl_Position.z = orgPos.z / abs(orgPos.w) * abs(outPos.w);\n", compat.vsOutPrefix);
}
// HUD scaling
WRITE(p, " %sgl_Position.x *= u_scaleX;\n", compat.vsOutPrefix);
WRITE(p, " %sgl_Position.y *= u_scaleY;\n", compat.vsOutPrefix);
}
if (fsDepthClamp) {
// Overwrite Z with a value that only the near plane clips.
// Then we will overwrite the Z in the fragment shader with the per-pixel value computed from the interpolated v_zw.
if (useHWTransform) {
WRITE(p, " %sgl_Position.z = nearPlaneZ;\n", compat.vsOutPrefix);
} else {
// Software transform has clipped against the near plane already.
WRITE(p, " %sgl_Position.z = (u_minZmaxZ.x + u_minZmaxZ.y) * (0.5 / 65536.0) * outPos.w;\n", compat.vsOutPrefix);
}
}
if (compat.depthMinusOneToOne) {
// Convert from 0->1 to -1->1 depth range.
WRITE(p, " %sgl_Position.z = %sgl_Position.z * 2.0 - %sgl_Position.w;\n", compat.vsOutPrefix, compat.vsOutPrefix, compat.vsOutPrefix);
// The formula takes the z component of gl_Position, which is currently in the range [0, w] (where w is the homogeneous coordinate), and transforms it to the range [-w, w]. This is done by first multiplying by 2 to scale the range from [0, w] to [0, 2w], and then subtracting w to shift the range to [-w, w]. This effectively converts the depth range from 0->1 to -1->1 after perspective division (when gl_Position is divided by w).
}
if (compat.shaderLanguage == HLSL_D3D11) {
WRITE(p, " return Out;\n");
}
WRITE(p, "}\n");
return true;
}