From 2c484fc7880732a449f8647bcce2a63828ecff23 Mon Sep 17 00:00:00 2001 From: Ufoex <57051039+Ufoex@users.noreply.github.com> Date: Sun, 5 Jul 2026 03:26:32 -0600 Subject: [PATCH 1/3] fix(spirv): use fetch constant word 3 for texture-fetch exponent bias The Vulkan SPIR-V texture-fetch translator read fetch constant word 4 for both LOD biasing and the result exponent bias, but exponent bias lives in word 3 (matching the D3D12 DXBC translator, which already reads word 3 for this). Reusing word 4 applied the wrong bias value, producing blown-out/incorrect brightness on affected texture fetches on the Vulkan backend only. Adds a dedicated word 3 load for the exponent bias while leaving word 4's LOD/stacked-texture-filtering usage untouched. --- .../shader/spirv_translator_fetch.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/graphics/pipeline/shader/spirv_translator_fetch.cpp b/src/graphics/pipeline/shader/spirv_translator_fetch.cpp index 9ce3bb4ad..c25af47dd 100644 --- a/src/graphics/pipeline/shader/spirv_translator_fetch.cpp +++ b/src/graphics/pipeline/shader/spirv_translator_fetch.cpp @@ -1409,8 +1409,7 @@ void SpirvShaderTranslator::ProcessTextureFetchInstruction( builder_->createUnaryOp(spv::OpLogicalNot, type_bool_, is_all_signed); // Load the fetch constant word 4, needed unconditionally for LOD - // biasing, for result exponent biasing, and conditionally for stacked - // texture filtering. + // biasing and conditionally for stacked texture filtering. id_vector_temp_.clear(); id_vector_temp_.push_back(const_int_0_); id_vector_temp_.push_back( @@ -2030,10 +2029,22 @@ void SpirvShaderTranslator::ProcessTextureFetchInstruction( } // Apply the exponent bias from the bits 13:18 of the fetch constant - // word 4. + // word 3. + id_vector_temp_.clear(); + id_vector_temp_.push_back(const_int_0_); + id_vector_temp_.push_back( + builder_->makeIntConstant(int((fetch_constant_word_0_index + 3) >> 2))); + id_vector_temp_.push_back( + builder_->makeIntConstant(int((fetch_constant_word_0_index + 3) & 3))); + spv::Id fetch_constant_word_3 = builder_->createLoad( + builder_->createAccessChain(spv::StorageClassUniform, uniform_fetch_constants_, + id_vector_temp_), + spv::NoPrecision); + spv::Id fetch_constant_word_3_signed = + builder_->createUnaryOp(spv::OpBitcast, type_int_, fetch_constant_word_3); spv::Id result_exponent_bias = builder_->createBinBuiltinCall( type_float_, ext_inst_glsl_std_450_, GLSLstd450Ldexp, const_float_1_, - builder_->createTriOp(spv::OpBitFieldSExtract, type_int_, fetch_constant_word_4_signed, + builder_->createTriOp(spv::OpBitFieldSExtract, type_int_, fetch_constant_word_3_signed, builder_->makeUintConstant(13), builder_->makeUintConstant(6))); { uint32_t result_remaining_components = used_result_nonzero_components; From b503006ccb7e8660102780d6ffad3c3f9ffe41aa Mon Sep 17 00:00:00 2001 From: Ufoex <57051039+Ufoex@users.noreply.github.com> Date: Sun, 5 Jul 2026 03:26:59 -0600 Subject: [PATCH 2/3] fix(vulkan): allow async compilation for pixel-shader-less pipelines Async pipeline compilation previously required a pixel shader (since the mechanism swaps in a fast placeholder pixel shader while the real one compiles on a background thread). Pipelines with no pixel shader at all (depth/shadow-only passes) were excluded from this check and always compiled synchronously on the render thread, regardless of the async_shader_compilation cvar. Depth/shadow-only pipeline permutations are created continuously during normal gameplay (not just at load), e.g. whenever the camera reveals previously-unseen shadow casters. Because there's nothing cheaper to substitute for a depth-only pipeline (no pixel shader to swap out), skip drawing with it for a few frames instead of blocking: queue the real pipeline on the background thread pool and mark it as a placeholder immediately, reusing the same "skip this draw, retry next frame" path already used for the pixel-shader placeholder-swap case. Measured on a real gameplay session: roughly half of all newly created pipeline permutations had no pixel shader, so this removes a frequent render-thread stall source tied to camera movement. --- src/graphics/vulkan/pipeline_cache.cpp | 42 +++++++++++++++++--------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/graphics/vulkan/pipeline_cache.cpp b/src/graphics/vulkan/pipeline_cache.cpp index 91ac08f6c..8692168b4 100644 --- a/src/graphics/vulkan/pipeline_cache.cpp +++ b/src/graphics/vulkan/pipeline_cache.cpp @@ -1111,8 +1111,8 @@ bool VulkanPipelineCache::ConfigurePipeline( } } - bool use_async = REXCVAR_GET(async_shader_compilation) && !creation_threads_.empty() && - pixel_shader && placeholder_pixel_shader_ != VK_NULL_HANDLE; + bool use_async = REXCVAR_GET(async_shader_compilation) && !creation_threads_.empty(); + bool use_placeholder_swap = use_async && pixel_shader && placeholder_pixel_shader_ != VK_NULL_HANDLE; uint8_t async_priority = pipeline_util::kPriorityLowest; if (use_async) { uint32_t bound_rts = @@ -1159,20 +1159,34 @@ bool VulkanPipelineCache::ConfigurePipeline( bool queued_async_creation = false; if (use_async) { creation_arguments_real.priority = async_priority; - PipelineCreationArguments creation_arguments_placeholder; - if (TryGetPipelineCreationArgumentsForDescription(description, &pipeline, - creation_arguments_placeholder, true)) { - pipeline.second.is_placeholder.store(true, std::memory_order_release); - if (EnsurePipelineCreated(creation_arguments_placeholder, placeholder_pixel_shader_)) { - { - std::lock_guard lock(creation_request_lock_); - creation_queue_.push(creation_arguments_real); + if (use_placeholder_swap) { + PipelineCreationArguments creation_arguments_placeholder; + if (TryGetPipelineCreationArgumentsForDescription(description, &pipeline, + creation_arguments_placeholder, true)) { + pipeline.second.is_placeholder.store(true, std::memory_order_release); + if (EnsurePipelineCreated(creation_arguments_placeholder, placeholder_pixel_shader_)) { + { + std::lock_guard lock(creation_request_lock_); + creation_queue_.push(creation_arguments_real); + } + creation_request_cond_.notify_one(); + queued_async_creation = true; + } else { + pipeline.second.is_placeholder.store(false, std::memory_order_release); } - creation_request_cond_.notify_one(); - queued_async_creation = true; - } else { - pipeline.second.is_placeholder.store(false, std::memory_order_release); } + } else { + // No pixel shader (e.g. depth/shadow-only pipelines): there is nothing + // cheaper to substitute while the real pipeline compiles on a + // background thread, so skip drawing with it for a few frames instead + // of blocking the render thread on synchronous compilation. + pipeline.second.is_placeholder.store(true, std::memory_order_release); + { + std::lock_guard lock(creation_request_lock_); + creation_queue_.push(creation_arguments_real); + } + creation_request_cond_.notify_one(); + queued_async_creation = true; } } From b6f76921569a530a7b5d1ff87bf9ab50d9dca576 Mon Sep 17 00:00:00 2001 From: Ufoex <57051039+Ufoex@users.noreply.github.com> Date: Sun, 5 Jul 2026 03:27:12 -0600 Subject: [PATCH 3/3] fix(core): back off poll interval in PosixConditionBase::WaitMultiple WaitMultiple (used to implement KeWaitForMultipleObjects-style waits across more than one handle) polled every handle's mutex via pthread_mutex_trylock on a fixed 1ms interval regardless of how long the wait had been idle. Any guest thread waiting on several kernel objects at once - e.g. AudioSystem's worker thread, which waits on up to 9 handles (8 client semaphores + a shutdown event) - paid this poll cost roughly 500 times/sec even when nothing was signaling. Profiling a real gameplay session (perf + Tracy) showed this self-time (WaitMultiple + pthread_mutex_trylock + clock_gettime) was one of the largest CPU consumers on the audio worker thread, ahead of most actual guest-code execution. The poll interval now backs off exponentially (1ms -> 2 -> 4 -> 8 -> 16ms cap) the longer a single WaitMultiple call goes without any handle becoming signaled, and resets on every new wait. Freshly started waits stay responsive at 1ms; only sustained idle waits back off. Semantics, return values, and timeout behavior are unchanged - only the sleep duration between polls differs. Measured effect on one affected thread (Audio Worker) in Lollipop: Chainsaw via Re-Cherry: CPU usage dropped from ~68% to ~21%, with no observed hangs or audio glitches across repeated test sessions. --- src/core/threading_posix.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/core/threading_posix.cpp b/src/core/threading_posix.cpp index e10e30b47..ca15d92c1 100644 --- a/src/core/threading_posix.cpp +++ b/src/core/threading_posix.cpp @@ -281,6 +281,14 @@ class PosixConditionBase { ? std::chrono::steady_clock::time_point::max() : start_time + timeout; + // The poll interval backs off exponentially (capped) the longer nothing + // is signaled, so a long idle wait doesn't keep re-locking every handle's + // mutex ~1000 times/sec (measured as a real CPU cost on waits across + // several handles, e.g. AudioSystem's per-client semaphore WaitAny). + // Freshly started/likely-to-resolve-soon waits still poll at 1ms. + auto poll_interval = std::chrono::milliseconds(1); + constexpr auto kMaxPollInterval = std::chrono::milliseconds(16); + while (true) { size_t first_signaled = std::numeric_limits::max(); bool condition_met = false; @@ -358,12 +366,13 @@ class PosixConditionBase { } if (timeout == std::chrono::milliseconds::max()) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + std::this_thread::sleep_for(poll_interval); } else { auto remaining = std::chrono::duration_cast(end_time - now); - auto sleep_time = std::min(remaining, std::chrono::milliseconds(1)); + auto sleep_time = std::min(remaining, poll_interval); std::this_thread::sleep_for(sleep_time); } + poll_interval = std::min(poll_interval * 2, kMaxPollInterval); } }