Repository: RPCS3/rpcs3
Pull request #16797: RSX: Story about four optimizations
Author: login: elad335
Labels: name: RSX
name: Shaders
name: Optimization
description: Optimizes existing code
Created: 2025-03-03T05:37:26Z | Merged: 2025-03-08T12:22:01Z

* Prevent reload of vertex transform program if program data write did not change the program.
* Optimize sucessive fragment program and vertex program use in two ways:
1. If no change was made to the shader program, try to reuse the compiled program output without heavy validation.
2. If data partially matches between current vertex and shader programs to the previous ones, try to do a fast comparison to see f they match. Skipping hash table resolving.
* Add a method that encapsulates fragment texture changes which is a minor optimization of FIFO execution.

**Metal Gear Online (Menu): ~7000 FPS to 10,000 fps**
**Ninja Guiden Sigma:  194 fps to 202 fps**
**Ratchet And Clank: Toold Of Destruction: 22 -> 24 FPS!**



Diff:
diff --git a/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp b/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp
index b65553976f0e..e11cc7a81722 100644
--- a/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp
+++ b/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp
@@ -8148,15 +8148,23 @@ std::array<reg_state_t, s_reg_max>& block_reg_info::evaluate_start_state(const s
 					}
 				}
 
-				auto& res_state = is_all_resolved ? cur_node->start_reg_state : temp;
-
-				if (!is_all_resolved)
+				if (qi == 0)
 				{
-					res_state = reg_state_t::make_unknown<s_reg_max>(it->block_pc);
+					// TODO: First block is always resolved here, but this logic can be improved to detect more cases of opportunistic resolving
+					is_all_resolved = true;
 				}
 
-				for (usz bi = 0; is_all_resolved && bi < it->state_prev.size(); bi++)
+				auto& res_state = is_all_resolved ? cur_node->start_reg_state : temp;
+
+				for (usz bi = 0, is_first = 1; bi < it->state_prev.size(); bi++)
 				{
+					if (it->state_prev[bi].disconnected)
+					{
+						// Loop state, even if not ignored for a million times the result would still be the same
+						// So ignore it
+						continue;
+					}
+
 					std::array<reg_state_t, s_reg_max>* arg_state{};
 					const auto& node = ::at32(map, it->state_prev[bi].block_pc);
 
@@ -8172,7 +8180,7 @@ std::array<reg_state_t, s_reg_max>& block_reg_info::evaluate_start_state(const s
 						ensure(it->state_prev[bi].state_written);
 					}
 
-					if (bi == 0)
+					if (is_first)
 					{
 						res_state = *arg_state;
 					}
@@ -8180,6 +8188,8 @@ std::array<reg_state_t, s_reg_max>& block_reg_info::evaluate_start_state(const s
 					{
 						merge(res_state, res_state, *arg_state, it->block_pc);
 					}
+
+					is_first = 0;
 				}
 
 				std::array<reg_state_t, s_reg_max>* result_storage{};
diff --git a/rpcs3/Emu/RSX/GL/GLGSRender.cpp b/rpcs3/Emu/RSX/GL/GLGSRender.cpp
index 6dbd7f919cd9..0a8b78bf5121 100644
--- a/rpcs3/Emu/RSX/GL/GLGSRender.cpp
+++ b/rpcs3/Emu/RSX/GL/GLGSRender.cpp
@@ -781,7 +781,10 @@ bool GLGSRender::load_program()
 	if (shadermode != shader_mode::interpreter_only) [[likely]]
 	{
 		void* pipeline_properties = nullptr;
-		std::tie(m_program, m_vertex_prog, m_fragment_prog) = m_prog_buffer.get_graphics_pipeline(current_vertex_program, current_fragment_program, pipeline_properties,
+		std::tie(m_program, m_vertex_prog, m_fragment_prog) = m_prog_buffer.get_graphics_pipeline(
+			current_vertex_program, vertex_program_invalidation_count, 
+			current_fragment_program, fragment_program_invalidation_count,
+			pipeline_properties,
 			shadermode != shader_mode::recompiler, true);
 
 		if (m_prog_buffer.check_cache_missed())
diff --git a/rpcs3/Emu/RSX/GL/GLProgramBuffer.h b/rpcs3/Emu/RSX/GL/GLProgramBuffer.h
index 2d6f19f2719d..9a1d249f5917 100644
--- a/rpcs3/Emu/RSX/GL/GLProgramBuffer.h
+++ b/rpcs3/Emu/RSX/GL/GLProgramBuffer.h
@@ -135,13 +135,13 @@ struct GLProgramBuffer : public program_state_cache<GLTraits>
 	template <typename... Args>
 	void add_pipeline_entry(const RSXVertexProgram& vp, const RSXFragmentProgram& fp, void* &props, Args&& ...args)
 	{
-		get_graphics_pipeline(vp, fp, props, false, false, std::forward<Args>(args)...);
+		get_graphics_pipeline(vp, umax, fp, umax, props, false, false, std::forward<Args>(args)...);
 	}
 
 	void preload_programs(const RSXVertexProgram& vp, const RSXFragmentProgram& fp)
 	{
-		search_vertex_program(vp);
-		search_fragment_program(fp);
+		search_vertex_program(vp, umax);
+		search_fragment_program(fp, umax);
 	}
 
 	bool check_cache_missed() const
diff --git a/rpcs3/Emu/RSX/NV47/HW/common.cpp b/rpcs3/Emu/RSX/NV47/HW/common.cpp
index f60ac94f5faa..558a5c8309a5 100644
--- a/rpcs3/Emu/RSX/NV47/HW/common.cpp
+++ b/rpcs3/Emu/RSX/NV47/HW/common.cpp
@@ -62,8 +62,13 @@ namespace rsx
 			return vm::cast(get_address(offset, location));
 		}
 
-		void set_fragment_texture_dirty_bit(rsx::context* ctx, u32 index)
+		void set_fragment_texture_dirty_bit(rsx::context* ctx, u32 arg, u32 index, bool /*is_shader_config*/)
 		{
+			if (REGS(ctx)->latch == arg)
+			{
+				return;
+			}
+
 			RSX(ctx)->m_textures_dirty[index] = true;
 
 			if (RSX(ctx)->current_fp_metadata.referenced_textures_mask & (1 << index))
@@ -72,6 +77,78 @@ namespace rsx
 			}
 		}
 
+		void set_texture_configuration_command(rsx::context* ctx, u32 reg)
+		{
+			const u32 reg_index = reg - NV4097_SET_TEXTURE_OFFSET;
+			ensure(reg_index % 8 == 0 && reg_index < 8 * 16); // Only NV4097_SET_TEXTURE_OFFSET is expected
+
+			const u32 texture_index = reg_index / 8;
+
+			// FIFO args count including this one
+			const u32 fifo_args_cnt = RSX(ctx)->fifo_ctrl->get_remaining_args_count() + 1;
+
+			// The range of methods this function resposible to
+			constexpr u32 method_range = 8;
+
+			// Get limit imposed by FIFO PUT (if put is behind get it will result in a number ignored by min)
+			const u32 fifo_read_limit = static_cast<u32>(((RSX(ctx)->ctrl->put & ~3ull) - (RSX(ctx)->fifo_ctrl->get_pos())) / 4);
+
+			const u32 count = std::min<u32>({ fifo_args_cnt, fifo_read_limit, method_range });
+
+			// Clamp by the count of methods this function is responsible to
+			std::span<const u32> command_span = RSX(ctx)->fifo_ctrl->get_current_arg_ptr();
+
+			if (command_span.size() > count)
+			{
+				command_span = command_span.subspan(0, count);
+			}
+
+			ensure(!command_span.empty());
+
+			u32* const dst_regs = &REGS(ctx)->registers[reg];
+ 
+			//u8 change_mask = 0;
+			u8 change_mask = 0xff;
+
+			if (dst_regs[0] != REGS(ctx)->latch)
+			{
+				// Fixup for the first method
+				change_mask |= 1;
+			}
+
+			for (usz i = 1; i < command_span.size(); i++)
+			{
+				const u32 command_data = std::bit_cast<be_t<u32>>(command_span[i]);
+
+				change_mask |= command_data != dst_regs[i] ? (1u << i) : 0;
+
+				dst_regs[i] = command_data;
+			}
+
+			// Disabled
+			// Bits set:
+			// NV4097_SET_TEXTURE_OFFSET
+			// NV4097_SET_TEXTURE_IMAGE_RECT
+			//constexpr u32 mask_of_texture_data_not_affecting_shader_config = 0x41;
+			constexpr u32 mask_of_texture_data_not_affecting_shader_config = 0;
+
+			if (change_mask)
+			{
+				RSX(ctx)->m_textures_dirty[texture_index] = true;
+
+				if (~mask_of_texture_data_not_affecting_shader_config & change_mask)
+				{
+					if (RSX(ctx)->current_fp_metadata.referenced_textures_mask & (1 << texture_index))
+					{
+						RSX(ctx)->m_graphics_state |= rsx::pipeline_state::fragment_program_state_dirty;
+					}
+				}
+			}
+
+			// Skip handled methods
+			RSX(ctx)->fifo_ctrl->skip_methods(static_cast<u32>(command_span.size()) - 1);
+		}
+
 		void set_vertex_texture_dirty_bit(rsx::context* ctx, u32 index)
 		{
 			RSX(ctx)->m_vertex_textures_dirty[index] = true;
diff --git a/rpcs3/Emu/RSX/NV47/HW/common.h b/rpcs3/Emu/RSX/NV47/HW/common.h
index bdcc367de011..0b22906ac9d3 100644
--- a/rpcs3/Emu/RSX/NV47/HW/common.h
+++ b/rpcs3/Emu/RSX/NV47/HW/common.h
@@ -17,7 +17,9 @@ namespace rsx
 
 		void push_draw_parameter_change(rsx::context* ctx, rsx::command_barrier_type type, u32 reg, u32 arg0, u32 arg1 = 0u, u32 index = 0u);
 
-		void set_fragment_texture_dirty_bit(rsx::context* ctx, u32 index);
+		void set_fragment_texture_dirty_bit(rsx::context* ctx, u32 arg, u32 index, bool is_shader_config);
+
+		void set_texture_configuration_command(rsx::context* ctx, u32 reg);
 
 		void set_vertex_texture_dirty_bit(rsx::context* ctx, u32 index);
 	}
diff --git a/rpcs3/Emu/RSX/NV47/HW/nv406e.cpp b/rpcs3/Emu/RSX/NV47/HW/nv406e.cpp
index ced6c3f38634..242b1bdb7356 100644
--- a/rpcs3/Emu/RSX/NV47/HW/nv406e.cpp
+++ b/rpcs3/Emu/RSX/NV47/HW/nv406e.cpp
@@ -25,6 +25,9 @@ namespace rsx
 			RSX(ctx)->sync_point_request.release(true);
 			const u32 addr = get_address(REGS(ctx)->semaphore_offset_406e(), REGS(ctx)->semaphore_context_dma_406e());
 
+			// Syncronization point, may be associated with memory changes without actually changing addresses
+			RSX(ctx)->m_graphics_state |= rsx::pipeline_state::fragment_program_ucode_dirty;
+
 			const auto& sema = vm::_ref<RsxSemaphore>(addr).val;
 
 			if (sema == arg)
diff --git a/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp b/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp
index 8d9aecfaee5e..ce7d0130a298 100644
--- a/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp
+++ b/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp
@@ -155,9 +155,38 @@ namespace rsx
 				rcount = ::size32(fifo_span);
 			}
 
-			copy_data_swap_u32(&REGS(ctx)->transform_program[load_pos * 4 + index % 4], fifo_span.data(), rcount);
+			const auto out_ptr = &REGS(ctx)->transform_program[load_pos * 4 + index % 4];
 
-			RSX(ctx)->m_graphics_state |= rsx::pipeline_state::vertex_program_ucode_dirty;
+			pipeline_state to_set_dirty = rsx::pipeline_state::vertex_program_ucode_dirty;
+
+			if (rcount >= 4 && !RSX(ctx)->m_graphics_state.test(rsx::pipeline_state::vertex_program_ucode_dirty))
+			{
+				// Assume clean
+				to_set_dirty = {};
+
+				const usz first_index_off = 0;
+				const usz second_index_off = (((rcount / 4) - 1) / 2) * 4;
+
+				const u64 src_op1_2 = read_from_ptr<be_t<u64>>(fifo_span.data() + first_index_off);
+				const u64 src_op2_2 = read_from_ptr<be_t<u64>>(fifo_span.data() + second_index_off);
+
+				// Fast comparison
+				if (src_op1_2 != read_from_ptr<u64>(out_ptr + first_index_off) || src_op2_2 != read_from_ptr<u64>(out_ptr + second_index_off))
+				{
+					to_set_dirty = rsx::pipeline_state::vertex_program_ucode_dirty;
+				}
+			}
+
+			if (to_set_dirty)
+			{
+				copy_data_swap_u32(out_ptr, fifo_span.data(), rcount);
+			}
+			else if (copy_data_swap_u32_cmp(out_ptr, fifo_span.data(), rcount))
+			{
+				to_set_dirty = rsx::pipeline_state::vertex_program_ucode_dirty;
+			}
+
+			RSX(ctx)->m_graphics_state |= to_set_dirty;
 			REGS(ctx)->transform_program_load_set(load_pos + ((rcount + index % 4) / 4));
 			RSX(ctx)->fifo_ctrl->skip_methods(rcount - 1);
 		}
diff --git a/rpcs3/Emu/RSX/NV47/HW/nv4097.h b/rpcs3/Emu/RSX/NV47/HW/nv4097.h
index 6d526d799a27..4dff809123e9 100644
--- a/rpcs3/Emu/RSX/NV47/HW/nv4097.h
+++ b/rpcs3/Emu/RSX/NV47/HW/nv4097.h
@@ -221,12 +221,31 @@ namespace rsx
 			}
 		};
 
-		template<u32 index>
-		struct set_texture_dirty_bit
+		template <u32 index>
+		struct set_texture_dirty_bit_texture_config
 		{
-			static void impl(context* ctx, u32 /*reg*/, u32 /*arg*/)
+			static void impl(context* ctx, u32 /*reg*/, u32 arg)
+			{
+				util::set_fragment_texture_dirty_bit(ctx, arg, index, true);
+			}
+		};
+
+		template <u32 index>
+		struct set_texture_offset
+		{
+			static void impl(context* ctx, u32 reg, u32 /*arg*/)
+			{
+				fmt::throw_exception("Unreacable!");
+				util::set_texture_configuration_command(ctx, reg);
+			}
+		};
+
+		template <u32 index>
+		struct set_texture_dirty_bit_location_and_area
+		{
+			static void impl(context* ctx, u32 /*reg*/, u32 arg)
 			{
-				util::set_fragment_texture_dirty_bit(ctx, index);
+				util::set_fragment_texture_dirty_bit(ctx, arg, index, false);
 			}
 		};
 
diff --git a/rpcs3/Emu/RSX/NV47/HW/nv47_sync.hpp b/rpcs3/Emu/RSX/NV47/HW/nv47_sync.hpp
index 9c153b205665..77a1a10b11c2 100644
--- a/rpcs3/Emu/RSX/NV47/HW/nv47_sync.hpp
+++ b/rpcs3/Emu/RSX/NV47/HW/nv47_sync.hpp
@@ -44,6 +44,9 @@ namespace rsx
 						// Manually flush the pipeline.
 						// It is possible to stream report writes using the host GPU, but that generates too much submit traffic.
 						RSX(ctx)->sync();
+
+						// Syncronization point, may be associated with memory changes without actually changing addresses
+						RSX(ctx)->m_graphics_state |= rsx::pipeline_state::fragment_program_ucode_dirty;
 					}
 				}
 
diff --git a/rpcs3/Emu/RSX/Program/ProgramStateCache.cpp b/rpcs3/Emu/RSX/Program/ProgramStateCache.cpp
index 3ce6dbca41e4..a6979a8f2baf 100644
--- a/rpcs3/Emu/RSX/Program/ProgramStateCache.cpp
+++ b/rpcs3/Emu/RSX/Program/ProgramStateCache.cpp
@@ -770,6 +770,21 @@ bool fragment_program_compare::operator()(const RSXFragmentProgram& binary1, con
 	return true;
 }
 
+bool fragment_program_compare::config_only(const RSXFragmentProgram& binary1, const RSXFragmentProgram& binary2)
+{
+	if (binary1.ucode_length != binary2.ucode_length ||
+		binary1.ctrl != binary2.ctrl ||
+		binary1.texture_state != binary2.texture_state ||
+		binary1.texcoord_control_mask != binary2.texcoord_control_mask ||
+		binary1.two_sided_lighting != binary2.two_sided_lighting ||
+		binary1.mrt_buffers_count != binary2.mrt_buffers_count)
+	{
+		return false;
+	}
+
+	return true;
+}
+
 namespace rsx
 {
 #if defined(ARCH_X64) || defined(ARCH_ARM64)
diff --git a/rpcs3/Emu/RSX/Program/ProgramStateCache.h b/rpcs3/Emu/RSX/Program/ProgramStateCache.h
index 17857b80ada3..6e460ab580c3 100644
--- a/rpcs3/Emu/RSX/Program/ProgramStateCache.h
+++ b/rpcs3/Emu/RSX/Program/ProgramStateCache.h
@@ -80,6 +80,7 @@ namespace program_hash_util
 	struct fragment_program_compare
 	{
 		bool operator()(const RSXFragmentProgram &binary1, const RSXFragmentProgram &binary2) const;
+		static bool config_only(const RSXFragmentProgram &binary1, const RSXFragmentProgram &binary2);
 	};
 }
 
@@ -167,22 +168,42 @@ class program_state_cache
 	pipeline_storage_type __null_pipeline_handle;
 
 	/// bool here to inform that the program was preexisting.
-	std::tuple<const vertex_program_type&, bool> search_vertex_program(const RSXVertexProgram& rsx_vp, bool force_load = true)
+	std::tuple<const vertex_program_type&, bool> search_vertex_program(const RSXVertexProgram& rsx_vp, usz rsx_vp_invalidation_count)
 	{
 		bool recompile = false;
 		vertex_program_type* new_shader;
 		{
+			thread_local const std::pair<const RSXVertexProgram, vertex_program_type>* prev_vp = nullptr;
+			thread_local usz prev_map_count = umax, prev_rsx_count = umax;
+			static atomic_t<usz> map_invl_count = 0;
+
 			reader_lock lock(m_vertex_mutex);
 
-			const auto& I = m_vertex_shader_cache.find(rsx_vp);
-			if (I != m_vertex_shader_cache.end())
+			if (prev_map_count == map_invl_count)
 			{
-				return std::forward_as_tuple(I->second, true);
+				// prev_vp must be non-null here
+				if (prev_vp->first.data.size() == rsx_vp.data.size() && prev_vp->first.output_mask == rsx_vp.output_mask)
+				{
+					if (rsx_vp_invalidation_count != umax && prev_rsx_count == rsx_vp_invalidation_count)
+					{
+						return std::forward_as_tuple(prev_vp->second, true);
+					}
+
+					if (program_hash_util::vertex_program_compare()(prev_vp->first, rsx_vp))
+					{
+						prev_rsx_count = rsx_vp_invalidation_count;
+						return std::forward_as_tuple(prev_vp->second, true);
+					}
+				}
 			}
 
-			if (!force_load)
+			const auto& I = m_vertex_shader_cache.find(rsx_vp);
+			if (I != m_vertex_shader_cache.end())
 			{
-				return std::forward_as_tuple(__null_vertex_program, false);
+				prev_vp = &*I;
+				prev_map_count = map_invl_count;
+				prev_rsx_count = rsx_vp_invalidation_count;
+				return std::forward_as_tuple(I->second, true);
 			}
 
 			rsx_log.trace("VP not found in buffer!");
@@ -191,6 +212,10 @@ class program_state_cache
 			auto [it, inserted] = m_vertex_shader_cache.try_emplace(rsx_vp);
 			new_shader = &(it->second);
 			recompile = inserted;
+			prev_map_count = umax;
+			prev_rsx_count = umax;
+			prev_vp = nullptr;
+			map_invl_count++;
 		}
 
 		if (recompile)
@@ -202,24 +227,48 @@ class program_state_cache
 	}
 
 	/// bool here to inform that the program was preexisting.
-	std::tuple<const fragment_program_type&, bool> search_fragment_program(const RSXFragmentProgram& rsx_fp, bool force_load = true)
+	std::tuple<const fragment_program_type&, bool> search_fragment_program(const RSXFragmentProgram& rsx_fp, usz rsx_fp_invalidation_count)
 	{
 		bool recompile = false;
 		typename binary_to_fragment_program::iterator it;
 		fragment_program_type* new_shader;
-
 		{
+			thread_local const std::pair<const RSXFragmentProgram, fragment_program_type>* prev_fp = nullptr;
+			thread_local usz prev_map_count = umax, prev_rsx_count = umax;
+			static atomic_t<usz> map_invl_count = 0;
+
 			reader_lock lock(m_fragment_mutex);
 
-			const auto& I = m_fragment_shader_cache.find(rsx_fp);
-			if (I != m_fragment_shader_cache.end())
+			if (prev_map_count == map_invl_count)
 			{
-				return std::forward_as_tuple(I->second, true);
+				// prev_vp must be non-null here
+				if (prev_fp->first.ucode_length == rsx_fp.ucode_length && prev_fp->first.texcoord_control_mask == rsx_fp.texcoord_control_mask)
+				{
+					if (rsx_fp_invalidation_count != umax && prev_rsx_count == rsx_fp_invalidation_count)
+					{
+						// Shader UCODE must be the same.
+						// Shader config changes are not tracked at the moment
+						// Compare manually
+						if (program_hash_util::fragment_program_compare::config_only(prev_fp->first, rsx_fp))
+						{
+							return std::forward_as_tuple(prev_fp->second, true);
+						}
+					}
+					else if (program_hash_util::fragment_program_compare()(prev_fp->first, rsx_fp))
+					{
+						prev_rsx_count = rsx_fp_invalidation_count;
+						return std::forward_as_tuple(prev_fp->second, true);
+					}
+				}
 			}
 
-			if (!force_load)
+			const auto& I = m_fragment_shader_cache.find(rsx_fp);
+			if (I != m_fragment_shader_cache.end())
 			{
-				return std::forward_as_tuple(__null_fragment_program, false);
+				prev_fp = &*I;
+				prev_rsx_count = rsx_fp_invalidation_count;
+				prev_map_count = map_invl_count;
+				return std::forward_as_tuple(I->second, true);
 			}
 
 			rsx_log.trace("FP not found in buffer!");
@@ -227,6 +276,10 @@ class program_state_cache
 			lock.upgrade();
 			std::tie(it, recompile) = m_fragment_shader_cache.try_emplace(rsx_fp);
 			new_shader = &(it->second);
+			prev_map_count = umax;
+			prev_rsx_count = umax;
+			prev_fp = nullptr;
+			map_invl_count++;
 		}
 
 		if (recompile)
@@ -290,15 +343,17 @@ class program_state_cache
 	template<typename... Args>
 	pipeline_data_type get_graphics_pipeline(
 		const RSXVertexProgram& vertexShader,
+		usz vertexShaderInvalidationCount,
 		const RSXFragmentProgram& fragmentShader,
+		usz fragmentShaderInvalidationCount,
 		pipeline_properties& pipelineProperties,
 		bool compile_async,
 		bool allow_notification,
 		Args&& ...args
 	)
 	{
-		const auto& vp_search = search_vertex_program(vertexShader);
-		const auto& fp_search = search_fragment_program(fragmentShader);
+		const auto& vp_search = search_vertex_program(vertexShader, vertexShaderInvalidationCount);
+		const auto& fp_search = search_fragment_program(fragmentShader, fragmentShaderInvalidationCount);
 
 		const bool already_existing_fragment_program = std::get<1>(fp_search);
 		const bool already_existing_vertex_program = std::get<1>(vp_search);
diff --git a/rpcs3/Emu/RSX/RSXThread.cpp b/rpcs3/Emu/RSX/RSXThread.cpp
index 420aa6eb8ef2..c12066ffb853 100644
--- a/rpcs3/Emu/RSX/RSXThread.cpp
+++ b/rpcs3/Emu/RSX/RSXThread.cpp
@@ -1904,6 +1904,7 @@ namespace rsx
 		}
 
 		m_graphics_state.clear(rsx::pipeline_state::fragment_program_ucode_dirty);
+		fragment_program_invalidation_count++;
 
 		// Request for update of fragment constants if the program block is invalidated
 		m_graphics_state |= rsx::pipeline_state::fragment_constants_dirty;
@@ -1953,6 +1954,7 @@ namespace rsx
 		}
 
 		m_graphics_state.clear(rsx::pipeline_state::vertex_program_ucode_dirty);
+		vertex_program_invalidation_count++;
 
 		// Reload transform constants unconditionally for now
 		m_graphics_state |= rsx::pipeline_state::transform_constants_dirty;
@@ -2010,6 +2012,7 @@ namespace rsx
 			return;
 		}
 
+		vertex_program_invalidation_count++;
 		ensure(!m_graphics_state.test(rsx::pipeline_state::vertex_program_ucode_dirty));
 		current_vertex_program.output_mask = rsx::method_registers.vertex_attrib_output_mask();
 
@@ -2045,6 +2048,9 @@ namespace rsx
 
 		m_graphics_state.clear(rsx::pipeline_state::fragment_program_dirty);
 
+		// FP config is always checked for now (see get_graphics_pipeline)
+		//fragment_program_invalidation_count++;
+
 		current_fragment_program.ctrl = m_ctx->register_state->shader_control() & (CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS | CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT);
 		current_fragment_program.texcoord_control_mask = m_ctx->register_state->texcoord_control_mask();
 		current_fragment_program.two_sided_lighting = m_ctx->register_state->two_side_light_en();
diff --git a/rpcs3/Emu/RSX/RSXThread.h b/rpcs3/Emu/RSX/RSXThread.h
index 8b5f2af10d58..19b40d308769 100644
--- a/rpcs3/Emu/RSX/RSXThread.h
+++ b/rpcs3/Emu/RSX/RSXThread.h
@@ -242,6 +242,8 @@ namespace rsx
 
 		rsx::atomic_bitmask_t<rsx::eng_interrupt_reason> m_eng_interrupt_mask;
 		rsx::bitmask_t<rsx::pipeline_state> m_graphics_state;
+		u64 fragment_program_invalidation_count = umax;
+		u64 vertex_program_invalidation_count = umax;
 		u64 ROP_sync_timestamp = 0;
 
 		program_hash_util::fragment_program_utils::fragment_program_metadata current_fp_metadata = {};
diff --git a/rpcs3/Emu/RSX/VK/VKGSRender.cpp b/rpcs3/Emu/RSX/VK/VKGSRender.cpp
index d37f6d62d0b9..2513b070eb8e 100644
--- a/rpcs3/Emu/RSX/VK/VKGSRender.cpp
+++ b/rpcs3/Emu/RSX/VK/VKGSRender.cpp
@@ -1960,7 +1960,10 @@ bool VKGSRender::load_program()
 		vk::enter_uninterruptible();
 
 		// Load current program from cache
-		std::tie(m_program, m_vertex_prog, m_fragment_prog) = m_prog_buffer->get_graphics_pipeline(vertex_program, fragment_program, m_pipeline_properties,
+		std::tie(m_program, m_vertex_prog, m_fragment_prog) = m_prog_buffer->get_graphics_pipeline(
+			vertex_program, vertex_program_invalidation_count, 
+			fragment_program, fragment_program_invalidation_count,
+			m_pipeline_properties,
 			shadermode != shader_mode::recompiler, true, m_pipeline_layout);
 
 		vk::leave_uninterruptible();
diff --git a/rpcs3/Emu/RSX/VK/VKProgramBuffer.h b/rpcs3/Emu/RSX/VK/VKProgramBuffer.h
index 34cb08bae42f..6483176c4323 100644
--- a/rpcs3/Emu/RSX/VK/VKProgramBuffer.h
+++ b/rpcs3/Emu/RSX/VK/VKProgramBuffer.h
@@ -88,13 +88,13 @@ namespace vk
 		template <typename... Args>
 		void add_pipeline_entry(RSXVertexProgram& vp, RSXFragmentProgram& fp, vk::pipeline_props& props, Args&& ...args)
 		{
-			get_graphics_pipeline(vp, fp, props, false, false, std::forward<Args>(args)...);
+			get_graphics_pipeline(vp, umax, fp, umax, props, false, false, std::forward<Args>(args)...);
 		}
 
 		void preload_programs(const RSXVertexProgram& vp, const RSXFragmentProgram& fp)
 		{
-			search_vertex_program(vp);
-			search_fragment_program(fp);
+			search_vertex_program(vp, umax);
+			search_fragment_program(fp, umax);
 		}
 
 		bool check_cache_missed() const
diff --git a/rpcs3/Emu/RSX/rsx_methods.cpp b/rpcs3/Emu/RSX/rsx_methods.cpp
index ac5d92eef2b6..babcc878ae63 100644
--- a/rpcs3/Emu/RSX/rsx_methods.cpp
+++ b/rpcs3/Emu/RSX/rsx_methods.cpp
@@ -1663,16 +1663,16 @@ namespace rsx
 		bind(NV4097_SET_SURFACE_PITCH_D, nv4097::set_surface_dirty_bit);
 		bind(NV4097_SET_SURFACE_PITCH_Z, nv4097::set_surface_dirty_bit);
 		bind(NV4097_SET_WINDOW_OFFSET, nv4097::set_surface_dirty_bit);
-		bind_range<NV4097_SET_TEXTURE_OFFSET, 8, 16, nv4097::set_texture_dirty_bit>();
-		bind_range<NV4097_SET_TEXTURE_FORMAT, 8, 16, nv4097::set_texture_dirty_bit>();
-		bind_range<NV4097_SET_TEXTURE_ADDRESS, 8, 16, nv4097::set_texture_dirty_bit>();
-		bind_range<NV4097_SET_TEXTURE_CONTROL0, 8, 16, nv4097::set_texture_dirty_bit>();
-		bind_range<NV4097_SET_TEXTURE_CONTROL1, 8, 16, nv4097::set_texture_dirty_bit>();
-		bind_range<NV4097_SET_TEXTURE_CONTROL2, 1, 16, nv4097::set_texture_dirty_bit>();
-		bind_range<NV4097_SET_TEXTURE_CONTROL3, 1, 16, nv4097::set_texture_dirty_bit>();
-		bind_range<NV4097_SET_TEXTURE_FILTER, 8, 16, nv4097::set_texture_dirty_bit>();
-		bind_range<NV4097_SET_TEXTURE_IMAGE_RECT, 8, 16, nv4097::set_texture_dirty_bit>();
-		bind_range<NV4097_SET_TEXTURE_BORDER_COLOR, 8, 16, nv4097::set_texture_dirty_bit>();
+		bind_range<NV4097_SET_TEXTURE_OFFSET, 8, 16, nv4097::set_texture_dirty_bit_location_and_area>();
+		bind_range<NV4097_SET_TEXTURE_FORMAT, 8, 16, nv4097::set_texture_dirty_bit_texture_config>();
+		bind_range<NV4097_SET_TEXTURE_ADDRESS, 8, 16, nv4097::set_texture_dirty_bit_texture_config>();
+		bind_range<NV4097_SET_TEXTURE_CONTROL0, 8, 16, nv4097::set_texture_dirty_bit_texture_config>();
+		bind_range<NV4097_SET_TEXTURE_CONTROL1, 8, 16, nv4097::set_texture_dirty_bit_texture_config>();
+		bind_range<NV4097_SET_TEXTURE_FILTER, 8, 16, nv4097::set_texture_dirty_bit_texture_config>();
+		bind_range<NV4097_SET_TEXTURE_IMAGE_RECT, 8, 16, nv4097::set_texture_dirty_bit_location_and_area>();
+		bind_range<NV4097_SET_TEXTURE_BORDER_COLOR, 8, 16, nv4097::set_texture_dirty_bit_texture_config>();
+		bind_range<NV4097_SET_TEXTURE_CONTROL2, 1, 16, nv4097::set_texture_dirty_bit_texture_config>();
+		bind_range<NV4097_SET_TEXTURE_CONTROL3, 1, 16, nv4097::set_texture_dirty_bit_texture_config>();
 		bind_range<NV4097_SET_VERTEX_TEXTURE_OFFSET, 8, 4, nv4097::set_vertex_texture_dirty_bit>();
 		bind_range<NV4097_SET_VERTEX_TEXTURE_FORMAT, 8, 4, nv4097::set_vertex_texture_dirty_bit>();
 		bind_range<NV4097_SET_VERTEX_TEXTURE_ADDRESS, 8, 4, nv4097::set_vertex_texture_dirty_bit>();


Reviews:
user: login: kd-11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T13:04:01Z
user: login: kd-11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T13:04:25Z
user: login: Whatcookie
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T14:41:43Z
user: login: elad335
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T15:03:12Z
user: login: kd-11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T15:33:18Z
user: login: kd-11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T15:34:43Z
user: login: kd-11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T21:52:44Z
user: login: kd-11
body: Nice work.
Do clean up the code as suggested though before merging.
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T21:55:09Z
user: login: kd-11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T22:53:18Z
user: login: elad335
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T07:25:11Z
user: login: kd-11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:07:50Z
user: login: kd-11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:09:23Z
user: login: kd-11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:31:05Z
user: login: kd-11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:33:27Z
user: login: kd-11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:36:52Z
user: login: kd-11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:38:56Z
user: login: elad335
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T08:56:46Z
user: login: elad335
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T09:05:20Z

Review comments:
diff_hunk: @@ -427,13 +427,13 @@ vertex_program_utils::vertex_program_metadata vertex_program_utils::analyse_vert
 
 usz vertex_program_storage_hash::operator()(const RSXVertexProgram &program) const
 {
-	const usz ucode_hash = vertex_program_utils::get_vertex_program_ucode_hash(program);
+	const usz ucode_hash = program.get_sub_hash();
path: rpcs3/Emu/RSX/Program/ProgramStateCache.cpp
user: login: kd-11
body: Storae hash is used to fingerprint ucode dumps on disk, cannot use subhash here.
created_at: 2025-03-03T13:04:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -427,13 +427,13 @@ vertex_program_utils::vertex_program_metadata vertex_program_utils::analyse_vert
 
 usz vertex_program_storage_hash::operator()(const RSXVertexProgram &program) const
 {
-	const usz ucode_hash = vertex_program_utils::get_vertex_program_ucode_hash(program);
+	const usz ucode_hash = program.get_sub_hash();
 	const u32 state_params[] =
 	{
 		program.ctrl,
 		program.output_mask,
-		program.texture_state.texture_dimensions,
-		program.texture_state.multisampled_textures,
+		//program.texture_state.texture_dimensions,
path: rpcs3/Emu/RSX/Program/ProgramStateCache.cpp
user: login: kd-11
body: Ditto
created_at: 2025-03-03T13:04:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -425,31 +348,41 @@ vertex_program_utils::vertex_program_metadata vertex_program_utils::analyse_vert
 	return result;
 }
 
+u64 RSXVertexProgram::get_sub_hash() const noexcept
+{
+	static constexpr u32 s_zero[4]{};
+
+	const usz prog_size = data.size() / 4;
+
+	const void* sdata_base = prog_size ? data.data() : s_zero;
+	const u8* sdata_start = static_cast<const u8*>(sdata_base);
+
+	const u8* sdata_mid1 = sdata_start + (prog_size / 8) * sizeof(v128);
+	const u8* sdata_mid2 = sdata_start + (prog_size / 4) * sizeof(v128);
path: rpcs3/Emu/RSX/Program/ProgramStateCache.cpp
user: login: Whatcookie
body: You're hashing this data without checking if they're constants or instructions first.
created_at: 2025-03-03T14:38:58Z
author_association: MEMBER
diff_hunk: @@ -425,31 +348,41 @@ vertex_program_utils::vertex_program_metadata vertex_program_utils::analyse_vert
 	return result;
 }
 
+u64 RSXVertexProgram::get_sub_hash() const noexcept
+{
+	static constexpr u32 s_zero[4]{};
+
+	const usz prog_size = data.size() / 4;
+
+	const void* sdata_base = prog_size ? data.data() : s_zero;
+	const u8* sdata_start = static_cast<const u8*>(sdata_base);
+
+	const u8* sdata_mid1 = sdata_start + (prog_size / 8) * sizeof(v128);
+	const u8* sdata_mid2 = sdata_start + (prog_size / 4) * sizeof(v128);
path: rpcs3/Emu/RSX/Program/ProgramStateCache.cpp
user: login: elad335
body: Thanks, I have implemented sample indices prefetching for this.
created_at: 2025-03-03T15:03:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -425,31 +354,42 @@ vertex_program_utils::vertex_program_metadata vertex_program_utils::analyse_vert
 	return result;
 }
 
+u64 RSXVertexProgram::get_sub_hash() const noexcept
+{
+	static constexpr u32 s_zero[4]{};
+
+	const usz prog_size = data.size() / 4;
+
+	const void* sdata_base = prog_size ? data.data() : s_zero;
path: rpcs3/Emu/RSX/Program/ProgramStateCache.cpp
user: login: kd-11
body: There are single-instruction shaders btw used to do FB clear or just flood fill
created_at: 2025-03-03T15:33:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -718,22 +660,46 @@ usz fragment_program_utils::get_fragment_program_ucode_hash(const RSXFragmentPro
 	return 0;
 }
 
+u64 RSXFragmentProgram::get_sub_hash() const noexcept
+{
+	static constexpr u32 s_zero[4]{};
+
+	const usz prog_size = ucode_length / 16;
+
+	const void* sdata_base = ucode_length ? data.data_ptr : s_zero;
path: rpcs3/Emu/RSX/Program/ProgramStateCache.cpp
user: login: kd-11
body: Same here. Watch out for very short programs. Min length is 1
created_at: 2025-03-03T15:34:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -155,9 +155,42 @@ namespace rsx
 				rcount = ::size32(fifo_span);
 			}
 
-			copy_data_swap_u32(&REGS(ctx)->transform_program[load_pos * 4 + index % 4], fifo_span.data(), rcount);
+			const auto out_ptr = &REGS(ctx)->transform_program[load_pos * 4 + index % 4];
path: rpcs3/Emu/RSX/NV47/HW/nv4097.cpp
user: login: kd-11
body: Avoid excessive if-else nesting. This logic can be structured like this to be more readable:
```
pipeline_state dirty_flags = vertex_program_ucode_dirty;

if (rcount >= 4 && !state.test(vertex_program_ucode_dirty))
{
	// Assume clean
	dirty_flags = 0;

	// Trivial sampling to test for changes. Check start and middle point for changes to avoid slow cmp
	const usz sample_offset0 = 0, sample_offset1 = (((rcount / 4) - 1) / 2) * 4;
	if (read_from_ptr(fifo_span + sample_offset0] != read_from_ptr(out_ptr + sample_offset0) ||
		read_from_ptr(fifo_span + sample_offset1] != read_from_ptr(out_ptr + sample_offset1)
	{
		dirty_flags = vertex_program_ucode_dirty;
	}
}

if (dirty_flags)
{
	copy_data_swap_u32(...);
}
else if (copy_data_swap_u32_cmp(...))
{
	dirty_flags = vertex_program_ucode_dirty;
}

rsx->m_graphics_state |= dirty_flags;
```
created_at: 2025-03-04T21:52:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,31 +225,52 @@ class program_state_cache
 	}
 
 	/// bool here to inform that the program was preexisting.
-	std::tuple<const fragment_program_type&, bool> search_fragment_program(const RSXFragmentProgram& rsx_fp, bool force_load = true)
+	std::tuple<const fragment_program_type&, bool> search_fragment_program(const RSXFragmentProgram& rsx_fp, usz /*rsx_fp_invalidation_count*/)
 	{
 		bool recompile = false;
 		typename binary_to_fragment_program::iterator it;
 		fragment_program_type* new_shader;
-
 		{
+			thread_local const std::pair<const RSXFragmentProgram, fragment_program_type>* prev_fp = nullptr;
path: rpcs3/Emu/RSX/Program/ProgramStateCache.h
user: login: kd-11
body: Isn't this pointer invalidated if the container has an insert done on it?
This whole approach is q bit too convoluted. Why integer counter instead of bool? Why thread-local instead of atomic? etc.
I get the general idea, but it requires a much more involved implementation to be "proper".
created_at: 2025-03-05T22:53:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,31 +225,52 @@ class program_state_cache
 	}
 
 	/// bool here to inform that the program was preexisting.
-	std::tuple<const fragment_program_type&, bool> search_fragment_program(const RSXFragmentProgram& rsx_fp, bool force_load = true)
+	std::tuple<const fragment_program_type&, bool> search_fragment_program(const RSXFragmentProgram& rsx_fp, usz /*rsx_fp_invalidation_count*/)
 	{
 		bool recompile = false;
 		typename binary_to_fragment_program::iterator it;
 		fragment_program_type* new_shader;
-
 		{
+			thread_local const std::pair<const RSXFragmentProgram, fragment_program_type>* prev_fp = nullptr;
path: rpcs3/Emu/RSX/Program/ProgramStateCache.h
user: login: elad335
body: Yep, unordered_map does. I set fetched invalidation count to umax in this case, I can also set to pointer to be null for the intention to be cleaer.
created_at: 2025-03-06T07:25:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,6 +77,84 @@ namespace rsx
 			}
 		}
 
+		void set_texture_configuration_command(rsx::context* ctx, u32 reg)
path: rpcs3/Emu/RSX/NV47/HW/common.cpp
user: login: kd-11
body: This is a restructure, not an optimization. I'd suggest moving this to a separate PR to avoid regressions.
created_at: 2025-03-07T11:07:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,6 +77,84 @@ namespace rsx
 			}
 		}
 
+		void set_texture_configuration_command(rsx::context* ctx, u32 reg)
+		{
+			const u32 reg_index = reg - NV4097_SET_TEXTURE_OFFSET;
+			ensure(reg_index % 8 == 0 && reg_index < 8 * 16); // Only NV4097_SET_TEXTURE_OFFSET is expected
+
+			const u32 texture_index = reg_index / 8;
+
+			// FIFO args count including this one
+			const u32 fifo_args_cnt = RSX(ctx)->fifo_ctrl->get_remaining_args_count() + 1;
+
+			// The range of methods this function resposible to
+			constexpr u32 method_range = 8;
+
+			// Get limit imposed by FIFO PUT (if put is behind get it will result in a number ignored by min)
+			const u32 fifo_read_limit = static_cast<u32>(((RSX(ctx)->ctrl->put & ~3ull) - (RSX(ctx)->fifo_ctrl->get_pos())) / 4);
+
+			const u32 count = std::min<u32>({ fifo_args_cnt, fifo_read_limit, method_range });
+
+			// Clamp by the count of methods this function is responsible to
+			std::span<const u32> command_span = RSX(ctx)->fifo_ctrl->get_current_arg_ptr();
+
+			if (command_span.size() > count)
+			{
+				command_span = command_span.subspan(0, count);
+			}
+
+			if (command_span.empty())
path: rpcs3/Emu/RSX/NV47/HW/common.cpp
user: login: kd-11
body: This should be an ensure then
created_at: 2025-03-07T11:09:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,8 +62,13 @@ namespace rsx
 			return vm::cast(get_address(offset, location));
 		}
 
-		void set_fragment_texture_dirty_bit(rsx::context* ctx, u32 index)
+		void set_fragment_texture_dirty_bit(rsx::context* ctx, u32 arg, u32 index, bool /*is_shader_config*/)
 		{
+			if (REGS(ctx)->latch == arg)
path: rpcs3/Emu/RSX/NV47/HW/common.cpp
user: login: kd-11
body: Caller should check this on their side.
created_at: 2025-03-07T11:31:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -245,10 +246,15 @@ class program_state_cache
 				{
 					if (rsx_fp_invalidation_count != umax && prev_rsx_count == rsx_fp_invalidation_count)
 					{
-						return std::forward_as_tuple(prev_fp->second, true);
+						// Shader UCODE must be the same.
+						// Shader config changes are not tracked at the moment
+						// Compare manually
+						if (program_hash_util::fragment_program_compare::config_only(prev_fp->first, rsx_fp))
path: rpcs3/Emu/RSX/Program/ProgramStateCache.h
user: login: kd-11
body: Config changes are tracked by the fragment_state_dirty flag. Though I suppose there's no harm in double-checking here since the test is simple.
created_at: 2025-03-07T11:33:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,6 +25,9 @@ namespace rsx
 			RSX(ctx)->sync_point_request.release(true);
 			const u32 addr = get_address(REGS(ctx)->semaphore_offset_406e(), REGS(ctx)->semaphore_context_dma_406e());
 
+			// Syncronization point, may be associated with memory changes without actually changing addresses
+			RSX(ctx)->m_graphics_state |= rsx::pipeline_state::fragment_program_ucode_dirty;
path: rpcs3/Emu/RSX/NV47/HW/nv406e.cpp
user: login: kd-11
body: Fair point. Since it is speculative it really should be a different flag though to re-run ucode comparison test when returning from this function.
created_at: 2025-03-07T11:36:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,6 +27,9 @@ namespace rsx
 					return;
 				}
 
+				// Syncronization point, may be associated with memory changes without actually changing addresses
+				RSX(ctx)->m_graphics_state |= rsx::pipeline_state::fragment_program_ucode_dirty;
path: rpcs3/Emu/RSX/NV47/HW/nv47_sync.hpp
user: login: kd-11
body: Move to caller or obey flush flags. Not all synchronization points mean that CPU is fully synced. e.g Texture read semaphore without SRM should ignore slow activity.
Only FlushPipe does a full sync, FlushDMA does not.
created_at: 2025-03-07T11:38:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,6 +25,9 @@ namespace rsx
 			RSX(ctx)->sync_point_request.release(true);
 			const u32 addr = get_address(REGS(ctx)->semaphore_offset_406e(), REGS(ctx)->semaphore_context_dma_406e());
 
+			// Syncronization point, may be associated with memory changes without actually changing addresses
+			RSX(ctx)->m_graphics_state |= rsx::pipeline_state::fragment_program_ucode_dirty;
path: rpcs3/Emu/RSX/NV47/HW/nv406e.cpp
user: login: elad335
body: I will leave it for a different pr, simple comparison should also be implemented in other places I believe such as NV4097_INVALIDATE_L2 and NV4097_SET_SHADER_PROGRAM with the same address.
created_at: 2025-03-08T08:56:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,8 +62,13 @@ namespace rsx
 			return vm::cast(get_address(offset, location));
 		}
 
-		void set_fragment_texture_dirty_bit(rsx::context* ctx, u32 index)
+		void set_fragment_texture_dirty_bit(rsx::context* ctx, u32 arg, u32 index, bool /*is_shader_config*/)
 		{
+			if (REGS(ctx)->latch == arg)
path: rpcs3/Emu/RSX/NV47/HW/common.cpp
user: login: elad335
body: This was to avoid including rsx_methods.h here.
created_at: 2025-03-08T09:05:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: elad335
created_at: 2025-03-03T05:46:42Z
body: I have removed some other state-management from storage hashing.
author_association: CONTRIBUTOR
user: login: cipherxof
created_at: 2025-03-03T06:59:44Z
body: After https://github.com/RPCS3/rpcs3/pull/16797/commits/55c6f021ea9afc371608e99f4bee6449b51dd8d3 (was about 7,000fps before this commit):

![Screenshot From 2025-03-02 22-56-03](https://github.com/user-attachments/assets/723d25bc-19ec-4b1b-bb81-19a177a14615)

![image](https://github.com/user-attachments/assets/98b404a1-8669-417a-8512-a5b4dbbcedc2)

Seems about the same everywhere else that I've tested compared to https://github.com/RPCS3/rpcs3/pull/16780
author_association: CONTRIBUTOR
user: login: elad335
created_at: 2025-03-03T07:24:52Z
body: @cipherxof thanks, pushed more commits which hopefully should eliminate hashing collisions. Please retest on b8dbf3d
author_association: CONTRIBUTOR
user: login: elad335
created_at: 2025-03-03T12:15:47Z
body: @cipherxof build is ready!
author_association: CONTRIBUTOR
user: login: kd-11
created_at: 2025-03-03T12:56:08Z
body: > Because it is somewhat unlikely that two distinct programs would have equal first instructions

This is extremely common. These are not like CPU programs. ISA is very limited, most programs have the same prologue.
In fact a common paradigm in AAA games is to have one shader and "delete" pieces in the middle and replace with others.
It's the same shader with holes added in/removed at runtime.
author_association: CONTRIBUTOR
user: login: Whatcookie
created_at: 2025-03-03T18:06:47Z
body: Latest build is still compiling shaders on every run
![image](https://github.com/user-attachments/assets/c6fa6e5e-c6a4-4db1-bc1a-0e9c3bdf450e)

author_association: MEMBER
user: login: elad335
created_at: 2025-03-04T05:56:28Z
body: I decided to go on a different route at the end - if the last program cached seems very similar to the current one, do not do hashing against the hash table, instead compare the two programs directly and return them if they match precisely.
The drawback is of course, that fast likely-ness test is not catching all cases of mismatch and thus this may add a second comparison in edge cases. May benefit RaC ToD which does not invalidate FP and VP programs down its pipeline.

I also added a minor optimization for VP UCODE invalidation checks.  

@cipherxof can you test it?
author_association: CONTRIBUTOR
user: login: Whatcookie
created_at: 2025-03-04T16:39:52Z
body: Provides a nice uplift when avx-512-icl is enabled, and an even bigger uplift for those without.

With AVX-512-ICL (~204->209avg)
![image](https://github.com/user-attachments/assets/ae4fe49c-07f6-4151-8472-068e94025dcd)

With AVX-512-ICL paths disabled at compile time (~194->202avg)
![image](https://github.com/user-attachments/assets/35151b5a-ae6b-41c5-b1f0-5f73973e8b2a)

author_association: MEMBER
user: login: Whatcookie
created_at: 2025-03-04T20:44:39Z
body: uplift is very good in ratchet tod
![image](https://github.com/user-attachments/assets/ea62cbea-13b4-46e4-800c-44a0aee21a1f)

author_association: MEMBER
user: login: cipherxof
created_at: 2025-03-04T23:36:15Z
body: > I decided to go on a different route at the end - if the last program cached seems very similar to the current one, do not do hashing against the hash table, instead compare the two programs directly and return them if they match precisely. The drawback is of course, that fast likely-ness test is not catching all cases of mismatch and thus this may add a second comparison in edge cases. May benefit RaC ToD which does not invalidate FP and VP programs down its pipeline.
> 
> I also added a minor optimization for VP UCODE invalidation checks.
> 
> @cipherxof can you test it?

Performance is the same as my last test :+1: 
author_association: CONTRIBUTOR
user: login: elad335
created_at: 2025-03-05T12:12:36Z
body: I have pushed another optimization, this time if VP shader has not been changed since last VP shader fetch we skip the comparison altogether. This is trivial for VP as its changes are placed on the instruction stream.

May benefit RaC ToD and other games.
Please test it.
author_association: CONTRIBUTOR
user: login: elad335
created_at: 2025-03-06T15:48:15Z
body: Pushed another optimization that reduces FP shader comparison between sequencing draws.
author_association: CONTRIBUTOR