diff --git a/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp b/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp index 67c460718..8094e2381 100644 --- a/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp +++ b/src/graphics/shader/recompiler/ir/passes/SrtWalker.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -74,8 +75,17 @@ bool IsDescriptorHandle(ValueOpcode opcode) { } } -bool IsRuntimeIntegerOp(ValueOpcode op) { +bool IsRuntimeSelect(ValueOpcode op) { + return op == ValueOpcode::SelectU1 || op == ValueOpcode::SelectU32 || + op == ValueOpcode::SelectF32; +} + +bool IsRuntimeUniformOp(ValueOpcode op) { switch (op) { + case ValueOpcode::BitCastU32F32: + case ValueOpcode::BitCastF32U32: + case ValueOpcode::ConvertU32F32: + case ValueOpcode::ConvertF32U32: case ValueOpcode::CompositeConstructU64: case ValueOpcode::CompositeExtractU64: case ValueOpcode::CompositeConstructU32x2: @@ -104,6 +114,7 @@ bool IsRuntimeIntegerOp(ValueOpcode op) { case ValueOpcode::BitwiseNot32: case ValueOpcode::SelectU1: case ValueOpcode::SelectU32: + case ValueOpcode::SelectF32: case ValueOpcode::ULessThan32: case ValueOpcode::IEqual32: case ValueOpcode::UGreaterThan32: @@ -111,14 +122,20 @@ bool IsRuntimeIntegerOp(ValueOpcode op) { case ValueOpcode::LogicalOr: case ValueOpcode::LogicalAnd: case ValueOpcode::LogicalXor: - case ValueOpcode::LogicalNot: return true; + case ValueOpcode::LogicalNot: + case ValueOpcode::FPOrdLessThanEqual32: + case ValueOpcode::FPOrdGreaterThanEqual32: + case ValueOpcode::FPIsNan32: + case ValueOpcode::FPMul32: + case ValueOpcode::FPTrunc32: return true; default: return false; } } class RuntimeValidator { public: - explicit RuntimeValidator(const Program& program): m_program(program) {} + explicit RuntimeValidator(const Program& program, Value active_mask = {}) + : m_program(program), m_active_mask(active_mask.Resolve()) {} bool Run(Value value) { return Validate(value); } @@ -132,7 +149,8 @@ private: case Type::U8: case Type::U16: case Type::U32: - case Type::U64: return true; + case Type::U64: + case Type::F32: return true; default: return false; } } @@ -144,6 +162,10 @@ private: return valid; }; const auto op = inst->GetOpcode(); + if (!m_active_mask.IsEmpty() && IsRuntimeSelect(op) && inst->NumArgs() == 3 && + inst->Arg(0).Resolve() == m_active_mask) { + return finish(Validate(inst->Arg(1))); + } if (op == ValueOpcode::UndefU1 || op == ValueOpcode::UndefU8 || op == ValueOpcode::UndefU16 || op == ValueOpcode::UndefU32 || op == ValueOpcode::UndefU64 || op == ValueOpcode::Void) { @@ -173,6 +195,13 @@ private: } return finish(Validate(invariant)); } + if (op == ValueOpcode::ReadFirstLane) { + if (inst->NumArgs() != 2 || inst->Arg(0).GetType() != Type::U32 || + inst->Arg(1).GetType() != Type::U1) { + return finish(false); + } + return finish(RuntimeValidator(m_program, inst->Arg(1)).Run(inst->Arg(0))); + } if (op == ValueOpcode::GetSrtResource) { if (inst->NumArgs() != 0) { return finish(false); @@ -222,7 +251,7 @@ private: return finish(false); } } else if (op != ValueOpcode::ReadConst && op != ValueOpcode::ReadConstBuffer && - op != ValueOpcode::LoadAddressU32 && !IsRuntimeIntegerOp(op)) { + op != ValueOpcode::LoadAddressU32 && !IsRuntimeUniformOp(op)) { return finish(false); } for (size_t index = 0; index < inst->NumArgs(); index++) { @@ -234,6 +263,7 @@ private: } const Program& m_program; + Value m_active_mask; std::unordered_set m_visiting; }; @@ -386,9 +416,10 @@ private: class Evaluator { public: Evaluator(const Program& program, const SrtRuntime& runtime, - std::span clean_flat_slots = {}, Evaluator* clean_evaluator = nullptr) + std::span clean_flat_slots = {}, Evaluator* clean_evaluator = nullptr, + Value active_mask = {}) : m_program(program), m_runtime(runtime), m_clean_flat_slots(clean_flat_slots), - m_clean_evaluator(clean_evaluator) {} + m_clean_evaluator(clean_evaluator), m_active_mask(active_mask.Resolve()) {} bool Evaluate(Value value, uint32_t& result) { uint64_t wide = 0; @@ -400,6 +431,14 @@ public: } private: + static float Float32(uint64_t bits) { + return std::bit_cast(static_cast(bits)); + } + + static uint64_t Float32Bits(float value) { + return std::bit_cast(value); + } + bool EvaluateWide(Value value, uint64_t& result) { value = value.Resolve(); if (value.IsImmediate()) { @@ -409,6 +448,7 @@ private: case Type::U16: result = value.U16(); return true; case Type::U32: result = value.U32(); return true; case Type::U64: result = value.U64(); return true; + case Type::F32: result = Float32Bits(value.F32Value()); return true; default: return false; } } @@ -416,6 +456,10 @@ private: if (inst == nullptr) { return false; } + if (!m_active_mask.IsEmpty() && IsRuntimeSelect(inst->GetOpcode()) && + inst->NumArgs() == 3 && inst->Arg(0).Resolve() == m_active_mask) { + return EvaluateWide(inst->Arg(1), result); + } if (const auto found = m_cache.find(inst); found != m_cache.end()) { result = found->second; return true; @@ -560,6 +604,13 @@ private: } case ValueOpcode::GetShaderBase: result = m_runtime.shader_base; return true; case ValueOpcode::Phi: return EvaluatePhi(inst, result); + case ValueOpcode::ReadFirstLane: { + Evaluator active(m_program, m_runtime, m_clean_flat_slots, m_clean_evaluator, + inst.Arg(1)); + return active.EvaluateWide(inst.Arg(0), result); + } + case ValueOpcode::BitCastU32F32: + case ValueOpcode::BitCastF32U32: return Arg(inst, 0, result); case ValueOpcode::CompositeExtractU64: case ValueOpcode::CompositeExtractU32x2: return EvaluateExtract(inst, result); case ValueOpcode::CompositeConstructU64: @@ -630,6 +681,53 @@ private: return true; } return false; + case ValueOpcode::ConvertF32U32: + if (Arg(inst, 0, a)) { + result = Float32Bits(static_cast(static_cast(a))); + return true; + } + return false; + case ValueOpcode::ConvertU32F32: + if (Arg(inst, 0, a)) { + const auto value = Float32(a); + if (!std::isfinite(value) || value < 0.0f || + static_cast(value) > UINT32_MAX) { + return false; + } + result = static_cast(value); + return true; + } + return false; + case ValueOpcode::FPMul32: + if (binary()) { + result = Float32Bits(Float32(a) * Float32(b)); + return true; + } + return false; + case ValueOpcode::FPTrunc32: + if (Arg(inst, 0, a)) { + result = Float32Bits(std::trunc(Float32(a))); + return true; + } + return false; + case ValueOpcode::FPIsNan32: + if (Arg(inst, 0, a)) { + result = std::isnan(Float32(a)); + return true; + } + return false; + case ValueOpcode::FPOrdLessThanEqual32: + if (binary()) { + result = Float32(a) <= Float32(b); + return true; + } + return false; + case ValueOpcode::FPOrdGreaterThanEqual32: + if (binary()) { + result = Float32(a) >= Float32(b); + return true; + } + return false; case ValueOpcode::BitwiseAnd32: if (binary()) { result = static_cast(a & b); @@ -753,6 +851,7 @@ private: } case ValueOpcode::SelectU32: case ValueOpcode::SelectU1: + case ValueOpcode::SelectF32: if (ternary()) { result = a != 0u ? b : c; return true; @@ -820,6 +919,7 @@ private: const SrtRuntime& m_runtime; std::span m_clean_flat_slots; Evaluator* m_clean_evaluator = nullptr; + Value m_active_mask; std::unordered_map m_cache; std::vector m_visiting; }; diff --git a/tests/ScalarProvenanceTests.cpp b/tests/ScalarProvenanceTests.cpp index 6372a0fb7..cfebc9aab 100644 --- a/tests/ScalarProvenanceTests.cpp +++ b/tests/ScalarProvenanceTests.cpp @@ -319,6 +319,84 @@ void TestRuntime64BitDescriptorOps() { "64-bit typed descriptor arithmetic evaluation is incorrect"); } +void TestUniformFirstLaneSamplerLod() { + Fixture fixture; + const auto active = fixture.Emit(ValueOpcode::WqmMask, {Value(true)}); + const auto stale = fixture.Emit( + ValueOpcode::GetVectorRegister, {Value(static_cast(0))}); + const auto write = [&](Value value, Value previous) { + return fixture.Emit(ValueOpcode::SelectU32, {active, value, previous}); + }; + const auto user = fixture.Emit( + ValueOpcode::GetUserData, {Value(static_cast(8))}); + const auto as_float = fixture.Emit(ValueOpcode::ConvertF32U32, {user}); + const auto initial = write( + fixture.Emit(ValueOpcode::BitCastU32F32, {as_float}), stale); + const auto initial_float = + fixture.Emit(ValueOpcode::BitCastF32U32, {initial}); + const auto scaled = fixture.Emit(ValueOpcode::FPMul32, + {Value::F32(256.0f), initial_float}); + const auto scaled_write = write( + fixture.Emit(ValueOpcode::BitCastU32F32, {scaled}), initial); + const auto scaled_float = + fixture.Emit(ValueOpcode::BitCastF32U32, {scaled_write}); + const auto nan = fixture.Emit(ValueOpcode::FPIsNan32, {scaled_float}); + const auto low = fixture.Emit(ValueOpcode::FPOrdLessThanEqual32, + {scaled_float, Value::F32(0.0f)}); + const auto high = fixture.Emit(ValueOpcode::FPOrdGreaterThanEqual32, + {scaled_float, Value::F32(4294967296.0f)}); + const auto truncated = fixture.Emit(ValueOpcode::FPTrunc32, {scaled_float}); + const auto safe_low = fixture.Emit( + ValueOpcode::SelectF32, + {fixture.Emit(ValueOpcode::LogicalOr, {nan, low}), Value::F32(0.0f), + truncated}); + const auto safe = fixture.Emit( + ValueOpcode::SelectF32, + {high, Value::F32(4294967040.0f), safe_low}); + const auto converted = fixture.Emit(ValueOpcode::ConvertU32F32, {safe}); + const auto saturated = write( + fixture.Emit(ValueOpcode::SelectU32, + {high, Value(UINT32_MAX), converted}), + scaled_write); + const auto lod = fixture.Emit(ValueOpcode::UMin32, + {saturated, Value(0xfffu)}); + const auto lod_write = write(lod, saturated); + const auto masked = fixture.Emit(ValueOpcode::BitwiseAnd32, + {lod_write, Value(0xfffu)}); + const auto masked_write = write(masked, lod_write); + const auto packed = write( + fixture.Emit( + ValueOpcode::BitwiseOr32, + {fixture.Emit(ValueOpcode::ShiftLeftLogical32, + {masked_write, Value(12u)}), + masked_write}), + masked_write); + const auto first = fixture.Emit(ValueOpcode::ReadFirstLane, + {packed, active}); + const auto divergent = fixture.Emit( + ValueOpcode::ReadFirstLane, {stale, active}); + fixture.Plan(); + + Check(ValidateRuntimeValue(fixture.program, first), + "uniform sampler LOD construction was rejected"); + Check(!ValidateRuntimeValue(fixture.program, divergent), + "divergent first-lane value was accepted as uniform"); + fixture.program.descriptor_sources.push_back( + {.dwords = {first}, .dword_count = 1}); + + std::array user_data{}; + user_data[6] = 3u; + SrtRuntime runtime{.user_data = user_data}; + DescriptorValue result; + Check(EvaluateDescriptorSource(fixture.program, 0, runtime, result) && + result.dwords[0] == 0x00300300u, + "uniform sampler LOD evaluated incorrectly"); + user_data[6] = 20u; + Check(EvaluateDescriptorSource(fixture.program, 0, runtime, result) && + result.dwords[0] == 0x00ffffffu, + "uniform sampler LOD clamp evaluated incorrectly"); +} + void TestConstantBufferBounds() { Fixture fixture; const auto memory = fixture.AddMemory(ResourceKind::ScalarBuffer); @@ -475,6 +553,7 @@ int main() { TestInvariantAndDivergentPhi(); TestControlDependentStandaloneLoadStaysTyped(); TestRuntime64BitDescriptorOps(); + TestUniformFirstLaneSamplerLod(); TestConstantBufferBounds(); TestReadLaneElimination(); TestOptimizationPipeline();