diff --git a/src/graphics/host_gpu/graphicContext.h b/src/graphics/host_gpu/graphicContext.h index 52e085393..c4c56039c 100644 --- a/src/graphics/host_gpu/graphicContext.h +++ b/src/graphics/host_gpu/graphicContext.h @@ -43,6 +43,7 @@ struct GraphicContext { uint32_t min_subgroup_size = 0; uint32_t max_subgroup_size = 0; uint32_t max_push_descriptors = 0; + uint32_t device_clock_period_ps = 0; vk::DescriptorSetLayout image_heap_layout = nullptr; vk::ShaderStageFlags required_subgroup_size_stages = {}; Common::Mutex queue_mutex; diff --git a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp index fd2e331bc..65de72cd0 100644 --- a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp @@ -487,7 +487,8 @@ struct PipelineCache::ProgramCache { // signed-zero results; GLSL.std.450 alone does not order -0 below +0. profile {.maintenance8 = graphics.maintenance8_enabled, .native_float_min_max = - graphics.physical_device_properties.vendorID == 0x1002u} { + graphics.physical_device_properties.vendorID == 0x1002u, + .device_clock_period_ps = graphics.device_clock_period_ps} { lookup_key.static_state.reserve(MaxStaticKeyWords); } ~ProgramCache() { diff --git a/src/graphics/presentation/window/vulkanWindow.cpp b/src/graphics/presentation/window/vulkanWindow.cpp index fab1c337b..f0b8598b9 100644 --- a/src/graphics/presentation/window/vulkanWindow.cpp +++ b/src/graphics/presentation/window/vulkanWindow.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -506,6 +507,13 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics, maintenance8.pNext = supported_features2.pNext; supported_features2.pNext = &maintenance8; } + const bool shader_clock_extension = + HasExtension(device_extensions, VK_KHR_SHADER_CLOCK_EXTENSION_NAME); + vk::PhysicalDeviceShaderClockFeaturesKHR shader_clock {}; + if (shader_clock_extension) { + shader_clock.pNext = supported_features2.pNext; + supported_features2.pNext = &shader_clock; + } const bool image_atomic_int64_extension = HasExtension(device_extensions, VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME); vk::PhysicalDeviceShaderImageAtomicInt64FeaturesEXT image_atomic_int64 {}; @@ -593,6 +601,11 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics, feedback_extensions && feedback_layout.attachmentFeedbackLoopLayout && feedback_dynamic.attachmentFeedbackLoopDynamicState; graphics.maintenance8_enabled = maintenance8_extension && maintenance8.maintenance8; + // The device clock ticks at timestampPeriod nanoseconds. + if (shader_clock_extension && shader_clock.shaderDeviceClock) { + graphics.device_clock_period_ps = static_cast( + std::lround(graphics.physical_device_properties.limits.timestampPeriod * 1000.0f)); + } graphics.conditional_rendering_enabled = conditional_rendering_extension && conditional_rendering.conditionalRendering; LOGF("Vulkan maintenance8 support: %s\n", @@ -702,6 +715,10 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics, maintenance8.pNext = const_cast(create_info.pNext); create_info.pNext = &maintenance8; } + if (graphics.device_clock_period_ps != 0u) { + shader_clock.pNext = const_cast(create_info.pNext); + create_info.pNext = &shader_clock; + } if (graphics.conditional_rendering_enabled) { conditional_rendering.pNext = const_cast(create_info.pNext); create_info.pNext = &conditional_rendering; @@ -1077,6 +1094,7 @@ void WindowContext::CreateVulkan() { graphic_ctx.memory_budget_ext_enabled = true; } for (const auto* extension: {VK_KHR_MAINTENANCE_8_EXTENSION_NAME, + VK_KHR_SHADER_CLOCK_EXTENSION_NAME, VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME, VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME, VK_EXT_MESH_SHADER_EXTENSION_NAME, diff --git a/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.h b/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.h index 80f98045d..fd5d6a55d 100644 --- a/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.h +++ b/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.h @@ -13,6 +13,8 @@ namespace Libs::Graphics::ShaderRecompiler::Spirv { struct Profile { bool maintenance8 = false; // Dynamic texel offsets on image samples. bool native_float_min_max = false; // NMin/NMax give the guest's NaN and signed-zero results. + // OpReadClockKHR Device tick in picoseconds; 0 without shaderDeviceClock. + uint32_t device_clock_period_ps = 0; }; inline constexpr Profile DefaultProfile {}; diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp index 791cae053..2027b3e97 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp @@ -4,6 +4,7 @@ #include #include +#include #include namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter { @@ -881,6 +882,22 @@ uint32_t EmitGetShaderBase(ValueEmitContext& ctx) { return ctx.Def(IR::Value(uint64_t {0})); } +uint32_t EmitReadRealtimeClock(ValueEmitContext& ctx, const IR::Inst& inst) { + auto& state = ctx.state; + const auto period = state.profile.device_clock_period_ps; + if (period == 0u) { + ctx.Fail(inst, "requires shaderDeviceClock"); + } + state.builder.RequireExtension("SPV_KHR_shader_clock"); + state.builder.RequireCapability(spv::CapabilityShaderClockKHR); + const auto clock = + Unary(state, spv::OpReadClockKHR, TypeU64(state), ConstantU32(state, spv::ScopeDevice)); + // S_MEMREALTIME counts at 100 MHz. Shift the host clock by the most bits that keep its tick at + // most 15000 ps (2/3 of 100 MHz): 1 GHz shifts by 3 to 125 MHz; 100 MHz and slower stay. + const uint32_t shift = std::bit_width(7500u / period); + return EmitShiftRightLogical64(ctx, clock, IR::Value(shift)); +} + void EmitUnreachable(ValueEmitContext& ctx, const IR::Inst& inst) { ctx.Fail(inst, "must be lowered before SPIR-V emission"); } diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h index 0a9556283..a99c7a7b5 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h @@ -240,6 +240,7 @@ uint32_t EmitGetAttributeF16(ValueEmitContext& ctx, const IR::Inst& uint32_t EmitGetInterpolationParameter(ValueEmitContext& ctx, const IR::Inst& inst); void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst); uint32_t EmitGetShaderBase(ValueEmitContext& ctx); +uint32_t EmitReadRealtimeClock(ValueEmitContext& ctx, const IR::Inst& inst); inline constexpr auto EmitGetSrtResource = EmitVoid; inline constexpr auto EmitGetBufferResource = EmitGetSrtResource; uint32_t EmitGetBufferResourceIndex(ValueEmitContext& ctx, const IR::Inst& inst); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterProgram.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterProgram.cpp index cd9e65595..c664c603c 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterProgram.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterProgram.cpp @@ -235,9 +235,9 @@ void EmitDirectInstruction(ValueEmitContext& ctx, const IR::Inst& inst) { const auto op = inst.GetOpcode(); if (ctx.half != 0 && (op == Op::Ballot || op == Op::ReadFirstLane || op == Op::SubgroupUMinU32 || - op == Op::ReadConstBuffer || + op == Op::ReadConstBuffer || op == Op::ReadRealtimeClock || (op == Op::LoadAddressU32 && ctx.Memory(inst).kind == IR::ResourceKind::ScalarAddress))) { - // Whole-wave results and SMEM loads (SGPR operands, no EXEC) are identical in both halves. + // Whole-wave results, clocks and SMEM loads (SGPR operands, no EXEC) match in both halves. ctx.Define(inst, ctx.other_half->Result(inst)); return; } diff --git a/src/graphics/shader/recompiler/frontend/translate/Memory.cpp b/src/graphics/shader/recompiler/frontend/translate/Memory.cpp index b8eba8daa..78928aa51 100644 --- a/src/graphics/shader/recompiler/frontend/translate/Memory.cpp +++ b/src/graphics/shader/recompiler/frontend/translate/Memory.cpp @@ -1,11 +1,9 @@ #include "common/assert.h" -#include "common/logging/log.h" #include "graphics/shader/recompiler/frontend/translate/Translator.h" #include "graphics/shader/recompiler/frontend/decode/ImageOps.h" #include #include -#include namespace Libs::Graphics::ShaderRecompiler::Frontend { @@ -969,17 +967,9 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) { case Decoder::Opcode::S_BUFFER_LOAD_DWORDX4: case Decoder::Opcode::S_BUFFER_LOAD_DWORDX8: case Decoder::Opcode::S_BUFFER_LOAD_DWORDX16: return S_LOAD(inst, false); - case Decoder::Opcode::S_MEMREALTIME: { - static std::atomic_flag warned = ATOMIC_FLAG_INIT; - if (!warned.test_and_set(std::memory_order_relaxed)) { - Log::WriteToConsoleAndLog( - "Warning: S_MEMREALTIME uses placeholder UINT64_MAX; real-time clock not implemented.\n"); - } - for (uint32_t component = 0; component < 2; component++) { - WriteOperand(ScalarDestinationOperand(inst.dst, component), IR::Value(UINT32_MAX)); - } - return; - } + case Decoder::Opcode::S_MEMREALTIME: + return WriteU32Pair(inst.dst, + ExtractU64(IR::U64(ir.Emit(IR::ValueOpcode::ReadRealtimeClock)))); case Decoder::Opcode::BUFFER_LOAD_UBYTE: case Decoder::Opcode::BUFFER_LOAD_UBYTE_D16: diff --git a/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc b/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc index 27da7d59a..6acda598e 100644 --- a/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc +++ b/src/graphics/shader/recompiler/ir/opcodes/ValueOpcodes.inc @@ -10,6 +10,7 @@ VALUE_OPCODE(ConditionRef, U1, U1) VALUE_OPCODE(GetUserData, U32, ScalarReg) VALUE_OPCODE(GetDispatchThreadExtent, U32, U32) VALUE_OPCODE(GetShaderBase, U64) +VALUE_OPCODE(ReadRealtimeClock, U64) VALUE_OPCODE(MeshDrawParameter, U32, U32) VALUE_OPCODE(MeshAllocate, Void, U32) VALUE_OPCODE(TessellationBase, U32, U32) diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index aa04df869..fd81aa2f0 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -21246,8 +21246,10 @@ private: available_min_lod.pNext = &available_provoking_vertex; vk::PhysicalDeviceMaintenance8FeaturesKHR available_maintenance8{}; available_maintenance8.pNext = &available_min_lod; + vk::PhysicalDeviceShaderClockFeaturesKHR available_clock{}; + available_clock.pNext = &available_maintenance8; vk::PhysicalDeviceRobustness2FeaturesEXT available_robustness2{}; - available_robustness2.pNext = &available_maintenance8; + available_robustness2.pNext = &available_clock; vk::PhysicalDeviceFeatures2 available_features2{}; available_features2.sType = vk::StructureType::ePhysicalDeviceFeatures2; available_features2.pNext = &available_robustness2; @@ -21295,6 +21297,8 @@ private: "image view minimum LOD is not supported"); Require("VulkanHarness", "dispatch", available_maintenance8.maintenance8 == true, "maintenance8 dynamic image offsets are not supported"); + Require("VulkanHarness", "dispatch", available_clock.shaderDeviceClock == true, + "shader device clock is not supported"); Require("VulkanHarness", "graphics", available_features12.shaderOutputLayer == true, "vertex layer output is not supported"); m_rasterization_supported = available_features.fillModeNonSolid && @@ -21377,7 +21381,10 @@ private: : static_cast(&derivatives); vk::PhysicalDeviceMaintenance8FeaturesKHR maintenance8{}; maintenance8.maintenance8 = true; - maintenance8.pNext = &min_lod; + vk::PhysicalDeviceShaderClockFeaturesKHR shader_clock{}; + shader_clock.shaderDeviceClock = true; + shader_clock.pNext = &min_lod; + maintenance8.pNext = &shader_clock; vk::PhysicalDeviceRobustness2FeaturesEXT robustness2{}; robustness2.nullDescriptor = true; robustness2.pNext = &maintenance8; @@ -21406,6 +21413,7 @@ private: VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME, VK_EXT_ROBUSTNESS_2_EXTENSION_NAME, VK_KHR_MAINTENANCE_8_EXTENSION_NAME, + VK_KHR_SHADER_CLOCK_EXTENSION_NAME, VK_EXT_IMAGE_VIEW_MIN_LOD_EXTENSION_NAME, VK_EXT_CONDITIONAL_RENDERING_EXTENSION_NAME}; if (m_rasterization_supported) { @@ -31970,29 +31978,60 @@ TestCase ScalarBranchRestoresInactiveLanes(u32 wave_size, u32 threads, bool uppe return test; } -TestCase ScalarMemRealtimeCapturedPlaceholder() { +// Each case spins until S_MEMREALTIME advances 64 ticks, capped at 2^20 reads, and stores +// min(delta, 64). The profile period picks the host clock shift. +std::vector ScalarMemRealtimeCases() { using O = ShaderOpcode; namespace D = ShaderRecompiler::Decoder; std::vector code = {0xf4940300u, 0xfa000000u}; D::Instruction decoded; D::DecodeInstruction(code, 0, decoded); - Require("ScalarMemRealtimeCapturedPlaceholder", "decode", + Require("ScalarMemRealtimeCases", "decode", decoded.opcode == O::S_MEMREALTIME && decoded.word_count == 2 && decoded.dst.kind == D::OperandKind::Sgpr && decoded.dst.reg == 12 && decoded.data_dwords == 2 && decoded.src_count == 0 && decoded.src0.kind == D::OperandKind::Unknown && decoded.src1.kind == D::OperandKind::Unknown, "captured clock instruction must write s12:s13 without memory operands"); - AppendStoreSgprPair(&code, 12, 0); - code.insert(code.end(), {0xf4940300u, 0xfa000000u}); - AppendStoreSgprPair(&code, 12, 2); + code.insert(code.end(), + {EncodeSop1(0x03, 17, InlineU32(0)), // s_mov_b32 s17, 0 + 0xf4940380u, 0xfa000000u, // loop: s_memrealtime s[14:15] + EncodeSop2(0x01, 16, 14, 12), // s_sub_u32 s16, s14, s12 + EncodeSop2(0x00, 17, 17, InlineU32(1)), // s_add_u32 s17, s17, 1 + EncodeSopc(0x09, 16, InlineU32(64)), // s_cmp_ge_u32 s16, 64 + EncodeSopp(0x05, 3), // s_cbranch_scc1 done + EncodeSopc(0x0a, 17, 255), 1u << 20, // s_cmp_lt_u32 s17, 1 << 20 + EncodeSopp(0x05, static_cast(-9)), // s_cbranch_scc1 loop + EncodeSop2(0x07, 16, 16, InlineU32(64))}); // done: s_min_u32 s16, s16, 64 + AppendStoreSgpr(&code, 16, 0); AppendEnd(&code); - return {"ScalarMemRealtimeCapturedPlaceholder", - code, - {}, - {UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX}, - {O::S_MEMREALTIME, O::V_MOV_B32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; + + static const struct { + const char *name; + ShaderProfile profile; + std::vector required, forbidden; + } variants[] = { + {"ScalarMemRealtimeShiftsNanosecondClock", + {.maintenance8 = true, .device_clock_period_ps = 1000}, + {"OpShiftRightLogical %ulong", "%uint_3\n"}, + {}}, + {"ScalarMemRealtimeKeeps100MHzClock", + {.maintenance8 = true, .device_clock_period_ps = 10000}, + {"OpReadClockKHR %ulong %uint_1"}, + {"OpShiftRightLogical %ulong"}}}; + std::vector tests; + for (const auto &variant : variants) { + TestCase test{variant.name, code, {}, {64}, + {O::S_MEMREALTIME, O::S_MOV_B32, O::S_SUB_U32, O::S_ADD_U32, O::S_CMP_GE_U32, + O::S_CBRANCH_SCC1, O::S_CMP_LT_U32, O::S_MIN_U32, O::V_MOV_B32, + O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; + test.profile = &variant.profile; + test.required_spirv = variant.required; + test.forbidden_spirv = variant.forbidden; + tests.push_back(std::move(test)); + } + return tests; } TestCase ScalarMemoryLoadVariants() { @@ -44047,7 +44086,9 @@ std::vector MakeCases() { AddCase([] { return ScalarBranchRestoresInactiveLanes(32, 32, true); }); AddCase([] { return ScalarBranchRestoresInactiveLanes(64, 64, true); }); AddCase(BranchVccnzUsesCarryProducedWaveMask); - AddCase(ScalarMemRealtimeCapturedPlaceholder); + for (auto &test : ScalarMemRealtimeCases()) { + cases.push_back(std::move(test)); + } AddCase(ScalarMemoryLoadVariants); AddCase(ScalarBufferOffsetAlignmentAndCarry); AddCase(ScalarLoadSignedImmediateOffsetAddsSoffset); @@ -50124,7 +50165,9 @@ int main(int argc, char **argv) { } if (argc == 2 && std::strcmp(argv[1], "--s-memrealtime-only") == 0) { VulkanHarness vulkan; - RunCase(&vulkan, ScalarMemRealtimeCapturedPlaceholder()); + for (const auto &test : ScalarMemRealtimeCases()) { + RunCase(&vulkan, test); + } return 0; } if (argc == 2 && std::strcmp(argv[1], "--packed-integer-neg-only") == 0) {