diff --git a/framework/spirv_simulator.cpp b/framework/spirv_simulator.cpp index 8a3abf4..0467468 100644 --- a/framework/spirv_simulator.cpp +++ b/framework/spirv_simulator.cpp @@ -3873,6 +3873,14 @@ bool SPIRVSimulator::TrySetComputeBuiltinValueAndRange(uint32_t result_id, const set_scalar_range(0, total ? total - 1 : 0); return true; } + case spv::BuiltIn::BuiltInNumWorkgroups: + if (result_type.kind == Type::Kind::Vector) + { + // Same value for every invocation of a given dispatch. + SetValue(result_id, make_uvec3(nx, ny, nz)); + return true; + } + break; default: break; } @@ -6982,9 +6990,12 @@ void SPIRVSimulator::GLSLExtHandler(uint32_t type_id, for (uint32_t i = 0; i < type.vector.elem_count; ++i) { - Value elem_result = (double)std::clamp(std::get(vec->elems[i]), - std::get(min_vec->elems[i]), - std::get(max_vec->elems[i])); + // GLSL defines clamp(x, min, max) = max(min, min(x, max)), which is well-defined + // even when min > max (unlike std::clamp, which asserts in debug builds). + const double v = std::get(vec->elems[i]); + const double lo = std::get(min_vec->elems[i]); + const double hi = std::get(max_vec->elems[i]); + Value elem_result = std::max(lo, std::min(v, hi)); result_vec->elems.push_back(elem_result); } @@ -6992,8 +7003,12 @@ void SPIRVSimulator::GLSLExtHandler(uint32_t type_id, } else if (type.kind == Type::Kind::Float) { - Value result = - (double)std::clamp(std::get(operand), std::get(min_val), std::get(max_val)); + // GLSL defines clamp(x, min, max) = max(min, min(x, max)), which is well-defined + // even when min > max (unlike std::clamp, which asserts in debug builds). + const double v = std::get(operand); + const double lo = std::get(min_val); + const double hi = std::get(max_val); + Value result = std::max(lo, std::min(v, hi)); SetValue(result_id, result); } else @@ -7028,9 +7043,12 @@ void SPIRVSimulator::GLSLExtHandler(uint32_t type_id, for (uint32_t i = 0; i < type.vector.elem_count; ++i) { - Value elem_result = (uint64_t)std::clamp(std::get(vec->elems[i]), - std::get(min_vec->elems[i]), - std::get(max_vec->elems[i])); + // GLSL defines clamp(x, min, max) = max(min, min(x, max)), which is well-defined + // even when min > max (unlike std::clamp, which asserts in debug builds). + const uint64_t v = std::get(vec->elems[i]); + const uint64_t lo = std::get(min_vec->elems[i]); + const uint64_t hi = std::get(max_vec->elems[i]); + Value elem_result = std::max(lo, std::min(v, hi)); result_vec->elems.push_back(elem_result); } @@ -7038,8 +7056,12 @@ void SPIRVSimulator::GLSLExtHandler(uint32_t type_id, } else if (type.kind == Type::Kind::Int) { - Value result = (uint64_t)std::clamp( - std::get(operand), std::get(min_val), std::get(max_val)); + // GLSL defines clamp(x, min, max) = max(min, min(x, max)), which is well-defined + // even when min > max (unlike std::clamp, which asserts in debug builds). + const uint64_t v = std::get(operand); + const uint64_t lo = std::get(min_val); + const uint64_t hi = std::get(max_val); + Value result = std::max(lo, std::min(v, hi)); SetValue(result_id, result); } else @@ -7074,9 +7096,12 @@ void SPIRVSimulator::GLSLExtHandler(uint32_t type_id, for (uint32_t i = 0; i < type.vector.elem_count; ++i) { - Value elem_result = (int64_t)std::clamp(std::get(vec->elems[i]), - std::get(min_vec->elems[i]), - std::get(max_vec->elems[i])); + // GLSL defines clamp(x, min, max) = max(min, min(x, max)), which is well-defined + // even when min > max (unlike std::clamp, which asserts in debug builds). + const int64_t v = std::get(vec->elems[i]); + const int64_t lo = std::get(min_vec->elems[i]); + const int64_t hi = std::get(max_vec->elems[i]); + Value elem_result = std::max(lo, std::min(v, hi)); result_vec->elems.push_back(elem_result); } @@ -7084,8 +7109,12 @@ void SPIRVSimulator::GLSLExtHandler(uint32_t type_id, } else if (type.kind == Type::Kind::Int) { - Value result = (int64_t)std::clamp( - std::get(operand), std::get(min_val), std::get(max_val)); + // GLSL defines clamp(x, min, max) = max(min, min(x, max)), which is well-defined + // even when min > max (unlike std::clamp, which asserts in debug builds). + const int64_t v = std::get(operand); + const int64_t lo = std::get(min_val); + const int64_t hi = std::get(max_val); + Value result = std::max(lo, std::min(v, hi)); SetValue(result_id, result); } else diff --git a/test/misc_test.cpp b/test/misc_test.cpp index a79db23..98cfed6 100644 --- a/test/misc_test.cpp +++ b/test/misc_test.cpp @@ -430,6 +430,73 @@ TEST_F(PointerResolutionTests, UndecoratedFunctionMatrixUsesPackedColumnStride) EXPECT_EQ(GetPointerOffsetForTest(pointer), 16u); } +TEST_F(GLSLExtInstructionTests, SClampAllowsMinGreaterThanMax) +{ + // Regression test: a shader can legally reach a clamp(x, min, max) state with min > max + // (eg clamping to [0, width - 1] where width is 0). GLSL defines the result as + // max(min, min(x, max)), so the simulator must not abort (std::clamp asserts on min > max). + constexpr uint32_t result_id = 500; + constexpr uint32_t operand_id = 501; + constexpr uint32_t min_id = 502; + constexpr uint32_t max_id = 503; + + ::SPIRVSimulator::Value operand = int64_t{0}; + ::SPIRVSimulator::Value min_val = int64_t{0}; + ::SPIRVSimulator::Value max_val = int64_t{-1}; + ::SPIRVSimulator::Value captured_result; + + EXPECT_CALL(*this, GetTypeByTypeId(CommonTypes::i32)).WillOnce(ReturnRef(types_.at(CommonTypes::i32))); + EXPECT_CALL(*this, GetValue(operand_id)).WillOnce(ReturnRef(operand)); + EXPECT_CALL(*this, GetValue(min_id)).WillOnce(ReturnRef(min_val)); + EXPECT_CALL(*this, GetValue(max_id)).WillOnce(ReturnRef(max_val)); + EXPECT_CALL(*this, SetValue(result_id, _, true)).WillOnce(SaveArg<1>(&captured_result)); + EXPECT_CALL(*this, TransferFlags(result_id, TypedEq(operand_id))); + EXPECT_CALL(*this, TransferFlags(result_id, TypedEq(min_id))); + EXPECT_CALL(*this, TransferFlags(result_id, TypedEq(max_id))); + + const std::vector operands{ operand_id, min_id, max_id }; + ExecuteGLSLExtInstruction(CommonTypes::i32, result_id, 45, operands); + + ASSERT_TRUE(std::holds_alternative(captured_result)); + EXPECT_EQ(std::get(captured_result), 0); +} + +TEST_F(GLSLExtInstructionTests, SClampVectorClampsPerElement) +{ + // max(min, min(x, max)) per element, including a min > max component. + constexpr uint32_t result_id = 510; + constexpr uint32_t operand_id = 511; + constexpr uint32_t min_id = 512; + constexpr uint32_t max_id = 513; + + ::SPIRVSimulator::Value operand = + std::make_shared<::SPIRVSimulator::VectorV>(std::initializer_list{ 15, -5, 0 }); + ::SPIRVSimulator::Value min_val = + std::make_shared<::SPIRVSimulator::VectorV>(std::initializer_list{ 2, 2, 0 }); + ::SPIRVSimulator::Value max_val = + std::make_shared<::SPIRVSimulator::VectorV>(std::initializer_list{ 10, 10, -1 }); + ::SPIRVSimulator::Value captured_result; + + EXPECT_CALL(*this, GetTypeByTypeId(CommonTypes::ivec3)).WillOnce(ReturnRef(types_.at(CommonTypes::ivec3))); + EXPECT_CALL(*this, GetValue(operand_id)).WillOnce(ReturnRef(operand)); + EXPECT_CALL(*this, GetValue(min_id)).WillOnce(ReturnRef(min_val)); + EXPECT_CALL(*this, GetValue(max_id)).WillOnce(ReturnRef(max_val)); + EXPECT_CALL(*this, SetValue(result_id, _, true)).WillOnce(SaveArg<1>(&captured_result)); + EXPECT_CALL(*this, TransferFlags(result_id, TypedEq(operand_id))); + EXPECT_CALL(*this, TransferFlags(result_id, TypedEq(min_id))); + EXPECT_CALL(*this, TransferFlags(result_id, TypedEq(max_id))); + + const std::vector operands{ operand_id, min_id, max_id }; + ExecuteGLSLExtInstruction(CommonTypes::ivec3, result_id, 45, operands); + + ASSERT_TRUE(std::holds_alternative>(captured_result)); + const auto& result = std::get>(captured_result); + ASSERT_EQ(result->elems.size(), 3u); + EXPECT_EQ(std::get(result->elems[0]), 10); + EXPECT_EQ(std::get(result->elems[1]), 2); + EXPECT_EQ(std::get(result->elems[2]), 0); +} + class MemoryBarrierTests : public SPIRVSimulatorMockBase, public ::testing::Test {};