diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props
index 0ce79918aa..13e9ff7a0c 100644
--- a/Source/Core/DolphinLib.props
+++ b/Source/Core/DolphinLib.props
@@ -747,6 +747,7 @@
+
@@ -1401,6 +1402,7 @@
+
diff --git a/Source/Core/VideoCommon/CMakeLists.txt b/Source/Core/VideoCommon/CMakeLists.txt
index 1b0906ca19..1a81b1a5f7 100644
--- a/Source/Core/VideoCommon/CMakeLists.txt
+++ b/Source/Core/VideoCommon/CMakeLists.txt
@@ -134,6 +134,8 @@ add_library(videocommon
PerformanceMetrics.h
PerformanceTracker.cpp
PerformanceTracker.h
+ PipelineUtils.cpp
+ PipelineUtils.h
PixelEngine.cpp
PixelEngine.h
PixelShaderGen.cpp
diff --git a/Source/Core/VideoCommon/PipelineUtils.cpp b/Source/Core/VideoCommon/PipelineUtils.cpp
new file mode 100644
index 0000000000..a63f7f00e8
--- /dev/null
+++ b/Source/Core/VideoCommon/PipelineUtils.cpp
@@ -0,0 +1,172 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "VideoCommon/PipelineUtils.h"
+
+#include "Common/Assert.h"
+#include "Common/Logging/Log.h"
+
+#include "VideoCommon/BPMemory.h"
+#include "VideoCommon/ConstantManager.h"
+#include "VideoCommon/DriverDetails.h"
+#include "VideoCommon/GeometryShaderGen.h"
+#include "VideoCommon/PixelShaderGen.h"
+#include "VideoCommon/RenderState.h"
+#include "VideoCommon/ShaderGenCommon.h"
+#include "VideoCommon/VertexShaderGen.h"
+#include "VideoCommon/VideoConfig.h"
+
+namespace VideoCommon
+{
+/// Edits the UID based on driver bugs and other special configurations
+GXPipelineUid ApplyDriverBugs(const GXPipelineUid& in)
+{
+ GXPipelineUid out;
+ // TODO: static_assert(std::is_trivially_copyable_v);
+ // GXPipelineUid is not trivially copyable because RasterizationState and BlendingState aren't
+ // either, but we can pretend it is for now. This will be improved after PR #10848 is finished.
+ memcpy(static_cast(&out), static_cast(&in), sizeof(out)); // copy padding
+ pixel_shader_uid_data* ps = out.ps_uid.GetUidData();
+ BlendingState& blend = out.blending_state;
+
+ if (ps->ztest == EmulatedZ::ForcedEarly && !out.depth_state.update_enable)
+ {
+ // No need to force early depth test if you're not writing z
+ ps->ztest = EmulatedZ::Early;
+ }
+
+ // If framebuffer fetch is available, we can emulate logic ops in the fragment shader
+ // and don't need the below blend approximation
+ if (blend.logic_op_enable && !g_backend_info.bSupportsLogicOp &&
+ !g_backend_info.bSupportsFramebufferFetch)
+ {
+ if (!blend.LogicOpApproximationIsExact())
+ WARN_LOG_FMT(VIDEO,
+ "Approximating logic op with blending, this will produce incorrect rendering.");
+ if (blend.LogicOpApproximationWantsShaderHelp())
+ {
+ ps->emulate_logic_op_with_blend = true;
+ ps->logic_op_mode = static_cast(blend.logic_mode.Value());
+ }
+ blend.ApproximateLogicOpWithBlending();
+ }
+
+ const bool benefits_from_ps_dual_source_off =
+ (!g_backend_info.bSupportsDualSourceBlend && g_backend_info.bSupportsFramebufferFetch) ||
+ DriverDetails::HasBug(DriverDetails::BUG_BROKEN_DUAL_SOURCE_BLENDING);
+ if (benefits_from_ps_dual_source_off && !blend.RequiresDualSrc())
+ {
+ // Only use dual-source blending when required on drivers that don't support it very well.
+ ps->no_dual_src = true;
+ blend.use_dual_src = false;
+ }
+
+ if (g_backend_info.bSupportsFramebufferFetch)
+ {
+ bool fbfetch_blend = false;
+ if ((DriverDetails::HasBug(DriverDetails::BUG_BROKEN_DISCARD_WITH_EARLY_Z) ||
+ !g_backend_info.bSupportsEarlyZ) &&
+ ps->ztest == EmulatedZ::ForcedEarly)
+ {
+ ps->ztest = EmulatedZ::EarlyWithFBFetch;
+ fbfetch_blend |= static_cast(out.blending_state.blend_enable);
+ ps->no_dual_src = true;
+ }
+ fbfetch_blend |= blend.logic_op_enable && !g_backend_info.bSupportsLogicOp;
+ fbfetch_blend |= blend.use_dual_src && !g_backend_info.bSupportsDualSourceBlend;
+ if (fbfetch_blend)
+ {
+ ps->no_dual_src = true;
+ if (blend.logic_op_enable)
+ {
+ ps->logic_op_enable = true;
+ ps->logic_op_mode = static_cast(blend.logic_mode.Value());
+ blend.logic_op_enable = false;
+ }
+ if (blend.blend_enable)
+ {
+ ps->blend_enable = true;
+ ps->blend_src_factor = blend.src_factor;
+ ps->blend_src_factor_alpha = blend.src_factor_alpha;
+ ps->blend_dst_factor = blend.dst_factor;
+ ps->blend_dst_factor_alpha = blend.dst_factor_alpha;
+ ps->blend_subtract = blend.subtract;
+ ps->blend_subtract_alpha = blend.subtract_alpha;
+ blend.blend_enable = false;
+ }
+ }
+ }
+
+ // force dual src off if we can't support it
+ if (!g_backend_info.bSupportsDualSourceBlend)
+ {
+ ps->no_dual_src = true;
+ blend.use_dual_src = false;
+ }
+
+ if (ps->ztest == EmulatedZ::ForcedEarly && !g_backend_info.bSupportsEarlyZ)
+ {
+ // These things should be false
+ ASSERT(!ps->zfreeze);
+ // ZCOMPLOC HACK:
+ // The only way to emulate alpha test + early-z is to force early-z in the shader.
+ // As this isn't available on all drivers and as we can't emulate this feature otherwise,
+ // we are only able to choose which one we want to respect more.
+ // Tests seem to have proven that writing depth even when the alpha test fails is more
+ // important that a reliable alpha test, so we just force the alpha test to always succeed.
+ // At least this seems to be less buggy.
+ ps->ztest = EmulatedZ::EarlyWithZComplocHack;
+ }
+
+ if (g_ActiveConfig.UseVSForLinePointExpand() &&
+ (out.rasterization_state.primitive == PrimitiveType::Points ||
+ out.rasterization_state.primitive == PrimitiveType::Lines))
+ {
+ // All primitives are expanded to triangles in the vertex shader
+ vertex_shader_uid_data* vs = out.vs_uid.GetUidData();
+ const PortableVertexDeclaration& decl = out.vertex_format->GetVertexDeclaration();
+ vs->position_has_3_elems = decl.position.components >= 3;
+ vs->texcoord_elem_count = 0;
+ for (int i = 0; i < 8; i++)
+ {
+ if (decl.texcoords[i].enable)
+ {
+ ASSERT(decl.texcoords[i].components <= 3);
+ vs->texcoord_elem_count |= decl.texcoords[i].components << (i * 2);
+ }
+ }
+ out.vertex_format = nullptr;
+ if (out.rasterization_state.primitive == PrimitiveType::Points)
+ vs->vs_expand = VSExpand::Point;
+ else
+ vs->vs_expand = VSExpand::Line;
+ PrimitiveType prim = g_backend_info.bSupportsPrimitiveRestart ? PrimitiveType::TriangleStrip :
+ PrimitiveType::Triangles;
+ out.rasterization_state.primitive = prim;
+ out.gs_uid.GetUidData()->primitive_type = static_cast(prim);
+ }
+
+ return out;
+}
+
+std::size_t PipelineToHash(const GXPipelineUid& in)
+{
+ XXH3_state_t pipeline_hash_state;
+ XXH3_INITSTATE(&pipeline_hash_state);
+ XXH3_64bits_reset_withSeed(&pipeline_hash_state, static_cast(1));
+ UpdateHashWithPipeline(in, &pipeline_hash_state);
+ return XXH3_64bits_digest(&pipeline_hash_state);
+}
+
+void UpdateHashWithPipeline(const GXPipelineUid& in, XXH3_state_t* hash_state)
+{
+ XXH3_64bits_update(hash_state, &in.vertex_format->GetVertexDeclaration(),
+ sizeof(PortableVertexDeclaration));
+ XXH3_64bits_update(hash_state, &in.blending_state, sizeof(BlendingState));
+ XXH3_64bits_update(hash_state, &in.depth_state, sizeof(DepthState));
+ XXH3_64bits_update(hash_state, &in.rasterization_state, sizeof(RasterizationState));
+ XXH3_64bits_update(hash_state, &in.gs_uid, sizeof(GeometryShaderUid));
+ XXH3_64bits_update(hash_state, &in.ps_uid, sizeof(PixelShaderUid));
+ XXH3_64bits_update(hash_state, &in.vs_uid, sizeof(VertexShaderUid));
+}
+} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/PipelineUtils.h b/Source/Core/VideoCommon/PipelineUtils.h
new file mode 100644
index 0000000000..4a9b7b1696
--- /dev/null
+++ b/Source/Core/VideoCommon/PipelineUtils.h
@@ -0,0 +1,22 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+
+#include "VideoCommon/GXPipelineTypes.h"
+
+namespace VideoCommon
+{
+GXPipelineUid ApplyDriverBugs(const GXPipelineUid& in);
+
+// Returns a hash of the pipeline, hashing the
+// vertex declarations instead of the native vertex format
+// object
+std::size_t PipelineToHash(const GXPipelineUid& in);
+
+// Updates an existing hash with the hash of the pipeline
+void UpdateHashWithPipeline(const GXPipelineUid& in, XXH3_state_t* hash_state);
+
+} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/ShaderCache.cpp b/Source/Core/VideoCommon/ShaderCache.cpp
index a3d07368f6..e67a23a785 100644
--- a/Source/Core/VideoCommon/ShaderCache.cpp
+++ b/Source/Core/VideoCommon/ShaderCache.cpp
@@ -15,6 +15,7 @@
#include "VideoCommon/DriverDetails.h"
#include "VideoCommon/FramebufferManager.h"
#include "VideoCommon/FramebufferShaderGen.h"
+#include "VideoCommon/PipelineUtils.h"
#include "VideoCommon/Present.h"
#include "VideoCommon/Statistics.h"
#include "VideoCommon/VertexLoaderManager.h"
@@ -604,141 +605,10 @@ AbstractPipelineConfig ShaderCache::GetGXPipelineConfig(
return config;
}
-/// Edits the UID based on driver bugs and other special configurations
-static GXPipelineUid ApplyDriverBugs(const GXPipelineUid& in)
-{
- GXPipelineUid out;
- // TODO: static_assert(std::is_trivially_copyable_v);
- // GXPipelineUid is not trivially copyable because RasterizationState and BlendingState aren't
- // either, but we can pretend it is for now. This will be improved after PR #10848 is finished.
- memcpy(static_cast(&out), static_cast(&in), sizeof(out)); // copy padding
- pixel_shader_uid_data* ps = out.ps_uid.GetUidData();
- BlendingState& blend = out.blending_state;
-
- if (ps->ztest == EmulatedZ::ForcedEarly && !out.depth_state.update_enable)
- {
- // No need to force early depth test if you're not writing z
- ps->ztest = EmulatedZ::Early;
- }
-
- // If framebuffer fetch is available, we can emulate logic ops in the fragment shader
- // and don't need the below blend approximation
- if (blend.logic_op_enable && !g_backend_info.bSupportsLogicOp &&
- !g_backend_info.bSupportsFramebufferFetch)
- {
- if (!blend.LogicOpApproximationIsExact())
- WARN_LOG_FMT(VIDEO,
- "Approximating logic op with blending, this will produce incorrect rendering.");
- if (blend.LogicOpApproximationWantsShaderHelp())
- {
- ps->emulate_logic_op_with_blend = true;
- ps->logic_op_mode = static_cast(blend.logic_mode.Value());
- }
- blend.ApproximateLogicOpWithBlending();
- }
-
- const bool benefits_from_ps_dual_source_off =
- (!g_backend_info.bSupportsDualSourceBlend && g_backend_info.bSupportsFramebufferFetch) ||
- DriverDetails::HasBug(DriverDetails::BUG_BROKEN_DUAL_SOURCE_BLENDING);
- if (benefits_from_ps_dual_source_off && !blend.RequiresDualSrc())
- {
- // Only use dual-source blending when required on drivers that don't support it very well.
- ps->no_dual_src = true;
- blend.use_dual_src = false;
- }
-
- if (g_backend_info.bSupportsFramebufferFetch)
- {
- bool fbfetch_blend = false;
- if ((DriverDetails::HasBug(DriverDetails::BUG_BROKEN_DISCARD_WITH_EARLY_Z) ||
- !g_backend_info.bSupportsEarlyZ) &&
- ps->ztest == EmulatedZ::ForcedEarly)
- {
- ps->ztest = EmulatedZ::EarlyWithFBFetch;
- fbfetch_blend |= static_cast(out.blending_state.blend_enable);
- ps->no_dual_src = true;
- }
- fbfetch_blend |= blend.logic_op_enable && !g_backend_info.bSupportsLogicOp;
- fbfetch_blend |= blend.use_dual_src && !g_backend_info.bSupportsDualSourceBlend;
- if (fbfetch_blend)
- {
- ps->no_dual_src = true;
- if (blend.logic_op_enable)
- {
- ps->logic_op_enable = true;
- ps->logic_op_mode = static_cast(blend.logic_mode.Value());
- blend.logic_op_enable = false;
- }
- if (blend.blend_enable)
- {
- ps->blend_enable = true;
- ps->blend_src_factor = blend.src_factor;
- ps->blend_src_factor_alpha = blend.src_factor_alpha;
- ps->blend_dst_factor = blend.dst_factor;
- ps->blend_dst_factor_alpha = blend.dst_factor_alpha;
- ps->blend_subtract = blend.subtract;
- ps->blend_subtract_alpha = blend.subtract_alpha;
- blend.blend_enable = false;
- }
- }
- }
-
- // force dual src off if we can't support it
- if (!g_backend_info.bSupportsDualSourceBlend)
- {
- ps->no_dual_src = true;
- blend.use_dual_src = false;
- }
-
- if (ps->ztest == EmulatedZ::ForcedEarly && !g_backend_info.bSupportsEarlyZ)
- {
- // These things should be false
- ASSERT(!ps->zfreeze);
- // ZCOMPLOC HACK:
- // The only way to emulate alpha test + early-z is to force early-z in the shader.
- // As this isn't available on all drivers and as we can't emulate this feature otherwise,
- // we are only able to choose which one we want to respect more.
- // Tests seem to have proven that writing depth even when the alpha test fails is more
- // important that a reliable alpha test, so we just force the alpha test to always succeed.
- // At least this seems to be less buggy.
- ps->ztest = EmulatedZ::EarlyWithZComplocHack;
- }
-
- if (g_ActiveConfig.UseVSForLinePointExpand() &&
- (out.rasterization_state.primitive == PrimitiveType::Points ||
- out.rasterization_state.primitive == PrimitiveType::Lines))
- {
- // All primitives are expanded to triangles in the vertex shader
- vertex_shader_uid_data* vs = out.vs_uid.GetUidData();
- const PortableVertexDeclaration& decl = out.vertex_format->GetVertexDeclaration();
- vs->position_has_3_elems = decl.position.components >= 3;
- vs->texcoord_elem_count = 0;
- for (int i = 0; i < 8; i++)
- {
- if (decl.texcoords[i].enable)
- {
- ASSERT(decl.texcoords[i].components <= 3);
- vs->texcoord_elem_count |= decl.texcoords[i].components << (i * 2);
- }
- }
- out.vertex_format = nullptr;
- if (out.rasterization_state.primitive == PrimitiveType::Points)
- vs->vs_expand = VSExpand::Point;
- else
- vs->vs_expand = VSExpand::Line;
- PrimitiveType prim = g_backend_info.bSupportsPrimitiveRestart ? PrimitiveType::TriangleStrip :
- PrimitiveType::Triangles;
- out.rasterization_state.primitive = prim;
- out.gs_uid.GetUidData()->primitive_type = static_cast(prim);
- }
-
- return out;
-}
-
std::optional
ShaderCache::GetGXPipelineConfig(const GXPipelineUid& config_in)
{
- GXPipelineUid config = ApplyDriverBugs(config_in);
+ GXPipelineUid config = VideoCommon::ApplyDriverBugs(config_in);
const AbstractShader* vs;
auto vs_iter = m_vs_cache.shader_map.find(config.vs_uid);
if (vs_iter != m_vs_cache.shader_map.end() && !vs_iter->second.pending)