SPIR-V debugger support for VK_KHR_compute_shader_derivatives

i.e.
SPV_KHR_compute_shader_derivatives
SPV_NV_compute_shader_derivatives
This commit is contained in:
Jake Turner
2025-11-26 12:17:45 +13:00
parent 97755b5ebb
commit a9acee3106
7 changed files with 223 additions and 43 deletions
@@ -174,9 +174,13 @@ static ShaderVariable MakeIdentity(const rdcspv::DataType &type, float val, bool
namespace rdcspv
{
ThreadState::ThreadState(Debugger &debug, const GlobalState &globalState)
ThreadState::ThreadState(Debugger &debug, const GlobalState &globalState, ShaderStage stage)
: debugger(debug), global(globalState)
{
// Default to Coarse, choose Fine for compute shaders
defaultDeriveType = DerivType::Coarse;
if(stage == ShaderStage::Compute)
defaultDeriveType = DerivType::Fine;
}
ThreadState::~ThreadState()
@@ -1173,6 +1177,7 @@ void ThreadState::StepNext(bool useDebugState, const uint32_t steps,
// spec allows the implementation to choose what DPdx means (coarse or fine), so we choose
// coarse which seems a reasonable default. In future we could driver-detect the selection in
// use (assuming it's not dynamic base on circumstances)
// Compute shaders use Fine by default
case Op::DPdx:
case Op::DPdy:
case Op::DPdxCoarse:
@@ -1187,9 +1192,11 @@ void ThreadState::StepNext(bool useDebugState, const uint32_t steps,
if(opdata.op == Op::DPdy || opdata.op == Op::DPdyCoarse || opdata.op == Op::DPdyFine)
dir = DDY;
DerivType type = Coarse;
DerivType type = defaultDeriveType;
if(opdata.op == Op::DPdxFine || opdata.op == Op::DPdyFine)
type = Fine;
if(opdata.op == Op::DPdxCoarse || opdata.op == Op::DPdyCoarse)
type = Coarse;
SetDst(deriv.result, CalcDeriv(dir, type, workgroup, deriv.p));
+2 -1
View File
@@ -248,7 +248,7 @@ class Debugger;
struct ThreadState
{
ThreadState(Debugger &debug, const GlobalState &globalState);
ThreadState(Debugger &debug, const GlobalState &globalState, ShaderStage stage);
~ThreadState();
void EnterEntryPoint(bool useDebugState);
@@ -454,6 +454,7 @@ private:
AtomicStore(&atomic_pendingResultStatus, (int32_t)status);
}
DerivType defaultDeriveType;
ShaderDebugState pendingDebugState;
bool hasDebugState = false;
uint32_t stepIndex = 0;
@@ -467,6 +467,7 @@ void Reflector::CheckDebuggable(bool &debuggable, rdcstr &debugStatus) const
"SPV_KHR_subgroup_vote",
"SPV_KHR_terminate_invocation",
"SPV_KHR_vulkan_memory_model",
"SPV_KHR_compute_shader_derivatives",
// EXT extensions
"SPV_EXT_demote_to_helper_invocation",
@@ -486,6 +487,7 @@ void Reflector::CheckDebuggable(bool &debuggable, rdcstr &debugStatus) const
"SPV_GOOGLE_decorate_string",
"SPV_GOOGLE_hlsl_functionality1",
"SPV_GOOGLE_user_type",
"SPV_NV_compute_shader_derivatives",
};
// whitelist supported extensions
@@ -616,6 +618,8 @@ void Reflector::CheckDebuggable(bool &debuggable, rdcstr &debugStatus) const
case Capability::GroupNonUniformVote:
case Capability::SubgroupBallotKHR:
case Capability::SubgroupVoteKHR:
case Capability::ComputeDerivativeGroupQuadsKHR:
case Capability::ComputeDerivativeGroupLinearKHR:
{
supported = true;
break;
@@ -665,14 +669,6 @@ void Reflector::CheckDebuggable(bool &debuggable, rdcstr &debugStatus) const
break;
}
// SPV_KHR_compute_shader_derivatives
case Capability::ComputeDerivativeGroupQuadsKHR:
case Capability::ComputeDerivativeGroupLinearKHR:
{
supported = false;
break;
}
// SPV_KHR_float_controls2
case Capability::FloatControls2:
{
@@ -1064,7 +1060,7 @@ ShaderDebugTrace *Debugger::BeginDebug(DebugAPIWrapper *api, const ShaderStage s
queuedJobs.resize(threadsInWorkgroup);
for(uint32_t i = 0; i < threadsInWorkgroup; i++)
{
workgroup.push_back(ThreadState(*this, global));
workgroup.push_back(ThreadState(*this, global, stage));
queuedDeviceThreadSteps[i] = false;
queuedGpuMathOps[i] = false;
queuedGpuSampleGatherOps[i] = false;
@@ -1706,6 +1702,11 @@ ShaderDebugTrace *Debugger::BeginDebug(DebugAPIWrapper *api, const ShaderStage s
lane.quadLaneIndex = apiWrapper->GetThreadProperty(i, ThreadProperty::QuadLane);
lane.quadId = apiWrapper->GetThreadProperty(i, ThreadProperty::QuadId);
}
if(stage == ShaderStage::Compute)
{
lane.quadLaneIndex = apiWrapper->GetThreadProperty(i, ThreadProperty::QuadLane);
lane.quadId = apiWrapper->GetThreadProperty(i, ThreadProperty::QuadId);
}
lane.subgroupId = apiWrapper->GetThreadProperty(i, ThreadProperty::SubgroupId);
lane.dead = apiWrapper->GetThreadProperty(i, ThreadProperty::Active) == 0;
@@ -547,6 +547,13 @@ enum class ThreadScope : uint32_t
BITMASK_OPERATORS(ThreadScope);
enum class ComputeDerivativeMode : uint8_t
{
None,
Linear,
Quad,
};
class Processor
{
public:
@@ -917,6 +917,8 @@ void Reflector::MakeReflection(const GraphicsAPI sourceAPI, const ShaderStage st
CheckDebuggable(reflection.debugInfo.debuggable, reflection.debugInfo.debugStatus);
patchData.derivativeMode = ComputeDerivativeMode::None;
const EntryPoint *entry = NULL;
for(const EntryPoint &e : entries)
{
@@ -967,6 +969,12 @@ void Reflector::MakeReflection(const GraphicsAPI sourceAPI, const ShaderStage st
patchData.maxPrimitives = e.executionModes.others[idx].outputPrimitivesEXT;
}
if(e.executionModes.others.contains(rdcspv::ExecutionMode::DerivativeGroupQuadsKHR))
patchData.derivativeMode = ComputeDerivativeMode::Quad;
if(e.executionModes.others.contains(rdcspv::ExecutionMode::DerivativeGroupLinearKHR))
patchData.derivativeMode = ComputeDerivativeMode::Linear;
// vulkan spec says "If an object is decorated with the WorkgroupSize decoration, this must take
// precedence over any execution mode set for LocalSize."
for(auto it : constants)
@@ -78,6 +78,7 @@ struct SPIRVPatchData
rdcarray<uint32_t> specIDs;
rdcspv::ThreadScope threadScope;
rdcspv::ComputeDerivativeMode derivativeMode;
// for mesh shaders, the maximum number of vertices/primitives generated by each meshlet
uint32_t maxVertices = 0, maxPrimitives = 0;
+186 -31
View File
@@ -6469,15 +6469,39 @@ ShaderDebugTrace *VulkanReplay::DebugComputeCommon(ShaderStage stage, uint32_t e
threadDim[1] = shadRefl.refl->dispatchThreadsDimension[1];
threadDim[2] = shadRefl.refl->dispatchThreadsDimension[2];
if((threadid[0] >= threadDim[0]) || (threadid[1] >= threadDim[1]) || (threadid[2] >= threadDim[2]))
{
RDCLOG("Invalid threadid %d,%d,%d selected from group %dx%dx%d", threadid[0], threadid[1],
threadid[2], threadDim[0], threadDim[1], threadDim[2]);
return new ShaderDebugTrace();
}
if((groupid[0] >= action->dispatchDimension[0]) || (groupid[1] >= action->dispatchDimension[1]) ||
(groupid[2] >= action->dispatchDimension[2]))
{
RDCLOG("Invalid groupid %d,%d,%d selected from dispatch %dx%dx%d", groupid[0], groupid[1],
groupid[2], action->dispatchDimension[0], action->dispatchDimension[1],
action->dispatchDimension[2]);
return new ShaderDebugTrace();
}
SubgroupCapability subgroupCapability = SubgroupCapability::None;
uint32_t maxSubgroupSize = 1;
CalculateSubgroupProperties(maxSubgroupSize, subgroupCapability);
uint32_t numThreads = 1;
if(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Subgroup)
bool hasQuadDerivatives =
(shadRefl.patchData.derivativeMode != rdcspv::ComputeDerivativeMode::None);
bool hasSubgroupScoope =
(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Subgroup) ? true : false;
bool hasWorkgroupScope =
(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Workgroup) ? true : false;
if(hasQuadDerivatives)
numThreads = RDCMAX(numThreads, 4U);
if(hasSubgroupScoope)
numThreads = RDCMAX(numThreads, maxSubgroupSize);
if(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Workgroup)
if(hasWorkgroupScope)
numThreads = RDCMAX(numThreads, threadDim[0] * threadDim[1] * threadDim[2]);
apiWrapper->thread_builtins.resize(numThreads);
@@ -6493,9 +6517,32 @@ ShaderDebugTrace *VulkanReplay::DebugComputeCommon(ShaderStage stage, uint32_t e
global_builtins[ShaderBuiltin::GroupIndex] =
ShaderVariable(rdcstr(), groupid[0], groupid[1], groupid[2], 0U);
const uint32_t quadIdOffset = 10000;
const uint32_t quadDerivMode = (uint32_t)shadRefl.patchData.derivativeMode;
uint32_t countQuadX = ~0U;
uint32_t countQuadY = ~0U;
uint32_t quadW = ~0U;
uint32_t quadH = ~0U;
if(hasQuadDerivatives)
{
// linear: 4x1x1
// quad: 2x2x1
const uint32_t quadWidths[3] = {~0U, 4, 2};
const uint32_t quadHeights[3] = {~0U, 1, 2};
quadW = quadWidths[quadDerivMode];
quadH = quadHeights[quadDerivMode];
countQuadX = threadDim[0] / quadW;
countQuadY = threadDim[1] / quadH;
RDCASSERTEQUAL(threadDim[0], countQuadX * quadW);
RDCASSERTEQUAL(threadDim[1], countQuadY * quadH);
}
// if we need to fetch subgroup data, do that now
uint32_t laneIndex = 0;
if(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Subgroup)
if(hasSubgroupScoope)
{
SpecData specData = {};
@@ -6643,19 +6690,16 @@ ShaderDebugTrace *VulkanReplay::DebugComputeCommon(ShaderStage stage, uint32_t e
// output is in input signature order.
byte *LaneData = (byte *)(winner + 1);
numThreads = 4;
const uint32_t subgroupSize = winner->subgroupSize;
if(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Subgroup)
{
RDCASSERTNOTEQUAL(subgroupSize, 0);
numThreads = RDCMAX(numThreads, subgroupSize);
}
RDCASSERTNOTEQUAL(subgroupSize, 0);
numThreads = RDCMAX(numThreads, subgroupSize);
if(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Workgroup)
{
if(hasQuadDerivatives)
RDCASSERT(numThreads >= 4);
if(hasWorkgroupScope)
numThreads = RDCMAX(numThreads, threadDim[0] * threadDim[1] * threadDim[2]);
}
apiWrapper->global_builtins[ShaderBuiltin::NumSubgroups] =
ShaderVariable(rdcstr(), winner->numSubgroups, 0U, 0U, 0U);
@@ -6676,13 +6720,36 @@ ShaderDebugTrace *VulkanReplay::DebugComputeCommon(ShaderStage stage, uint32_t e
ComputeLaneData *compData = (ComputeLaneData *)value;
value += sizeof(ComputeLaneData);
// should we try to verify that the GPU assigned subgroups as we expect? this assumes tightly wrapped subgroups
uint32_t lane = t;
if(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Workgroup)
uint32_t quadId = ~0U;
uint32_t quadLaneIndex = ~0U;
if(hasQuadDerivatives)
{
lane = compData->threadid[2] * threadDim[0] * threadDim[1] +
compData->threadid[1] * threadDim[0] + compData->threadid[0];
uint32_t quadX = (compData->threadid[0] / quadW);
uint32_t quadY = (compData->threadid[1] / quadH);
uint32_t quadZ = compData->threadid[2];
quadId = quadX + (quadY * countQuadX) + (quadZ * countQuadY * countQuadX);
quadLaneIndex = (compData->threadid[0] % quadW) + (compData->threadid[1] % quadH) * 2;
apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::QuadLane] = quadLaneIndex;
apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::QuadId] =
quadId + quadIdOffset;
}
if(hasWorkgroupScope)
{
// When quad derivatives are enabled, use the quad derivative layout
if(hasQuadDerivatives)
{
lane = quadId * 4 + quadLaneIndex;
}
else
{
// Assume linear layout for the subgroup : tightly wrapped
lane = compData->threadid[2] * threadDim[0] * threadDim[1] +
compData->threadid[1] * threadDim[0] + compData->threadid[0];
}
}
if(rdcfixedarray<uint32_t, 3>(compData->threadid) == threadid && subgroupData->isActive)
@@ -6716,24 +6783,42 @@ ShaderDebugTrace *VulkanReplay::DebugComputeCommon(ShaderStage stage, uint32_t e
}
// if we're simulating the whole workgroup we need to fill in the thread IDs of other threads
if(shadRefl.patchData.threadScope & rdcspv::ThreadScope::Workgroup)
if(hasWorkgroupScope)
{
uint32_t i = 0;
for(uint32_t tz = 0; tz < threadDim[2]; tz++)
{
for(uint32_t ty = 0; ty < threadDim[1]; ty++)
{
for(uint32_t tx = 0; tx < threadDim[0]; tx++)
{
uint32_t quadId = ~0U;
uint32_t quadLaneIndex = ~0U;
uint32_t lane = ~0U;
if(hasQuadDerivatives)
{
// When quad derivatives are enabled, use the quad derivative layout
uint32_t quadX = (tx / quadW);
uint32_t quadY = (ty / quadH);
uint32_t quadZ = tz;
quadId = quadX + (quadY * countQuadX) + (quadZ * countQuadY * countQuadX);
quadLaneIndex = (tx % quadW) + (ty % quadH) * 2;
lane = quadId * 4 + quadLaneIndex;
}
else
{
// Assume linear layout for the subgroup : tightly wrapped
lane = tz * threadDim[0] * threadDim[1] + ty * threadDim[0] + tx;
}
std::unordered_map<ShaderBuiltin, ShaderVariable> &thread_builtins =
apiWrapper->thread_builtins[i];
apiWrapper->thread_builtins[lane];
thread_builtins[ShaderBuiltin::GroupThreadIndex] =
ShaderVariable(rdcstr(), tx, ty, tz, 0U);
thread_builtins[ShaderBuiltin::GroupFlatIndex] = ShaderVariable(
rdcstr(), tz * threadDim[0] * threadDim[1] + ty * threadDim[0] + tx, 0U, 0U, 0U);
if(apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::Active])
if(apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::Active])
{
// assert that this is the thread we expect it to be
RDCASSERTEQUAL(thread_builtins[ShaderBuiltin::DispatchThreadIndex].value.u32v[0],
@@ -6744,9 +6829,18 @@ ShaderDebugTrace *VulkanReplay::DebugComputeCommon(ShaderStage stage, uint32_t e
groupid[2] * threadDim[2] + tz);
RDCASSERTEQUAL(thread_builtins[ShaderBuiltin::IndexInSubgroup].value.u32v[0],
i % subgroupSize);
lane % subgroupSize);
RDCASSERTEQUAL(thread_builtins[ShaderBuiltin::SubgroupIndexInWorkgroup].value.u32v[0],
i / subgroupSize);
lane / subgroupSize);
if(hasQuadDerivatives)
{
RDCASSERTEQUAL(
apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::QuadLane],
quadLaneIndex);
RDCASSERTEQUAL(apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::QuadId],
quadId + quadIdOffset);
}
}
else
{
@@ -6755,20 +6849,27 @@ ShaderDebugTrace *VulkanReplay::DebugComputeCommon(ShaderStage stage, uint32_t e
groupid[1] * threadDim[1] + ty, groupid[2] * threadDim[2] + tz, 0U);
// tightly wrap subgroups, this is likely not how the GPU actually assigns them
thread_builtins[ShaderBuiltin::IndexInSubgroup] =
ShaderVariable(rdcstr(), i % subgroupSize, 0U, 0U, 0U);
ShaderVariable(rdcstr(), lane % subgroupSize, 0U, 0U, 0U);
thread_builtins[ShaderBuiltin::SubgroupIndexInWorkgroup] =
ShaderVariable(rdcstr(), i / subgroupSize, 0U, 0U, 0U);
apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::Active] = 1;
apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::SubgroupId] =
i % subgroupSize;
}
ShaderVariable(rdcstr(), lane / subgroupSize, 0U, 0U, 0U);
apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::Active] = 1;
apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::SubgroupId] =
lane % subgroupSize;
i++;
if(hasQuadDerivatives)
{
apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::QuadLane] =
quadLaneIndex;
apiWrapper->thread_props[lane][(size_t)rdcspv::ThreadProperty::QuadId] = quadId;
}
}
}
}
}
}
// Each member of a quad should belong to the same subgroup. We assume this and do not validate it
// Add inactive padding lanes to round up to the subgroup size
const uint32_t numPaddingThreads = AlignUp(numThreads, subgroupSize) - numThreads;
if(numPaddingThreads > 0)
@@ -6806,11 +6907,11 @@ ShaderDebugTrace *VulkanReplay::DebugComputeCommon(ShaderStage stage, uint32_t e
}
else
{
// if we have more than one thread here, that means we need to simulate the whole workgroup.
// if we need to simulate the whole workgroup.
// we assume the layout of this is irrelevant and don't attempt to read it back from the GPU
// like we do with subgroups. We lay things out in plain linear order, along X and then Y and
// then Z, with groups iterated together.
if(numThreads > 1)
if(hasWorkgroupScope)
{
uint32_t i = 0;
for(uint32_t tz = 0; tz < threadDim[2]; tz++)
@@ -6830,6 +6931,19 @@ ShaderDebugTrace *VulkanReplay::DebugComputeCommon(ShaderStage stage, uint32_t e
rdcstr(), tz * threadDim[0] * threadDim[1] + ty * threadDim[0] + tx, 0U, 0U, 0U);
apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::Active] = 1;
if(hasQuadDerivatives)
{
uint32_t quadX = (tx / quadW);
uint32_t quadY = (ty / quadH);
uint32_t quadZ = tz;
uint32_t quadId =
quadIdOffset + quadX + (quadY * countQuadX) + (quadZ * countQuadY * countQuadX);
uint32_t quadLaneIndex = (tx % quadW) + (ty % quadH) * 2;
apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::QuadLane] = quadLaneIndex;
apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::QuadId] = quadId;
}
if(rdcfixedarray<uint32_t, 3>({tx, ty, tz}) == threadid)
{
laneIndex = i;
@@ -6840,8 +6954,49 @@ ShaderDebugTrace *VulkanReplay::DebugComputeCommon(ShaderStage stage, uint32_t e
}
}
}
else if(hasQuadDerivatives)
{
// need to simulate the whole quad, do not readback from the GPU like we do with subgroups
// the quad is guaranteed to be in the same subgroup
// We lay things out in linear or quad order
RDCASSERTEQUAL(numThreads, 4U);
uint32_t txMin = (threadid[0] / quadW) * quadW;
uint32_t tyMin = (threadid[1] / quadH) * quadH;
uint32_t tz = threadid[2];
uint32_t quadZ = tz;
for(uint32_t i = 0; i < 4U; ++i)
{
uint32_t tx = txMin + (i % quadW);
uint32_t ty = tyMin + (i / quadW);
std::unordered_map<ShaderBuiltin, ShaderVariable> &thread_builtins =
apiWrapper->thread_builtins[i];
thread_builtins[ShaderBuiltin::DispatchThreadIndex] =
ShaderVariable(rdcstr(), groupid[0] * threadDim[0] + tx, groupid[1] * threadDim[1] + ty,
groupid[2] * threadDim[2] + tz, 0U);
thread_builtins[ShaderBuiltin::GroupThreadIndex] = ShaderVariable(rdcstr(), tx, ty, tz, 0U);
thread_builtins[ShaderBuiltin::GroupFlatIndex] = ShaderVariable(
rdcstr(), tz * threadDim[0] * threadDim[1] + ty * threadDim[0] + tx, 0U, 0U, 0U);
apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::Active] = 1;
if(hasQuadDerivatives)
{
uint32_t quadX = (tx / quadW);
uint32_t quadY = (ty / quadH);
uint32_t quadId =
quadIdOffset + quadX + (quadY * countQuadX) + (quadZ * countQuadY * countQuadX);
uint32_t quadLaneIndex = (tx % quadW) + (ty % quadH) * 2;
apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::QuadLane] = quadLaneIndex;
apiWrapper->thread_props[i][(size_t)rdcspv::ThreadProperty::QuadId] = quadId;
}
if(rdcfixedarray<uint32_t, 3>({tx, ty, tz}) == threadid)
laneIndex = i;
}
}
else
{
RDCASSERTEQUAL(numThreads, 1U);
// simple single-thread case
apiWrapper->thread_props[0][(size_t)rdcspv::ThreadProperty::Active] = 1;
apiWrapper->thread_props[0][(size_t)rdcspv::ThreadProperty::SubgroupId] = 0;