diff --git a/renderdoc/driver/shaders/spirv/spirv_debug.cpp b/renderdoc/driver/shaders/spirv/spirv_debug.cpp index eb8e0e65b..e1f05b07f 100644 --- a/renderdoc/driver/shaders/spirv/spirv_debug.cpp +++ b/renderdoc/driver/shaders/spirv/spirv_debug.cpp @@ -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)); diff --git a/renderdoc/driver/shaders/spirv/spirv_debug.h b/renderdoc/driver/shaders/spirv/spirv_debug.h index 833ae574e..7f5df719c 100644 --- a/renderdoc/driver/shaders/spirv/spirv_debug.h +++ b/renderdoc/driver/shaders/spirv/spirv_debug.h @@ -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; diff --git a/renderdoc/driver/shaders/spirv/spirv_debug_setup.cpp b/renderdoc/driver/shaders/spirv/spirv_debug_setup.cpp index 2239b2b02..9259cb3aa 100644 --- a/renderdoc/driver/shaders/spirv/spirv_debug_setup.cpp +++ b/renderdoc/driver/shaders/spirv/spirv_debug_setup.cpp @@ -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; diff --git a/renderdoc/driver/shaders/spirv/spirv_processor.h b/renderdoc/driver/shaders/spirv/spirv_processor.h index d1ad9f2c1..2422ad5d2 100644 --- a/renderdoc/driver/shaders/spirv/spirv_processor.h +++ b/renderdoc/driver/shaders/spirv/spirv_processor.h @@ -547,6 +547,13 @@ enum class ThreadScope : uint32_t BITMASK_OPERATORS(ThreadScope); +enum class ComputeDerivativeMode : uint8_t +{ + None, + Linear, + Quad, +}; + class Processor { public: diff --git a/renderdoc/driver/shaders/spirv/spirv_reflect.cpp b/renderdoc/driver/shaders/spirv/spirv_reflect.cpp index 19061fba3..95425e031 100644 --- a/renderdoc/driver/shaders/spirv/spirv_reflect.cpp +++ b/renderdoc/driver/shaders/spirv/spirv_reflect.cpp @@ -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) diff --git a/renderdoc/driver/shaders/spirv/spirv_reflect.h b/renderdoc/driver/shaders/spirv/spirv_reflect.h index 9cfbdd2a9..71523c319 100644 --- a/renderdoc/driver/shaders/spirv/spirv_reflect.h +++ b/renderdoc/driver/shaders/spirv/spirv_reflect.h @@ -78,6 +78,7 @@ struct SPIRVPatchData rdcarray 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; diff --git a/renderdoc/driver/vulkan/vk_shaderdebug.cpp b/renderdoc/driver/vulkan/vk_shaderdebug.cpp index 355ed6e5d..3df4eff07 100644 --- a/renderdoc/driver/vulkan/vk_shaderdebug.cpp +++ b/renderdoc/driver/vulkan/vk_shaderdebug.cpp @@ -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(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 &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({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 &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({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;