blob: b7459802825a587dc9547bfe2772618548ea6b97 [file]
/* Copyright (c) 2024-2026 LunarG, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "sanitizer_pass.h"
#include "containers/container_utils.h"
#include "cooperative_matrix.h"
#include "function_basic_block.h"
#include "gpuav/shaders/gpuav_error_codes.h"
#include "module.h"
#include <spirv/unified1/GLSL.std.450.h>
#include <cstdint>
#include <spirv/unified1/spirv.hpp>
#include <iostream>
#include <cstring>
#include "generated/gpuav_offline_spirv.h"
namespace gpuav {
namespace spirv {
const static OfflineModule kOfflineModule = {instrumentation_sanitizer_comp, instrumentation_sanitizer_comp_size,
UseErrorPayloadVariable};
const static OfflineFunction kOfflineFunctions[glsl::kErrorSubCode_Sanitizer_Count] = {
{"empty", 0},
{"inst_sanitizer_divide_by_zero", instrumentation_sanitizer_comp_function_0_offset},
{"inst_sanitizer_image_gather", instrumentation_sanitizer_comp_function_1_offset},
{"inst_sanitizer_pow", instrumentation_sanitizer_comp_function_2_offset},
{"inst_sanitizer_atan2", instrumentation_sanitizer_comp_function_3_offset},
{"inst_sanitizer_fminmax", instrumentation_sanitizer_comp_function_4_offset},
{"inst_sanitizer_coop_mat_alignment", instrumentation_sanitizer_comp_function_5_offset},
};
SanitizerPass::SanitizerPass(Module& module) : Pass(module, kOfflineModule) {
for (uint32_t i = 0; i < glsl::kErrorSubCode_Sanitizer_Count; i++) {
link_function_ids_[i] = 0;
}
}
// By appending the LinkInfo, it will attempt at linking stage to add the function.
uint32_t SanitizerPass::GetLinkFunctionId(uint32_t sub_code) {
return GetLinkFunction(link_function_ids_[sub_code], kOfflineFunctions[sub_code]);
}
// We do the check for zero in C++ to make it easier to handle the various cases of signed/unsigned/64bit/etc
// Returns an ID of type OpTypeBool
uint32_t SanitizerPass::DivideByZeroCheck(BasicBlock& block, InstructionIt* inst_it, const InstructionMeta& meta) {
const Type& bool_type = type_manager_.GetTypeBool();
const uint32_t vector_size = meta.result_type->VectorSize();
const uint32_t divisor_id = meta.target_instruction->Word(4);
const bool is_float = meta.target_instruction->Opcode() == spv::OpFMod || meta.target_instruction->Opcode() == spv::OpFRem;
const spv::Op compare_op = is_float ? spv::OpFOrdEqual : spv::OpIEqual;
if (vector_size == 0) {
const uint32_t null_type_id = type_manager_.GetConstantNull(*meta.result_type).Id();
const uint32_t compare_id = module_.TakeNextId();
block.CreateInstruction(compare_op, {bool_type.Id(), compare_id, null_type_id, divisor_id}, inst_it);
return compare_id;
} else {
const uint32_t bool_vector_type_id = type_manager_.GetTypeVector(bool_type, vector_size).Id();
const uint32_t zero_id = type_manager_.GetConstantZeroVector(*meta.result_type).Id();
const uint32_t compare_id = module_.TakeNextId();
block.CreateInstruction(compare_op, {bool_vector_type_id, compare_id, zero_id, divisor_id}, inst_it);
const uint32_t any_id = module_.TakeNextId();
block.CreateInstruction(spv::OpAny, {bool_type.Id(), any_id, compare_id}, inst_it);
return any_id;
}
}
// Based off https://godbolt.org/z/dbToMGKTd - but found can hand-roll the spirv much better
// bool is_invalid = (x < 0.0 || (x == 0.0 && y <= 0.0));
// Returns an ID of type OpTypeBool
uint32_t SanitizerPass::PowCheck(BasicBlock& block, InstructionIt* inst_it, const InstructionMeta& meta) {
const Type& bool_type = type_manager_.GetTypeBool();
const uint32_t vector_size = meta.result_type->VectorSize();
uint32_t bool_compare_type_id = 0;
uint32_t null_type_id = 0;
if (vector_size == 0) {
bool_compare_type_id = bool_type.Id();
null_type_id = type_manager_.GetConstantNull(*meta.result_type).Id();
} else {
bool_compare_type_id = type_manager_.GetTypeVector(bool_type, vector_size).Id();
null_type_id = type_manager_.GetConstantZeroVector(*meta.result_type).Id();
}
const uint32_t x_value_id = meta.target_instruction->Word(5);
const uint32_t y_value_id = meta.target_instruction->Word(6);
const uint32_t compare_1_id = module_.TakeNextId();
const uint32_t compare_2_id = module_.TakeNextId();
const uint32_t compare_3_id = module_.TakeNextId();
block.CreateInstruction(spv::OpFOrdLessThan, {bool_compare_type_id, compare_1_id, x_value_id, null_type_id}, inst_it);
block.CreateInstruction(spv::OpFOrdEqual, {bool_compare_type_id, compare_2_id, x_value_id, null_type_id}, inst_it);
block.CreateInstruction(spv::OpFOrdLessThanEqual, {bool_compare_type_id, compare_3_id, y_value_id, null_type_id}, inst_it);
const uint32_t compare_and_id = module_.TakeNextId();
const uint32_t compare_or_id = module_.TakeNextId();
block.CreateInstruction(spv::OpLogicalAnd, {bool_compare_type_id, compare_and_id, compare_2_id, compare_3_id}, inst_it);
block.CreateInstruction(spv::OpLogicalOr, {bool_compare_type_id, compare_or_id, compare_1_id, compare_and_id}, inst_it);
uint32_t result_bool_id = 0;
if (vector_size == 0) {
result_bool_id = compare_or_id;
} else {
result_bool_id = module_.TakeNextId();
block.CreateInstruction(spv::OpAny, {bool_type.Id(), result_bool_id, compare_or_id}, inst_it);
}
return result_bool_id;
}
// Returns an ID of type OpTypeBool
uint32_t SanitizerPass::Atan2Check(BasicBlock& block, InstructionIt* inst_it, const InstructionMeta& meta) {
const Type& bool_type = type_manager_.GetTypeBool();
const uint32_t vector_size = meta.result_type->VectorSize();
uint32_t bool_compare_type_id = 0;
uint32_t null_type_id = 0;
if (vector_size == 0) {
bool_compare_type_id = bool_type.Id();
null_type_id = type_manager_.GetConstantNull(*meta.result_type).Id();
} else {
bool_compare_type_id = type_manager_.GetTypeVector(bool_type, vector_size).Id();
null_type_id = type_manager_.GetConstantZeroVector(*meta.result_type).Id();
}
// Seems Atan flips the x/y order from other functions, doesn't really matter, but noting here
const uint32_t y_value_id = meta.target_instruction->Word(5);
const uint32_t x_value_id = meta.target_instruction->Word(6);
const uint32_t compare_y_id = module_.TakeNextId();
const uint32_t compare_x_id = module_.TakeNextId();
block.CreateInstruction(spv::OpFOrdEqual, {bool_compare_type_id, compare_y_id, y_value_id, null_type_id}, inst_it);
block.CreateInstruction(spv::OpFOrdEqual, {bool_compare_type_id, compare_x_id, x_value_id, null_type_id}, inst_it);
const uint32_t compare_and_id = module_.TakeNextId();
block.CreateInstruction(spv::OpLogicalAnd, {bool_compare_type_id, compare_and_id, compare_y_id, compare_x_id}, inst_it);
uint32_t result_bool_id = 0;
if (vector_size == 0) {
result_bool_id = compare_and_id;
} else {
result_bool_id = module_.TakeNextId();
block.CreateInstruction(spv::OpAny, {bool_type.Id(), result_bool_id, compare_and_id}, inst_it);
}
return result_bool_id;
}
// Returns an ID of type OpTypeBool for X and Y
BoolResultXY SanitizerPass::FminmaxCheck(BasicBlock& block, InstructionIt* inst_it, const InstructionMeta& meta) {
BoolResultXY result_bool_id;
const Type& bool_type = type_manager_.GetTypeBool();
const uint32_t vector_size = meta.result_type->VectorSize();
const uint32_t bool_result_type_id =
(vector_size == 0) ? bool_type.Id() : type_manager_.GetTypeVector(bool_type, vector_size).Id();
const uint32_t x_value_id = meta.target_instruction->Word(5);
const uint32_t y_value_id = meta.target_instruction->Word(6);
const uint32_t nan_x_id = module_.TakeNextId();
const uint32_t nan_y_id = module_.TakeNextId();
block.CreateInstruction(spv::OpIsNan, {bool_result_type_id, nan_x_id, x_value_id}, inst_it);
block.CreateInstruction(spv::OpIsNan, {bool_result_type_id, nan_y_id, y_value_id}, inst_it);
if (vector_size == 0) {
result_bool_id.x = nan_x_id;
result_bool_id.y = nan_y_id;
} else {
result_bool_id.x = module_.TakeNextId();
result_bool_id.y = module_.TakeNextId();
block.CreateInstruction(spv::OpAny, {bool_type.Id(), result_bool_id.x, nan_x_id}, inst_it);
block.CreateInstruction(spv::OpAny, {bool_type.Id(), result_bool_id.y, nan_y_id}, inst_it);
}
return result_bool_id;
}
// For BDA pointers, converts the pointer to its low 32 bits. Returns 0 if not a BDA pointer.
uint32_t SanitizerPass::GetCoopMatPointerAddress(const Type* pointer_type, uint32_t pointer_id, BasicBlock& block,
InstructionIt* inst_it) {
if (pointer_type && pointer_type->spv_type_ == SpvType::kPointer &&
pointer_type->inst_.StorageClass() == spv::StorageClassPhysicalStorageBuffer) {
module_.use_bda_ = true;
const Type& uint32_type = type_manager_.GetTypeInt(32, false);
const Type& uint64_type = type_manager_.GetTypeInt(64, false);
const uint32_t ptr_uint64_id = module_.TakeNextId();
block.CreateInstruction(spv::OpConvertPtrToU, {uint64_type.Id(), ptr_uint64_id, pointer_id}, inst_it);
const uint32_t ptr_uint32_id = module_.TakeNextId();
block.CreateInstruction(spv::OpUConvert, {uint32_type.Id(), ptr_uint32_id, ptr_uint64_id}, inst_it);
return ptr_uint32_id;
}
return type_manager_.GetConstantZeroUint32().Id();
}
// For SSBO pointers via OpAccessChain, extracts the last index. Returns 0 if not an access chain.
uint32_t SanitizerPass::GetCoopMatElementIndex(const Instruction* pointer_inst, BasicBlock& block, InstructionIt* inst_it) {
if (pointer_inst && pointer_inst->IsNonPtrAccessChain()) {
const uint32_t last_index_id = pointer_inst->Word(pointer_inst->Length() - 1);
return CastToUint32(last_index_id, block, inst_it);
}
return type_manager_.GetConstantZeroUint32().Id();
}
uint32_t SanitizerPass::CreateFunctionCall(BasicBlock& block, InstructionIt* inst_it, const InstructionMeta& meta) {
const uint32_t function_result = module_.TakeNextId();
const uint32_t function_def = GetLinkFunctionId(meta.sub_code);
const uint32_t inst_position = meta.target_instruction->GetPositionOffset();
const uint32_t inst_position_id = type_manager_.CreateConstantUInt32(inst_position).Id();
if (meta.sub_code == glsl::kErrorSubCode_Sanitizer_DivideZero) {
const uint32_t is_invalid_id = DivideByZeroCheck(block, inst_it, meta);
const uint32_t bool_type = type_manager_.GetTypeBool().Id();
const uint32_t opcode_id = type_manager_.CreateConstantUInt32(meta.target_instruction->Opcode()).Id();
const uint32_t vector_size_id = type_manager_.CreateConstantUInt32(meta.result_type->VectorSize()).Id();
block.CreateInstruction(
spv::OpFunctionCall,
{bool_type, function_result, function_def, is_invalid_id, inst_position_id, opcode_id, vector_size_id}, inst_it);
} else if (meta.sub_code == glsl::kErrorSubCode_Sanitizer_ImageGather) {
const uint32_t void_type = type_manager_.GetTypeVoid().Id();
// If the OpConstant was a signed int, this will "cast" it by making a new OpConstant with the same value
const uint32_t component_value_id = type_manager_.CreateConstantUInt32(meta.constant_value).Id();
// Because we know this is 100% is an error if executed, just replace with a safe value
const uint32_t safe_value_id = type_manager_.GetConstantZeroUint32().Id();
const_cast<Instruction*>(meta.target_instruction)->UpdateWord(5, safe_value_id);
block.CreateInstruction(spv::OpFunctionCall,
{void_type, function_result, function_def, inst_position_id, component_value_id}, inst_it);
} else if (meta.sub_code == glsl::kErrorSubCode_Sanitizer_Pow) {
const uint32_t is_invalid_id = PowCheck(block, inst_it, meta);
const uint32_t bool_type = type_manager_.GetTypeBool().Id();
const uint32_t vector_size = meta.result_type->VectorSize();
const uint32_t vector_size_id = type_manager_.CreateConstantUInt32(vector_size).Id();
uint32_t x_value_id = 0;
uint32_t y_value_id = 0;
if (vector_size == 0) {
// cast as uint as that is how we are encoding the payload currently
const Type& uint32_type = type_manager_.GetTypeInt(32, false);
const uint32_t x_value_float = meta.target_instruction->Word(5);
const uint32_t y_value_float = meta.target_instruction->Word(6);
const uint32_t float_bit_width = meta.result_type->meta_.scalar.bit_width;
if (float_bit_width == 16) {
// Cast to f32 before OpBitCast
const uint32_t float32_type_id = type_manager_.GetTypeFloat(32).Id();
const uint32_t x_f32_id = module_.TakeNextId();
const uint32_t y_f32_id = module_.TakeNextId();
block.CreateInstruction(spv::OpFConvert, {float32_type_id, x_f32_id, x_value_float}, inst_it);
block.CreateInstruction(spv::OpFConvert, {float32_type_id, y_f32_id, y_value_float}, inst_it);
x_value_id = module_.TakeNextId();
y_value_id = module_.TakeNextId();
block.CreateInstruction(spv::OpBitcast, {uint32_type.Id(), x_value_id, x_f32_id}, inst_it);
block.CreateInstruction(spv::OpBitcast, {uint32_type.Id(), y_value_id, y_f32_id}, inst_it);
} else if (float_bit_width == 32) {
x_value_id = module_.TakeNextId();
y_value_id = module_.TakeNextId();
block.CreateInstruction(spv::OpBitcast, {uint32_type.Id(), x_value_id, x_value_float}, inst_it);
block.CreateInstruction(spv::OpBitcast, {uint32_type.Id(), y_value_id, y_value_float}, inst_it);
} else {
// GLSL.std.450 spec only allows 16 or 32 bit floats
assert(false);
x_value_id = type_manager_.GetConstantZeroUint32().Id();
y_value_id = type_manager_.GetConstantZeroUint32().Id();
}
} else {
// Put something valid, these are ignored on when printing error
x_value_id = type_manager_.GetConstantZeroUint32().Id();
y_value_id = type_manager_.GetConstantZeroUint32().Id();
}
block.CreateInstruction(
spv::OpFunctionCall,
{bool_type, function_result, function_def, is_invalid_id, inst_position_id, vector_size_id, x_value_id, y_value_id},
inst_it);
} else if (meta.sub_code == glsl::kErrorSubCode_Sanitizer_Atan2) {
const uint32_t is_invalid_id = Atan2Check(block, inst_it, meta);
const uint32_t bool_type = type_manager_.GetTypeBool().Id();
block.CreateInstruction(spv::OpFunctionCall, {bool_type, function_result, function_def, is_invalid_id, inst_position_id},
inst_it);
} else if (meta.sub_code == glsl::kErrorSubCode_Sanitizer_Fminmax) {
const BoolResultXY is_invalid_id = FminmaxCheck(block, inst_it, meta);
const uint32_t bool_type = type_manager_.GetTypeBool().Id();
const uint32_t vector_size = meta.result_type->VectorSize();
const uint32_t vector_size_id = type_manager_.CreateConstantUInt32(vector_size).Id();
const uint32_t glsl_opcode_id = type_manager_.CreateConstantUInt32(meta.glsl_opcode).Id();
block.CreateInstruction(spv::OpFunctionCall,
{bool_type, function_result, function_def, is_invalid_id.x, is_invalid_id.y, inst_position_id,
vector_size_id, glsl_opcode_id},
inst_it);
} else if (meta.sub_code == glsl::kErrorSubCode_Sanitizer_CoopMatAlignment) {
const bool is_load = meta.target_instruction->Opcode() == spv::OpCooperativeMatrixLoadKHR;
const uint32_t stride_id = is_load ? meta.target_instruction->Word(5) : meta.target_instruction->Word(4);
const uint32_t pointer_id = is_load ? meta.target_instruction->Word(3) : meta.target_instruction->Word(1);
const uint32_t stride_uint_id = CastToUint32(stride_id, block, inst_it);
const Instruction* pointer_inst = block.function_->FindInstruction(pointer_id);
const Type* pointer_type = pointer_inst ? type_manager_.FindTypeById(pointer_inst->TypeId()) : nullptr;
const uint32_t pointer_address_id = GetCoopMatPointerAddress(pointer_type, pointer_id, block, inst_it);
const uint32_t element_index_id = GetCoopMatElementIndex(pointer_inst, block, inst_it);
const uint32_t bool_type = type_manager_.GetTypeBool().Id();
const uint32_t component_size_id = type_manager_.CreateConstantUInt32(meta.component_size).Id();
const uint32_t alignment_id = type_manager_.CreateConstantUInt32(meta.constant_value).Id();
const uint32_t opcode_id = type_manager_.CreateConstantUInt32(meta.target_instruction->Opcode()).Id();
block.CreateInstruction(spv::OpFunctionCall,
{bool_type, function_result, function_def, stride_uint_id, component_size_id, alignment_id,
pointer_address_id, element_index_id, inst_position_id, opcode_id},
inst_it);
} else {
assert(false);
}
module_.need_log_error_ = true;
return function_result;
}
bool SanitizerPass::IsConstantZero(const Constant& constant) const {
if (constant.is_spec_constant_) {
assert(false);
return false;
}
const spv::Op opcode = (spv::Op)constant.inst_.Opcode();
if (opcode == spv::OpConstantNull || opcode == spv::OpConstantFalse) {
return true;
} else if (opcode == spv::OpConstant) {
// This works for signed ints and floats because zero is same for all
if (constant.type_.Is64Bit()) {
return constant.inst_.Word(3) == 0 && constant.inst_.Word(4) == 0;
} else {
return constant.inst_.Word(3) == 0;
}
} else if (opcode == spv::OpConstantComposite) {
const size_t constituent_count = constant.inst_.Length() - 3;
for (uint32_t i = 0; i < constituent_count; i++) {
const Constant* component_constant = type_manager_.FindConstantById(constant.inst_.Operand(i));
if (component_constant && IsConstantZero(*component_constant)) {
return true;
}
}
}
return false;
}
bool SanitizerPass::RequiresInstrumentation(const Function& function, const Instruction& inst, InstructionMeta& meta) {
const spv::Op opcode = (spv::Op)inst.Opcode();
meta.target_instruction = &inst;
if (IsValueIn(opcode, {spv::OpUDiv, spv::OpSDiv, spv::OpUMod, spv::OpSMod, spv::OpSRem, spv::OpFMod, spv::OpFRem})) {
// Note - It is valid to divide by zero for a float (you get NaN), but invalid for an int.
if (const Constant* constant = type_manager_.FindConstantById(inst.Word(4))) {
// If its a constant, no reason to instrument, unless its a constant value of zero,
// then it is only invalid if executed on the GPU.
// (Tried to add to spirv-val, decided it should be a warning at most in VVL)
if (!IsConstantZero(*constant)) {
return false;
}
}
// FMod/FRem are exceptions for float, they have undefined value if zero
if (opcode == spv::OpFMod || opcode == spv::OpFRem) {
meta.skip_safe_mode = true;
}
meta.result_type = type_manager_.FindTypeById(inst.TypeId());
meta.sub_code = glsl::kErrorSubCode_Sanitizer_DivideZero;
return true;
} else if (opcode == spv::OpImageGather) {
// 04664 requires this to be a constant
if (const Constant* constant = type_manager_.FindConstantById(inst.Word(5))) {
const uint32_t constant_value = constant->GetValueUint32();
assert(!constant->is_spec_constant_);
if (constant_value > 3) {
meta.sub_code = glsl::kErrorSubCode_Sanitizer_ImageGather;
meta.skip_safe_mode = true;
meta.constant_value = constant_value;
return true;
}
}
} else if (opcode == spv::OpExtInst && inst.Word(3) == glsl_std450_id_) {
uint32_t glsl_opcode = inst.Word(4);
if (glsl_opcode == GLSLstd450Pow) {
meta.sub_code = glsl::kErrorSubCode_Sanitizer_Pow;
} else if (glsl_opcode == GLSLstd450Atan2) {
meta.sub_code = glsl::kErrorSubCode_Sanitizer_Atan2;
} else if (glsl_opcode == GLSLstd450FMin || glsl_opcode == GLSLstd450FMax) {
meta.sub_code = glsl::kErrorSubCode_Sanitizer_Fminmax;
meta.glsl_opcode = glsl_opcode;
} else {
return false;
}
// all of these only have results that are undefined
meta.skip_safe_mode = true;
meta.result_type = type_manager_.FindTypeById(inst.TypeId());
return true;
} else if (opcode == spv::OpCooperativeMatrixLoadKHR || opcode == spv::OpCooperativeMatrixStoreKHR) {
CooperativeMatrixAccess cma = GetCooperativeMatrixAccess(inst, function);
const uint32_t natural_alignment = (cma.is_row_major ? cma.columns : cma.rows) * cma.component_size;
const uint32_t required_alignment = natural_alignment < 16 ? natural_alignment : 16;
if (required_alignment <= 1) {
return false;
}
meta.sub_code = glsl::kErrorSubCode_Sanitizer_CoopMatAlignment;
meta.constant_value = required_alignment;
meta.component_size = cma.component_size;
meta.skip_safe_mode = true;
return true;
}
return false;
}
bool SanitizerPass::Instrument() {
for (const auto& inst : module_.ext_inst_imports_) {
const char* import_string = inst->GetAsString(2);
if (strcmp(import_string, "GLSL.std.450") == 0) {
glsl_std450_id_ = inst->ResultId();
break;
}
}
// Can safely loop function list as there is no injecting of new Functions until linking time
for (Function& function : module_.functions_) {
if (!function.called_from_target_) {
continue;
}
for (auto block_it = function.blocks_.begin(); block_it != function.blocks_.end(); ++block_it) {
BasicBlock& current_block = **block_it;
cf_.Update(current_block);
if (debug_disable_loops_ && cf_.in_loop) {
continue;
}
if (current_block.IsLoopHeader()) {
continue; // Currently can't properly handle injecting CFG logic into a loop header block
}
auto& block_instructions = current_block.instructions_;
for (auto inst_it = block_instructions.begin(); inst_it != block_instructions.end(); ++inst_it) {
InstructionMeta meta;
// Every instruction is analyzed by the specific pass and lets us know if we need to inject a function or not
if (!RequiresInstrumentation(function, *(inst_it->get()), meta)) {
continue;
}
if (MaxInstrumentationsCountReached()) {
return instrumentations_count_ != 0;
}
instrumentations_count_++;
if (!module_.settings_.safe_mode || meta.skip_safe_mode) {
CreateFunctionCall(current_block, &inst_it, meta);
} else {
InjectConditionalData ic_data = InjectFunctionPre(function, block_it, inst_it);
ic_data.function_result_id = CreateFunctionCall(current_block, nullptr, meta);
InjectFunctionPost(current_block, ic_data);
// Skip the newly added valid and invalid block. Start searching again from newly split merge block
block_it++;
block_it++;
break;
}
}
}
}
return instrumentations_count_ != 0;
}
void SanitizerPass::PrintDebugInfo() const {
std::cout << "SanitizerPass instrumentation count: " << instrumentations_count_ << '\n';
}
} // namespace spirv
} // namespace gpuav