diff --git a/renderdoc/driver/shaders/spirv/spirv_debug.cpp b/renderdoc/driver/shaders/spirv/spirv_debug.cpp index c451a7a5d..c38bb02aa 100644 --- a/renderdoc/driver/shaders/spirv/spirv_debug.cpp +++ b/renderdoc/driver/shaders/spirv/spirv_debug.cpp @@ -23,9 +23,43 @@ ******************************************************************************/ #include "spirv_debug.h" +#include #include "common/formatting.h" #include "spirv_op_helpers.h" +#if defined(_MSC_VER) +#define finite _finite +#endif + +static bool ContainsNaNInf(const ShaderVariable &val) +{ + bool ret = false; + + for(const ShaderVariable &member : val.members) + ret |= ContainsNaNInf(member); + + int count = int(val.rows) * int(val.columns); + + if(val.type == VarType::Float || val.type == VarType::Half) + { + for(int i = 0; i < count; i++) + { + ret |= !finite(val.value.fv[i]); + ret |= isnan(val.value.fv[i]) != 0; + } + } + else if(val.type == VarType::Double) + { + for(int i = 0; i < count; i++) + { + ret |= !finite(val.value.dv[i]); + ret |= isnan(val.value.dv[i]) != 0; + } + } + + return ret; +} + namespace rdcspv { ThreadState::ThreadState(int workgroupIdx, Debugger &debug, const GlobalState &globalState) @@ -79,9 +113,7 @@ void ThreadState::EnterFunction(ShaderDebugState *state, const rdcarray &arg // copied in and points to whatever storage that is. // That means we don't have to allocate anything here, we just set up the ID and copy the // value from the argument - ids[param.result] = ids[arguments[arg]]; - ids[param.result].name = debugger.GetRawName(param.result); - live.push_back(param.result); + SetDst(state, param.result, ids[arguments[arg]]); } else { @@ -123,10 +155,10 @@ void ThreadState::EnterFunction(ShaderDebugState *state, const rdcarray &arg if(decl.HasInitializer()) AssignValue(stackvar, ids[decl.initializer]); + // TODO we need to handle re-entry into functions (ID lifetime) live.removeOne(decl.result); - ids[decl.result] = debugger.MakePointerVariable(decl.result, &stackvar); - live.push_back(decl.result); + SetDst(state, decl.result, debugger.MakePointerVariable(decl.result, &stackvar)); it++; i++; @@ -136,6 +168,108 @@ void ThreadState::EnterFunction(ShaderDebugState *state, const rdcarray &arg nextInstruction = debugger.GetInstructionForIter(it); } +const ShaderVariable &ThreadState::GetSrc(Id id) +{ + return ids[id]; +} + +void ThreadState::SetDst(ShaderDebugState *state, Id id, const ShaderVariable &val) +{ + // if we don't have a state to track, we can take a much faster pa We just update the value and + // return. We don't have to propagate ID changes between aliased pointers because *internally* we + // always look up pointers and don't care about aliasing. That's only needed for *external* facing + // things because we have to update the changes for all copied pointer values. + if(!state) + { + if(ids[id].name.empty()) + { + // for uninitialised values, init by copying + ids[id] = val; + ids[id].name = debugger.GetRawName(id); + live.push_back(id); + } + else + { + RDCASSERT(ids[id].isPointer); + // otherwise just update the pointed-to value (only pointers should be existing before being + // assigned) + debugger.WriteThroughPointer(ids[id], val); + } + + return; + } + + // otherwise when we're tracking changes, take a slower pa + + if(ContainsNaNInf(val)) + state->flags |= ShaderEvents::GeneratedNanOrInf; + + ShaderVariable &var = ids[id]; + + if(!var.name.empty()) + { + // if this ID was already initialised, we must have a change of a pointer. Otherwise we're SSA + // form so no other ID should change. + + RDCASSERT(var.isPointer); + + // if var is a pointer we update the underlying storage and generate at least one change, plus + // any additional ones for other pointers. + Id ptrid = debugger.GetPointerBaseId(var); + + rdcarray changes; + ShaderVariableChange basechange; + basechange.before = debugger.EvaluatePointerVariable(ids[ptrid]); + + rdcarray &pointers = pointersForId[ptrid]; + + changes.resize(pointers.size()); + + // for every other pointer, evaluate its value now before + for(size_t i = 0; i < pointers.size(); i++) + changes[i].before = debugger.EvaluatePointerVariable(ids[pointers[i]]); + + debugger.WriteThroughPointer(var, val); + + // now evaluate the value after + for(size_t i = 0; i < pointers.size(); i++) + changes[i].after = debugger.EvaluatePointerVariable(ids[pointers[i]]); + + // if the pointer we're writing is one of the aliased pointers, be sure we add it even if it's a + // no-op change + int ptrIdx = pointers.indexOf(id); + + if(ptrIdx >= 0) + { + state->changes.push_back(changes[ptrIdx]); + changes.erase(ptrIdx); + } + + // remove any no-op changes. Some pointers might point to the same ID but a child that wasn't + // written to. Note that this might not actually mean nothing was changed (if e.g. we're + // assigning the same value) but that false negative is not a concern. + changes.removeIf([](const ShaderVariableChange &c) { return c.before == c.after; }); + + state->changes.append(changes); + + // always add a change for the base storage variable written itself, even if that's a no-op. + // This one is not included in any of the pointers lists above + basechange.after = debugger.EvaluatePointerVariable(ids[ptrid]); + state->changes.push_back(basechange); + } + else + { + // otherwise it's a new SSA variable, record a change-from-nothing + ids[id] = val; + ids[id].name = debugger.GetRawName(id); + live.push_back(id); + + ShaderVariableChange change; + change.after = debugger.EvaluatePointerVariable(ids[id]); + state->changes.push_back(change); + } +} + void ThreadState::StepNext(ShaderDebugState *state, const rdcarray> &prevWorkgroup) { @@ -154,6 +288,142 @@ void ThreadState::StepNext(ShaderDebugState *state, switch(opdata.op) { + case Op::Load: + { + // we currently handle pointers as fixed storage, so a load becomes a copy + OpLoad load(it); + + // ignore + (void)load.memoryAccess; + + // get the pointer value, evaluate it (i.e. dereference) and store the result + SetDst(state, load.result, debugger.EvaluatePointerVariable(GetSrc(load.pointer))); + + break; + } + case Op::Store: + { + OpStore store(it); + + // ignore + (void)store.memoryAccess; + + RDCASSERT(ids[store.pointer].isPointer); + + SetDst(state, store.pointer, GetSrc(store.object)); + + break; + } + case Op::AccessChain: + { + OpAccessChain chain(it); + + rdcarray indices; + + // evaluate the indices + indices.reserve(chain.indexes.size()); + for(Id id : chain.indexes) + indices.push_back(GetSrc(id).value.u.x); + + SetDst(state, chain.result, + debugger.MakeCompositePointer(ids[chain.base], chain.base, indices)); + + break; + } + case Op::CompositeExtract: + { + OpCompositeExtract extract(it); + + // to re-use composite/access chain logic, temporarily make a pointer to the composite + // (illegal in SPIR-V) + ShaderVariable ptr = + debugger.MakeCompositePointer(ids[extract.composite], extract.composite, extract.indexes); + + // then evaluate it, to get the extracted value + SetDst(state, extract.result, debugger.EvaluatePointerVariable(ptr)); + + break; + } + case Op::CompositeConstruct: + { + OpCompositeConstruct construct(it); + + ShaderVariable var; + + const DataType &type = debugger.GetType(construct.resultType); + + RDCASSERT(!construct.constituents.empty()); + + if(type.type == DataType::ArrayType || type.type == DataType::StructType) + { + var.members.resize(construct.constituents.size()); + for(size_t i = 0; i < construct.constituents.size(); i++) + { + ShaderVariable &mem = var.members[i]; + mem = GetSrc(construct.constituents[i]); + + if(type.type == DataType::ArrayType) + mem.name = StringFormat::Fmt("[%zu]", i); + else + mem.name = StringFormat::Fmt("_child%zu", i); + } + } + else if(type.type == DataType::VectorType) + { + RDCASSERT(construct.constituents.size() <= 4); + + var.type = type.scalar().Type(); + var.rows = 1; + var.columns = RDCMAX(1U, type.vector().count); + + // it is possible to construct larger vectors from a collection of scalars and smaller + // vectors. + size_t dst = 0; + for(size_t i = 0; i < construct.constituents.size(); i++) + { + ShaderVariable src = GetSrc(construct.constituents[i]); + + RDCASSERTEQUAL(src.rows, 1); + + for(size_t j = 0; j < src.columns; j++) + { + if(VarTypeByteSize(var.type) == 8) + var.value.u64v[dst++] = src.value.u64v[j]; + else + var.value.uv[dst++] = src.value.uv[j]; + } + } + } + else if(type.type == DataType::MatrixType) + { + // matrices are constructed from a list of columns + var.type = type.scalar().Type(); + var.columns = RDCMAX(1U, type.matrix().count); + var.rows = RDCMAX(1U, type.vector().count); + + RDCASSERTEQUAL(var.columns, construct.constituents.size()); + + rdcarray columns; + for(size_t i = 0; i < construct.constituents.size(); i++) + columns[i] = GetSrc(construct.constituents[i]); + + for(size_t r = 0; r < var.rows; r++) + { + for(size_t c = 0; c < var.columns; c++) + { + if(VarTypeByteSize(var.type) == 8) + var.value.u64v[r * var.columns + c] = columns[c].value.u64v[r]; + else + var.value.uv[r * var.columns + c] = columns[c].value.uv[r]; + } + } + } + + SetDst(state, construct.result, var); + + break; + } + case Op::FunctionCall: { OpFunctionCall call(it); @@ -172,7 +442,7 @@ void ThreadState::StepNext(ShaderDebugState *state, } else { - // process ret; + SetDst(state, call.result, returnValue); returnValue.name.clear(); } break; @@ -203,7 +473,7 @@ void ThreadState::StepNext(ShaderDebugState *state, { OpReturnValue ret(it); - // TODO: returnValue = MakeValue(ret.value); + returnValue = GetSrc(ret.value); returnValue.name = ""; } diff --git a/renderdoc/driver/shaders/spirv/spirv_debug.h b/renderdoc/driver/shaders/spirv/spirv_debug.h index 7c1e39ffb..0e881bef1 100644 --- a/renderdoc/driver/shaders/spirv/spirv_debug.h +++ b/renderdoc/driver/shaders/spirv/spirv_debug.h @@ -108,6 +108,10 @@ struct ThreadState // index in the pixel quad int workgroupIndex; bool done; + +private: + const ShaderVariable &GetSrc(Id id); + void SetDst(ShaderDebugState *state, Id id, const ShaderVariable &val); }; class Debugger : public Processor, public ShaderDebugger