// Copyright 2019 The SwiftShader Authors. All Rights Reserved. // // 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 "SpirvShader.hpp" #include "System/Types.hpp" // If enabled, each instruction will be printed before defining. #define PRINT_EACH_DEFINED_DBG_INSTRUCTION 0 // If enabled, each instruction will be printed before emitting. #define PRINT_EACH_EMITTED_INSTRUCTION 0 // If enabled, each instruction will be printed before executing. #define PRINT_EACH_EXECUTED_INSTRUCTION 0 // If enabled, debugger variables will contain debug information (addresses, // byte offset, etc). #define DEBUG_ANNOTATE_VARIABLE_KEYS 0 #ifdef ENABLE_VK_DEBUGGER # include "Vulkan/Debug/Context.hpp" # include "Vulkan/Debug/File.hpp" # include "Vulkan/Debug/Thread.hpp" # include "Vulkan/Debug/Variable.hpp" # include "Vulkan/Debug/EventListener.hpp" # include "spirv/unified1/OpenCLDebugInfo100.h" # include "spirv-tools/libspirv.h" # include # include //////////////////////////////////////////////////////////////////////////////// // namespace sw::SIMD // Adds sw::SIMD::PerLane<> and typedefs for C++ versions of the Reactor SIMD // types (sw::SIMD::Int, etc) //////////////////////////////////////////////////////////////////////////////// namespace sw { namespace SIMD { // PerLane is a SIMD vector that holds N vectors of width SIMD::Width. // PerLane operator[] returns the elements of a single lane (a transpose of the // storage arrays). template struct PerLane { sw::vec operator[](int lane) const { sw::vec out; for(int i = 0; i < N; i++) { out[i] = elements[i][lane]; } return out; } std::array, N> elements; }; template struct PerLane { const T &operator[](int lane) const { return data[lane]; } std::array data; }; using uint_t = PerLane; using uint2 = PerLane; using uint3 = PerLane; using uint4 = PerLane; using int_t = PerLane; using int2 = PerLane; using int3 = PerLane; using int4 = PerLane; using float_t = PerLane; using vec2 = PerLane; using vec3 = PerLane; using vec4 = PerLane; } // namespace SIMD } // namespace sw //////////////////////////////////////////////////////////////////////////////// // namespace ::(anonymous) // Utility functions //////////////////////////////////////////////////////////////////////////////// namespace { // vecElementName() returns the element name for the i'th vector element of // size n. // Vectors of size 4 or less use a [x,y,z,w] element naming scheme. // Larger vectors use a number index naming scheme. std::string vecElementName(int i, int n) { return (n > 4) ? std::to_string(i) : &"x\0y\0z\0w\0"[i * 2]; } // laneName() returns a string describing values for the lane i. std::string laneName(int i) { return "Lane " + std::to_string(i); } // isEntryBreakpointForShaderType() returns true if name is equal to the // special entry breakpoint name for the given shader type. // This allows the IDE to request all shaders of the given type to break on // entry. bool isEntryBreakpointForShaderType(spv::ExecutionModel type, const std::string &name) { switch(type) { case spv::ExecutionModelGLCompute: return name == "ComputeShader"; case spv::ExecutionModelFragment: return name == "FragmentShader"; case spv::ExecutionModelVertex: return name == "VertexShader"; default: return false; } } // makeDbgValue() returns a vk::dbg::Value that contains a copy of val. template std::shared_ptr makeDbgValue(const T &val) { return vk::dbg::make_constant(val); } // makeDbgValue() returns a vk::dbg::Value that contains a copy of vec. template std::shared_ptr makeDbgValue(const sw::vec &vec) { return vk::dbg::Struct::create("vec" + std::to_string(N), [&](auto &vc) { for(int i = 0; i < N; i++) { vc->put(vecElementName(i, N), makeDbgValue(vec[i])); } }); } // NullptrValue is an implementation of vk::dbg::Value that simply displays // "" for the given type. class NullptrValue : public vk::dbg::Value { public: NullptrValue(const std::string &ty) : ty(ty) {} std::string type() override { return ty; } std::string get(const vk::dbg::FormatFlags &) { return ""; } private: std::string ty; }; // store() emits a store instruction to copy val into ptr. template void store(const rr::RValue> &ptr, const rr::RValue &val) { *rr::Pointer(ptr) = val; } // store() emits a store instruction to copy val into ptr. template void store(const rr::RValue> &ptr, const T &val) { *rr::Pointer(ptr) = val; } // clang-format off template struct ReactorTypeSize {}; template<> struct ReactorTypeSize { static constexpr const int value = 4; }; template<> struct ReactorTypeSize { static constexpr const int value = 4; }; template<> struct ReactorTypeSize { static constexpr const int value = 16; }; template<> struct ReactorTypeSize { static constexpr const int value = 16; }; // clang-format on // store() emits a store instruction to copy val into ptr. template void store(const rr::RValue> &ptr, const std::array &val) { for(std::size_t i = 0; i < N; i++) { store(ptr + i * ReactorTypeSize::value, val[i]); } } // ArgTy::type resolves to the single argument type of the function F. template struct ArgTy { using type = typename ArgTy::type; }; // ArgTy::type resolves to the single argument type of the template method. template struct ArgTy { using type = typename std::decay::type; }; // ArgTyT resolves to the single argument type of the template function or // method F. template using ArgTyT = typename ArgTy::type; // getOrCreate() searchs the map for the given key. If the map contains an entry // with the given key, then the value is returned. Otherwise, create() is called // and the returned value is placed into the map with the given key, and this // value is returned. // create is a function with the signature: // V() template V getOrCreate(std::unordered_map &map, const K &key, CREATE &&create) { auto it = map.find(key); if(it != map.end()) { return it->second; } auto val = create(); map.emplace(key, val); return val; } // HoversFromLocals is an implementation of vk::dbg::Variables that is used to // provide a scope's 'hover' variables - those that appear when you place the // cursor over a variable in an IDE. // Unlike the top-level SIMD lane grouping of variables in Frame::locals, // Frame::hovers displays each variable as a value per SIMD lane. // Instead maintaining another collection of variables per scope, // HoversFromLocals dynamically builds the hover information from the locals. class HoversFromLocals : public vk::dbg::Variables { public: HoversFromLocals(const std::shared_ptr &locals) : locals(locals) {} void foreach(size_t startIndex, size_t count, const ForeachCallback &cb) override { // No op - hovers are only searched, never iterated. } std::shared_ptr get(const std::string &name) override { // Is the hover variable a SIMD-common variable? If so, just return // that. if(auto val = locals->get(name)) { return val; } // Search each of the lanes for the named variable. // Collect them all up, and return that in a new Struct value. bool found = false; auto str = vk::dbg::Struct::create("", [&](auto &vc) { for(int lane = 0; lane < sw::SIMD::Width; lane++) { auto laneN = laneName(lane); if(auto laneV = locals->get(laneN)) { if(auto children = laneV->children()) { if(auto val = children->get(name)) { vc->put(laneN, val); found = true; } } } } }); if(found) { // The value returned will be returned to the debug client by // identifier. As the value is a Struct, the server will include // a handle to the Variables, which needs to be kept alive so the // client can send a request for its members. // lastFind keeps any nested Variables alive long enough for them to // be requested. lastFind = str; return str; } return nullptr; } private: std::shared_ptr locals; std::shared_ptr lastFind; }; } // anonymous namespace namespace spvtools { // Function implemented in third_party/SPIRV-Tools/source/disassemble.cpp // but with no public header. // This is a C++ function, so the name is mangled, and signature changes will // result in a linker error instead of runtime signature mismatches. extern std::string spvInstructionBinaryToText(const spv_target_env env, const uint32_t *inst_binary, const size_t inst_word_count, const uint32_t *binary, const size_t word_count, const uint32_t options); } // namespace spvtools //////////////////////////////////////////////////////////////////////////////// // namespace ::(anonymous)::debug // OpenCL.Debug.100 data structures //////////////////////////////////////////////////////////////////////////////// namespace { namespace debug { struct Declare; struct LocalVariable; struct Member; struct Value; // Object is the common base class for all the OpenCL.Debug.100 data structures. struct Object { enum class Kind { Object, Declare, Expression, Function, InlinedAt, GlobalVariable, LocalVariable, Member, Operation, Source, SourceScope, Value, TemplateParameter, // Scopes CompilationUnit, LexicalBlock, // Types BasicType, ArrayType, VectorType, FunctionType, CompositeType, TemplateType, }; using ID = sw::SpirvID; static constexpr auto KIND = Kind::Object; inline Object(Kind kind) : kind(kind) { (void)KIND; // Used in debug builds. Avoid unused variable warnings in NDEBUG builds. } const Kind kind; // kindof() returns true iff kind is of this type, or any type deriving from // this type. static constexpr bool kindof(Object::Kind kind) { return true; } virtual ~Object() = default; }; // cstr() returns the c-string name of the given Object::Kind. constexpr const char *cstr(Object::Kind k) { switch(k) { case Object::Kind::Object: return "Object"; case Object::Kind::Declare: return "Declare"; case Object::Kind::Expression: return "Expression"; case Object::Kind::Function: return "Function"; case Object::Kind::InlinedAt: return "InlinedAt"; case Object::Kind::GlobalVariable: return "GlobalVariable"; case Object::Kind::LocalVariable: return "LocalVariable"; case Object::Kind::Member: return "Member"; case Object::Kind::Operation: return "Operation"; case Object::Kind::Source: return "Source"; case Object::Kind::SourceScope: return "SourceScope"; case Object::Kind::Value: return "Value"; case Object::Kind::TemplateParameter: return "TemplateParameter"; case Object::Kind::CompilationUnit: return "CompilationUnit"; case Object::Kind::LexicalBlock: return "LexicalBlock"; case Object::Kind::BasicType: return "BasicType"; case Object::Kind::ArrayType: return "ArrayType"; case Object::Kind::VectorType: return "VectorType"; case Object::Kind::FunctionType: return "FunctionType"; case Object::Kind::CompositeType: return "CompositeType"; case Object::Kind::TemplateType: return "TemplateType"; } return ""; } // ObjectImpl is a helper template struct which simplifies deriving from Object. // ObjectImpl passes down the KIND to the Object constructor, and implements // kindof(). template struct ObjectImpl : public BASE { using ID = sw::SpirvID; static constexpr auto Kind = KIND; ObjectImpl() : BASE(Kind) {} static_assert(BASE::kindof(KIND), "BASE::kindof() returned false"); // kindof() returns true iff kind is of this type, or any type deriving from // this type. static constexpr bool kindof(Object::Kind kind) { return kind == Kind; } }; // cast() casts the debug type pointer obj to TO. // If obj is null or not of the type TO, then nullptr is returned. template TO *cast(FROM *obj) { if(obj == nullptr) { return nullptr; } // None return (TO::kindof(obj->kind)) ? static_cast(obj) : nullptr; } // cast() casts the debug type pointer obj to TO. // If obj is null or not of the type TO, then nullptr is returned. template const TO *cast(const FROM *obj) { if(obj == nullptr) { return nullptr; } // None return (TO::kindof(obj->kind)) ? static_cast(obj) : nullptr; } // Scope is the base class for all OpenCL.DebugInfo.100 scope objects. struct Scope : public Object { using ID = sw::SpirvID; inline Scope(Kind kind) : Object(kind) {} // kindof() returns true iff kind is of this type, or any type deriving from // this type. static constexpr bool kindof(Kind kind) { return kind == Kind::CompilationUnit || kind == Kind::Function || kind == Kind::LexicalBlock; } struct Source *source = nullptr; Scope *parent = nullptr; }; // Type is the base class for all OpenCL.DebugInfo.100 type objects. struct Type : public Object { using ID = sw::SpirvID; struct Member { Type *type; std::string name; }; inline Type(Kind kind) : Object(kind) {} // kindof() returns true iff kind is of this type, or any type deriving from // this type. static constexpr bool kindof(Kind kind) { return kind == Kind::BasicType || kind == Kind::ArrayType || kind == Kind::VectorType || kind == Kind::FunctionType || kind == Kind::CompositeType || kind == Kind::TemplateType; } // name() returns the type name. virtual std::string name() const = 0; // sizeInBytes() returns the number of bytes of the given debug type. virtual uint32_t sizeInBytes() const = 0; // value() returns a shared pointer to a vk::dbg::Value that views the data // at ptr of this type. virtual std::shared_ptr value(void *ptr, bool interleaved) const = 0; // numMembers() returns the number of members for the given type. virtual size_t numMembers() const = 0; // getMember() returns the member by index. virtual Member getMember(size_t) const = 0; // undefined() returns a shared pointer to a vk::dbg::Value that represents // an undefined value of this type. std::shared_ptr undefined() const { struct Undef : public vk::dbg::Value { Undef(const std::string &ty) : ty(ty) {} const std::string ty; std::string type() override { return ty; } std::string get(const vk::dbg::FormatFlags &) override { return ""; } }; return std::make_shared(name()); } }; // CompilationUnit represents the OpenCL.DebugInfo.100 DebugCompilationUnit // instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugCompilationUnit struct CompilationUnit : ObjectImpl { }; // Source represents the OpenCL.DebugInfo.100 DebugSource instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugSource struct Source : ObjectImpl { spv::SourceLanguage language; uint32_t version = 0; std::string file; std::string source; std::shared_ptr dbgFile; }; // BasicType represents the OpenCL.DebugInfo.100 DebugBasicType instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugBasicType struct BasicType : ObjectImpl { std::string name_; uint32_t size = 0; // in bits. OpenCLDebugInfo100DebugBaseTypeAttributeEncoding encoding = OpenCLDebugInfo100Unspecified; std::string name() const override { return name_; } uint32_t sizeInBytes() const override { return size / 8; } size_t numMembers() const override { return 0; } Member getMember(size_t) const override { return {}; } std::shared_ptr value(void *ptr, bool interleaved) const override { if(ptr == nullptr) { return std::make_shared(name()); } switch(encoding) { case OpenCLDebugInfo100Address: // return vk::dbg::make_reference(*static_cast(ptr)); UNIMPLEMENTED("b/148401179 OpenCLDebugInfo100 OpenCLDebugInfo100Address BasicType"); return nullptr; case OpenCLDebugInfo100Boolean: return vk::dbg::make_reference(*static_cast(ptr)); case OpenCLDebugInfo100Float: return vk::dbg::make_reference(*static_cast(ptr)); case OpenCLDebugInfo100Signed: return vk::dbg::make_reference(*static_cast(ptr)); case OpenCLDebugInfo100SignedChar: return vk::dbg::make_reference(*static_cast(ptr)); case OpenCLDebugInfo100Unsigned: return vk::dbg::make_reference(*static_cast(ptr)); case OpenCLDebugInfo100UnsignedChar: return vk::dbg::make_reference(*static_cast(ptr)); default: UNIMPLEMENTED("b/148401179 OpenCLDebugInfo100 encoding %d", int(encoding)); return nullptr; } } }; // ArrayType represents the OpenCL.DebugInfo.100 DebugTypeArray instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugTypeArray // // Unlike OpenCL.DebugInfo.100's DebugTypeArray, ArrayType is always // single-dimensional. Multi-dimensional arrays are transformed into multiple // nested ArrayTypes. This is done to simplify logic. struct ArrayType : ObjectImpl { Type *base = nullptr; bool ownsBase = false; // If true, base is owned by this ArrayType. uint32_t size; // In elements ~ArrayType() { if(ownsBase) { delete base; } } std::string name() const override { return base->name() + "[]"; } uint32_t sizeInBytes() const override { return base->sizeInBytes() * size; } size_t numMembers() const override { return size; } Member getMember(size_t i) const override { return { base, std::to_string(i) }; } std::shared_ptr value(void *ptr, bool interleaved) const override { if(ptr == nullptr) { return std::make_shared(name()); } auto members = std::make_shared(); auto addr = static_cast(ptr); for(size_t i = 0; i < size; i++) { auto member = getMember(i); # if DEBUG_ANNOTATE_VARIABLE_KEYS key += " (" + std::to_string(addr) + " +" + std::to_string(offset) + ", i: " + std::to_string(i) + ")" + (interleaved ? "I" : "F"); # endif members->put(member.name, base->value(addr, interleaved)); addr += base->sizeInBytes() * (interleaved ? sw::SIMD::Width : 1); } return std::make_shared(name(), members); } }; // VectorType represents the OpenCL.DebugInfo.100 DebugTypeVector instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugTypeVector struct VectorType : ObjectImpl { Type *base = nullptr; uint32_t components = 0; std::string name() const override { return "vec" + std::to_string(components) + "<" + base->name() + ">"; } uint32_t sizeInBytes() const override { return base->sizeInBytes() * components; } size_t numMembers() const override { return components; } Member getMember(size_t i) const override { return { base, vecElementName(i, components) }; } std::shared_ptr value(void *ptr, bool interleaved) const override { if(ptr == nullptr) { return std::make_shared(name()); } const auto elSize = base->sizeInBytes(); auto members = std::make_shared(); for(uint32_t i = 0; i < components; i++) { auto offset = elSize * i * (interleaved ? sw::SIMD::Width : 1); auto elPtr = static_cast(ptr) + offset; # if DEBUG_ANNOTATE_VARIABLE_KEYS elKey += " (" + std::to_string(elPtr) + " +" + std::to_string(offset) + ")" + (interleaved ? "I" : "F"); # endif members->put(getMember(i).name, base->value(elPtr, interleaved)); } return std::make_shared(name(), members); } }; // FunctionType represents the OpenCL.DebugInfo.100 DebugTypeFunction // instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugTypeFunction struct FunctionType : ObjectImpl { uint32_t flags = 0; // OR'd from OpenCLDebugInfo100DebugInfoFlags Type *returnTy = nullptr; std::vector paramTys; std::string name() const override { return "function"; } uint32_t sizeInBytes() const override { return 0; } size_t numMembers() const override { return 0; } Member getMember(size_t i) const override { return {}; } std::shared_ptr value(void *ptr, bool interleaved) const override { return nullptr; } }; // Member represents the OpenCL.DebugInfo.100 DebugTypeMember instruction. // Despite the instruction name, this is not a type - rather a member of a type. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugTypeMember struct Member : ObjectImpl { std::string name; Type *type = nullptr; Source *source = nullptr; uint32_t line = 0; uint32_t column = 0; struct CompositeType *parent = nullptr; uint32_t offset = 0; // in bits uint32_t size = 0; // in bits uint32_t flags = 0; // OR'd from OpenCLDebugInfo100DebugInfoFlags }; // CompositeType represents the OpenCL.DebugInfo.100 DebugTypeComposite // instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugTypeComposite struct CompositeType : ObjectImpl { std::string name_; OpenCLDebugInfo100DebugCompositeType tag = OpenCLDebugInfo100Class; Source *source = nullptr; uint32_t line = 0; uint32_t column = 0; Object *parent = nullptr; std::string linkage; uint32_t size = 0; // in bits. uint32_t flags = 0; // OR'd from OpenCLDebugInfo100DebugInfoFlags std::vector members_; std::string name() const override { return name_; } uint32_t sizeInBytes() const override { return size / 8; } size_t numMembers() const override { return members_.size(); } Member getMember(size_t i) const override { return { members_[i]->type, members_[i]->name }; } std::shared_ptr value(void *ptr, bool interleaved) const override { auto fields = std::make_shared(); for(auto &member : members_) { auto offset = (member->offset / 8) * (interleaved ? sw::SIMD::Width : 1); auto elPtr = static_cast(ptr) + offset; auto elKey = member->name; # if DEBUG_ANNOTATE_VARIABLE_KEYS // elKey += " (" + std::to_string(elPtr) + " +" + std::to_string(offset) + ")" + (interleaved ? "I" : "F"); # endif fields->put(elKey, member->type->value(elPtr, interleaved)); } return std::make_shared(name_, fields); } }; // TemplateParameter represents the OpenCL.DebugInfo.100 // DebugTypeTemplateParameter instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugTypeTemplateParameter struct TemplateParameter : ObjectImpl { std::string name; Type *type = nullptr; uint32_t value = 0; Source *source = nullptr; uint32_t line = 0; uint32_t column = 0; }; // TemplateType represents the OpenCL.DebugInfo.100 DebugTypeTemplate // instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugTypeTemplate struct TemplateType : ObjectImpl { Type *target = nullptr; // Class, struct or function. std::vector parameters; std::string name() const override { return "template<>"; } uint32_t sizeInBytes() const override { return target->sizeInBytes(); } size_t numMembers() const override { return 0; } Member getMember(size_t i) const override { return {}; } std::shared_ptr value(void *ptr, bool interleaved) const override { return target->value(ptr, interleaved); } }; // LexicalBlock represents the OpenCL.DebugInfo.100 DebugLexicalBlock instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugLexicalBlock struct LexicalBlock : Scope { using ID = sw::SpirvID; static constexpr auto Kind = Object::Kind::LexicalBlock; inline LexicalBlock(Object::Kind kind = Kind) : Scope(kind) {} uint32_t line = 0; uint32_t column = 0; std::string name; std::vector variables; static constexpr bool kindof(Object::Kind kind) { return kind == Kind || kind == Object::Kind::Function; } }; // Function represents the OpenCL.DebugInfo.100 DebugFunction instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugFunction struct Function : ObjectImpl { std::string name; FunctionType *type = nullptr; uint32_t declLine = 0; uint32_t declColumn = 0; std::string linkage; uint32_t flags = 0; // OR'd from OpenCLDebugInfo100DebugInfoFlags sw::SpirvShader::Function::ID function; }; // InlinedAt represents the OpenCL.DebugInfo.100 DebugInlinedAt instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugInlinedAt struct InlinedAt : ObjectImpl { uint32_t line = 0; Scope *scope = nullptr; InlinedAt *inlined = nullptr; }; // SourceScope represents the OpenCL.DebugInfo.100 DebugScope instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugScope struct SourceScope : ObjectImpl { Scope *scope = nullptr; InlinedAt *inlinedAt = nullptr; }; // GlobalVariable represents the OpenCL.DebugInfo.100 DebugGlobalVariable instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugGlobalVariable struct GlobalVariable : ObjectImpl { std::string name; Type *type = nullptr; Source *source = nullptr; uint32_t line = 0; uint32_t column = 0; Scope *parent = nullptr; std::string linkage; sw::SpirvShader::Object::ID variable; uint32_t flags = 0; // OR'd from OpenCLDebugInfo100DebugInfoFlags }; // LocalVariable represents the OpenCL.DebugInfo.100 DebugLocalVariable // instruction. // Local variables are essentially just a scoped variable name. // Their value comes from either a DebugDeclare (which has an immutable pointer // to the actual data), or from a number of DebugValues (which can change // any nested members of the variable over time). // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugLocalVariable struct LocalVariable : ObjectImpl { static constexpr uint32_t NoArg = ~uint32_t(0); enum class Definition { Undefined, // Variable has no defined value Declaration, // Variable value comes from definition Values // Variable value comes from values }; std::string name; Type *type = nullptr; Source *source = nullptr; uint32_t line = 0; uint32_t column = 0; Scope *parent = nullptr; uint32_t arg = NoArg; Definition definition = Definition::Undefined; Declare *declaration = nullptr; // Used if definition == Definition::Declaration // ValueNode is a tree node of debug::Value definitions. // Each node in the tree represents an element in the type tree. struct ValueNode { // NoDebugValueIndex indicates that this node is never assigned a value. static constexpr const uint32_t NoDebugValueIndex = ~0u; uint32_t debugValueIndex = NoDebugValueIndex; // Index into State::lastReachedDebugValues std::unordered_map> children; }; ValueNode values; // Used if definition == Definition::Values }; // Operation represents the OpenCL.DebugInfo.100 DebugOperation instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugOperation struct Operation : ObjectImpl { uint32_t opcode = 0; std::vector operands; }; // Expression represents the OpenCL.DebugInfo.100 DebugExpression instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugExpression struct Expression : ObjectImpl { std::vector operations; }; // Declare represents the OpenCL.DebugInfo.100 DebugDeclare instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugDeclare struct Declare : ObjectImpl { LocalVariable *local = nullptr; sw::SpirvShader::Object::ID variable; Expression *expression = nullptr; }; // Value represents the OpenCL.DebugInfo.100 DebugValue instruction. // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugValue struct Value : ObjectImpl { LocalVariable *local = nullptr; sw::SpirvShader::Object::ID value; Expression *expression = nullptr; std::vector indexes; }; // find() searches the nested scopes, returning for the first scope that is // castable to type T. If no scope can be found of type T, then nullptr is // returned. template T *find(Scope *scope) { if(auto out = cast(scope)) { return out; } return scope->parent ? find(scope->parent) : nullptr; } inline const char *tostring(LocalVariable::Definition def) { switch(def) { case LocalVariable::Definition::Undefined: return "Undefined"; case LocalVariable::Definition::Declaration: return "Declaration"; case LocalVariable::Definition::Values: return "Values"; default: return ""; } } } // namespace debug } // anonymous namespace //////////////////////////////////////////////////////////////////////////////// // namespace ::sw // // Implementations for: // sw::SpirvShader::Impl::Debugger // sw::SpirvShader::Impl::Debugger::LocalVariableValue // sw::SpirvShader::Impl::Debugger::State // sw::SpirvShader::Impl::Debugger::State::Data //////////////////////////////////////////////////////////////////////////////// namespace sw { //////////////////////////////////////////////////////////////////////////////// // sw::SpirvShader::Impl::Debugger // // SpirvShader-private struct holding compile-time-mutable and // execution-time-immutable debugger information. // // There is an instance of this class per shader program. //////////////////////////////////////////////////////////////////////////////// struct SpirvShader::Impl::Debugger : public vk::dbg::ClientEventListener { class State; class LocalVariableValue; Debugger(const SpirvShader *shader, const std::shared_ptr &ctx); ~Debugger(); enum class Pass { Define, // Pre-pass (called from SpirvShader constructor) Emit // Code generation pass (called from SpirvShader::emit()). }; // process() is called for each debugger instruction in two compiler passes. // For the Define pass, process() constructs ::debug objects and // registers them in the objects map. // For the Emit pass, process() populates the fields of ::debug objects and // potentially emits instructions for the shader program. void process(const InsnIterator &insn, EmitState *state, Pass pass); // finalize() must be called after all shader instruction have been emitted. // finalize() allocates the trap memory and registers the Debugger for // client debugger events so that it can monitor for changes in breakpoints. void finalize(); // setNextSetLocationIsSteppable() indicates that the next call to // setLocation() must be a debugger steppable line. void setNextSetLocationIsSteppable(); // setScope() sets the current debug source scope. Used by setLocation() // when the next location is debugger steppable. void setScope(debug::SourceScope *); // setLocation() sets the current codegen source location to the given file // and line. void setLocation(EmitState *state, const std::shared_ptr &, int line); void setLocation(EmitState *state, const char *file, int line); using SpirvInstruction = const void *; const SpirvShader *const shader; // The shader program being debugged std::shared_ptr const ctx; // The debugger context bool shaderHasDebugInfo; // True if the shader has high-level debug info (OpenCL.Debug100 instructions) std::shared_ptr spirvFile; // Virtual file containing SPIR-V disassembly instructions std::unordered_map spirvLineMappings; // Instruction pointer to line std::unordered_map results; // Instruction pointer to result ID // LocationAndScope holds a source location and scope pair. struct LocationAndScope { vk::dbg::Location location; debug::SourceScope *scope; inline bool operator==(const LocationAndScope &other) const { return location == other.location && scope == other.scope; } struct Hash { uint64_t operator()(const LocationAndScope &l) const { return std::hash()(l.location) ^ std::hash()(l.scope); } }; }; // Traps holds information about debugger traps - points in the shader // program where execution may pause for the debugger, either due to hitting // a breakpoint or following a single line step. // The Traps::memory is continually read during execution of a shader, // triggering a trap when the byte is non-zero. Traps can also be enabled // via the State::alwaysTrap field. struct Traps { // Source location + scope -> line trap index std::unordered_map byLocationAndScope; // Function name -> entry trap index std::unordered_map byFunctionName; // Trap index -> source location + scope std::vector byIndex; // Trap memory - shared for all running instances of the shader. // Each byte represents a single trap enabled (1) / disabled (0) state. std::unique_ptr memory; } traps; // Shadow memory is used to construct a contiguous memory block // (State::shadow) that contains an up-to-date copy of each // SpirvShader::Object's value(s) in the currently executing shader. // Shadow memory either contains SIMD-interleaved values for all components // in the object, or a SIMD-pointer (Shadow::Pointer). struct Shadow { // Entry describes the byte offset and kind of the shadow memory for // a single SpirvShader::Object. struct Entry { enum class Kind { Value, Pointer, }; Kind kind; uint32_t offset; }; // Pointer is the structure stored in shadow memory for pointer types. // The address for a given SIMD lane is the base + offsets[lane]. struct Pointer { uint8_t *base; // Common base address for all SIMD lanes. uint32_t offsets[sw::SIMD::Width]; // Per lane offsets. }; // Memory is returned by get(). // Memory holds a pointer (addr) to the entry in the shadow memory, and // provides the dref() method for dereferencing a pointer for the given // SIMD lane. struct Memory { inline operator void *(); inline Memory dref(int lane) const; uint8_t *addr; }; // create() adds a new entry for the object with the given id. void create(const SpirvShader *, const EmitState *, Object::ID); // get() returns a Memory pointing to the shadow memory for the object // with the given id. Memory get(const State *, Object::ID) const; std::unordered_map entries; uint32_t size = 0; // Total size of the shadow memory in bytes. } shadow; // vk::dbg::ClientEventListener void onSetBreakpoint(const vk::dbg::Location &location, bool &handled) override; void onSetBreakpoint(const std::string &func, bool &handled) override; void onBreakpointsChanged() override; private: // add() registers the debug object with the given id. template void add(ID id, std::unique_ptr &&); // addNone() registers given id as a None value or type. void addNone(debug::Object::ID id); // isNone() returns true if the given id was registered as none with // addNone(). bool isNone(debug::Object::ID id) const; // get() returns the debug object with the given id. // The object must exist and be of type (or derive from type) T. // A returned nullptr represents a None value or type. template T *get(SpirvID id) const; // getOrNull() returns the debug object with the given id if // the object exists and is of type (or derive from type) T. // Otherwise, returns nullptr. template T *getOrNull(SpirvID id) const; // use get() and add() to access this std::unordered_map> objects; // defineOrEmit() when called in Pass::Define, creates and stores a // zero-initialized object into the Debugger::objects map using the // object identifier held by second instruction operand. // When called in Pass::Emit, defineOrEmit() calls the function F with the // previously-built object. // // F must be a function with the signature: // void(OBJECT_TYPE *) // // The object type is automatically inferred from the function signature. template>::type> void defineOrEmit(InsnIterator insn, Pass pass, F &&emit); std::unordered_map> files; uint32_t numDebugValueSlots = 0; // Number of independent debug::Values which need to be tracked bool nextSetLocationIsSteppable = true; debug::SourceScope *lastSetScope = nullptr; vk::dbg::Location lastSetLocation; }; //////////////////////////////////////////////////////////////////////////////// // sw::SpirvShader::Impl::Debugger::LocalVariableValue // // Implementation of vk::dbg::Value that displays a debug::LocalVariable that // has its value(s) defined by debug::Value(s). // // TODO(b/145351270) Note: The OpenCL.DebugInfo.100 spec does not state how // DebugValues should be applied to the DebugLocalVariable. // // This implementation keeps track of the order of DebugValues as they are // 'executed', and uses the most recent values for each specific index. // OpenCL.DebugInfo.100 is significantly derived from the LLVM debug // instructions, and so it can be assumed that DebugValue is intended to behave // like llvm.dbg.value. // // https://llvm.org/docs/SourceLevelDebugging.html#object-lifetime-in-optimized-code // describes the expected behavior of llvm.dbg.value, which instead of runtime // tracking, uses static analysis of the LLVM IR to determine which debug // values should be used. // // If DebugValue is to behave the same way as llvm.dbg.value, then this // implementation should be changed to examine the order of DebugValue // instructions in the SPIR-V. This can only be done once the SPIR-V generating // compiler and SPIR-V optimization passes generate and preserve the DebugValue // ordering as described in the LLVM SourceLevelDebugging document. //////////////////////////////////////////////////////////////////////////////// class sw::SpirvShader::Impl::Debugger::LocalVariableValue : public vk::dbg::Value { public: // Data shared across all nodes in the LocalVariableValue. struct Shared { Shared(debug::LocalVariable const *const variable, State const *const state, int const lane) : variable(variable) , state(state) , lane(lane) { ASSERT(variable->definition == debug::LocalVariable::Definition::Values); } debug::LocalVariable const *const variable; State const *const state; int const lane; }; LocalVariableValue(debug::LocalVariable *variable, State const *const state, int lane); LocalVariableValue( std::shared_ptr const &shared, debug::Type const *ty, debug::LocalVariable::ValueNode const *node); private: // vk::dbg::Value std::string type() override; std::string get(const vk::dbg::FormatFlags &) override; std::shared_ptr children() override; void updateValue(); std::shared_ptr const shared; debug::Type const *const ty; debug::LocalVariable::ValueNode const *const node; debug::Value *activeValue = nullptr; std::shared_ptr value; }; //////////////////////////////////////////////////////////////////////////////// // sw::SpirvShader::Impl::Debugger::State // // State holds the runtime data structures for the shader debug session. // // When debugging is enabled, the shader program will construct a State with a // call to create(), and during execution write shader information into fields // of this class, including: // * Shadow memory for keeping track of register-held values. // * Global variables. // * Last reached ::debug::Values (see LocalVariableValue) // // Bulky data that is only needed once the shader has hit a trap is held by // State::Data. This is lazily constructed by the first call to trap(). // // There is an instance of this class per shader invocation. //////////////////////////////////////////////////////////////////////////////// class SpirvShader::Impl::Debugger::State { public: // Globals holds a copy of the shader's builtin global variables. struct Globals { struct Compute { sw::uint3 numWorkgroups; sw::uint3 workgroupID; sw::uint3 workgroupSize; uint32_t numSubgroups; uint32_t subgroupIndex; sw::SIMD::uint3 globalInvocationId; sw::SIMD::uint3 localInvocationId; sw::SIMD::uint3 localInvocationIndex; }; struct Fragment { uint32_t viewIndex; sw::SIMD::vec4 fragCoord; sw::SIMD::vec4 pointCoord; sw::SIMD::int2 windowSpacePosition; sw::SIMD::uint_t helperInvocation; }; struct Vertex { uint32_t viewIndex; uint32_t instanceIndex; sw::SIMD::uint_t vertexIndex; }; // Common for all shader types uint32_t subgroupSize; sw::SIMD::uint_t activeLaneMask; // Shader type specific globals union { Compute compute; Fragment fragment; Vertex vertex; }; }; // create() allocates, constructs and returns a State. // Called at the start of the debugger-enabled shader program. static State *create(const Debugger *debugger); // destroy() destructs and frees a state. // Called at the end of the debugger-enabled shader program. static void destroy(State *); // trap() is called by the debugger-enabled shader program to suspend // execution of the shader. This will appear in the attached debugger as if // a breakpoint has been hit. // trap() will be called if the Debugger::Traps::memory[index] is non-zero, // or if alwaysTrap is non-zero. // index is the index of the trap (see Debugger::Traps). void trap(int index); const Debugger *const debugger; // traps is a simple copy of Debugger::Traps::memory. // Copied here to reduce pointer chasing during shader execution. uint8_t *traps = nullptr; // alwaysTrap (if non-zero) forces a call trap() even if // Debugger::Traps::memory[index] is zero. Used to perform single line // stepping (pause at next line / instruction). uint8_t alwaysTrap = 0; // Global variable values. Written to at shader start. Globals globals; // Shadow memory for all SpirvShader::Objects in the executing shader // program. // See Debugger::Shadow for more information. std::unique_ptr const shadow; // Array of last reached debug::Value. // Indexed by ::debug::LocalVariable::ValueNode::debugValueIndex. std::unique_ptr const lastReachedDebugValues; private: // Data holds the debugger-interface state (vk::dbg::*). // This is only constructed on the first call to Debugger::State::trap() as // it contains data that is only needed when the debugger is actively // inspecting execution of the shader program. struct Data { Data(State *state); // terminate() is called at the end of execution of the shader program. // terminate() ensures that the debugger thread stack is at the same // level as when the program entered. void terminate(State *state); // trap() updates the debugger thread with the stack frames and // variables at the trap's scoped location. // trap() will notify the debugger that the thread has paused, and will // block until instructed to resume (either continue or step) by the // user. void trap(int index, State *state); private: using PerLaneVariables = std::array, sw::SIMD::Width>; struct StackEntry { debug::LexicalBlock *block; uint32_t line; bool operator!=(const StackEntry &other) const { return block != other.block || line != other.line; } }; struct GlobalVariables { std::shared_ptr common; PerLaneVariables lanes; }; // updateFrameLocals() updates the local variables in the frame with // those in the lexical block. void updateFrameLocals(State *state, vk::dbg::Frame &frame, debug::LexicalBlock *block); // getOrCreateLocals() creates and returns the per-lane local variables // from those in the lexical block. PerLaneVariables getOrCreateLocals(State *state, debug::LexicalBlock const *block); // buildGlobal() creates and adds to globals global variable with the // given name and value. The value is copied instead of holding a // pointer to val. template void buildGlobal(const char *name, const T &val); template void buildGlobal(const char *name, const sw::SIMD::PerLane &vec); // buildGlobals() builds all the global variable values, populating // globals. void buildGlobals(State *state); // buildSpirvVariables() builds a Struct holding all the SPIR-V named // values for the given lane. std::shared_ptr buildSpirvVariables(State *state, int lane) const; // buildSpirvValue() returns a debugger value for the SPIR-V shadow // value at memory of the given type and for the given lane. std::shared_ptr buildSpirvValue(State *state, Shadow::Memory memory, const SpirvShader::Type &type, int lane) const; GlobalVariables globals; std::shared_ptr thread; std::vector stack; std::unordered_map locals; }; State(const Debugger *debugger); ~State(); std::unique_ptr data; }; //////////////////////////////////////////////////////////////////////////////// // sw::SpirvShader::Impl::Debugger methods //////////////////////////////////////////////////////////////////////////////// SpirvShader::Impl::Debugger::Debugger(const SpirvShader *shader, const std::shared_ptr &ctx) : shader(shader) , ctx(ctx) { } SpirvShader::Impl::Debugger::~Debugger() { ctx->removeListener(this); } void SpirvShader::Impl::Debugger::finalize() { ASSERT(traps.byIndex.size() == traps.byLocationAndScope.size()); traps.memory = std::make_unique(traps.byIndex.size()); ctx->addListener(this); // Register existing breakpoints. onBreakpointsChanged(); } void sw::SpirvShader::Impl::Debugger::setNextSetLocationIsSteppable() { nextSetLocationIsSteppable = true; } void SpirvShader::Impl::Debugger::setScope(debug::SourceScope *scope) { lastSetScope = scope; } void SpirvShader::Impl::Debugger::setLocation(EmitState *state, const std::shared_ptr &file, int line) { vk::dbg::Location location{ file, line }; if(location != lastSetLocation) { // If the location has changed, then this is always a step. nextSetLocationIsSteppable = true; lastSetLocation = location; } if(nextSetLocationIsSteppable) { // Get or create the trap for the given location and scope. LocationAndScope locationAndScope{ location, lastSetScope }; int index = getOrCreate(traps.byLocationAndScope, locationAndScope, [&] { traps.byIndex.emplace_back(locationAndScope); return traps.byIndex.size() - 1; }); // Also create a map index for the given scope's function so we can // break on function entry. if(lastSetScope) { if(auto func = debug::find(lastSetScope->scope)) { getOrCreate(traps.byFunctionName, func->name, [&] { return index; }); } } // Emit the shader logic to test the trap value (either through via // Debugger::State::traps[] or Debugger::State::alwaysTrap), and call // Debugger::State::trap() if either are true. auto dbgState = state->routine->dbgState; auto alwaysTrap = *Pointer(dbgState + OFFSET(Impl::Debugger::State, alwaysTrap)); auto traps = *Pointer>(dbgState + OFFSET(Impl::Debugger::State, traps)); auto trap = Pointer(traps)[index]; If(alwaysTrap != Byte(0) || trap != Byte(0)) { rr::Call(&State::trap, state->routine->dbgState, index); } nextSetLocationIsSteppable = false; } } void SpirvShader::Impl::Debugger::setLocation(EmitState *state, const char *path, int line) { auto lock = ctx->lock(); auto file = lock.findFile(path); if(!file) { file = lock.createPhysicalFile(path); } setLocation(state, file, line); } void SpirvShader::Impl::Debugger::onSetBreakpoint(const vk::dbg::Location &location, bool &handled) { // Notify the debugger if the breakpoint location is handled. // We don't actually set the trap here as this is performed by // onBreakpointsChanged(), which is only called once, even for multiple // breakpoint changes. for(auto it : traps.byLocationAndScope) { if(location == it.first.location) { handled = true; return; } } } void SpirvShader::Impl::Debugger::onSetBreakpoint(const std::string &func, bool &handled) { // Notify the debugger if the function-entry breakpoint is handled. // We don't actually set the trap here as this is performed by // onBreakpointsChanged(), which is only called once, even for multiple // breakpoint changes. auto it = traps.byFunctionName.find(func); if(it != traps.byFunctionName.end()) { handled = true; } if(isEntryBreakpointForShaderType(shader->executionModel, func)) { handled = true; } } void SpirvShader::Impl::Debugger::onBreakpointsChanged() { // TODO(b/145351270): TSAN will probably moan that traps.memory is being // modified while being read on othe threads. We can solve this by adding // a shared mutex (RWMutex) for the traps, read-locking for execution, and // write locking here. This will prevent setting breakpoints while a shader // is executing (maybe problematic if you want to debug a slow or // never-completing shader). // For now, just be racy. It's unlikely that this will cause any noticable // problems. // Start by disabling all traps. memset(traps.memory.get(), 0, traps.byIndex.size() * sizeof(traps.memory[0])); // Add traps for all breakpoints by location. for(auto it : files) { auto &file = it.second; for(auto line : file->getBreakpoints()) { for(auto it : traps.byLocationAndScope) { if(it.first.location == vk::dbg::Location{ file, line }) { traps.memory[it.second] = 1; } } } } // Add traps for all breakpoints by function name. auto lock = ctx->lock(); for(auto it : traps.byFunctionName) { if(lock.isFunctionBreakpoint(it.first)) { traps.memory[it.second] = 1; } } // Add traps for breakpoints by shader type. for(auto bp : lock.getFunctionBreakpoints()) { if(isEntryBreakpointForShaderType(shader->executionModel, bp)) { traps.memory[0] = 1; } } } template void SpirvShader::Impl::Debugger::defineOrEmit(InsnIterator insn, Pass pass, F &&emit) { auto id = SpirvID(insn.word(2)); switch(pass) { case Pass::Define: add(id, std::unique_ptr(new T())); break; case Pass::Emit: emit(get(id)); break; } } void SpirvShader::Impl::Debugger::process(const InsnIterator &insn, EmitState *state, Pass pass) { auto extInstIndex = insn.word(4); switch(extInstIndex) { case OpenCLDebugInfo100DebugInfoNone: if(pass == Pass::Define) { addNone(debug::Object::ID(insn.word(2))); } break; case OpenCLDebugInfo100DebugCompilationUnit: defineOrEmit(insn, pass, [&](debug::CompilationUnit *cu) { cu->source = get(debug::Source::ID(insn.word(7))); }); break; case OpenCLDebugInfo100DebugTypeBasic: defineOrEmit(insn, pass, [&](debug::BasicType *type) { type->name_ = shader->getString(insn.word(5)); type->size = shader->GetConstScalarInt(insn.word(6)); type->encoding = static_cast(insn.word(7)); }); break; case OpenCLDebugInfo100DebugTypeArray: defineOrEmit(insn, pass, [&](debug::ArrayType *type) { type->base = get(debug::Type::ID(insn.word(5))); type->size = shader->GetConstScalarInt(insn.word(6)); for(uint32_t i = 7; i < insn.wordCount(); i++) { // Decompose multi-dimentional into nested single // dimensional arrays. Greatly simplifies logic. auto inner = new debug::ArrayType(); inner->base = type->base; type->size = shader->GetConstScalarInt(insn.word(i)); type->base = inner; type->ownsBase = true; type = inner; } }); break; case OpenCLDebugInfo100DebugTypeVector: defineOrEmit(insn, pass, [&](debug::VectorType *type) { type->base = get(debug::Type::ID(insn.word(5))); type->components = insn.word(6); }); break; case OpenCLDebugInfo100DebugTypeFunction: defineOrEmit(insn, pass, [&](debug::FunctionType *type) { type->flags = insn.word(5); type->returnTy = getOrNull(debug::Type::ID(insn.word(6))); // 'Return Type' operand must be a debug type or OpTypeVoid. See // https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.html#DebugTypeFunction ASSERT_MSG(type->returnTy != nullptr || shader->getType(insn.word(6)).opcode() == spv::Op::OpTypeVoid, "Invalid return type of DebugTypeFunction: %d", insn.word(6)); for(uint32_t i = 7; i < insn.wordCount(); i++) { type->paramTys.push_back(get(debug::Type::ID(insn.word(i)))); } }); break; case OpenCLDebugInfo100DebugTypeComposite: defineOrEmit(insn, pass, [&](debug::CompositeType *type) { type->name_ = shader->getString(insn.word(5)); type->tag = static_cast(insn.word(6)); type->source = get(debug::Source::ID(insn.word(7))); type->line = insn.word(8); type->column = insn.word(9); type->parent = get(debug::Object::ID(insn.word(10))); type->linkage = shader->getString(insn.word(11)); type->size = isNone(insn.word(12)) ? 0 : shader->GetConstScalarInt(insn.word(12)); type->flags = insn.word(13); for(uint32_t i = 14; i < insn.wordCount(); i++) { auto obj = get(debug::Object::ID(insn.word(i))); if(auto member = debug::cast(obj)) // Can also be Function or TypeInheritance, which we don't care about. { type->members_.push_back(member); } } }); break; case OpenCLDebugInfo100DebugTypeMember: defineOrEmit(insn, pass, [&](debug::Member *member) { member->name = shader->getString(insn.word(5)); member->type = get(debug::Type::ID(insn.word(6))); member->source = get(debug::Source::ID(insn.word(7))); member->line = insn.word(8); member->column = insn.word(9); member->parent = get(debug::CompositeType::ID(insn.word(10))); member->offset = shader->GetConstScalarInt(insn.word(11)); member->size = shader->GetConstScalarInt(insn.word(12)); member->flags = insn.word(13); }); break; case OpenCLDebugInfo100DebugTypeTemplate: defineOrEmit(insn, pass, [&](debug::TemplateType *tpl) { tpl->target = get(debug::Type::ID(insn.word(5))); for(size_t i = 6, c = insn.wordCount(); i < c; i++) { tpl->parameters.emplace_back(get(debug::TemplateParameter::ID(insn.word(i)))); } }); break; case OpenCLDebugInfo100DebugTypeTemplateParameter: defineOrEmit(insn, pass, [&](debug::TemplateParameter *param) { param->name = shader->getString(insn.word(5)); param->type = get(debug::Type::ID(insn.word(6))); param->value = 0; // TODO: Get value from OpConstant if "a template value parameter". param->source = get(debug::Source::ID(insn.word(8))); param->line = insn.word(9); param->column = insn.word(10); }); break; case OpenCLDebugInfo100DebugGlobalVariable: defineOrEmit(insn, pass, [&](debug::GlobalVariable *var) { var->name = shader->getString(insn.word(5)); var->type = get(debug::Type::ID(insn.word(6))); var->source = get(debug::Source::ID(insn.word(7))); var->line = insn.word(8); var->column = insn.word(9); var->parent = get(debug::Scope::ID(insn.word(10))); var->linkage = shader->getString(insn.word(11)); var->variable = isNone(insn.word(12)) ? 0 : insn.word(12); var->flags = insn.word(13); // static member declaration: word(14) }); break; case OpenCLDebugInfo100DebugFunction: defineOrEmit(insn, pass, [&](debug::Function *func) { func->name = shader->getString(insn.word(5)); func->type = get(debug::FunctionType::ID(insn.word(6))); func->source = get(debug::Source::ID(insn.word(7))); func->declLine = insn.word(8); func->declColumn = insn.word(9); func->parent = get(debug::Scope::ID(insn.word(10))); func->linkage = shader->getString(insn.word(11)); func->flags = insn.word(12); func->line = insn.word(13); func->function = Function::ID(insn.word(14)); // declaration: word(13) }); break; case OpenCLDebugInfo100DebugLexicalBlock: defineOrEmit(insn, pass, [&](debug::LexicalBlock *scope) { scope->source = get(debug::Source::ID(insn.word(5))); scope->line = insn.word(6); scope->column = insn.word(7); scope->parent = get(debug::Scope::ID(insn.word(8))); if(insn.wordCount() > 9) { scope->name = shader->getString(insn.word(9)); } }); break; case OpenCLDebugInfo100DebugScope: defineOrEmit(insn, pass, [&](debug::SourceScope *ss) { ss->scope = get(debug::Scope::ID(insn.word(5))); if(insn.wordCount() > 6) { ss->inlinedAt = get(debug::InlinedAt::ID(insn.word(6))); } setScope(ss); }); break; case OpenCLDebugInfo100DebugNoScope: break; case OpenCLDebugInfo100DebugInlinedAt: defineOrEmit(insn, pass, [&](debug::InlinedAt *ia) { ia->line = insn.word(5); ia->scope = get(debug::Scope::ID(insn.word(6))); if(insn.wordCount() > 7) { ia->inlined = get(debug::InlinedAt::ID(insn.word(7))); } }); break; case OpenCLDebugInfo100DebugLocalVariable: defineOrEmit(insn, pass, [&](debug::LocalVariable *var) { var->name = shader->getString(insn.word(5)); var->type = get(debug::Type::ID(insn.word(6))); var->source = get(debug::Source::ID(insn.word(7))); var->line = insn.word(8); var->column = insn.word(9); var->parent = get(debug::Scope::ID(insn.word(10))); if(insn.wordCount() > 11) { var->arg = insn.word(11); } if(auto block = debug::find(var->parent)) { block->variables.emplace_back(var); } }); break; case OpenCLDebugInfo100DebugDeclare: defineOrEmit(insn, pass, [&](debug::Declare *decl) { decl->local = get(debug::LocalVariable::ID(insn.word(5))); decl->variable = Object::ID(insn.word(6)); decl->expression = get(debug::Expression::ID(insn.word(7))); decl->local->declaration = decl; ASSERT_MSG(decl->local->definition == debug::LocalVariable::Definition::Undefined, "DebugLocalVariable '%s' declared at %s:%d was previously defined as %s, now again as %s", decl->local->name.c_str(), decl->local->source ? decl->local->source->file.c_str() : "", (int)decl->local->line, tostring(decl->local->definition), tostring(debug::LocalVariable::Definition::Declaration)); decl->local->definition = debug::LocalVariable::Definition::Declaration; }); break; case OpenCLDebugInfo100DebugValue: defineOrEmit(insn, pass, [&](debug::Value *value) { value->local = get(debug::LocalVariable::ID(insn.word(5))); value->value = insn.word(6); value->expression = get(debug::Expression::ID(insn.word(7))); if(value->local->definition == debug::LocalVariable::Definition::Undefined) { value->local->definition = debug::LocalVariable::Definition::Values; } else { ASSERT_MSG(value->local->definition == debug::LocalVariable::Definition::Values, "DebugLocalVariable '%s' declared at %s:%d was previously defined as %s, now again as %s", value->local->name.c_str(), value->local->source ? value->local->source->file.c_str() : "", (int)value->local->line, tostring(value->local->definition), tostring(debug::LocalVariable::Definition::Values)); } auto node = &value->local->values; for(uint32_t i = 8; i < insn.wordCount(); i++) { auto idx = shader->GetConstScalarInt(insn.word(i)); value->indexes.push_back(idx); auto it = node->children.find(idx); if(it != node->children.end()) { node = it->second.get(); } else { auto parent = node; auto child = std::make_unique(); node = child.get(); parent->children.emplace(idx, std::move(child)); } } if(node->debugValueIndex == debug::LocalVariable::ValueNode::NoDebugValueIndex) { node->debugValueIndex = numDebugValueSlots++; } rr::Pointer> lastReachedArray = *rr::Pointer>>( state->routine->dbgState + OFFSET(Impl::Debugger::State, lastReachedDebugValues)); rr::Pointer> lastReached = &lastReachedArray[node->debugValueIndex]; *lastReached = rr::ConstantPointer(value); }); break; case OpenCLDebugInfo100DebugExpression: defineOrEmit(insn, pass, [&](debug::Expression *expr) { for(uint32_t i = 5; i < insn.wordCount(); i++) { expr->operations.push_back(get(debug::Operation::ID(insn.word(i)))); } }); break; case OpenCLDebugInfo100DebugSource: defineOrEmit(insn, pass, [&](debug::Source *source) { source->file = shader->getString(insn.word(5)); if(insn.wordCount() > 6) { source->source = shader->getString(insn.word(6)); auto file = ctx->lock().createVirtualFile(source->file.c_str(), source->source.c_str()); source->dbgFile = file; files.emplace(source->file.c_str(), file); } else { auto file = ctx->lock().createPhysicalFile(source->file.c_str()); source->dbgFile = file; files.emplace(source->file.c_str(), file); } }); break; case OpenCLDebugInfo100DebugOperation: defineOrEmit(insn, pass, [&](debug::Operation *operation) { operation->opcode = insn.word(5); for(uint32_t i = 6; i < insn.wordCount(); i++) { operation->operands.push_back(insn.word(i)); } }); break; case OpenCLDebugInfo100DebugTypePointer: case OpenCLDebugInfo100DebugTypeQualifier: case OpenCLDebugInfo100DebugTypedef: case OpenCLDebugInfo100DebugTypeEnum: case OpenCLDebugInfo100DebugTypeInheritance: case OpenCLDebugInfo100DebugTypePtrToMember: case OpenCLDebugInfo100DebugTypeTemplateTemplateParameter: case OpenCLDebugInfo100DebugTypeTemplateParameterPack: case OpenCLDebugInfo100DebugFunctionDeclaration: case OpenCLDebugInfo100DebugLexicalBlockDiscriminator: case OpenCLDebugInfo100DebugInlinedVariable: case OpenCLDebugInfo100DebugMacroDef: case OpenCLDebugInfo100DebugMacroUndef: case OpenCLDebugInfo100DebugImportedEntity: UNIMPLEMENTED("b/148401179 OpenCLDebugInfo100 instruction %d", int(extInstIndex)); break; default: UNSUPPORTED("OpenCLDebugInfo100 instruction %d", int(extInstIndex)); } } template void SpirvShader::Impl::Debugger::add(ID id, std::unique_ptr &&obj) { ASSERT_MSG(obj != nullptr, "add() called with nullptr obj"); bool added = objects.emplace(debug::Object::ID(id.value()), std::move(obj)).second; ASSERT_MSG(added, "Debug object with %d already exists", id.value()); } void SpirvShader::Impl::Debugger::addNone(debug::Object::ID id) { bool added = objects.emplace(debug::Object::ID(id.value()), nullptr).second; ASSERT_MSG(added, "Debug object with %d already exists", id.value()); } bool SpirvShader::Impl::Debugger::isNone(debug::Object::ID id) const { auto it = objects.find(debug::Object::ID(id.value())); if(it == objects.end()) { return false; } return it->second.get() == nullptr; } template T *SpirvShader::Impl::Debugger::get(SpirvID id) const { auto it = objects.find(debug::Object::ID(id.value())); ASSERT_MSG(it != objects.end(), "Unknown debug object %d", id.value()); auto ptr = debug::cast(it->second.get()); ASSERT_MSG(ptr, "Debug object %d is not of the correct type. Got: %s, want: %s", id.value(), cstr(it->second->kind), cstr(T::KIND)); return ptr; } template T *SpirvShader::Impl::Debugger::getOrNull(SpirvID id) const { auto it = objects.find(debug::Object::ID(id.value())); if(it == objects.end()) { return nullptr; } // Not found. auto ptr = debug::cast(it->second.get()); ASSERT_MSG(ptr, "Debug object %d is not of the correct type. Got: %s, want: %s", id.value(), cstr(it->second->kind), cstr(T::KIND)); return ptr; } //////////////////////////////////////////////////////////////////////////////// // SpirvShader::Impl::Debugger::Shadow methods //////////////////////////////////////////////////////////////////////////////// void SpirvShader::Impl::Debugger::Shadow::create(const SpirvShader *shader, const EmitState *state, Object::ID objId) { ASSERT_MSG(entries.find(objId) == entries.end(), "Object %%%d already has shadow memory allocated?", (int)objId.value()); Entry entry{}; entry.offset = size; rr::Pointer base = *rr::Pointer>(state->routine->dbgState + OFFSET(Impl::Debugger::State, shadow)); base += entry.offset; auto &obj = shader->getObject(objId); auto &objTy = shader->getType(obj.typeId()); auto mask = state->activeLaneMask(); switch(obj.kind) { case Object::Kind::Constant: case Object::Kind::Intermediate: { size += objTy.componentCount * sizeof(uint32_t) * sw::SIMD::Width; auto dst = InterleaveByLane(SIMD::Pointer(base, 0)); for(uint32_t i = 0u; i < objTy.componentCount; i++) { auto val = SpirvShader::Operand(shader, state, objId).Int(i); dst.Store(val, sw::OutOfBoundsBehavior::UndefinedBehavior, mask); dst += sizeof(uint32_t) * SIMD::Width; } entry.kind = Entry::Kind::Value; break; } case Object::Kind::Pointer: case Object::Kind::InterfaceVariable: { size += sizeof(void *) + sizeof(uint32_t) * SIMD::Width; auto ptr = state->getPointer(objId); store(base, ptr.base); store(base + sizeof(void *), ptr.offsets()); entry.kind = Entry::Kind::Pointer; break; } default: break; } entries.emplace(objId, entry); } SpirvShader::Impl::Debugger::Shadow::Memory SpirvShader::Impl::Debugger::Shadow::get(const State *state, Object::ID objId) const { auto entryIt = entries.find(objId); ASSERT_MSG(entryIt != entries.end(), "Missing shadow entry for object %%%d (%s)", (int)objId.value(), OpcodeName(state->debugger->shader->getObject(objId).opcode())); auto &entry = entryIt->second; auto data = &state->shadow[entry.offset]; return Memory{ data }; } SpirvShader::Impl::Debugger::Shadow::Memory::operator void *() { return addr; } SpirvShader::Impl::Debugger::Shadow::Memory SpirvShader::Impl::Debugger::Shadow::Memory::dref(int lane) const { auto ptr = *reinterpret_cast(addr); return Memory{ ptr.base + ptr.offsets[lane] }; } //////////////////////////////////////////////////////////////////////////////// // sw::SpirvShader::Impl::Debugger::LocalVariableValue methods //////////////////////////////////////////////////////////////////////////////// sw::SpirvShader::Impl::Debugger::LocalVariableValue::LocalVariableValue( debug::LocalVariable *variable, State const *const state, int lane) : LocalVariableValue(std::make_shared(variable, state, lane), variable->type, &variable->values) {} sw::SpirvShader::Impl::Debugger::LocalVariableValue::LocalVariableValue( std::shared_ptr const &shared, debug::Type const *ty, debug::LocalVariable::ValueNode const *node) : shared(shared) , ty(ty) , node(node) { } std::string sw::SpirvShader::Impl::Debugger::LocalVariableValue::type() { updateValue(); return value->type(); } std::string sw::SpirvShader::Impl::Debugger::LocalVariableValue::get(const vk::dbg::FormatFlags &fmt) { updateValue(); return value->get(fmt); } std::shared_ptr sw::SpirvShader::Impl::Debugger::LocalVariableValue::children() { updateValue(); return value->children(); } void sw::SpirvShader::Impl::Debugger::LocalVariableValue::updateValue() { // Fetch the last reached ::debug::Value for this local variable node. auto newActiveValue = (node->debugValueIndex != debug::LocalVariable::ValueNode::NoDebugValueIndex) ? shared->state->lastReachedDebugValues[node->debugValueIndex] : nullptr; auto activeValueChanged = activeValue != newActiveValue; activeValue = newActiveValue; if(activeValue && activeValueChanged) { // We have a new ::debug::Value, read it. ASSERT(activeValue->local == shared->variable); // If this isn't true, then something is very wonky. // Update the value. auto ptr = shared->state->debugger->shadow.get(shared->state, activeValue->value); for(auto op : activeValue->expression->operations) { switch(op->opcode) { case OpenCLDebugInfo100Deref: ptr = ptr.dref(shared->lane); break; default: UNIMPLEMENTED("b/148401179 OpenCLDebugInfo100DebugOperation %d", (int)op->opcode); break; } } value = ty->value(ptr, true); } else if(!value || activeValueChanged) { // We have no ::debug::Value. Display if(node->children.empty()) { // No children? Just have the node display value = ty->undefined(); } else { // Node has children. // Display for those that don't have sub-nodes, and // create child LocalVariableValues for those that do. value = vk::dbg::Struct::create(ty->name(), [&](auto &vc) { auto numMembers = ty->numMembers(); for(size_t i = 0; i < numMembers; i++) { auto member = ty->getMember(i); auto it = node->children.find(i); if(it != node->children.end()) { auto child = std::make_shared(shared, member.type, it->second.get()); vc->put(member.name, child); } else { vc->put(member.name, member.type->undefined()); } } }); } } } //////////////////////////////////////////////////////////////////////////////// // sw::SpirvShader::Impl::Debugger::State methods //////////////////////////////////////////////////////////////////////////////// SpirvShader::Impl::Debugger::State *SpirvShader::Impl::Debugger::State::create(const Debugger *debugger) { return new State(debugger); } void SpirvShader::Impl::Debugger::State::destroy(State *state) { delete state; } SpirvShader::Impl::Debugger::State::State(const Debugger *debugger) : debugger(debugger) , traps(debugger->traps.memory.get()) , shadow(new uint8_t[debugger->shadow.size]) , lastReachedDebugValues(new debug::Value *[debugger->numDebugValueSlots]) { memset(shadow.get(), 0, debugger->shadow.size); memset(lastReachedDebugValues.get(), 0, sizeof(lastReachedDebugValues[0]) * debugger->numDebugValueSlots); } SpirvShader::Impl::Debugger::State::~State() { if(data) { data->terminate(this); } } void SpirvShader::Impl::Debugger::State::trap(int index) { if(std::all_of(globals.activeLaneMask.data.begin(), globals.activeLaneMask.data.end(), [](auto v) { return v == 0; })) { // Don't trap if no lanes are active. // Ideally, we would be simply jumping over blocks that have no active // lanes, but this is complicated due to ensuring that all reactor // RValues dominate their usage blocks. return; } if(!data) { data = std::make_unique(this); } data->trap(index, this); } SpirvShader::Impl::Debugger::State::Data::Data(State *state) { buildGlobals(state); thread = state->debugger->ctx->lock().currentThread(); if(!state->debugger->shaderHasDebugInfo) { // Enter the stack frame entry for the SPIR-V. thread->enter(state->debugger->spirvFile, "SPIR-V", [&](vk::dbg::Frame &frame) { for(size_t lane = 0; lane < sw::SIMD::Width; lane++) { auto laneLocals = std::make_shared("Lane", globals.lanes[lane]); frame.locals->variables->put(laneName(lane), laneLocals); frame.hovers->variables->extend(std::make_shared(frame.locals->variables)); } }); } } void SpirvShader::Impl::Debugger::State::Data::terminate(State *state) { if(state->debugger->shaderHasDebugInfo) { for(size_t i = 0; i < stack.size(); i++) { thread->exit(); } } else { thread->exit(); } } void SpirvShader::Impl::Debugger::State::Data::trap(int index, State *state) { auto debugger = state->debugger; // Update the thread frames from the stack of scopes auto const &locationAndScope = debugger->traps.byIndex[index]; if(locationAndScope.scope) { // Gather the new stack as LexicalBlocks. std::vector newStack; if(auto block = debug::find(locationAndScope.scope->scope)) { newStack.emplace_back(StackEntry{ block, block->line }); } for(auto inlined = locationAndScope.scope->inlinedAt; inlined != nullptr; inlined = inlined->inlined) { if(auto block = debug::find(inlined->scope)) { newStack.emplace_back(StackEntry{ block, inlined->line }); } } std::reverse(newStack.begin(), newStack.end()); // shrink pop stack frames until stack length is at most maxLen. auto shrink = [&](size_t maxLen) { while(stack.size() > maxLen) { thread->exit(true); stack.pop_back(); } }; // Pop stack frames until stack length is at most newStack length. shrink(newStack.size()); // Find first deviation in stack frames, and shrink to that point. // Special care is taken for deviation in just the top most frame so we // don't end up reconstructing the top most stack frame every scope // change. for(size_t i = 0; i < stack.size(); i++) { if(stack[i] != newStack[i]) { bool wasTopMostFrame = i == (stack.size() - 1); auto oldFunction = debug::find(stack[i].block); auto newFunction = debug::find(newStack[i].block); if(wasTopMostFrame && oldFunction == newFunction) { // Deviation is just a movement in the top most frame's // function. // Don't exit() and enter() for the same function - it'll // be treated as a step out and step in, breaking stepping // commands. Instead, just update the frame variables for // the new scope. stack[i] = newStack[i]; thread->update(true, [&](vk::dbg::Frame &frame) { // Update the frame location if we're entering a // function. This allows the debugger to pause at the // line (which may not have any instructions or OpLines) // of a inlined function call. This is less jarring // than magically appearing in another function before // you've reached the line of the call site. // See b/170650010 for more context. if(stack.size() < newStack.size()) { auto function = debug::find(stack[i].block); frame.location = vk::dbg::Location{ function->source->dbgFile, (int)stack[i].line }; } updateFrameLocals(state, frame, stack[i].block); }); } else { shrink(i); } break; } } // Now rebuild the parts of stack frames that are new. // // This is done in two stages: // (1) thread->enter() is called to construct the new stack frame with // the opening scope line. The frames locals and hovers are built // and assigned. // (2) thread->update() is called to adjust the frame's location to // entry.line. This may be different to the function entry in the // case of multiple nested inline functions. If its the same, then // this is a no-op. // // This two-stage approach allows the debugger to step through chains of // inlined function calls without having a jarring jump from the outer // function to the first statement within the function. // See b/170650010 for more context. for(size_t i = stack.size(); i < newStack.size(); i++) { auto entry = newStack[i]; stack.emplace_back(entry); auto function = debug::find(entry.block); thread->enter(entry.block->source->dbgFile, function->name, [&](vk::dbg::Frame &frame) { frame.location = vk::dbg::Location{ function->source->dbgFile, (int)function->line }; frame.hovers->variables->extend(std::make_shared(frame.locals->variables)); updateFrameLocals(state, frame, entry.block); }); thread->update(true, [&](vk::dbg::Frame &frame) { frame.location.line = (int)entry.line; }); } } // If the debugger thread is running, notify that we're pausing due to the // trap. if(thread->state() == vk::dbg::Thread::State::Running) { // pause() changes the thread state Paused, and will cause the next // frame location changing call update() to block until the debugger // instructs the thread to resume or step. thread->pause(); debugger->ctx->serverEventBroadcast()->onLineBreakpointHit(thread->id); } // Update the frame location. This will likely block until the debugger // instructs the thread to resume or step. thread->update(true, [&](vk::dbg::Frame &frame) { frame.location = locationAndScope.location; }); // Clear the alwaysTrap state if the debugger instructed the thread to // resume, or set it if we're single line stepping (so we can keep track of // location). state->alwaysTrap = thread->state() != vk::dbg::Thread::State::Running; } void SpirvShader::Impl::Debugger::State::Data::updateFrameLocals(State *state, vk::dbg::Frame &frame, debug::LexicalBlock *block) { auto locals = getOrCreateLocals(state, block); for(size_t lane = 0; lane < sw::SIMD::Width; lane++) { auto laneLocals = std::make_shared("Lane", locals[lane]); frame.locals->variables->put(laneName(lane), laneLocals); } } SpirvShader::Impl::Debugger::State::Data::PerLaneVariables SpirvShader::Impl::Debugger::State::Data::getOrCreateLocals(State *state, debug::LexicalBlock const *block) { return getOrCreate(locals, block, [&] { PerLaneVariables locals; for(int lane = 0; lane < sw::SIMD::Width; lane++) { auto vc = std::make_shared(); for(auto var : block->variables) { auto name = var->name; switch(var->definition) { case debug::LocalVariable::Definition::Undefined: { vc->put(name, var->type->undefined()); break; } case debug::LocalVariable::Definition::Declaration: { auto data = state->debugger->shadow.get(state, var->declaration->variable); vc->put(name, var->type->value(data.dref(lane), true)); break; } case debug::LocalVariable::Definition::Values: { vc->put(name, std::make_shared(var, state, lane)); break; } } } locals[lane] = std::move(vc); } if(auto parent = debug::find(block->parent)) { auto extend = getOrCreateLocals(state, parent); for(int lane = 0; lane < sw::SIMD::Width; lane++) { locals[lane]->extend(extend[lane]); } } else { for(int lane = 0; lane < sw::SIMD::Width; lane++) { locals[lane]->extend(globals.lanes[lane]); } } return locals; }); } template void SpirvShader::Impl::Debugger::State::Data::buildGlobal(const char *name, const T &val) { globals.common->put(name, makeDbgValue(val)); } template void SpirvShader::Impl::Debugger::State::Data::buildGlobal(const char *name, const sw::SIMD::PerLane &simd) { for(int lane = 0; lane < sw::SIMD::Width; lane++) { globals.lanes[lane]->put(name, makeDbgValue(simd[lane])); } } void SpirvShader::Impl::Debugger::State::Data::buildGlobals(State *state) { globals.common = std::make_shared(); globals.common->put("subgroupSize", vk::dbg::make_reference(state->globals.subgroupSize)); for(int lane = 0; lane < sw::SIMD::Width; lane++) { auto vc = std::make_shared(); vc->put("enabled", vk::dbg::make_reference(reinterpret_cast(state->globals.activeLaneMask[lane]))); for(auto &it : state->debugger->objects) { if(auto var = debug::cast(it.second.get())) { if(var->variable != 0) { auto data = state->debugger->shadow.get(state, var->variable); vc->put(var->name, var->type->value(data.dref(lane), true)); } } } auto spirv = buildSpirvVariables(state, lane); if(state->debugger->shaderHasDebugInfo) { vc->put("SPIR-V", spirv); } else { vc->extend(spirv->children()); } vc->extend(globals.common); globals.lanes[lane] = vc; } switch(state->debugger->shader->executionModel) { case spv::ExecutionModelGLCompute: { buildGlobal("numWorkgroups", state->globals.compute.numWorkgroups); buildGlobal("workgroupID", state->globals.compute.workgroupID); buildGlobal("workgroupSize", state->globals.compute.workgroupSize); buildGlobal("numSubgroups", state->globals.compute.numSubgroups); buildGlobal("subgroupIndex", state->globals.compute.subgroupIndex); buildGlobal("globalInvocationId", state->globals.compute.globalInvocationId); buildGlobal("localInvocationIndex", state->globals.compute.localInvocationIndex); break; } case spv::ExecutionModelFragment: { buildGlobal("viewIndex", state->globals.fragment.viewIndex); buildGlobal("fragCoord", state->globals.fragment.fragCoord); buildGlobal("pointCoord", state->globals.fragment.pointCoord); buildGlobal("windowSpacePosition", state->globals.fragment.windowSpacePosition); buildGlobal("helperInvocation", state->globals.fragment.helperInvocation); break; } case spv::ExecutionModelVertex: { buildGlobal("viewIndex", state->globals.vertex.viewIndex); buildGlobal("instanceIndex", state->globals.vertex.instanceIndex); buildGlobal("vertexIndex", state->globals.vertex.vertexIndex); break; } default: break; } } std::shared_ptr SpirvShader::Impl::Debugger::State::Data::buildSpirvVariables(State *state, int lane) const { return vk::dbg::Struct::create("SPIR-V", [&](auto &vc) { auto debugger = state->debugger; auto &entries = debugger->shadow.entries; std::vector ids; ids.reserve(entries.size()); for(auto it : entries) { ids.emplace_back(it.first); } std::sort(ids.begin(), ids.end()); for(auto id : ids) { auto &obj = debugger->shader->getObject(id); auto &objTy = debugger->shader->getType(obj.typeId()); auto name = "%" + std::to_string(id.value()); auto memory = debugger->shadow.get(state, id); switch(obj.kind) { case Object::Kind::Intermediate: case Object::Kind::Constant: if(auto val = buildSpirvValue(state, memory, objTy, lane)) { vc->put(name, val); } break; default: break; // Not handled yet. } } }); } std::shared_ptr SpirvShader::Impl::Debugger::State::Data::buildSpirvValue(State *state, Shadow::Memory memory, const SpirvShader::Type &type, int lane) const { auto debugger = state->debugger; auto shader = debugger->shader; switch(type.definition.opcode()) { case spv::OpTypeInt: return vk::dbg::make_reference(reinterpret_cast(memory.addr)[lane]); case spv::OpTypeFloat: return vk::dbg::make_reference(reinterpret_cast(memory.addr)[lane]); case spv::OpTypeVector: { auto elTy = shader->getType(type.element); return vk::dbg::Struct::create("vector", [&](auto &fields) { for(uint32_t i = 0; i < type.componentCount; i++) { if(auto val = buildSpirvValue(state, memory, elTy, lane)) { fields->put(vecElementName(i, type.componentCount), val); memory.addr += sizeof(uint32_t) * sw::SIMD::Width; } } }); } default: return nullptr; // Not handled yet } } //////////////////////////////////////////////////////////////////////////////// // sw::SpirvShader methods //////////////////////////////////////////////////////////////////////////////// void SpirvShader::dbgInit(const std::shared_ptr &ctx) { impl.debugger = new Impl::Debugger(this, ctx); } void SpirvShader::dbgTerm() { if(impl.debugger) { delete impl.debugger; } } void SpirvShader::dbgCreateFile() { auto dbg = impl.debugger; if(!dbg) { return; } int currentLine = 1; std::string source; for(auto insn : *this) { auto instruction = spvtools::spvInstructionBinaryToText( vk::SPIRV_VERSION, insn.wordPointer(0), insn.wordCount(), insns.data(), insns.size(), SPV_BINARY_TO_TEXT_OPTION_NO_HEADER) + "\n"; dbg->spirvLineMappings[insn.wordPointer(0)] = currentLine; currentLine += std::count(instruction.begin(), instruction.end(), '\n'); source += instruction; } std::string name; switch(executionModel) { case spv::ExecutionModelVertex: name = "VertexShader"; break; case spv::ExecutionModelFragment: name = "FragmentShader"; break; case spv::ExecutionModelGLCompute: name = "ComputeShader"; break; default: name = "SPIR-V Shader"; break; } static std::atomic id = { 0 }; name += std::to_string(id++) + ".spvasm"; dbg->spirvFile = dbg->ctx->lock().createVirtualFile(name.c_str(), source.c_str()); } void SpirvShader::dbgBeginEmit(EmitState *state) const { auto dbg = impl.debugger; if(!dbg) { return; } dbg->shaderHasDebugInfo = extensionsImported.count(Extension::OpenCLDebugInfo100) > 0; auto routine = state->routine; auto dbgState = rr::Call(&Impl::Debugger::State::create, dbg); routine->dbgState = dbgState; SetActiveLaneMask(state->activeLaneMask(), state); for(int i = 0; i < SIMD::Width; i++) { using Globals = Impl::Debugger::State::Globals; auto globals = dbgState + OFFSET(Impl::Debugger::State, globals); store(globals + OFFSET(Globals, subgroupSize), routine->invocationsPerSubgroup); switch(executionModel) { case spv::ExecutionModelGLCompute: { auto compute = globals + OFFSET(Globals, compute); store(compute + OFFSET(Globals::Compute, numWorkgroups), routine->numWorkgroups); store(compute + OFFSET(Globals::Compute, workgroupID), routine->workgroupID); store(compute + OFFSET(Globals::Compute, workgroupSize), routine->workgroupSize); store(compute + OFFSET(Globals::Compute, numSubgroups), routine->subgroupsPerWorkgroup); store(compute + OFFSET(Globals::Compute, subgroupIndex), routine->subgroupIndex); store(compute + OFFSET(Globals::Compute, globalInvocationId), routine->globalInvocationID); store(compute + OFFSET(Globals::Compute, localInvocationIndex), routine->localInvocationIndex); break; } case spv::ExecutionModelFragment: { auto fragment = globals + OFFSET(Globals, fragment); store(fragment + OFFSET(Globals::Fragment, viewIndex), routine->viewID); store(fragment + OFFSET(Globals::Fragment, fragCoord), routine->fragCoord); store(fragment + OFFSET(Globals::Fragment, pointCoord), routine->pointCoord); store(fragment + OFFSET(Globals::Fragment, windowSpacePosition), routine->windowSpacePosition); store(fragment + OFFSET(Globals::Fragment, helperInvocation), routine->helperInvocation); break; } case spv::ExecutionModelVertex: { auto vertex = globals + OFFSET(Globals, vertex); store(vertex + OFFSET(Globals::Vertex, viewIndex), routine->viewID); store(vertex + OFFSET(Globals::Vertex, instanceIndex), routine->instanceID); store(vertex + OFFSET(Globals::Vertex, vertexIndex), routine->vertexIndex); break; } default: break; } } } void SpirvShader::dbgEndEmit(EmitState *state) const { auto dbg = impl.debugger; if(!dbg) { return; } dbg->finalize(); rr::Call(&Impl::Debugger::State::destroy, state->routine->dbgState); } void SpirvShader::dbgBeginEmitInstruction(InsnIterator insn, EmitState *state) const { # if PRINT_EACH_EMITTED_INSTRUCTION { auto instruction = spvtools::spvInstructionBinaryToText( vk::SPIRV_VERSION, insn.wordPointer(0), insn.wordCount(), insns.data(), insns.size(), SPV_BINARY_TO_TEXT_OPTION_NO_HEADER); printf("%s\n", instruction.c_str()); } # endif // PRINT_EACH_EMITTED_INSTRUCTION # if PRINT_EACH_EXECUTED_INSTRUCTION { auto instruction = spvtools::spvInstructionBinaryToText( vk::SPIRV_VERSION, insn.wordPointer(0), insn.wordCount(), insns.data(), insns.size(), SPV_BINARY_TO_TEXT_OPTION_NO_HEADER); rr::Print("{0}\n", instruction); } # endif // PRINT_EACH_EXECUTED_INSTRUCTION // Only single line step over statement instructions. if(auto dbg = impl.debugger) { if(insn.opcode() == spv::OpLabel) { // Whenever we hit a label, force the next OpLine to be steppable. // This handles the case where we have control flow on the same line // For example: // while(true) { foo(); } // foo() should be repeatedly steppable. dbg->setNextSetLocationIsSteppable(); } if(!dbg->shaderHasDebugInfo) { // We're emitting debugger logic for SPIR-V. if(IsStatement(insn.opcode())) { auto line = dbg->spirvLineMappings.at(insn.wordPointer(0)); dbg->setLocation(state, dbg->spirvFile, line); } } } } void SpirvShader::dbgEndEmitInstruction(InsnIterator insn, EmitState *state) const { auto dbg = impl.debugger; if(!dbg) { return; } switch(insn.opcode()) { case spv::OpVariable: case spv::OpConstant: // TODO: Move constants out of shadow memory. case spv::OpConstantNull: case spv::OpConstantTrue: case spv::OpConstantFalse: case spv::OpConstantComposite: dbg->shadow.create(this, state, insn.resultId()); break; default: { auto resIt = dbg->results.find(insn.wordPointer(0)); if(resIt != dbg->results.end()) { dbg->shadow.create(this, state, resIt->second); } } } } void SpirvShader::dbgUpdateActiveLaneMask(RValue mask, EmitState *state) const { auto dbg = impl.debugger; if(!dbg) { return; } auto dbgState = state->routine->dbgState; auto globals = dbgState + OFFSET(Impl::Debugger::State, globals); store(globals + OFFSET(Impl::Debugger::State::Globals, activeLaneMask), mask); } void SpirvShader::dbgDeclareResult(const InsnIterator &insn, Object::ID resultId) const { auto dbg = impl.debugger; if(!dbg) { return; } dbg->results.emplace(insn.wordPointer(0), resultId); } SpirvShader::EmitResult SpirvShader::EmitLine(InsnIterator insn, EmitState *state) const { if(auto dbg = impl.debugger) { auto path = getString(insn.word(1)); auto line = insn.word(2); dbg->setLocation(state, path.c_str(), line); } return EmitResult::Continue; } void SpirvShader::DefineOpenCLDebugInfo100(const InsnIterator &insn) { # if PRINT_EACH_DEFINED_DBG_INSTRUCTION { auto instruction = spvtools::spvInstructionBinaryToText( vk::SPIRV_VERSION, insn.wordPointer(0), insn.wordCount(), insns.data(), insns.size(), SPV_BINARY_TO_TEXT_OPTION_NO_HEADER); printf("%s\n", instruction.c_str()); } # endif // PRINT_EACH_DEFINED_DBG_INSTRUCTION auto dbg = impl.debugger; if(!dbg) { return; } dbg->process(insn, nullptr, Impl::Debugger::Pass::Define); } SpirvShader::EmitResult SpirvShader::EmitOpenCLDebugInfo100(InsnIterator insn, EmitState *state) const { if(auto dbg = impl.debugger) { dbg->process(insn, state, Impl::Debugger::Pass::Emit); } return EmitResult::Continue; } } // namespace sw #else // ENABLE_VK_DEBUGGER // Stub implementations of the dbgXXX functions. namespace sw { void SpirvShader::dbgInit(const std::shared_ptr &dbgctx) {} void SpirvShader::dbgTerm() {} void SpirvShader::dbgCreateFile() {} void SpirvShader::dbgBeginEmit(EmitState *state) const {} void SpirvShader::dbgEndEmit(EmitState *state) const {} void SpirvShader::dbgBeginEmitInstruction(InsnIterator insn, EmitState *state) const {} void SpirvShader::dbgEndEmitInstruction(InsnIterator insn, EmitState *state) const {} void SpirvShader::dbgExposeIntermediate(Object::ID id, EmitState *state) const {} void SpirvShader::dbgUpdateActiveLaneMask(RValue mask, EmitState *state) const {} void SpirvShader::dbgDeclareResult(const InsnIterator &insn, Object::ID resultId) const {} void SpirvShader::DefineOpenCLDebugInfo100(const InsnIterator &insn) {} SpirvShader::EmitResult SpirvShader::EmitOpenCLDebugInfo100(InsnIterator insn, EmitState *state) const { return EmitResult::Continue; } SpirvShader::EmitResult SpirvShader::EmitLine(InsnIterator insn, EmitState *state) const { return EmitResult::Continue; } } // namespace sw #endif // ENABLE_VK_DEBUGGER