From 135aa685b9ec6c8ba1b64bcbf12b1affb5dadd67 Mon Sep 17 00:00:00 2001 From: Justin Marshall Date: Tue, 12 May 2026 14:08:28 -0700 Subject: [PATCH] Added tesselation --- neo/engine/opengl/gl_d3d12shim.cpp | 380 +++++++++++++++++++++++++++-- 1 file changed, 359 insertions(+), 21 deletions(-) diff --git a/neo/engine/opengl/gl_d3d12shim.cpp b/neo/engine/opengl/gl_d3d12shim.cpp index 05a2ae2a..6a3acd41 100644 --- a/neo/engine/opengl/gl_d3d12shim.cpp +++ b/neo/engine/opengl/gl_d3d12shim.cpp @@ -659,6 +659,7 @@ struct BatchKey PipelineMode pipeline = PIPE_OPAQUE_UNTEX; D3D12_PRIMITIVE_TOPOLOGY topology = D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST; + bool useTessellation = false; UINT tex0SrvIndex = 0; UINT tex1SrvIndex = 0; @@ -775,6 +776,7 @@ static bool BatchKeyEquals(const BatchKey& a, const BatchKey& b) RectEquals(a.scissor, b.scissor) && a.pipeline == b.pipeline && a.topology == b.topology && + a.useTessellation == b.useTessellation && a.tex0SrvIndex == b.tex0SrvIndex && a.tex1SrvIndex == b.tex1SrvIndex && a.normalMapSrvIndex == b.normalMapSrvIndex && @@ -1294,6 +1296,8 @@ struct GLState ComPtr postGBufferPointResolveMsaaPSO; ComPtr vsMainBlob; + ComPtr hsMainBlob; + ComPtr dsMainBlob; ComPtr psMainBlob; ComPtr psAlphaBlob; ComPtr psUntexturedBlob; @@ -2301,6 +2305,7 @@ cbuffer DrawCB : register(b0) #define gUseSpecularMap gMaterialMapPad.x #define gSpecularMapStrength gMaterialMapPad.y #define gAlphaBlendPass gMaterialMapPad.z +#define gTessellationDisplacement max(gMaterialMapPad.w, 0.0) Texture2D gTex0 : register(t0); Texture2D gTex1 : register(t1); @@ -2338,6 +2343,8 @@ struct VSOut float4 attr : TEXCOORD7; float4 prevClip : TEXCOORD8; float4 currClip : TEXCOORD9; + float3 objPos : TEXCOORD10; + float3 objNormal : TEXCOORD11; float psize : PSIZE; }; @@ -2529,14 +2536,28 @@ float3 QD3D12_DecodeTangentSpaceNormal(float3 encodedNormal) return QD3D12_SafeNormalize(n, float3(0.0, 0.0, 1.0)); } +float QD3D12_HeightFromNormalSample(float4 nm) +{ + // Many legacy normal maps are RGB/DXT uploads with an implicitly forced + // opaque alpha channel. Treat alpha as height only when it looks like real + // authored data; otherwise derive a conservative pseudo-height from the + // normal itself. A flat normal (0.5, 0.5, 1.0) stays centered at 0.5, so + // RGB-only normal maps do not push whole surfaces outward. + bool hasAuthoredHeight = (nm.a > 0.0001 && nm.a < 0.999); + float3 decoded = nm.xyz * 2.0 - 1.0; + float slopeHeight = 0.5 + (1.0 - saturate(abs(decoded.z))) * 0.25; + float h = hasAuthoredHeight ? nm.a : slopeHeight; + return saturate(h); +} + float QD3D12_GetHeightFromNormalMap(float2 uv) { - float4 nm = gNormalMap.Sample(gSamp2, uv); + return QD3D12_HeightFromNormalSample(gNormalMap.Sample(gSamp2, uv)); +} - float lum = dot(nm.rgb, float3(0.299, 0.587, 0.114)); - float h = (nm.a > 0.0001) ? nm.a : ((lum + nm.b) * 0.5); - - return saturate(h); +float QD3D12_GetHeightFromNormalMapLOD(float2 uv, float lod) +{ + return QD3D12_HeightFromNormalSample(gNormalMap.SampleLevel(gSamp2, uv, lod)); } float3 QD3D12_BuildFallbackTangent(float3 n) @@ -2669,8 +2690,11 @@ float4 BuildTexturedColor(VSOut i) gTexComb0RGB, gTexComb0Alpha, gTexComb0Operand, gTexEnvColor0); } - if (gUseTex1 > 0.5 && !gUseNormalMap) + if (gUseTex1 > 0.5) { + // Normal maps are bound through t2. Texture unit 1 remains a real fixed-function + // color/lightmap stage when the CPU side marks it as usable, so do not drop + // legacy lightmaps just because a normal map is active. float4 tex1 = gTex1.Sample(gSamp1, uv1); outColor = ApplyTexCombine(outColor, tex1, primary, gTexEnvMode1, gTexComb1RGB, gTexComb1Alpha, gTexComb1Operand, gTexEnvColor1); @@ -2749,6 +2773,8 @@ VSOut VSMain(VSIn i) o.pos = currClip; o.currClip = currClip; o.prevClip = prevClip; + o.objPos = i.pos; + o.objNormal = i.normal; o.fogCoord = abs(currClip.z / max(abs(currClip.w), 0.00001)); o.uv0 = i.uv0; o.uv1 = i.uv1; @@ -2761,6 +2787,153 @@ VSOut VSMain(VSIn i) o.psize = gPointSize; return o; } +)HLSL" +R"HLSL( +struct TessCP +{ + float4 pos : POSITION0; + float2 uv0 : TEXCOORD0; + float2 uv1 : TEXCOORD1; + float4 col : COLOR0; + float fogCoord : TEXCOORD2; + float3 worldPos : TEXCOORD3; + float3 normal : TEXCOORD4; + float3 tangent : TEXCOORD5; + float3 binormal : TEXCOORD6; + float4 attr : TEXCOORD7; + float4 prevClip : TEXCOORD8; + float4 currClip : TEXCOORD9; + float3 objPos : TEXCOORD10; + float3 objNormal : TEXCOORD11; +}; + +struct HSConstOut +{ + float edge[3] : SV_TessFactor; + float inside : SV_InsideTessFactor; +}; + +TessCP QD3D12_MakeTessCP(VSOut i) +{ + TessCP o; + o.pos = i.pos; + o.uv0 = i.uv0; + o.uv1 = i.uv1; + o.col = i.col; + o.fogCoord = i.fogCoord; + o.worldPos = i.worldPos; + o.normal = i.normal; + o.tangent = i.tangent; + o.binormal = i.binormal; + o.attr = i.attr; + o.prevClip = i.prevClip; + o.currClip = i.currClip; + o.objPos = i.objPos; + o.objNormal = i.objNormal; + return o; +} + +float QD3D12_ComputeNormalMapTessFactor(InputPatch patch) +{ + float h0 = QD3D12_GetHeightFromNormalMapLOD(patch[0].uv0, 0.0); + float h1 = QD3D12_GetHeightFromNormalMapLOD(patch[1].uv0, 0.0); + float h2 = QD3D12_GetHeightFromNormalMapLOD(patch[2].uv0, 0.0); + float heightRange = max(abs(h0 - h1), max(abs(h1 - h2), abs(h2 - h0))); + + // Keep the factor conservative. Most legacy normal maps are not authored as + // height maps, so aggressive tessellation creates cracks and flickering without + // adding useful detail. Height variation still nudges the factor upward. + float strength = saturate(max(gNormalMapStrength, 0.0)); + float factor = 1.0 + strength + heightRange * 4.0; + return (gUseNormalMap > 0.5) ? clamp(factor, 1.0, 4.0) : 1.0; +} + +HSConstOut HSMainConstants(InputPatch patch) +{ + HSConstOut o; + float factor = QD3D12_ComputeNormalMapTessFactor(patch); + o.edge[0] = factor; + o.edge[1] = factor; + o.edge[2] = factor; + o.inside = factor; + return o; +} + +[domain("tri")] +[partitioning("fractional_odd")] +[outputtopology("triangle_ccw")] +[outputcontrolpoints(3)] +[patchconstantfunc("HSMainConstants")] +TessCP HSMain(InputPatch patch, uint cpId : SV_OutputControlPointID) +{ + return QD3D12_MakeTessCP(patch[cpId]); +} + +float2 QD3D12_Interp2(float2 a, float2 b, float2 c, float3 w) +{ + return a * w.x + b * w.y + c * w.z; +} + +float3 QD3D12_Interp3(float3 a, float3 b, float3 c, float3 w) +{ + return a * w.x + b * w.y + c * w.z; +} + +float4 QD3D12_Interp4(float4 a, float4 b, float4 c, float3 w) +{ + return a * w.x + b * w.y + c * w.z; +} + +[domain("tri")] +VSOut DSMain(HSConstOut tessFactors, float3 bary : SV_DomainLocation, const OutputPatch patch) +{ + TessCP i; + i.pos = QD3D12_Interp4(patch[0].pos, patch[1].pos, patch[2].pos, bary); + i.uv0 = QD3D12_Interp2(patch[0].uv0, patch[1].uv0, patch[2].uv0, bary); + i.uv1 = QD3D12_Interp2(patch[0].uv1, patch[1].uv1, patch[2].uv1, bary); + i.col = QD3D12_Interp4(patch[0].col, patch[1].col, patch[2].col, bary); + i.fogCoord = patch[0].fogCoord * bary.x + patch[1].fogCoord * bary.y + patch[2].fogCoord * bary.z; + i.worldPos = QD3D12_Interp3(patch[0].worldPos, patch[1].worldPos, patch[2].worldPos, bary); + i.normal = QD3D12_Interp3(patch[0].normal, patch[1].normal, patch[2].normal, bary); + i.tangent = QD3D12_Interp3(patch[0].tangent, patch[1].tangent, patch[2].tangent, bary); + i.binormal = QD3D12_Interp3(patch[0].binormal, patch[1].binormal, patch[2].binormal, bary); + i.attr = QD3D12_Interp4(patch[0].attr, patch[1].attr, patch[2].attr, bary); + i.prevClip = QD3D12_Interp4(patch[0].prevClip, patch[1].prevClip, patch[2].prevClip, bary); + i.currClip = QD3D12_Interp4(patch[0].currClip, patch[1].currClip, patch[2].currClip, bary); + i.objPos = QD3D12_Interp3(patch[0].objPos, patch[1].objPos, patch[2].objPos, bary); + i.objNormal = QD3D12_Interp3(patch[0].objNormal, patch[1].objNormal, patch[2].objNormal, bary); + + VSOut o; + + float3 objNormal = QD3D12_SafeNormalize(i.objNormal, float3(0.0, 0.0, 1.0)); + float height = QD3D12_GetHeightFromNormalMapLOD(i.uv0, 0.0); + float displacement = (height - 0.5) * gTessellationDisplacement; + float3 displacedObjPos = i.objPos + objNormal * displacement; + + float4 worldPos = mul(gModelMatrix, float4(displacedObjPos, 1.0)); + float4 currClip = mul(gMVP, float4(displacedObjPos, 1.0)); + float4 prevClip = mul(gPrevMVP, float4(displacedObjPos, 1.0)); + float3 worldNormal = mul((float3x3)gModelMatrix, objNormal); + + currClip.z = 0.5 * (currClip.z + currClip.w); + + o.pos = currClip; + o.currClip = currClip; + o.prevClip = prevClip; + o.objPos = displacedObjPos; + o.objNormal = objNormal; + o.fogCoord = abs(currClip.z / max(abs(currClip.w), 0.00001)); + o.uv0 = i.uv0; + o.uv1 = i.uv1; + o.col = i.col; + o.worldPos = worldPos.xyz; + o.normal = QD3D12_SafeNormalize(worldNormal, QD3D12_SafeNormalize(i.normal, float3(0.0, 0.0, 1.0))); + o.tangent = QD3D12_SafeNormalize(i.tangent, QD3D12_BuildFallbackTangent(o.normal)); + o.binormal = QD3D12_SafeNormalize(i.binormal, QD3D12_SafeNormalize(cross(o.normal, o.tangent), float3(0.0, 1.0, 0.0))); + o.attr = float4(geometryFlag, gRoughness, gMaterialType, 0.0); + o.psize = gPointSize; + return o; +} PSOut PSMain(VSOut i) { @@ -3318,7 +3491,8 @@ static D3D12_PRIMITIVE_TOPOLOGY GetDrawTopology(GLenum originalMode) return D3D_PRIMITIVE_TOPOLOGY_LINELIST; // we manually convert this, so im adjusting to be a linelist. case GL_TRIANGLE_STRIP: - return D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP; + // FlushImmediate expands strips into standalone triangles, so the IA sees a list. + return D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST; case GL_TRIANGLES: default: @@ -3334,6 +3508,11 @@ static float MapTexCombineMode(GLenum mode); static float MapTexCombineSource(GLenum source); static float MapTexCombineOperandRGB(GLenum operand); static float MapTexCombineOperandAlpha(GLenum operand); +static bool QD3D12_ShouldEnableNormalMapTessellation(GLenum originalMode, const BatchKey& key); +static bool QD3D12_ImmediateVertexCountCanUseNormalMapTessellation(GLenum originalMode, size_t vertexCount); +static bool QD3D12_UseNormalMapTessellationPSO(const BatchKey& key, bool nativeColorOnly); +static D3D12_PRIMITIVE_TOPOLOGY_TYPE QD3D12_EffectiveTopologyTypeForPSO(const BatchKey& key, bool nativeColorOnly); +static D3D12_PRIMITIVE_TOPOLOGY QD3D12_EffectiveIATopologyForDraw(const BatchKey& key, bool nativeColorOnly); static UINT8 QD3D12_CurrentColorWriteMask() { @@ -3383,24 +3562,42 @@ static void QD3D12_FillTexCombineKey(BatchKey& key, UINT unit) color[3] = g_gl.texEnvColor[unit][3]; } +static bool QD3D12_TextureIsMaterialMapOnlyForFixedFunctionColor(const TextureResource* tex) +{ + if (!tex || tex == &g_gl.whiteTexture || tex->glId == 0) + return false; + + // Tagged/explicit material maps are sampled through their dedicated shader + // slots. Do not also feed them into fixed-function texture unit 0/1, or a + // normal/glow/specular map can replace the diffuse/lightmap color stage. + if (tex->isNormalMap || tex->isGlowMap || tex->isSpecularMap) + return true; + + return tex->glId == g_gl.currentNormalMapTexture || + tex->glId == g_gl.currentGlowMapTexture || + tex->glId == g_gl.currentSpecularMapTexture; +} + #ifdef _DEBUG #pragma optimize off #endif static BatchKey BuildCurrentBatchKey(GLenum originalMode, const TextureResource* tex0, const TextureResource* tex1, TextureResource* const* allTextures) { - const bool useTex0 = g_gl.texture2D[0]; - const bool useTex1 = g_gl.texture2D[1]; + const bool tex0IsMaterialMapOnly = QD3D12_TextureIsMaterialMapOnlyForFixedFunctionColor(tex0); + const bool tex1IsMaterialMapOnly = QD3D12_TextureIsMaterialMapOnlyForFixedFunctionColor(tex1); + const bool useTex0 = g_gl.texture2D[0] && !tex0IsMaterialMapOnly; + const bool useTex1 = g_gl.texture2D[1] && !tex1IsMaterialMapOnly; BatchKey key{}; key.pipeline = PickPipeline(useTex0, useTex1); key.topology = GetDrawTopology(originalMode); - key.tex0SrvIndex = tex0 ? tex0->srvIndex : 0; - key.tex1SrvIndex = tex1 ? tex1->srvIndex : 0; + key.tex0SrvIndex = (useTex0 && tex0 && tex0->srvIndex != UINT_MAX) ? tex0->srvIndex : g_gl.whiteTexture.srvIndex; + key.tex1SrvIndex = (useTex1 && tex1 && tex1->srvIndex != UINT_MAX) ? tex1->srvIndex : g_gl.whiteTexture.srvIndex; for (UINT i = 0; i < QD3D12_MaxTextureUnits; ++i) key.textureSrvIndex[i] = (allTextures && allTextures[i]) ? allTextures[i]->srvIndex : 0; TextureResource* normalMap = QD3D12_SelectNormalMapTexture(allTextures); - if (normalMap && normalMap->texture && normalMap->srvIndex != UINT_MAX) + if (normalMap && normalMap->texture && normalMap->srvIndex != UINT_MAX && normalMap->gpuValid) { key.normalMapSrvIndex = normalMap->srvIndex; key.useNormalMap = 1.0f; @@ -3412,6 +3609,7 @@ static BatchKey BuildCurrentBatchKey(GLenum originalMode, const TextureResource* } key.normalMapStrength = g_gl.currentNormalMapStrength; key.normalMapYSign = g_gl.currentNormalMapYSign; + key.useTessellation = false; TextureResource* glowMap = QD3D12_SelectGlowMapTexture(allTextures); if (glowMap && glowMap->texture && glowMap->srvIndex != UINT_MAX) @@ -3496,6 +3694,9 @@ static BatchKey BuildCurrentBatchKey(GLenum originalMode, const TextureResource* key.geometryFlag = QD3D12_CurrentEffectiveGeometryFlag(); key.roughness = g_gl.currentSurfaceRoughness; key.materialType = QD3D12_CurrentEffectiveMaterialType(); + key.useTessellation = QD3D12_ShouldEnableNormalMapTessellation(originalMode, key); + if (key.useARBPrograms) + key.useTessellation = false; g_gl.currObjectMVPs[key.motionObjectId] = key.mvp; return key; } @@ -3944,6 +4145,115 @@ static D3D12_PRIMITIVE_TOPOLOGY_TYPE GetTopologyTypeFromTopology(D3D12_PRIMITIVE } } +static bool QD3D12_OriginalModeCanUseNormalMapTessellation(GLenum originalMode) +{ + switch (originalMode) + { + case GL_TRIANGLES: + case GL_TRIANGLE_STRIP: + case GL_TRIANGLE_FAN: + case GL_QUADS: + case GL_QUAD_STRIP: + case GL_POLYGON: + return true; + default: + return false; + } +} + +static bool QD3D12_ImmediateVertexCountCanUseNormalMapTessellation(GLenum originalMode, size_t vertexCount) +{ + if (!QD3D12_OriginalModeCanUseNormalMapTessellation(originalMode)) + return false; + + switch (originalMode) + { + case GL_TRIANGLES: + return vertexCount >= 3 && (vertexCount % 3) == 0; + case GL_TRIANGLE_STRIP: + case GL_TRIANGLE_FAN: + case GL_POLYGON: + return vertexCount >= 3; + case GL_QUADS: + case GL_QUAD_STRIP: + return vertexCount >= 4; + default: + return false; + } +} + +static bool QD3D12_CurrentDrawLooks3DForTessellation() +{ + if (g_gl.projStack.empty()) + return false; + + // UI/HUD paths in these engines normally use glOrtho. Keep tessellation out + // of those 2D passes and only allow it under a perspective projection. + return QD3D12_IsPerspectiveProjectionCM(g_gl.projStack.back().m); +} + +static bool QD3D12_ShouldEnableNormalMapTessellation(GLenum originalMode, const BatchKey& key) +{ + if (key.useARBPrograms) + return false; + + if (key.useNormalMap <= 0.5f) + return false; + + if (key.normalMapSrvIndex == UINT_MAX || key.normalMapSrvIndex == g_gl.whiteTexture.srvIndex) + return false; + + if (key.topology != D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST) + return false; + + // Keep decals, alpha-tested cutouts, translucent passes, post-upscale overlays, + // shadow/color-mask-only work, and depth-disabled geometry on the proven VS/PS route. + if (key.pipeline != PIPE_OPAQUE_TEX && key.pipeline != PIPE_OPAQUE_UNTEX) + return false; + + if (!key.depthTest || !key.depthWrite || key.colorWriteMask == 0) + return false; + + if (!QD3D12_OriginalModeCanUseNormalMapTessellation(originalMode)) + return false; + + if (!QD3D12_CurrentDrawLooks3DForTessellation()) + return false; + + if (g_gl.framePhase == QD3D12_FRAME_NATIVE_POST_UPSCALE) + return false; + + return true; +} + +static bool QD3D12_UseNormalMapTessellationPSO(const BatchKey& key, bool nativeColorOnly) +{ + return key.useTessellation && + !nativeColorOnly && + !key.useARBPrograms && + (key.pipeline == PIPE_OPAQUE_TEX || key.pipeline == PIPE_OPAQUE_UNTEX) && + key.depthTest && + key.depthWrite && + key.useNormalMap > 0.5f && + key.normalMapSrvIndex != UINT_MAX && + key.normalMapSrvIndex != g_gl.whiteTexture.srvIndex && + key.topology == D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST; +} + +static D3D12_PRIMITIVE_TOPOLOGY_TYPE QD3D12_EffectiveTopologyTypeForPSO(const BatchKey& key, bool nativeColorOnly) +{ + return QD3D12_UseNormalMapTessellationPSO(key, nativeColorOnly) + ? D3D12_PRIMITIVE_TOPOLOGY_TYPE_PATCH + : GetTopologyTypeFromTopology(key.topology); +} + +static D3D12_PRIMITIVE_TOPOLOGY QD3D12_EffectiveIATopologyForDraw(const BatchKey& key, bool nativeColorOnly) +{ + return QD3D12_UseNormalMapTessellationPSO(key, nativeColorOnly) + ? D3D_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST + : key.topology; +} + // ============================================================ // SECTION 6: upload helpers // ============================================================ @@ -5827,7 +6137,7 @@ static void QD3D12_CreateRootSignature() params[1 + i].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; params[1 + i].DescriptorTable.NumDescriptorRanges = 1; params[1 + i].DescriptorTable.pDescriptorRanges = &ranges[i]; - params[1 + i].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + params[1 + i].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; } D3D12_STATIC_SAMPLER_DESC samps[QD3D12_MaxTextureUnits] = {}; @@ -5839,7 +6149,7 @@ static void QD3D12_CreateRootSignature() samps[i].AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP; samps[i].ShaderRegister = i; samps[i].RegisterSpace = 0; - samps[i].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + samps[i].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; samps[i].MaxLOD = D3D12_FLOAT32_MAX; } @@ -5861,6 +6171,8 @@ static void QD3D12_CreateRootSignature() static void QD3D12_CompileShaders() { g_gl.vsMainBlob = CompileShaderVariant("VSMain", "vs_6_0"); + g_gl.hsMainBlob = CompileShaderVariant("HSMain", "hs_6_0"); + g_gl.dsMainBlob = CompileShaderVariant("DSMain", "ds_6_0"); g_gl.psMainBlob = CompileShaderVariant("PSMain", "ps_6_0"); g_gl.psAlphaBlob = CompileShaderVariant("PSMainAlphaTest", "ps_6_0"); g_gl.psUntexturedBlob = CompileShaderVariant("PSMainUntextured", "ps_6_0"); @@ -5916,6 +6228,8 @@ static D3D12_CPU_DESCRIPTOR_HANDLE CurrentNormalRTV() static D3D12_GRAPHICS_PIPELINE_STATE_DESC BuildPSODesc( PipelineMode mode, ID3DBlob* vs, + ID3DBlob* hs, + ID3DBlob* ds, ID3DBlob* ps, const BatchKey& key, D3D12_PRIMITIVE_TOPOLOGY_TYPE topoType, @@ -5924,12 +6238,18 @@ static D3D12_GRAPHICS_PIPELINE_STATE_DESC BuildPSODesc( D3D12_GRAPHICS_PIPELINE_STATE_DESC d{}; d.pRootSignature = g_gl.rootSig.Get(); d.VS = { vs->GetBufferPointer(), vs->GetBufferSize() }; + const bool useTessellation = QD3D12_UseNormalMapTessellationPSO(key, nativeColorOnly) && hs && ds; + if (useTessellation) + { + d.HS = { hs->GetBufferPointer(), hs->GetBufferSize() }; + d.DS = { ds->GetBufferPointer(), ds->GetBufferSize() }; + } d.PS = { ps->GetBufferPointer(), ps->GetBufferSize() }; d.InputLayout.pInputElementDescs = kGLVertexInputLayout; d.InputLayout.NumElements = _countof(kGLVertexInputLayout); - d.PrimitiveTopologyType = topoType; + d.PrimitiveTopologyType = useTessellation ? D3D12_PRIMITIVE_TOPOLOGY_TYPE_PATCH : topoType; d.NumRenderTargets = nativeColorOnly ? 1u : 6u; d.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; @@ -6006,6 +6326,13 @@ static D3D12_GRAPHICS_PIPELINE_STATE_DESC BuildPSODesc( if (alphaBlended) depthStencilKey.depthWrite = false; ApplyRasterDepthStencilState(d, depthStencilKey); + if (useTessellation) + { + // Hull/domain shader triangle orientation can differ on legacy content that + // flips GL front-face state. Avoid dropping tessellated patches entirely; + // depth still rejects hidden surfaces. + d.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + } return d; } @@ -6125,6 +6452,7 @@ static uint64_t MakePSOKey( mix(key.depthWrite ? 1ull : 0ull); mix(uint32_t(key.depthFunc)); mix(uint32_t(topoType)); + mix(QD3D12_UseNormalMapTessellationPSO(key, nativeColorOnly) ? 1ull : 0ull); mix(nativeColorOnly ? 1ull : 0ull); mix(nativeColorOnly ? 1ull : uint64_t(QD3D12_GBufferSampleCount())); mix(uint32_t(key.colorWriteMask)); @@ -6178,6 +6506,8 @@ static ID3D12PipelineState* QD3D12_GetPSO( auto desc = BuildPSODesc( key.pipeline, g_gl.vsMainBlob.Get(), + g_gl.hsMainBlob.Get(), + g_gl.dsMainBlob.Get(), psBlob, key, topoType, @@ -8190,6 +8520,8 @@ static void FlushImmediate(GLenum mode, const GLVertex* src, size_t n) TextureResource* tex1 = boundTextures[1]; BatchKey key = BuildCurrentBatchKey(mode, tex0, tex1, boundTextures); + if (key.useTessellation && !QD3D12_ImmediateVertexCountCanUseNormalMapTessellation(mode, n)) + key.useTessellation = false; const size_t markerCursor = g_gl.queryMarkers.size(); QueuedBatch* batch = nullptr; @@ -8596,7 +8928,12 @@ static void QD3D12_FlushQueuedBatches() dc->materialMapPad[0] = batch.key.useSpecularMap; dc->materialMapPad[1] = batch.key.specularMapStrength; dc->materialMapPad[2] = QD3D12_PipelineUsesAlphaBlend(batch.key.pipeline) ? 1.0f : 0.0f; - dc->materialMapPad[3] = 0.0f; + // Normal-map strength is a shading control, not an object-space height in + // Quake/idTech units. Use it only as a small scalar for displacement so the + // tessellation pass cannot shove surfaces through walls or the near plane. + const float tessDisplacementScale = 0.03f; + dc->materialMapPad[3] = QD3D12_UseNormalMapTessellationPSO(batch.key, nativeColorOnly) ? + (ClampValue(batch.key.normalMapStrength, 0.0f, 4.0f) * tessDisplacementScale) : 0.0f; if (batch.key.useARBPrograms) { @@ -8612,9 +8949,9 @@ static void QD3D12_FlushQueuedBatches() ID3D12PipelineState* pso = nullptr; const D3D12_PRIMITIVE_TOPOLOGY_TYPE topoType = - GetTopologyTypeFromTopology(batch.key.topology); + QD3D12_EffectiveTopologyTypeForPSO(batch.key, nativeColorOnly); - if (topoType == D3D12_PRIMITIVE_TOPOLOGY_TYPE_POINT) { + if (GetTopologyTypeFromTopology(batch.key.topology) == D3D12_PRIMITIVE_TOPOLOGY_TYPE_POINT) { dc->PointSize = g_gl.pointSize; } else { @@ -8719,10 +9056,11 @@ static void QD3D12_FlushQueuedBatches() g_gl.cmdList->SetGraphicsRootConstantBufferView(0, cbAlloc.gpu); g_gl.cmdList->OMSetStencilRef(batch.key.stencilRef); - if (batch.key.topology != lastTopo) + const D3D12_PRIMITIVE_TOPOLOGY drawTopology = QD3D12_EffectiveIATopologyForDraw(batch.key, nativeColorOnly); + if (drawTopology != lastTopo) { - g_gl.cmdList->IASetPrimitiveTopology(batch.key.topology); - lastTopo = batch.key.topology; + g_gl.cmdList->IASetPrimitiveTopology(drawTopology); + lastTopo = drawTopology; } g_gl.cmdList->IASetVertexBuffers(0, 1, &vbv);