Implement load/store through access chains and composite swizzles

* This is enough for very simple shaders. We add a utility function for setting
  values and handling pointer changes.
This commit is contained in:
baldurk
2020-02-20 17:38:30 +00:00
parent 5fee776a52
commit 5ae8fc1ed7
2 changed files with 281 additions and 7 deletions
+277 -7
View File
@@ -23,9 +23,43 @@
******************************************************************************/
#include "spirv_debug.h"
#include <math.h>
#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<Id> &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<Id> &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<Id> &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<ShaderVariableChange> changes;
ShaderVariableChange basechange;
basechange.before = debugger.EvaluatePointerVariable(ids[ptrid]);
rdcarray<Id> &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<rdcarray<ShaderVariable>> &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<uint32_t> 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<ShaderVariable> 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 = "<return value>";
}
@@ -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