| // Copyright (c) 2020 The Khronos Group Inc. |
| // Copyright (c) 2020 Valve Corporation |
| // Copyright (c) 2020 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 "inst_debug_printf_pass.h" |
| |
| #include "source/spirv_constant.h" |
| #include "source/util/string_utils.h" |
| #include "spirv/unified1/NonSemanticDebugPrintf.h" |
| |
| namespace spvtools { |
| namespace opt { |
| |
| void InstDebugPrintfPass::GenOutputValues(Instruction* val_inst, |
| std::vector<uint32_t>* val_ids, |
| InstructionBuilder* builder) { |
| uint32_t val_ty_id = val_inst->type_id(); |
| analysis::TypeManager* type_mgr = context()->get_type_mgr(); |
| analysis::Type* val_ty = type_mgr->GetType(val_ty_id); |
| switch (val_ty->kind()) { |
| case analysis::Type::kVector: { |
| analysis::Vector* v_ty = val_ty->AsVector(); |
| const analysis::Type* c_ty = v_ty->element_type(); |
| uint32_t c_ty_id = type_mgr->GetId(c_ty); |
| for (uint32_t c = 0; c < v_ty->element_count(); ++c) { |
| Instruction* c_inst = |
| builder->AddCompositeExtract(c_ty_id, val_inst->result_id(), {c}); |
| GenOutputValues(c_inst, val_ids, builder); |
| } |
| return; |
| } |
| case analysis::Type::kBool: { |
| // Select between uint32 zero or one |
| uint32_t zero_id = builder->GetUintConstantId(0); |
| uint32_t one_id = builder->GetUintConstantId(1); |
| Instruction* sel_inst = builder->AddSelect( |
| GetUintId(), val_inst->result_id(), one_id, zero_id); |
| val_ids->push_back(sel_inst->result_id()); |
| return; |
| } |
| case analysis::Type::kFloat: { |
| analysis::Float* f_ty = val_ty->AsFloat(); |
| switch (f_ty->width()) { |
| case 16: { |
| // Convert float16 to float32 and recurse |
| Instruction* f32_inst = builder->AddUnaryOp( |
| GetFloatId(), spv::Op::OpFConvert, val_inst->result_id()); |
| GenOutputValues(f32_inst, val_ids, builder); |
| return; |
| } |
| case 64: { |
| // Bitcast float64 to uint64 and recurse |
| Instruction* ui64_inst = builder->AddUnaryOp( |
| GetUint64Id(), spv::Op::OpBitcast, val_inst->result_id()); |
| GenOutputValues(ui64_inst, val_ids, builder); |
| return; |
| } |
| case 32: { |
| // Bitcase float32 to uint32 |
| Instruction* bc_inst = builder->AddUnaryOp( |
| GetUintId(), spv::Op::OpBitcast, val_inst->result_id()); |
| val_ids->push_back(bc_inst->result_id()); |
| return; |
| } |
| default: |
| assert(false && "unsupported float width"); |
| return; |
| } |
| } |
| case analysis::Type::kInteger: { |
| analysis::Integer* i_ty = val_ty->AsInteger(); |
| switch (i_ty->width()) { |
| case 64: { |
| Instruction* ui64_inst = val_inst; |
| if (i_ty->IsSigned()) { |
| // Bitcast sint64 to uint64 |
| ui64_inst = builder->AddUnaryOp(GetUint64Id(), spv::Op::OpBitcast, |
| val_inst->result_id()); |
| } |
| // Break uint64 into 2x uint32 |
| Instruction* lo_ui64_inst = builder->AddUnaryOp( |
| GetUintId(), spv::Op::OpUConvert, ui64_inst->result_id()); |
| Instruction* rshift_ui64_inst = builder->AddBinaryOp( |
| GetUint64Id(), spv::Op::OpShiftRightLogical, |
| ui64_inst->result_id(), builder->GetUintConstantId(32)); |
| Instruction* hi_ui64_inst = builder->AddUnaryOp( |
| GetUintId(), spv::Op::OpUConvert, rshift_ui64_inst->result_id()); |
| val_ids->push_back(lo_ui64_inst->result_id()); |
| val_ids->push_back(hi_ui64_inst->result_id()); |
| return; |
| } |
| case 8: { |
| Instruction* ui8_inst = val_inst; |
| if (i_ty->IsSigned()) { |
| // Bitcast sint8 to uint8 |
| ui8_inst = builder->AddUnaryOp(GetUint8Id(), spv::Op::OpBitcast, |
| val_inst->result_id()); |
| } |
| // Convert uint8 to uint32 |
| Instruction* ui32_inst = builder->AddUnaryOp( |
| GetUintId(), spv::Op::OpUConvert, ui8_inst->result_id()); |
| val_ids->push_back(ui32_inst->result_id()); |
| return; |
| } |
| case 32: { |
| Instruction* ui32_inst = val_inst; |
| if (i_ty->IsSigned()) { |
| // Bitcast sint32 to uint32 |
| ui32_inst = builder->AddUnaryOp(GetUintId(), spv::Op::OpBitcast, |
| val_inst->result_id()); |
| } |
| // uint32 needs no further processing |
| val_ids->push_back(ui32_inst->result_id()); |
| return; |
| } |
| default: |
| // TODO(greg-lunarg): Support non-32-bit int |
| assert(false && "unsupported int width"); |
| return; |
| } |
| } |
| default: |
| assert(false && "unsupported type"); |
| return; |
| } |
| } |
| |
| void InstDebugPrintfPass::GenOutputCode( |
| Instruction* printf_inst, |
| std::vector<std::unique_ptr<BasicBlock>>* new_blocks) { |
| BasicBlock* back_blk_ptr = &*new_blocks->back(); |
| InstructionBuilder builder( |
| context(), back_blk_ptr, |
| IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping); |
| // Gen debug printf record validation-specific values. The format string |
| // will have its id written. Vectors will need to be broken down into |
| // component values. float16 will need to be converted to float32. Pointer |
| // and uint64 will need to be converted to two uint32 values. float32 will |
| // need to be bitcast to uint32. int32 will need to be bitcast to uint32. |
| std::vector<uint32_t> val_ids; |
| bool is_first_operand = false; |
| printf_inst->ForEachInId( |
| [&is_first_operand, &val_ids, &builder, this](const uint32_t* iid) { |
| // skip set operand |
| if (!is_first_operand) { |
| is_first_operand = true; |
| return; |
| } |
| Instruction* opnd_inst = get_def_use_mgr()->GetDef(*iid); |
| if (opnd_inst->opcode() == spv::Op::OpString) { |
| uint32_t string_id_id = builder.GetUintConstantId(*iid); |
| val_ids.push_back(string_id_id); |
| } else { |
| GenOutputValues(opnd_inst, &val_ids, &builder); |
| } |
| }); |
| GenDebugStreamWrite( |
| builder.GetUintConstantId(shader_id_), |
| builder.GetUintConstantId(uid2offset_[printf_inst->unique_id()]), val_ids, |
| &builder); |
| context()->KillInst(printf_inst); |
| } |
| |
| void InstDebugPrintfPass::GenDebugPrintfCode( |
| BasicBlock::iterator ref_inst_itr, |
| UptrVectorIterator<BasicBlock> ref_block_itr, |
| std::vector<std::unique_ptr<BasicBlock>>* new_blocks) { |
| // If not DebugPrintf OpExtInst, return. |
| Instruction* printf_inst = &*ref_inst_itr; |
| if (printf_inst->opcode() != spv::Op::OpExtInst) return; |
| if (printf_inst->GetSingleWordInOperand(0) != ext_inst_printf_id_) return; |
| if (printf_inst->GetSingleWordInOperand(1) != |
| NonSemanticDebugPrintfDebugPrintf) |
| return; |
| // Initialize DefUse manager before dismantling module |
| (void)get_def_use_mgr(); |
| // Move original block's preceding instructions into first new block |
| std::unique_ptr<BasicBlock> new_blk_ptr; |
| MovePreludeCode(ref_inst_itr, ref_block_itr, &new_blk_ptr); |
| new_blocks->push_back(std::move(new_blk_ptr)); |
| // Generate instructions to output printf args to printf buffer |
| GenOutputCode(printf_inst, new_blocks); |
| // Caller expects at least two blocks with last block containing remaining |
| // code, so end block after instrumentation, create remainder block, and |
| // branch to it |
| uint32_t rem_blk_id = TakeNextId(); |
| std::unique_ptr<Instruction> rem_label(NewLabel(rem_blk_id)); |
| BasicBlock* back_blk_ptr = &*new_blocks->back(); |
| InstructionBuilder builder( |
| context(), back_blk_ptr, |
| IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping); |
| (void)builder.AddBranch(rem_blk_id); |
| // Gen remainder block |
| new_blk_ptr.reset(new BasicBlock(std::move(rem_label))); |
| builder.SetInsertPoint(&*new_blk_ptr); |
| // Move original block's remaining code into remainder block and add |
| // to new blocks |
| MovePostludeCode(ref_block_itr, &*new_blk_ptr); |
| new_blocks->push_back(std::move(new_blk_ptr)); |
| } |
| |
| // Return id for output buffer |
| uint32_t InstDebugPrintfPass::GetOutputBufferId() { |
| if (output_buffer_id_ == 0) { |
| // If not created yet, create one |
| analysis::DecorationManager* deco_mgr = get_decoration_mgr(); |
| analysis::TypeManager* type_mgr = context()->get_type_mgr(); |
| analysis::RuntimeArray* reg_uint_rarr_ty = GetUintRuntimeArrayType(32); |
| analysis::Integer* reg_uint_ty = GetInteger(32, false); |
| analysis::Type* reg_buf_ty = |
| GetStruct({reg_uint_ty, reg_uint_ty, reg_uint_rarr_ty}); |
| uint32_t obufTyId = type_mgr->GetTypeInstruction(reg_buf_ty); |
| // By the Vulkan spec, a pre-existing struct containing a RuntimeArray |
| // must be a block, and will therefore be decorated with Block. Therefore |
| // the undecorated type returned here will not be pre-existing and can |
| // safely be decorated. Since this type is now decorated, it is out of |
| // sync with the TypeManager and therefore the TypeManager must be |
| // invalidated after this pass. |
| assert(context()->get_def_use_mgr()->NumUses(obufTyId) == 0 && |
| "used struct type returned"); |
| deco_mgr->AddDecoration(obufTyId, uint32_t(spv::Decoration::Block)); |
| deco_mgr->AddMemberDecoration(obufTyId, kDebugOutputFlagsOffset, |
| uint32_t(spv::Decoration::Offset), 0); |
| deco_mgr->AddMemberDecoration(obufTyId, kDebugOutputSizeOffset, |
| uint32_t(spv::Decoration::Offset), 4); |
| deco_mgr->AddMemberDecoration(obufTyId, kDebugOutputDataOffset, |
| uint32_t(spv::Decoration::Offset), 8); |
| uint32_t obufTyPtrId_ = |
| type_mgr->FindPointerToType(obufTyId, spv::StorageClass::StorageBuffer); |
| output_buffer_id_ = TakeNextId(); |
| std::unique_ptr<Instruction> newVarOp(new Instruction( |
| context(), spv::Op::OpVariable, obufTyPtrId_, output_buffer_id_, |
| {{spv_operand_type_t::SPV_OPERAND_TYPE_LITERAL_INTEGER, |
| {uint32_t(spv::StorageClass::StorageBuffer)}}})); |
| context()->AddGlobalValue(std::move(newVarOp)); |
| context()->AddDebug2Inst(NewGlobalName(obufTyId, "OutputBuffer")); |
| context()->AddDebug2Inst(NewMemberName(obufTyId, 0, "flags")); |
| context()->AddDebug2Inst(NewMemberName(obufTyId, 1, "written_count")); |
| context()->AddDebug2Inst(NewMemberName(obufTyId, 2, "data")); |
| context()->AddDebug2Inst(NewGlobalName(output_buffer_id_, "output_buffer")); |
| deco_mgr->AddDecorationVal( |
| output_buffer_id_, uint32_t(spv::Decoration::DescriptorSet), desc_set_); |
| deco_mgr->AddDecorationVal(output_buffer_id_, |
| uint32_t(spv::Decoration::Binding), |
| GetOutputBufferBinding()); |
| AddStorageBufferExt(); |
| if (get_module()->version() >= SPV_SPIRV_VERSION_WORD(1, 4)) { |
| // Add the new buffer to all entry points. |
| for (auto& entry : get_module()->entry_points()) { |
| entry.AddOperand({SPV_OPERAND_TYPE_ID, {output_buffer_id_}}); |
| context()->AnalyzeUses(&entry); |
| } |
| } |
| } |
| return output_buffer_id_; |
| } |
| |
| uint32_t InstDebugPrintfPass::GetOutputBufferPtrId() { |
| if (output_buffer_ptr_id_ == 0) { |
| output_buffer_ptr_id_ = context()->get_type_mgr()->FindPointerToType( |
| GetUintId(), spv::StorageClass::StorageBuffer); |
| } |
| return output_buffer_ptr_id_; |
| } |
| |
| uint32_t InstDebugPrintfPass::GetOutputBufferBinding() { |
| return kDebugOutputPrintfStream; |
| } |
| |
| void InstDebugPrintfPass::GenDebugOutputFieldCode(uint32_t base_offset_id, |
| uint32_t field_offset, |
| uint32_t field_value_id, |
| InstructionBuilder* builder) { |
| // Cast value to 32-bit unsigned if necessary |
| uint32_t val_id = GenUintCastCode(field_value_id, builder); |
| // Store value |
| Instruction* data_idx_inst = builder->AddIAdd( |
| GetUintId(), base_offset_id, builder->GetUintConstantId(field_offset)); |
| uint32_t buf_id = GetOutputBufferId(); |
| uint32_t buf_uint_ptr_id = GetOutputBufferPtrId(); |
| Instruction* achain_inst = builder->AddAccessChain( |
| buf_uint_ptr_id, buf_id, |
| {builder->GetUintConstantId(kDebugOutputDataOffset), |
| data_idx_inst->result_id()}); |
| (void)builder->AddStore(achain_inst->result_id(), val_id); |
| } |
| |
| uint32_t InstDebugPrintfPass::GetStreamWriteFunctionId(uint32_t param_cnt) { |
| enum { |
| kShaderId = 0, |
| kInstructionIndex = 1, |
| kFirstParam = 2, |
| }; |
| // Total param count is common params plus validation-specific |
| // params |
| if (param2output_func_id_[param_cnt] == 0) { |
| // Create function |
| param2output_func_id_[param_cnt] = TakeNextId(); |
| analysis::TypeManager* type_mgr = context()->get_type_mgr(); |
| |
| const analysis::Type* uint_type = GetInteger(32, false); |
| |
| std::vector<const analysis::Type*> param_types(kFirstParam + param_cnt, |
| uint_type); |
| std::unique_ptr<Function> output_func = StartFunction( |
| param2output_func_id_[param_cnt], type_mgr->GetVoidType(), param_types); |
| |
| std::vector<uint32_t> param_ids = AddParameters(*output_func, param_types); |
| |
| // Create first block |
| auto new_blk_ptr = MakeUnique<BasicBlock>(NewLabel(TakeNextId())); |
| |
| InstructionBuilder builder( |
| context(), &*new_blk_ptr, |
| IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping); |
| // Gen test if debug output buffer size will not be exceeded. |
| const uint32_t first_param_offset = kInstCommonOutInstructionIdx + 1; |
| const uint32_t obuf_record_sz = first_param_offset + param_cnt; |
| const uint32_t buf_id = GetOutputBufferId(); |
| const uint32_t buf_uint_ptr_id = GetOutputBufferPtrId(); |
| Instruction* obuf_curr_sz_ac_inst = builder.AddAccessChain( |
| buf_uint_ptr_id, buf_id, |
| {builder.GetUintConstantId(kDebugOutputSizeOffset)}); |
| // Fetch the current debug buffer written size atomically, adding the |
| // size of the record to be written. |
| uint32_t obuf_record_sz_id = builder.GetUintConstantId(obuf_record_sz); |
| uint32_t mask_none_id = |
| builder.GetUintConstantId(uint32_t(spv::MemoryAccessMask::MaskNone)); |
| uint32_t scope_invok_id = |
| builder.GetUintConstantId(uint32_t(spv::Scope::Invocation)); |
| Instruction* obuf_curr_sz_inst = builder.AddQuadOp( |
| GetUintId(), spv::Op::OpAtomicIAdd, obuf_curr_sz_ac_inst->result_id(), |
| scope_invok_id, mask_none_id, obuf_record_sz_id); |
| uint32_t obuf_curr_sz_id = obuf_curr_sz_inst->result_id(); |
| // Compute new written size |
| Instruction* obuf_new_sz_inst = |
| builder.AddIAdd(GetUintId(), obuf_curr_sz_id, |
| builder.GetUintConstantId(obuf_record_sz)); |
| // Fetch the data bound |
| Instruction* obuf_bnd_inst = |
| builder.AddIdLiteralOp(GetUintId(), spv::Op::OpArrayLength, |
| GetOutputBufferId(), kDebugOutputDataOffset); |
| // Test that new written size is less than or equal to debug output |
| // data bound |
| Instruction* obuf_safe_inst = builder.AddBinaryOp( |
| GetBoolId(), spv::Op::OpULessThanEqual, obuf_new_sz_inst->result_id(), |
| obuf_bnd_inst->result_id()); |
| uint32_t merge_blk_id = TakeNextId(); |
| uint32_t write_blk_id = TakeNextId(); |
| std::unique_ptr<Instruction> merge_label(NewLabel(merge_blk_id)); |
| std::unique_ptr<Instruction> write_label(NewLabel(write_blk_id)); |
| (void)builder.AddConditionalBranch( |
| obuf_safe_inst->result_id(), write_blk_id, merge_blk_id, merge_blk_id, |
| uint32_t(spv::SelectionControlMask::MaskNone)); |
| // Close safety test block and gen write block |
| output_func->AddBasicBlock(std::move(new_blk_ptr)); |
| new_blk_ptr = MakeUnique<BasicBlock>(std::move(write_label)); |
| builder.SetInsertPoint(&*new_blk_ptr); |
| // Generate common and stage-specific debug record members |
| GenDebugOutputFieldCode(obuf_curr_sz_id, kInstCommonOutSize, |
| builder.GetUintConstantId(obuf_record_sz), |
| &builder); |
| // Store Shader Id |
| GenDebugOutputFieldCode(obuf_curr_sz_id, kInstCommonOutShaderId, |
| param_ids[kShaderId], &builder); |
| // Store Instruction Idx |
| GenDebugOutputFieldCode(obuf_curr_sz_id, kInstCommonOutInstructionIdx, |
| param_ids[kInstructionIndex], &builder); |
| // Gen writes of validation specific data |
| for (uint32_t i = 0; i < param_cnt; ++i) { |
| GenDebugOutputFieldCode(obuf_curr_sz_id, first_param_offset + i, |
| param_ids[kFirstParam + i], &builder); |
| } |
| // Close write block and gen merge block |
| (void)builder.AddBranch(merge_blk_id); |
| output_func->AddBasicBlock(std::move(new_blk_ptr)); |
| new_blk_ptr = MakeUnique<BasicBlock>(std::move(merge_label)); |
| builder.SetInsertPoint(&*new_blk_ptr); |
| // Close merge block and function and add function to module |
| (void)builder.AddNullaryOp(0, spv::Op::OpReturn); |
| |
| output_func->AddBasicBlock(std::move(new_blk_ptr)); |
| output_func->SetFunctionEnd(EndFunction()); |
| context()->AddFunction(std::move(output_func)); |
| |
| std::string name("stream_write_"); |
| name += std::to_string(param_cnt); |
| |
| context()->AddDebug2Inst( |
| NewGlobalName(param2output_func_id_[param_cnt], name)); |
| } |
| return param2output_func_id_[param_cnt]; |
| } |
| |
| void InstDebugPrintfPass::GenDebugStreamWrite( |
| uint32_t shader_id, uint32_t instruction_idx_id, |
| const std::vector<uint32_t>& validation_ids, InstructionBuilder* builder) { |
| // Call debug output function. Pass func_idx, instruction_idx and |
| // validation ids as args. |
| uint32_t val_id_cnt = static_cast<uint32_t>(validation_ids.size()); |
| std::vector<uint32_t> args = {shader_id, instruction_idx_id}; |
| (void)args.insert(args.end(), validation_ids.begin(), validation_ids.end()); |
| (void)builder->AddFunctionCall(GetVoidId(), |
| GetStreamWriteFunctionId(val_id_cnt), args); |
| } |
| |
| std::unique_ptr<Instruction> InstDebugPrintfPass::NewGlobalName( |
| uint32_t id, const std::string& name_str) { |
| std::string prefixed_name{"inst_printf_"}; |
| prefixed_name += name_str; |
| return NewName(id, prefixed_name); |
| } |
| |
| std::unique_ptr<Instruction> InstDebugPrintfPass::NewMemberName( |
| uint32_t id, uint32_t member_index, const std::string& name_str) { |
| return MakeUnique<Instruction>( |
| context(), spv::Op::OpMemberName, 0, 0, |
| std::initializer_list<Operand>{ |
| {SPV_OPERAND_TYPE_ID, {id}}, |
| {SPV_OPERAND_TYPE_LITERAL_INTEGER, {member_index}}, |
| {SPV_OPERAND_TYPE_LITERAL_STRING, utils::MakeVector(name_str)}}); |
| } |
| |
| void InstDebugPrintfPass::InitializeInstDebugPrintf() { |
| // Initialize base class |
| InitializeInstrument(); |
| output_buffer_id_ = 0; |
| output_buffer_ptr_id_ = 0; |
| } |
| |
| Pass::Status InstDebugPrintfPass::ProcessImpl() { |
| // Perform printf instrumentation on each entry point function in module |
| InstProcessFunction pfn = |
| [this](BasicBlock::iterator ref_inst_itr, |
| UptrVectorIterator<BasicBlock> ref_block_itr, |
| [[maybe_unused]] uint32_t stage_idx, |
| std::vector<std::unique_ptr<BasicBlock>>* new_blocks) { |
| return GenDebugPrintfCode(ref_inst_itr, ref_block_itr, new_blocks); |
| }; |
| (void)InstProcessEntryPointCallTree(pfn); |
| // Remove DebugPrintf OpExtInstImport instruction |
| Instruction* ext_inst_import_inst = |
| get_def_use_mgr()->GetDef(ext_inst_printf_id_); |
| context()->KillInst(ext_inst_import_inst); |
| // If no remaining non-semantic instruction sets, remove non-semantic debug |
| // info extension from module and feature manager |
| bool non_sem_set_seen = false; |
| for (auto c_itr = context()->module()->ext_inst_import_begin(); |
| c_itr != context()->module()->ext_inst_import_end(); ++c_itr) { |
| const std::string set_name = c_itr->GetInOperand(0).AsString(); |
| if (spvtools::utils::starts_with(set_name, "NonSemantic.")) { |
| non_sem_set_seen = true; |
| break; |
| } |
| } |
| if (!non_sem_set_seen) { |
| context()->RemoveExtension(kSPV_KHR_non_semantic_info); |
| } |
| return Status::SuccessWithChange; |
| } |
| |
| Pass::Status InstDebugPrintfPass::Process() { |
| ext_inst_printf_id_ = |
| get_module()->GetExtInstImportId("NonSemantic.DebugPrintf"); |
| if (ext_inst_printf_id_ == 0) return Status::SuccessWithoutChange; |
| InitializeInstDebugPrintf(); |
| return ProcessImpl(); |
| } |
| |
| } // namespace opt |
| } // namespace spvtools |