From 9ff723b382540d342690eb5c5cd287d31c28283d Mon Sep 17 00:00:00 2001 From: thibaut Date: Sun, 18 Oct 2026 19:33:42 +0200 Subject: [PATCH] perf(shader): reuse the source of a bitcast round trip Registers are 32-bit words, so a float result is bitcast to u32 when it is written and back to f32 when it is read: the emitted module carries pairs of OpBitcast that return to the type of the first one's source. A bitcast back to that type now returns the source itself, which dominates every use of the first bitcast. The module notes the result type and the bitcast source of each function instruction; Unary and EmitNative bitcasts look them up. Test: agc_bitcast_round_trip recompiles v_cvt_f32_u32 results added with v_add_f32 and requires no bitcast back to the type of a bitcast's source; it fails on main (one of six) and passes with this change. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +- .../tests/BitcastRoundTrip.cpp | 105 ++++++++++++++++++ .../SpirvBackend/SpirvEmitterHelpers.hpp | 4 + .../include/SpirvBackend/SpirvModule.hpp | 8 ++ .../SpirvBackend/src/SpirvModule.cpp | 21 ++++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 3 + 6 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/BitcastRoundTrip.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 0b658068557..ed9f0da8b3b 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1336,6 +1336,10 @@ target_link_libraries(agc_fma_mix_once_tests PRIVATE shader_recompiler_agc_drive set_target_properties(agc_fma_mix_once_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_fma_mix_once_tests) +add_test_executable(agc_bitcast_round_trip_tests EXCLUDE_FROM_ALL tests/BitcastRoundTrip.cpp) +target_link_libraries(agc_bitcast_round_trip_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_bitcast_round_trip_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_bitcast_round_trip_tests) add_test_executable(agc_f16_single_convert_tests EXCLUDE_FROM_ALL tests/F16SingleConvert.cpp) target_link_libraries(agc_f16_single_convert_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_f16_single_convert_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -2533,7 +2537,7 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_shader_bindless agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_tests agc_locks_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vertex_input_validation_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests) + foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_tests agc_locks_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vertex_input_validation_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests agc_bitcast_round_trip_tests) if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests" OR agcTest STREQUAL "agc_driver_rcp_hardware_tests") continue() endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/BitcastRoundTrip.cpp b/core/libs/prx/libSceAgcDriver/tests/BitcastRoundTrip.cpp new file mode 100644 index 00000000000..7a6e3ee9a10 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/BitcastRoundTrip.cpp @@ -0,0 +1,105 @@ +#define SPV_ENABLE_UTILITY_CODE +#include "BdaAbi.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void Check(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +SpirvTarget Target() { + static constexpr std::array capabilities{ + spv::CapabilityInt64, spv::CapabilityFloat64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance, + spv::CapabilityFloat16, spv::CapabilityRoundingModeRTE, spv::CapabilityDenormPreserve, + }; + static constexpr std::array extensions{ + "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", + }; + return {0x00401000u, 0x00010300u, 32, BdaAbi::Version, capabilities, extensions, false, + {1024, 1024, 64}, 1024, 32768, {}, {}}; +} + +struct Counts { + std::size_t bitcasts = 0; + std::size_t roundTrips = 0; +}; + +Counts Count(std::span words) { + Check(words.size() >= 5 && words[0] == spv::MagicNumber, "bitcast round trip: missing SPIR-V module"); + std::unordered_map types; + std::unordered_map sources; + Counts counts; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + Check(count != 0 && count <= words.size() - offset, "bitcast round trip: malformed SPIR-V instruction"); + const auto opcode = static_cast(words[offset] & spv::OpCodeMask); + bool hasResult = false; + bool hasType = false; + spv::HasResultAndType(opcode, &hasResult, &hasType); + if (hasResult && hasType && count >= 3u) types[words[offset + 2u]] = words[offset + 1u]; + if (opcode == spv::OpBitcast && count == 4u) { + ++counts.bitcasts; + const auto type = words[offset + 1u]; + const auto operand = words[offset + 3u]; + if (const auto source = sources.find(operand); source != sources.end() && types[source->second] == type) ++counts.roundTrips; + sources[words[offset + 2u]] = operand; + } + offset += count; + } + return counts; +} + +Counts Compile(std::uint32_t adds) { + std::vector code; + for (std::uint32_t load = 0; load < 3u; ++load) code.insert(code.end(), {0xe0300000u | (load * 4u), 0x80010000u | ((1u + load) << 8u)}); + code.push_back(0xbf8c3f70u); + for (std::uint32_t add = 0; add < adds; ++add) { + code.push_back(0x7e080d00u | (256u + 1u + add % 3u)); + code.push_back(0x060a0004u | (2u << 9u) | (256u + 4u)); + code.push_back(0x060c0004u | (5u << 9u) | (256u + 4u)); + } + code.insert(code.end(), {0xe0700000u, 0x80010000u | (6u << 8u)}); + code.push_back(0xbf810000u); + const std::span words(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; + const std::array userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}; + request.target = Target(); + request.layout = {0, 0, 0, 128}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.context.memory = memory; + request.useCache = false; + return Count(Recompile(request).spirv.Words()); +} + +} + +int main() { + try { + const auto counts = Compile(6); + Check(counts.bitcasts != 0u, "bitcast round trip: v_cvt_f32_u32 and v_add_f32 emitted no bitcast, so the test no longer covers register round trips"); + Check(counts.roundTrips == 0u, "bitcast round trip: v_cvt_f32_u32 results added with v_add_f32 emitted " + std::to_string(counts.roundTrips) + " bitcasts back to the type of a bitcast's source (" + std::to_string(counts.bitcasts) + " bitcasts in all)"); + std::printf("bitcast round trip tests passed (%zu bitcasts)\n", counts.bitcasts); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 4be0132c1db..e3f4afa307a 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -170,6 +170,10 @@ std::uint32_t AtomicFloatCompareSwap(SpirvEmitterState& state, std::uint32_t old template std::uint32_t EmitNative(SpirvEmitterState& state, TArguments... arguments) { + if constexpr (TOpcode == spv::OpBitcast && sizeof...(TArguments) == 1u) { + const std::uint32_t value = (arguments, ...); + if (const auto source = state.module.BitcastSource(value); source != 0u && state.module.ResultType(source) == TypeId(state, TValueType)) return source; + } const auto result = state.module.AllocateId(); state.module.AddFunction(TOpcode, TypeId(state, TValueType), result, arguments...); return result; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp index a6a4b132967..354325f00d0 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace ShaderRecompiler { @@ -137,8 +138,12 @@ class SpirvModule { template void AddFunction(std::uint32_t opcode, const TOperands&... operands) { + const auto offset = functionInstructions.size(); appendInstruction(functionInstructions, opcode, operands...); + noteFunctionResult(offset); } + [[nodiscard]] std::uint32_t ResultType(std::uint32_t id) const; + [[nodiscard]] std::uint32_t BitcastSource(std::uint32_t id) const; private: static constexpr std::size_t DeclarationCacheSize = 512; @@ -239,6 +244,9 @@ class SpirvModule { std::uint32_t declareConstant(std::uint32_t opcode, std::vector key); static void appendString(std::vector& words, const std::string& text); + void noteFunctionResult(std::size_t offset); + std::unordered_map resultTypes; + std::unordered_map bitcastSources; std::uint32_t nextId = 1; std::uint32_t version = 0x00010300u; std::vector extInstImports; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp index f10a1cdd9ea..6f43ba85e3a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModule.cpp @@ -220,7 +220,28 @@ void SpirvModule::AddName(std::uint32_t target, const std::string& name) { } void SpirvModule::AddFunction(std::span words) { + const auto offset = functionInstructions.size(); AppendInstructionWords(functionInstructions, words); + noteFunctionResult(offset); +} + +void SpirvModule::noteFunctionResult(std::size_t offset) { + if (functionInstructions.size() < offset + 3u) return; + const auto opcode = functionInstructions[offset] & spv::OpCodeMask; + const auto type = functionInstructions[offset + 1u]; + const auto result = functionInstructions[offset + 2u]; + resultTypes.emplace(result, type); + if (opcode == spv::OpBitcast && functionInstructions.size() >= offset + 4u) bitcastSources.emplace(result, functionInstructions[offset + 3u]); +} + +std::uint32_t SpirvModule::ResultType(std::uint32_t id) const { + const auto found = resultTypes.find(id); + return found == resultTypes.end() ? 0u : found->second; +} + +std::uint32_t SpirvModule::BitcastSource(std::uint32_t id) const { + const auto found = bitcastSources.find(id); + return found == bitcastSources.end() ? 0u : found->second; } void SpirvModule::AddSubgroupBallot(std::uint32_t type, std::uint32_t result, std::uint32_t scope, std::uint32_t predicate) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 1bc242f265a..4762770c7ba 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1771,6 +1771,9 @@ void EmitLabel(SpirvEmitterState& state, std::uint32_t label) { } std::uint32_t Unary(SpirvEmitterState& state, std::uint32_t opcode, std::uint32_t type, std::uint32_t value) { + if (opcode == spv::OpBitcast) { + if (const auto source = state.module.BitcastSource(value); source != 0u && state.module.ResultType(source) == type) return source; + } const auto result = state.module.AllocateId(); state.module.AddFunction(opcode, type, result, value); return result;