mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 21:28:32 +09:00
[Fix] (DirectVulkan, ShaderTranspiler, MG_IntegrationTest, SelfTest, TraceReplay): use native subgroups and patch iterationRP's under-declared scratch
iterationRP's Program 203 declares shared vec2 prefixSumCache[32] for a 512-invocation workgroup indexed by gl_SubgroupID; any device narrower than 16 lanes partitions into more than 32 subgroups and the pack writes shared memory out of bounds (heap corruption on lavapipe's CPU rasterizer, ssim 0.028 on the CI retrace). Fix it where the fault lies - in the fixture - and keep the GL contract sound everywhere else: - FixIterationRPSubgroupScratchPass: fingerprint-gated SPIR-V pass that grows exactly that array to ceil(invocations/width) entries on sub-16-lane devices; every other module passes through byte-identical. - DeriveNumSubgroupsPass stays default-on for the Adreno topology bug and is made spec-sound: pipelines request REQUIRE_FULL_SUBGROUPS whenever the workgroup shape makes the flag legal (computeFullSubgroups enabled, local_size_x a multiple of the native width, subgroup count within maxComputeWorkgroupSubgroups). - EmulateSubgroupsPass: 32-lane virtual-subgroup lowering kept in-tree as a last resort, enabled only by MOBILEGL_MAGMA_EMULATE_SUBGROUP=1 on devices with no native subgroup support; fails closed on extended subgroup instructions and on modules whose added scratch would exceed maxComputeSharedMemorySize. - IterationRPFirstReductionScenario skips gracefully outside the pack's 16..256-lane source domain; the new IterationRPScratchFixScenario runs the fixture-shaped reduction on any width and asserts the exact width-independent total. DriverPost keeps reporting FAIL on out-of-domain devices. - Program203 -> IterationRP rename throughout; the per-trace num_subgroups_quirk plumbing is removed from the trace replayer, JNI chain, and CI workflows.
This commit is contained in:
@@ -7,8 +7,8 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "DriverPost.h"
|
||||
#include "DriverPostProgram203Witness.h"
|
||||
#include "DriverPostProgram203WitnessSpv.h"
|
||||
#include "DriverPostIterationRPWitness.h"
|
||||
#include "DriverPostIterationRPWitnessSpv.h"
|
||||
#include "MG_Util/BackendLoaders/OpenGL/Loader.h"
|
||||
#include <Config.h>
|
||||
#include <MGGitHash.h>
|
||||
@@ -1458,11 +1458,11 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
disabledNote);
|
||||
}
|
||||
|
||||
// Native Program-203 compute witness. This deliberately uses a separate
|
||||
// Native iterationRP compute witness. This deliberately uses a separate
|
||||
// throwaway Vulkan device rather than the real renderer's queues, and it
|
||||
// treats MOBILEGL_DISABLE_SUBGROUP as irrelevant: the row reports what the
|
||||
// driver does, not what MobileGL elects to advertise to applications.
|
||||
void ProbeVulkanProgram203Witness(ReportBuilder& builder, PFN_vkGetInstanceProcAddr getInstanceProcAddr,
|
||||
void ProbeVulkanIterationRPWitness(ReportBuilder& builder, PFN_vkGetInstanceProcAddr getInstanceProcAddr,
|
||||
VkInstance instance, VkPhysicalDevice physicalDevice,
|
||||
Uint32 computeQueueFamilyIndex,
|
||||
const VkPhysicalDeviceProperties& properties,
|
||||
@@ -1476,7 +1476,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
return;
|
||||
}
|
||||
|
||||
Program203WitnessLimits limits{};
|
||||
IterationRPWitnessLimits limits{};
|
||||
limits.computeStageSupported =
|
||||
(subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0;
|
||||
limits.basicSubgroupSupported =
|
||||
@@ -1494,12 +1494,12 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
limits.maxBoundDescriptorSets = properties.limits.maxBoundDescriptorSets;
|
||||
limits.maxStorageBufferRange = properties.limits.maxStorageBufferRange;
|
||||
|
||||
const Program203WitnessEligibilityResult eligibility = EvaluateProgram203WitnessEligibility(limits);
|
||||
if (eligibility.eligibility == Program203WitnessEligibility::SkipUnsupportedNativeFeatureSet) {
|
||||
const IterationRPWitnessEligibilityResult eligibility = EvaluateIterationRPWitnessEligibility(limits);
|
||||
if (eligibility.eligibility == IterationRPWitnessEligibility::SkipUnsupportedNativeFeatureSet) {
|
||||
builder.Info(RowName, eligibility.detail);
|
||||
return;
|
||||
}
|
||||
if (eligibility.eligibility == Program203WitnessEligibility::FailInadequateLimits) {
|
||||
if (eligibility.eligibility == IterationRPWitnessEligibility::FailInadequateLimits) {
|
||||
fail(eligibility.detail);
|
||||
return;
|
||||
}
|
||||
@@ -1668,7 +1668,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
|
||||
VkBufferCreateInfo bufferInfo{};
|
||||
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
||||
bufferInfo.size = sizeof(Program203WitnessOutput);
|
||||
bufferInfo.size = sizeof(IterationRPWitnessOutput);
|
||||
bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
|
||||
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
result = vkCreateBufferFn(device, &bufferInfo, nullptr, &outputBuffer);
|
||||
@@ -1710,12 +1710,12 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
fail(format("vkBindBufferMemory(output SSBO) failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
result = vkMapMemoryFn(device, outputMemory, 0, sizeof(Program203WitnessOutput), 0, &mappedOutput);
|
||||
result = vkMapMemoryFn(device, outputMemory, 0, sizeof(IterationRPWitnessOutput), 0, &mappedOutput);
|
||||
if (result != VK_SUCCESS || mappedOutput == nullptr) {
|
||||
fail(format("vkMapMemory(output SSBO) failed (VkResult = {})", static_cast<Int>(result)));
|
||||
return;
|
||||
}
|
||||
std::memset(mappedOutput, 0xa5, sizeof(Program203WitnessOutput));
|
||||
std::memset(mappedOutput, 0xa5, sizeof(IterationRPWitnessOutput));
|
||||
|
||||
VkDescriptorSetLayoutBinding outputBinding{};
|
||||
outputBinding.binding = 0;
|
||||
@@ -1760,7 +1760,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
VkDescriptorBufferInfo outputDescriptor{};
|
||||
outputDescriptor.buffer = outputBuffer;
|
||||
outputDescriptor.offset = 0;
|
||||
outputDescriptor.range = sizeof(Program203WitnessOutput);
|
||||
outputDescriptor.range = sizeof(IterationRPWitnessOutput);
|
||||
VkWriteDescriptorSet descriptorWrite{};
|
||||
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
descriptorWrite.dstSet = descriptorSet;
|
||||
@@ -1772,8 +1772,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
|
||||
VkShaderModuleCreateInfo shaderModuleInfo{};
|
||||
shaderModuleInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
|
||||
shaderModuleInfo.codeSize = sizeof(kDriverPostProgram203WitnessSpv);
|
||||
shaderModuleInfo.pCode = kDriverPostProgram203WitnessSpv;
|
||||
shaderModuleInfo.codeSize = sizeof(kDriverPostIterationRPWitnessSpv);
|
||||
shaderModuleInfo.pCode = kDriverPostIterationRPWitnessSpv;
|
||||
result = vkCreateShaderModuleFn(device, &shaderModuleInfo, nullptr, &shaderModule);
|
||||
if (result != VK_SUCCESS) {
|
||||
fail(format("vkCreateShaderModule failed (VkResult = {})", static_cast<Int>(result)));
|
||||
@@ -1845,7 +1845,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
hostReadBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
hostReadBarrier.buffer = outputBuffer;
|
||||
hostReadBarrier.offset = 0;
|
||||
hostReadBarrier.size = sizeof(Program203WitnessOutput);
|
||||
hostReadBarrier.size = sizeof(IterationRPWitnessOutput);
|
||||
vkCmdPipelineBarrierFn(commandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0,
|
||||
0, nullptr, 1, &hostReadBarrier, 0, nullptr);
|
||||
result = vkEndCommandBufferFn(commandBuffer);
|
||||
@@ -1879,9 +1879,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
return;
|
||||
}
|
||||
|
||||
Program203WitnessOutput output{};
|
||||
IterationRPWitnessOutput output{};
|
||||
std::memcpy(&output, mappedOutput, sizeof(output));
|
||||
const Program203WitnessValidationResult validation = ValidateProgram203Witness(output);
|
||||
const IterationRPWitnessValidationResult validation = ValidateIterationRPWitness(output);
|
||||
if (!validation.ok) {
|
||||
fail(validation.detail);
|
||||
return;
|
||||
@@ -2491,7 +2491,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.Warn("Compute shader subgroup", "subgroup properties could not be queried");
|
||||
}
|
||||
|
||||
ProbeVulkanProgram203Witness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex,
|
||||
ProbeVulkanIterationRPWitness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex,
|
||||
properties, subgroupPropertiesAvailable, subgroupProperties);
|
||||
|
||||
if (HasVkExtension(deviceExtensions, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME)) {
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.comp
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.comp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
@@ -6,9 +6,9 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Native Vulkan GLSL 450 witness for Program 203's first subgroup reduction.
|
||||
// Native Vulkan GLSL 450 witness for iterationRP's first subgroup reduction.
|
||||
// It is intentionally independent of the GL 430 integration scenario. The body
|
||||
// below preserves Program 203's source reduction; the surrounding diagnostics
|
||||
// below preserves iterationRP's source reduction; the surrounding diagnostics
|
||||
// only observe its topology and cache handoffs.
|
||||
|
||||
#version 450
|
||||
@@ -23,7 +23,7 @@ const uint kTopologyInvalidSubgroupId = 1u << 2u;
|
||||
const uint kTopologyInvalidSubgroupLane = 1u << 3u;
|
||||
const uint kWitnessMagic = 0x50323033u;
|
||||
|
||||
layout(std430, set = 0, binding = 0) buffer Program203WitnessOutput {
|
||||
layout(std430, set = 0, binding = 0) buffer IterationRPWitnessOutput {
|
||||
uint magic;
|
||||
uint topologyFlags;
|
||||
uint numSubgroups;
|
||||
@@ -40,7 +40,7 @@ layout(std430, set = 0, binding = 0) buffer Program203WitnessOutput {
|
||||
vec2 finalAverage;
|
||||
} outWitness;
|
||||
|
||||
// Program 203's cache stays separate from all diagnostic shared state. In
|
||||
// iterationRP's cache stays separate from all diagnostic shared state. In
|
||||
// particular, no instrumentation stores through prefixSumCache except source
|
||||
// writes retained below.
|
||||
shared vec2 prefixSumCache[32];
|
||||
@@ -108,7 +108,7 @@ void main() {
|
||||
}
|
||||
|
||||
// This branch is uniform after collection and is solely a safety guard for
|
||||
// broken topology reports. The valid side retains Program 203 verbatim.
|
||||
// broken topology reports. The valid side retains iterationRP verbatim.
|
||||
const bool sourceDomain = canonicalDomain && topologyFlagsShared == 0u;
|
||||
if (sourceDomain) {
|
||||
vec2 sampleLuminance = vec2(float(gl_LocalInvocationIndex + 1u), 0.0);
|
||||
+55
-55
@@ -1,4 +1,4 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.cpp
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
@@ -6,7 +6,7 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "DriverPostProgram203Witness.h"
|
||||
#include "DriverPostIterationRPWitness.h"
|
||||
|
||||
#include <bit>
|
||||
#include <sstream>
|
||||
@@ -15,11 +15,11 @@
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
namespace {
|
||||
[[nodiscard]] Program203WitnessValidationResult Failure(Program203WitnessValidationFailure failure,
|
||||
[[nodiscard]] IterationRPWitnessValidationResult Failure(IterationRPWitnessValidationFailure failure,
|
||||
std::string detail,
|
||||
std::uint32_t scanStage = 0u,
|
||||
std::uint32_t subgroup = 0u) {
|
||||
Program203WitnessValidationResult result;
|
||||
IterationRPWitnessValidationResult result;
|
||||
result.ok = false;
|
||||
result.failure = failure;
|
||||
result.scanStage = scanStage;
|
||||
@@ -36,18 +36,18 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
return FloatBits(lhs) == FloatBits(rhs);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool SameBits(const Program203WitnessVec2& lhs, const Program203WitnessVec2& rhs) {
|
||||
[[nodiscard]] bool SameBits(const IterationRPWitnessVec2& lhs, const IterationRPWitnessVec2& rhs) {
|
||||
return SameBits(lhs.x, rhs.x) && SameBits(lhs.y, rhs.y);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string Vec2String(const Program203WitnessVec2& value) {
|
||||
[[nodiscard]] std::string Vec2String(const IterationRPWitnessVec2& value) {
|
||||
std::ostringstream output;
|
||||
output << '(' << value.x << ',' << value.y << ')';
|
||||
return output.str();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t ExpectedSeenSubgroupMask(std::uint32_t numSubgroups) {
|
||||
return numSubgroups == kProgram203WitnessMaxSubgroups ? 0xffffffffu : (1u << numSubgroups) - 1u;
|
||||
return numSubgroups == kIterationRPWitnessMaxSubgroups ? 0xffffffffu : (1u << numSubgroups) - 1u;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string JoinRequirements(const std::vector<std::string>& requirements) {
|
||||
@@ -60,8 +60,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Program203WitnessEligibilityResult
|
||||
EvaluateProgram203WitnessEligibility(const Program203WitnessLimits& limits) {
|
||||
IterationRPWitnessEligibilityResult
|
||||
EvaluateIterationRPWitnessEligibility(const IterationRPWitnessLimits& limits) {
|
||||
// This classification deliberately precedes numeric limits. An absent native
|
||||
// compute/basic/arithmetic subgroup contract means there is nothing to witness,
|
||||
// whereas every resource/entry-point failure on a capable device is a POST FAIL.
|
||||
@@ -70,7 +70,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
if (!limits.computeStageSupported) missing.emplace_back("VK_SHADER_STAGE_COMPUTE_BIT");
|
||||
if (!limits.basicSubgroupSupported) missing.emplace_back("VK_SUBGROUP_FEATURE_BASIC_BIT");
|
||||
if (!limits.arithmeticSubgroupSupported) missing.emplace_back("VK_SUBGROUP_FEATURE_ARITHMETIC_BIT");
|
||||
return {Program203WitnessEligibility::SkipUnsupportedNativeFeatureSet,
|
||||
return {IterationRPWitnessEligibility::SkipUnsupportedNativeFeatureSet,
|
||||
"skipped because the native compute/basic/arithmetic subgroup feature set is unsupported (missing " +
|
||||
JoinRequirements(missing) + ')'};
|
||||
}
|
||||
@@ -79,16 +79,16 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
if (limits.subgroupSize == 0u) {
|
||||
inadequate.emplace_back("subgroupSize == 0");
|
||||
}
|
||||
if (limits.maxComputeWorkGroupInvocations < kProgram203WitnessInvocationCount) {
|
||||
if (limits.maxComputeWorkGroupInvocations < kIterationRPWitnessInvocationCount) {
|
||||
inadequate.emplace_back("maxComputeWorkGroupInvocations < 512");
|
||||
}
|
||||
if (limits.maxComputeWorkGroupSize[0] < 32u || limits.maxComputeWorkGroupSize[1] < 16u ||
|
||||
limits.maxComputeWorkGroupSize[2] < 1u) {
|
||||
inadequate.emplace_back("maxComputeWorkGroupSize does not cover 32x16x1");
|
||||
}
|
||||
if (limits.maxComputeSharedMemorySize < kProgram203WitnessSharedMemoryBytes) {
|
||||
if (limits.maxComputeSharedMemorySize < kIterationRPWitnessSharedMemoryBytes) {
|
||||
inadequate.emplace_back("maxComputeSharedMemorySize < " +
|
||||
std::to_string(kProgram203WitnessSharedMemoryBytes));
|
||||
std::to_string(kIterationRPWitnessSharedMemoryBytes));
|
||||
}
|
||||
if (limits.maxPerStageDescriptorStorageBuffers < 1u) {
|
||||
inadequate.emplace_back("maxPerStageDescriptorStorageBuffers < 1");
|
||||
@@ -99,21 +99,21 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
if (limits.maxBoundDescriptorSets < 1u) {
|
||||
inadequate.emplace_back("maxBoundDescriptorSets < 1");
|
||||
}
|
||||
if (limits.maxStorageBufferRange < sizeof(Program203WitnessOutput)) {
|
||||
if (limits.maxStorageBufferRange < sizeof(IterationRPWitnessOutput)) {
|
||||
inadequate.emplace_back("maxStorageBufferRange < " +
|
||||
std::to_string(sizeof(Program203WitnessOutput)));
|
||||
std::to_string(sizeof(IterationRPWitnessOutput)));
|
||||
}
|
||||
if (!inadequate.empty()) {
|
||||
return {Program203WitnessEligibility::FailInadequateLimits,
|
||||
return {IterationRPWitnessEligibility::FailInadequateLimits,
|
||||
"insufficient Vulkan limits for a 32x16x1 workgroup, one output SSBO, and " +
|
||||
std::to_string(kProgram203WitnessSharedMemoryBytes) + " bytes of shared memory: " +
|
||||
std::to_string(kIterationRPWitnessSharedMemoryBytes) + " bytes of shared memory: " +
|
||||
JoinRequirements(inadequate)};
|
||||
}
|
||||
return {Program203WitnessEligibility::Execute, {}};
|
||||
return {IterationRPWitnessEligibility::Execute, {}};
|
||||
}
|
||||
|
||||
std::uint32_t ComputeProgram203WitnessLoopLength(std::uint32_t numSubgroups) {
|
||||
if (numSubgroups < 2u || numSubgroups > kProgram203WitnessMaxSubgroups) return 0u;
|
||||
std::uint32_t ComputeIterationRPWitnessLoopLength(std::uint32_t numSubgroups) {
|
||||
if (numSubgroups < 2u || numSubgroups > kIterationRPWitnessMaxSubgroups) return 0u;
|
||||
|
||||
// Exact C++ spelling of the source's findMSB-based calculation. In
|
||||
// particular, its final iteration for powers of two is intentional.
|
||||
@@ -125,87 +125,87 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
return loopLength;
|
||||
}
|
||||
|
||||
Program203WitnessValidationResult ValidateProgram203Witness(const Program203WitnessOutput& output) {
|
||||
IterationRPWitnessValidationResult ValidateIterationRPWitness(const IterationRPWitnessOutput& output) {
|
||||
// 1. Completion. A poisoned or unwritten result must never turn into a
|
||||
// topology diagnosis, because it says nothing about execution.
|
||||
if (output.magic != kProgram203WitnessMagic) {
|
||||
if (output.magic != kIterationRPWitnessMagic) {
|
||||
std::ostringstream detail;
|
||||
detail << "completion: magic was 0x" << std::hex << output.magic << ", expected 0x"
|
||||
<< kProgram203WitnessMagic;
|
||||
return Failure(Program203WitnessValidationFailure::Completion, detail.str());
|
||||
<< kIterationRPWitnessMagic;
|
||||
return Failure(IterationRPWitnessValidationFailure::Completion, detail.str());
|
||||
}
|
||||
|
||||
// 2. Observed topology. All checks consume observations written by the
|
||||
// shader, rather than inferring subgroup layout from invocation indices.
|
||||
const std::uint32_t numSubgroups = output.numSubgroups;
|
||||
if (numSubgroups < 2u || numSubgroups > kProgram203WitnessMaxSubgroups) {
|
||||
if (numSubgroups < 2u || numSubgroups > kIterationRPWitnessMaxSubgroups) {
|
||||
std::ostringstream detail;
|
||||
detail << "topology: canonical gl_NumSubgroups=" << numSubgroups << " is outside [2, 32]";
|
||||
return Failure(Program203WitnessValidationFailure::Topology, detail.str());
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str());
|
||||
}
|
||||
if ((output.topologyFlags & Program203WitnessNonuniformNumSubgroups) != 0u) {
|
||||
return Failure(Program203WitnessValidationFailure::Topology,
|
||||
if ((output.topologyFlags & IterationRPWitnessNonuniformNumSubgroups) != 0u) {
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology,
|
||||
"topology: gl_NumSubgroups differed across workgroup");
|
||||
}
|
||||
if ((output.topologyFlags & Program203WitnessInvalidNumSubgroups) != 0u) {
|
||||
return Failure(Program203WitnessValidationFailure::Topology,
|
||||
if ((output.topologyFlags & IterationRPWitnessInvalidNumSubgroups) != 0u) {
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology,
|
||||
"topology: an invocation reported gl_NumSubgroups outside [2, 32]");
|
||||
}
|
||||
if ((output.topologyFlags & Program203WitnessInvalidSubgroupId) != 0u) {
|
||||
return Failure(Program203WitnessValidationFailure::Topology,
|
||||
if ((output.topologyFlags & IterationRPWitnessInvalidSubgroupId) != 0u) {
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology,
|
||||
"topology: an invocation reported an invalid gl_SubgroupID");
|
||||
}
|
||||
if ((output.topologyFlags & Program203WitnessInvalidSubgroupLane) != 0u) {
|
||||
return Failure(Program203WitnessValidationFailure::Topology,
|
||||
if ((output.topologyFlags & IterationRPWitnessInvalidSubgroupLane) != 0u) {
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology,
|
||||
"topology: an invocation reported an invalid subgroup lane");
|
||||
}
|
||||
if ((output.topologyFlags & ~(Program203WitnessNonuniformNumSubgroups |
|
||||
Program203WitnessInvalidNumSubgroups |
|
||||
Program203WitnessInvalidSubgroupId |
|
||||
Program203WitnessInvalidSubgroupLane)) != 0u) {
|
||||
if ((output.topologyFlags & ~(IterationRPWitnessNonuniformNumSubgroups |
|
||||
IterationRPWitnessInvalidNumSubgroups |
|
||||
IterationRPWitnessInvalidSubgroupId |
|
||||
IterationRPWitnessInvalidSubgroupLane)) != 0u) {
|
||||
std::ostringstream detail;
|
||||
detail << "topology: unknown topology flags 0x" << std::hex << output.topologyFlags;
|
||||
return Failure(Program203WitnessValidationFailure::Topology, detail.str());
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str());
|
||||
}
|
||||
const std::uint32_t expectedMask = ExpectedSeenSubgroupMask(numSubgroups);
|
||||
if (output.seenSubgroupMask != expectedMask) {
|
||||
std::ostringstream detail;
|
||||
detail << "topology: seen subgroup-ID mask was 0x" << std::hex << output.seenSubgroupMask
|
||||
<< ", expected 0x" << expectedMask;
|
||||
return Failure(Program203WitnessValidationFailure::Topology, detail.str());
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str());
|
||||
}
|
||||
const std::uint32_t expectedLoopLength = ComputeProgram203WitnessLoopLength(numSubgroups);
|
||||
const std::uint32_t expectedLoopLength = ComputeIterationRPWitnessLoopLength(numSubgroups);
|
||||
if (output.loopLength != expectedLoopLength) {
|
||||
std::ostringstream detail;
|
||||
detail << "topology: loopLength was " << std::dec << output.loopLength << ", expected "
|
||||
<< expectedLoopLength;
|
||||
return Failure(Program203WitnessValidationFailure::Topology, detail.str());
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str());
|
||||
}
|
||||
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
|
||||
if (output.lastLaneWriterCount[subgroup] != 1u) {
|
||||
std::ostringstream detail;
|
||||
detail << "topology: subgroup " << subgroup << " has "
|
||||
<< output.lastLaneWriterCount[subgroup] << " source last-lane writers, expected exactly 1";
|
||||
return Failure(Program203WitnessValidationFailure::Topology, detail.str(), 0u, subgroup);
|
||||
return Failure(IterationRPWitnessValidationFailure::Topology, detail.str(), 0u, subgroup);
|
||||
}
|
||||
}
|
||||
if (output.owner511.y != numSubgroups) {
|
||||
std::ostringstream detail;
|
||||
detail << "final owner: invocation 511 reported gl_NumSubgroups=" << output.owner511.y << ", expected "
|
||||
<< numSubgroups;
|
||||
return Failure(Program203WitnessValidationFailure::FinalOwner, detail.str());
|
||||
return Failure(IterationRPWitnessValidationFailure::FinalOwner, detail.str());
|
||||
}
|
||||
if (output.owner511.z != numSubgroups - 1u) {
|
||||
std::ostringstream detail;
|
||||
detail << "final owner: invocation 511 is not in the highest subgroup (id" << output.owner511.z
|
||||
<< ", expected id" << (numSubgroups - 1u) << ')';
|
||||
return Failure(Program203WitnessValidationFailure::FinalOwner, detail.str());
|
||||
return Failure(IterationRPWitnessValidationFailure::FinalOwner, detail.str());
|
||||
}
|
||||
if (output.owner511.x == 0u || output.owner511.w != output.owner511.x - 1u) {
|
||||
std::ostringstream detail;
|
||||
detail << "final owner: invocation 511 is not the last lane of highest subgroup (size "
|
||||
<< output.owner511.x << ", lane " << output.owner511.w << ')';
|
||||
return Failure(Program203WitnessValidationFailure::FinalOwner, detail.str());
|
||||
return Failure(IterationRPWitnessValidationFailure::FinalOwner, detail.str());
|
||||
}
|
||||
|
||||
// 3. Initial subgroup handoff. The atomic scalar totals are independent
|
||||
@@ -218,22 +218,22 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
if (indexedTotal != 131328u) {
|
||||
std::ostringstream detail;
|
||||
detail << "initial subgroup handoff: indexed input total was " << indexedTotal << ", expected 131328";
|
||||
return Failure(Program203WitnessValidationFailure::InitialSubgroupHandoff, detail.str());
|
||||
return Failure(IterationRPWitnessValidationFailure::InitialSubgroupHandoff, detail.str());
|
||||
}
|
||||
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
|
||||
const Program203WitnessVec2 expected = {static_cast<float>(output.indexedInputTotal[subgroup]), 0.0f};
|
||||
const IterationRPWitnessVec2 expected = {static_cast<float>(output.indexedInputTotal[subgroup]), 0.0f};
|
||||
if (!SameBits(output.rawPrefix[subgroup], expected)) {
|
||||
std::ostringstream detail;
|
||||
detail << "initial subgroup handoff: subgroup " << subgroup << " rawPrefix observed "
|
||||
<< Vec2String(output.rawPrefix[subgroup]) << ", expected " << Vec2String(expected);
|
||||
return Failure(Program203WitnessValidationFailure::InitialSubgroupHandoff, detail.str(), 0u,
|
||||
return Failure(IterationRPWitnessValidationFailure::InitialSubgroupHandoff, detail.str(), 0u,
|
||||
subgroup);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Source scan. Do not substitute a conventional scan: this reproduces
|
||||
// the source cache index expression and stage ordering word for word.
|
||||
std::array<Program203WitnessVec2, kProgram203WitnessMaxSubgroups> expectedCache = output.rawPrefix;
|
||||
std::array<IterationRPWitnessVec2, kIterationRPWitnessMaxSubgroups> expectedCache = output.rawPrefix;
|
||||
for (std::uint32_t scanStage = 0u; scanStage < expectedLoopLength; ++scanStage) {
|
||||
auto cacheAfterStage = expectedCache;
|
||||
for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) {
|
||||
@@ -250,27 +250,27 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
detail << "source scan stage " << scanStage << ", subgroup " << subgroup << ": observed "
|
||||
<< Vec2String(output.scanCache[scanStage][subgroup]) << ", expected "
|
||||
<< Vec2String(expectedCache[subgroup]);
|
||||
return Failure(Program203WitnessValidationFailure::SourceScan, detail.str(), scanStage, subgroup);
|
||||
return Failure(IterationRPWitnessValidationFailure::SourceScan, detail.str(), scanStage, subgroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. The owner contract was checked above with the other topology facts;
|
||||
// this final result remains a separate exact-vector check.
|
||||
const Program203WitnessVec2 expectedAverage = {256.5f, 0.0f};
|
||||
const IterationRPWitnessVec2 expectedAverage = {256.5f, 0.0f};
|
||||
if (!SameBits(output.finalAverage, expectedAverage)) {
|
||||
std::ostringstream detail;
|
||||
detail << "final average: observed " << Vec2String(output.finalAverage) << ", expected "
|
||||
<< Vec2String(expectedAverage);
|
||||
return Failure(Program203WitnessValidationFailure::FinalAverage, detail.str());
|
||||
return Failure(IterationRPWitnessValidationFailure::FinalAverage, detail.str());
|
||||
}
|
||||
|
||||
std::ostringstream detail;
|
||||
detail << "N=" << numSubgroups << ", owner511=id" << output.owner511.z << "/lane" << output.owner511.w
|
||||
<< ", " << expectedLoopLength << " scan stages, average=" << Vec2String(output.finalAverage);
|
||||
Program203WitnessValidationResult result;
|
||||
IterationRPWitnessValidationResult result;
|
||||
result.ok = true;
|
||||
result.failure = Program203WitnessValidationFailure::None;
|
||||
result.failure = IterationRPWitnessValidationFailure::None;
|
||||
result.detail = detail.str();
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Compact, native-Vulkan iterationRP first-reduction witness ABI and its pure
|
||||
// validator. The types below deliberately mirror DriverPostIterationRPWitness.comp's
|
||||
// single std430 storage block; changing either side requires updating the static
|
||||
// layout assertions here.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
// "P203": the pack's trace program id, kept stable so the checked-in witness
|
||||
// SPIR-V (DriverPostIterationRPWitnessSpv.h) needs no regeneration.
|
||||
constexpr std::uint32_t kIterationRPWitnessMagic = 0x50323033u;
|
||||
constexpr std::uint32_t kIterationRPWitnessInvocationCount = 512u;
|
||||
constexpr std::uint32_t kIterationRPWitnessMaxSubgroups = 32u;
|
||||
constexpr std::uint32_t kIterationRPWitnessMaxScanStages = 6u;
|
||||
|
||||
// These bit values are shared with the GLSL source. They document failures in
|
||||
// topology observations rather than guessing a topology from local IDs on the host.
|
||||
enum IterationRPWitnessTopologyFlag : std::uint32_t {
|
||||
IterationRPWitnessNonuniformNumSubgroups = 1u << 0u,
|
||||
IterationRPWitnessInvalidNumSubgroups = 1u << 1u,
|
||||
IterationRPWitnessInvalidSubgroupId = 1u << 2u,
|
||||
IterationRPWitnessInvalidSubgroupLane = 1u << 3u,
|
||||
};
|
||||
|
||||
struct alignas(8) IterationRPWitnessVec2 {
|
||||
float x;
|
||||
float y;
|
||||
};
|
||||
|
||||
struct alignas(16) IterationRPWitnessUVec4 {
|
||||
std::uint32_t x;
|
||||
std::uint32_t y;
|
||||
std::uint32_t z;
|
||||
std::uint32_t w;
|
||||
};
|
||||
|
||||
// std430 layout of DriverPostIterationRPWitness.comp's IterationRPWitnessOutput block.
|
||||
struct alignas(16) IterationRPWitnessOutput {
|
||||
std::uint32_t magic;
|
||||
std::uint32_t topologyFlags;
|
||||
std::uint32_t numSubgroups;
|
||||
std::uint32_t loopLength;
|
||||
std::uint32_t seenSubgroupMask;
|
||||
|
||||
IterationRPWitnessUVec4 owner511;
|
||||
|
||||
std::array<std::uint32_t, kIterationRPWitnessMaxSubgroups> lastLaneWriterCount;
|
||||
std::array<std::uint32_t, kIterationRPWitnessMaxSubgroups> indexedInputTotal;
|
||||
|
||||
std::array<IterationRPWitnessVec2, kIterationRPWitnessMaxSubgroups> rawPrefix;
|
||||
std::array<std::array<IterationRPWitnessVec2, kIterationRPWitnessMaxSubgroups>,
|
||||
kIterationRPWitnessMaxScanStages>
|
||||
scanCache;
|
||||
IterationRPWitnessVec2 finalAverage;
|
||||
};
|
||||
|
||||
static_assert(std::is_standard_layout_v<IterationRPWitnessVec2>);
|
||||
static_assert(std::is_standard_layout_v<IterationRPWitnessUVec4>);
|
||||
static_assert(std::is_standard_layout_v<IterationRPWitnessOutput>);
|
||||
static_assert(sizeof(IterationRPWitnessVec2) == 8u);
|
||||
static_assert(alignof(IterationRPWitnessVec2) == 8u);
|
||||
static_assert(sizeof(IterationRPWitnessUVec4) == 16u);
|
||||
static_assert(alignof(IterationRPWitnessUVec4) == 16u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, magic) == 0u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, topologyFlags) == 4u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, numSubgroups) == 8u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, loopLength) == 12u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, seenSubgroupMask) == 16u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, owner511) == 32u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, lastLaneWriterCount) == 48u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, indexedInputTotal) == 176u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, rawPrefix) == 304u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, scanCache) == 560u);
|
||||
static_assert(offsetof(IterationRPWitnessOutput, finalAverage) == 2096u);
|
||||
static_assert(sizeof(IterationRPWitnessOutput) == 2112u);
|
||||
|
||||
// The witness uses prefixSumCache[32], three scalar shared diagnostics, and
|
||||
// two 32-entry scalar diagnostic arrays in the GLSL source. Keep this
|
||||
// independent of the output SSBO size.
|
||||
constexpr std::uint32_t kIterationRPWitnessSharedMemoryBytes =
|
||||
kIterationRPWitnessMaxSubgroups * sizeof(IterationRPWitnessVec2) +
|
||||
3u * sizeof(std::uint32_t) +
|
||||
2u * kIterationRPWitnessMaxSubgroups * sizeof(std::uint32_t);
|
||||
|
||||
enum class IterationRPWitnessEligibility {
|
||||
Execute,
|
||||
SkipUnsupportedNativeFeatureSet,
|
||||
FailInadequateLimits,
|
||||
};
|
||||
|
||||
// The raw physical-device conditions needed by the native witness. This is
|
||||
// intentionally distinct from MobileGL's advertised-extension policy.
|
||||
struct IterationRPWitnessLimits {
|
||||
bool computeStageSupported = false;
|
||||
bool basicSubgroupSupported = false;
|
||||
bool arithmeticSubgroupSupported = false;
|
||||
std::uint32_t subgroupSize = 0u;
|
||||
|
||||
std::uint32_t maxComputeWorkGroupInvocations = 0u;
|
||||
std::array<std::uint32_t, 3> maxComputeWorkGroupSize{};
|
||||
std::uint32_t maxComputeSharedMemorySize = 0u;
|
||||
std::uint32_t maxPerStageDescriptorStorageBuffers = 0u;
|
||||
std::uint32_t maxDescriptorSetStorageBuffers = 0u;
|
||||
std::uint32_t maxBoundDescriptorSets = 0u;
|
||||
std::uint64_t maxStorageBufferRange = 0u;
|
||||
};
|
||||
|
||||
struct IterationRPWitnessEligibilityResult {
|
||||
IterationRPWitnessEligibility eligibility = IterationRPWitnessEligibility::FailInadequateLimits;
|
||||
std::string detail;
|
||||
};
|
||||
|
||||
enum class IterationRPWitnessValidationFailure {
|
||||
None,
|
||||
Completion,
|
||||
Topology,
|
||||
InitialSubgroupHandoff,
|
||||
SourceScan,
|
||||
FinalOwner,
|
||||
FinalAverage,
|
||||
};
|
||||
|
||||
struct IterationRPWitnessValidationResult {
|
||||
bool ok = false;
|
||||
IterationRPWitnessValidationFailure failure = IterationRPWitnessValidationFailure::Completion;
|
||||
std::uint32_t scanStage = 0u;
|
||||
std::uint32_t subgroup = 0u;
|
||||
std::string detail;
|
||||
};
|
||||
|
||||
[[nodiscard]] IterationRPWitnessEligibilityResult
|
||||
EvaluateIterationRPWitnessEligibility(const IterationRPWitnessLimits& limits);
|
||||
|
||||
// Mirrors the source's findMSB expression for valid N in [2, 32].
|
||||
[[nodiscard]] std::uint32_t ComputeIterationRPWitnessLoopLength(std::uint32_t numSubgroups);
|
||||
|
||||
[[nodiscard]] IterationRPWitnessValidationResult
|
||||
ValidateIterationRPWitness(const IterationRPWitnessOutput& output);
|
||||
} // namespace MobileGL::MG_Util::SelfTest
|
||||
+12
-6
@@ -1,4 +1,4 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostProgram203WitnessSpv.h
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitnessSpv.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
@@ -6,9 +6,15 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Generated from DriverPostProgram203Witness.comp with:
|
||||
// glslangValidator --target-env vulkan1.1 -V DriverPostProgram203Witness.comp
|
||||
// Generated from DriverPostIterationRPWitness.comp with:
|
||||
// glslangValidator --target-env vulkan1.1 -V DriverPostIterationRPWitness.comp
|
||||
// Validated with spirv-val --target-env vulkan1.1. Do not edit words by hand.
|
||||
//
|
||||
// The stored words predate the Program203 -> IterationRP source rename, so their
|
||||
// embedded OpName debug strings still spell the old identifiers; regeneration from
|
||||
// the renamed source produces semantically identical code differing only in those
|
||||
// strings. The witness magic stays 0x50323033 ("P203" - the trace's program id) so
|
||||
// these words remain valid without regeneration.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -16,7 +22,7 @@
|
||||
#include <cstdint>
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
inline constexpr std::uint32_t kDriverPostProgram203WitnessSpv[] = {
|
||||
inline constexpr std::uint32_t kDriverPostIterationRPWitnessSpv[] = {
|
||||
0x07230203u, 0x00010300u, 0x0008000bu, 0x00000145u, 0x00000000u, 0x00020011u, 0x00000001u, 0x00020011u,
|
||||
0x0000003du, 0x00020011u, 0x0000003fu, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
|
||||
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x000a000fu, 0x00000005u, 0x00000004u, 0x6e69616du,
|
||||
@@ -286,6 +292,6 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
0x00050041u, 0x0000003bu, 0x00000141u, 0x00000037u, 0x00000124u, 0x0003003eu, 0x00000141u, 0x00000140u,
|
||||
0x000200f9u, 0x0000013du, 0x000200f8u, 0x0000013du, 0x000100fdu, 0x00010038u,
|
||||
};
|
||||
inline constexpr std::size_t kDriverPostProgram203WitnessSpvWordCount =
|
||||
sizeof(kDriverPostProgram203WitnessSpv) / sizeof(kDriverPostProgram203WitnessSpv[0]);
|
||||
inline constexpr std::size_t kDriverPostIterationRPWitnessSpvWordCount =
|
||||
sizeof(kDriverPostIterationRPWitnessSpv) / sizeof(kDriverPostIterationRPWitnessSpv[0]);
|
||||
} // namespace MobileGL::MG_Util::SelfTest
|
||||
@@ -1,151 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Compact, native-Vulkan Program-203 first-reduction witness ABI and its pure
|
||||
// validator. The types below deliberately mirror DriverPostProgram203Witness.comp's
|
||||
// single std430 storage block; changing either side requires updating the static
|
||||
// layout assertions here.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
namespace MobileGL::MG_Util::SelfTest {
|
||||
constexpr std::uint32_t kProgram203WitnessMagic = 0x50323033u; // "P203"
|
||||
constexpr std::uint32_t kProgram203WitnessInvocationCount = 512u;
|
||||
constexpr std::uint32_t kProgram203WitnessMaxSubgroups = 32u;
|
||||
constexpr std::uint32_t kProgram203WitnessMaxScanStages = 6u;
|
||||
|
||||
// These bit values are shared with the GLSL source. They document failures in
|
||||
// topology observations rather than guessing a topology from local IDs on the host.
|
||||
enum Program203WitnessTopologyFlag : std::uint32_t {
|
||||
Program203WitnessNonuniformNumSubgroups = 1u << 0u,
|
||||
Program203WitnessInvalidNumSubgroups = 1u << 1u,
|
||||
Program203WitnessInvalidSubgroupId = 1u << 2u,
|
||||
Program203WitnessInvalidSubgroupLane = 1u << 3u,
|
||||
};
|
||||
|
||||
struct alignas(8) Program203WitnessVec2 {
|
||||
float x;
|
||||
float y;
|
||||
};
|
||||
|
||||
struct alignas(16) Program203WitnessUVec4 {
|
||||
std::uint32_t x;
|
||||
std::uint32_t y;
|
||||
std::uint32_t z;
|
||||
std::uint32_t w;
|
||||
};
|
||||
|
||||
// std430 layout of DriverPostProgram203Witness.comp's Program203WitnessOutput block.
|
||||
struct alignas(16) Program203WitnessOutput {
|
||||
std::uint32_t magic;
|
||||
std::uint32_t topologyFlags;
|
||||
std::uint32_t numSubgroups;
|
||||
std::uint32_t loopLength;
|
||||
std::uint32_t seenSubgroupMask;
|
||||
|
||||
Program203WitnessUVec4 owner511;
|
||||
|
||||
std::array<std::uint32_t, kProgram203WitnessMaxSubgroups> lastLaneWriterCount;
|
||||
std::array<std::uint32_t, kProgram203WitnessMaxSubgroups> indexedInputTotal;
|
||||
|
||||
std::array<Program203WitnessVec2, kProgram203WitnessMaxSubgroups> rawPrefix;
|
||||
std::array<std::array<Program203WitnessVec2, kProgram203WitnessMaxSubgroups>,
|
||||
kProgram203WitnessMaxScanStages>
|
||||
scanCache;
|
||||
Program203WitnessVec2 finalAverage;
|
||||
};
|
||||
|
||||
static_assert(std::is_standard_layout_v<Program203WitnessVec2>);
|
||||
static_assert(std::is_standard_layout_v<Program203WitnessUVec4>);
|
||||
static_assert(std::is_standard_layout_v<Program203WitnessOutput>);
|
||||
static_assert(sizeof(Program203WitnessVec2) == 8u);
|
||||
static_assert(alignof(Program203WitnessVec2) == 8u);
|
||||
static_assert(sizeof(Program203WitnessUVec4) == 16u);
|
||||
static_assert(alignof(Program203WitnessUVec4) == 16u);
|
||||
static_assert(offsetof(Program203WitnessOutput, magic) == 0u);
|
||||
static_assert(offsetof(Program203WitnessOutput, topologyFlags) == 4u);
|
||||
static_assert(offsetof(Program203WitnessOutput, numSubgroups) == 8u);
|
||||
static_assert(offsetof(Program203WitnessOutput, loopLength) == 12u);
|
||||
static_assert(offsetof(Program203WitnessOutput, seenSubgroupMask) == 16u);
|
||||
static_assert(offsetof(Program203WitnessOutput, owner511) == 32u);
|
||||
static_assert(offsetof(Program203WitnessOutput, lastLaneWriterCount) == 48u);
|
||||
static_assert(offsetof(Program203WitnessOutput, indexedInputTotal) == 176u);
|
||||
static_assert(offsetof(Program203WitnessOutput, rawPrefix) == 304u);
|
||||
static_assert(offsetof(Program203WitnessOutput, scanCache) == 560u);
|
||||
static_assert(offsetof(Program203WitnessOutput, finalAverage) == 2096u);
|
||||
static_assert(sizeof(Program203WitnessOutput) == 2112u);
|
||||
|
||||
// The witness uses prefixSumCache[32], three scalar shared diagnostics, and
|
||||
// two 32-entry scalar diagnostic arrays in the GLSL source. Keep this
|
||||
// independent of the output SSBO size.
|
||||
constexpr std::uint32_t kProgram203WitnessSharedMemoryBytes =
|
||||
kProgram203WitnessMaxSubgroups * sizeof(Program203WitnessVec2) +
|
||||
3u * sizeof(std::uint32_t) +
|
||||
2u * kProgram203WitnessMaxSubgroups * sizeof(std::uint32_t);
|
||||
|
||||
enum class Program203WitnessEligibility {
|
||||
Execute,
|
||||
SkipUnsupportedNativeFeatureSet,
|
||||
FailInadequateLimits,
|
||||
};
|
||||
|
||||
// The raw physical-device conditions needed by the native witness. This is
|
||||
// intentionally distinct from MobileGL's advertised-extension policy.
|
||||
struct Program203WitnessLimits {
|
||||
bool computeStageSupported = false;
|
||||
bool basicSubgroupSupported = false;
|
||||
bool arithmeticSubgroupSupported = false;
|
||||
std::uint32_t subgroupSize = 0u;
|
||||
|
||||
std::uint32_t maxComputeWorkGroupInvocations = 0u;
|
||||
std::array<std::uint32_t, 3> maxComputeWorkGroupSize{};
|
||||
std::uint32_t maxComputeSharedMemorySize = 0u;
|
||||
std::uint32_t maxPerStageDescriptorStorageBuffers = 0u;
|
||||
std::uint32_t maxDescriptorSetStorageBuffers = 0u;
|
||||
std::uint32_t maxBoundDescriptorSets = 0u;
|
||||
std::uint64_t maxStorageBufferRange = 0u;
|
||||
};
|
||||
|
||||
struct Program203WitnessEligibilityResult {
|
||||
Program203WitnessEligibility eligibility = Program203WitnessEligibility::FailInadequateLimits;
|
||||
std::string detail;
|
||||
};
|
||||
|
||||
enum class Program203WitnessValidationFailure {
|
||||
None,
|
||||
Completion,
|
||||
Topology,
|
||||
InitialSubgroupHandoff,
|
||||
SourceScan,
|
||||
FinalOwner,
|
||||
FinalAverage,
|
||||
};
|
||||
|
||||
struct Program203WitnessValidationResult {
|
||||
bool ok = false;
|
||||
Program203WitnessValidationFailure failure = Program203WitnessValidationFailure::Completion;
|
||||
std::uint32_t scanStage = 0u;
|
||||
std::uint32_t subgroup = 0u;
|
||||
std::string detail;
|
||||
};
|
||||
|
||||
[[nodiscard]] Program203WitnessEligibilityResult
|
||||
EvaluateProgram203WitnessEligibility(const Program203WitnessLimits& limits);
|
||||
|
||||
// Mirrors the source's findMSB expression for valid N in [2, 32].
|
||||
[[nodiscard]] std::uint32_t ComputeProgram203WitnessLoopLength(std::uint32_t numSubgroups);
|
||||
|
||||
[[nodiscard]] Program203WitnessValidationResult
|
||||
ValidateProgram203Witness(const Program203WitnessOutput& output);
|
||||
} // namespace MobileGL::MG_Util::SelfTest
|
||||
@@ -26,6 +26,8 @@
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/ZeroBaseVertexPass.h"
|
||||
#include "SpirvPasses/DeriveNumSubgroupsPass.h"
|
||||
#include "SpirvPasses/EmulateSubgroupsPass.h"
|
||||
#include "SpirvPasses/FixIterationRPSubgroupScratchPass.h"
|
||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||
#include "SpirvPasses/Lower1DArrayImagesPass.h"
|
||||
#include "SpirvPasses/BakeImageFormatsPass.h"
|
||||
@@ -895,6 +897,32 @@ namespace MobileGL {
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::EmulateSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const Uint32 maxWorkgroupScratchBytes,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(
|
||||
EmulateSubgroupsPass::CreateEmulateSubgroupsPass(maxWorkgroupScratchBytes));
|
||||
|
||||
return RunOptimizerChecked("EmulateSubgroupsForVulkan", optimizer, inputBinary,
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
|
||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
|
||||
const Uint32 nativeSubgroupSize, const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(
|
||||
FixIterationRPSubgroupScratchPass::CreateFixIterationRPSubgroupScratchPass(
|
||||
nativeSubgroupSize));
|
||||
|
||||
return RunOptimizerChecked("FixIterationRPSubgroupScratchForVulkan", optimizer,
|
||||
inputBinary, outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -145,13 +145,35 @@ namespace MobileGL {
|
||||
static bool ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Replaces compute gl_NumSubgroups loads with the value derived from the local
|
||||
// workgroup dimensions and gl_SubgroupSize. DirectVulkan only; this avoids a
|
||||
// driver builtin that can disagree with the subgroup IDs the same dispatch emits.
|
||||
// See DeriveNumSubgroupsPass.
|
||||
// Replaces compute gl_NumSubgroups loads with ceil(workgroup invocations /
|
||||
// gl_SubgroupSize). DirectVulkan only; this repairs drivers whose builtin
|
||||
// disagrees with the subgroup IDs the same dispatch emits (Adreno reports 1
|
||||
// while emitting IDs 0..7). The ceil() partition is only spec-guaranteed
|
||||
// under VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT, which
|
||||
// the caller requests whenever it is legal for the workgroup shape; see
|
||||
// DeriveNumSubgroupsPass.
|
||||
static bool DeriveNumSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Lowers every GL_KHR_shader_subgroup construct in a compute module onto a
|
||||
// 32-lane virtual subgroup built from workgroup-shared memory. Last-resort
|
||||
// path for devices with NO native subgroup support, opt-in via
|
||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP=1; a device with native subgroup
|
||||
// operations always uses them. maxWorkgroupScratchBytes bounds the shared
|
||||
// scratch the lowering may add (pass the device's
|
||||
// maxComputeSharedMemorySize; 0 falls back to the 16384-byte Vulkan
|
||||
// minimum). See EmulateSubgroupsPass.
|
||||
static bool EmulateSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
Uint32 maxWorkgroupScratchBytes,
|
||||
bool enableSpirvValidation = false);
|
||||
// Patches iterationRP's under-declared prefixSumCache[32] on sub-16-lane
|
||||
// devices, fingerprint-gated to that pack's reduction; every other module
|
||||
// passes through byte-identical. See FixIterationRPSubgroupScratchPass.
|
||||
static bool FixIterationRPSubgroupScratchForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
Uint32 nativeSubgroupSize,
|
||||
bool enableSpirvValidation = false);
|
||||
// Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair
|
||||
// (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no
|
||||
// VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex
|
||||
|
||||
@@ -179,8 +179,14 @@ namespace MobileGL {
|
||||
: SynthesizeSubgroupSizeVariable(irContext, numSubgroupsVar->type_id());
|
||||
const uint32_t workgroupSizeId = workgroupSize->result_id();
|
||||
|
||||
// The pipeline never enables ALLOW_VARYING_SUBGROUP_SIZE, so Vulkan's fixed
|
||||
// subgroup partition is exactly ceil(local invocation count / SubgroupSize).
|
||||
// ceil(local invocation count / SubgroupSize): the subgroup count of a
|
||||
// full-subgroup launch. Vulkan only guarantees that partition under
|
||||
// REQUIRE_FULL_SUBGROUPS - which ProgramFactory requests whenever
|
||||
// local_size_x is a multiple of the subgroup size makes it legal
|
||||
// (VUID-VkPipelineShaderStageCreateInfo-flags-02759) - and calls the
|
||||
// tighter behaviour "encouraged" everywhere else; the DriverPost witness
|
||||
// verifies it per device where the flag cannot be set. The absence of
|
||||
// ALLOW_VARYING_SUBGROUP_SIZE pins only the SubgroupSize builtin itself.
|
||||
// `(count - 1) / size + 1` avoids an addition overflow at count + size - 1.
|
||||
for (Instruction* load : numSubgroupsLoads) {
|
||||
const uint32_t localSizeXId = irContext->TakeNextId();
|
||||
|
||||
@@ -19,12 +19,14 @@ namespace MobileGL {
|
||||
// Replaces compute-stage NumSubgroups builtin loads with
|
||||
// ceil(WorkgroupSize.x * WorkgroupSize.y * WorkgroupSize.z / SubgroupSize).
|
||||
//
|
||||
// That is the value Vulkan defines for NumSubgroups when the pipeline does not
|
||||
// enable varying subgroup sizes, which MobileGL never does. Deriving it avoids
|
||||
// drivers that expose the real SubgroupId topology but return an inconsistent
|
||||
// NumSubgroups value. This is a DirectVulkan semantic repair, not a source-shader
|
||||
// rewrite; the application's subgroup arithmetic and shared-memory logic remain
|
||||
// unchanged.
|
||||
// That is the subgroup count of a full-subgroup launch - guaranteed by Vulkan
|
||||
// under REQUIRE_FULL_SUBGROUPS (which ProgramFactory requests whenever the
|
||||
// workgroup shape makes it legal), spec-"encouraged" and witness-verified
|
||||
// (DriverPost) elsewhere. Deriving it repairs drivers that expose the real
|
||||
// SubgroupId topology but return an inconsistent NumSubgroups value, breaking
|
||||
// GL's gl_SubgroupID < gl_NumSubgroups contract. This is a DirectVulkan
|
||||
// semantic repair, not a source-shader rewrite; the application's subgroup
|
||||
// arithmetic and shared-memory logic remain unchanged.
|
||||
class DeriveNumSubgroupsPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "derive-num-subgroups"; }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Lowers every GL_KHR_shader_subgroup construct in a compute module onto a
|
||||
// 32-lane VIRTUAL subgroup implemented with workgroup-shared memory. Virtual
|
||||
// subgroups partition the workgroup by gl_LocalInvocationIndex:
|
||||
// lane = index & 31, id = index >> 5, count = ceil(invocations / 32).
|
||||
//
|
||||
// This is a LAST-RESORT path, never a substitute for real subgroups: it only
|
||||
// runs when MOBILEGL_MAGMA_EMULATE_SUBGROUP=1 is set explicitly and the device
|
||||
// has no native subgroup support at all (SubgroupSupportPolicy.h). A device
|
||||
// with native subgroup operations - however narrow - uses them natively, with
|
||||
// FixIterationRPSubgroupScratchPass patching the known pack bug instead.
|
||||
//
|
||||
// Lowered constructs:
|
||||
// - the builtins gl_SubgroupSize / gl_SubgroupInvocationID / gl_SubgroupID /
|
||||
// gl_NumSubgroups and the five gl_Subgroup*Mask ballot builtins;
|
||||
// - OpGroupNonUniform{Elect,All,Any,AllEqual,Broadcast,BroadcastFirst,
|
||||
// Ballot,InverseBallot,BallotBitExtract,BallotBitCount,BallotFind{L,M}SB,
|
||||
// Shuffle,ShuffleXor,ShuffleUp,ShuffleDown,
|
||||
// <arithmetic/min/max/bitwise/logical reduce+scans+clustered>,
|
||||
// QuadBroadcast,QuadSwap};
|
||||
// - subgroupBarrier()/subgroupMemoryBarrier*() (their Subgroup scopes widen
|
||||
// to Workgroup, which is strictly stronger).
|
||||
// The output uses no GroupNonUniform* instruction or capability at all, which
|
||||
// is what lets it run on devices with no subgroup feature bits.
|
||||
//
|
||||
// Semantic contract, narrower than native subgroups in exactly one way: every
|
||||
// emulated exchange synchronizes through OpControlBarrier, so subgroup
|
||||
// operations must sit in WORKGROUP-uniform control flow (the shape every
|
||||
// Iris-style pack reduction has). GLSL already imposes this for barrier();
|
||||
// a subgroup op in divergent flow - legal on native subgroups - is undefined
|
||||
// here.
|
||||
//
|
||||
// Fails (Status::Failure, leaving the input module unchanged) on anything it
|
||||
// cannot lower faithfully: extended subgroup ops (partitioned-NV, rotate,
|
||||
// quad-all/any), non-32-bit participating types, spec-constant workgroup
|
||||
// sizes, a subgroup builtin reached by anything but a direct OpLoad, or a
|
||||
// module whose lowering would add more workgroup scratch than
|
||||
// maxWorkgroupScratchBytes (pass the device's maxComputeSharedMemorySize;
|
||||
// 0 falls back to the 16384-byte Vulkan minimum).
|
||||
class EmulateSubgroupsPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
explicit EmulateSubgroupsPass(Uint32 maxWorkgroupScratchBytes)
|
||||
: m_maxWorkgroupScratchBytes(maxWorkgroupScratchBytes) {}
|
||||
|
||||
const char* name() const override { return "emulate-subgroups"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateEmulateSubgroupsPass(
|
||||
Uint32 maxWorkgroupScratchBytes);
|
||||
|
||||
private:
|
||||
Uint32 m_maxWorkgroupScratchBytes;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,352 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "FixIterationRPSubgroupScratchPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
|
||||
// iterationRP's reduction fingerprint, spelled out.
|
||||
constexpr uint32_t kIterationRPLocalSizeX = 32u;
|
||||
constexpr uint32_t kIterationRPLocalSizeY = 16u;
|
||||
constexpr uint32_t kIterationRPLocalSizeZ = 1u;
|
||||
constexpr uint32_t kIterationRPInvocations =
|
||||
kIterationRPLocalSizeX * kIterationRPLocalSizeY * kIterationRPLocalSizeZ;
|
||||
constexpr uint32_t kIterationRPScratchLength = 32u;
|
||||
|
||||
Instruction* FindBuiltinDefinition(IRContext* context, spv::BuiltIn builtin) {
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
for (auto& annotation : context->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate || annotation.NumInOperands() < 3) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
|
||||
spv::Decoration::BuiltIn) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) != builtin) {
|
||||
continue;
|
||||
}
|
||||
return defUseMgr->GetDef(annotation.GetSingleWordInOperand(0));
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Walks an access-chain pointer expression back to the variable it is
|
||||
// rooted at; returns nullptr for anything that is not a plain chain.
|
||||
const Instruction* RootVariable(IRContext* context, uint32_t pointerId) {
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
const Instruction* def = defUseMgr->GetDef(pointerId);
|
||||
while (def != nullptr) {
|
||||
switch (def->opcode()) {
|
||||
case spv::Op::OpVariable:
|
||||
return def;
|
||||
case spv::Op::OpAccessChain:
|
||||
case spv::Op::OpInBoundsAccessChain:
|
||||
case spv::Op::OpCopyObject:
|
||||
def = defUseMgr->GetDef(def->GetSingleWordInOperand(0));
|
||||
break;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// vec2 of 32-bit float - the type of iterationRP's luminance/exposure
|
||||
// accumulator and of its prefixSumCache entries.
|
||||
bool IsVec2Float32(IRContext* context, uint32_t typeId) {
|
||||
const Instruction* type = context->get_def_use_mgr()->GetDef(typeId);
|
||||
if (type == nullptr || type->opcode() != spv::Op::OpTypeVector ||
|
||||
type->GetSingleWordInOperand(1) != 2u) {
|
||||
return false;
|
||||
}
|
||||
const Instruction* component =
|
||||
context->get_def_use_mgr()->GetDef(type->GetSingleWordInOperand(0));
|
||||
return component != nullptr && component->opcode() == spv::Op::OpTypeFloat &&
|
||||
component->GetSingleWordInOperand(0) == 32u;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status FixIterationRPSubgroupScratchPass::Process() {
|
||||
auto* irContext = context();
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
|
||||
// A device whose native width already satisfies the pack's assumption
|
||||
// (>= 16 lanes -> at most 32 subgroups) needs no patch at all.
|
||||
if (m_nativeSubgroupSize == 0u || m_nativeSubgroupSize >= 16u) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
const uint32_t requiredLength =
|
||||
(kIterationRPInvocations + m_nativeSubgroupSize - 1u) / m_nativeSubgroupSize;
|
||||
|
||||
for (const Instruction& entryPoint : irContext->module()->entry_points()) {
|
||||
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
|
||||
spv::ExecutionModel::GLCompute) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
}
|
||||
|
||||
// Fingerprint 1: the pack's exposure-pass workgroup shape, 32x16x1.
|
||||
const auto resolveUintConstant = [&](uint32_t id, uint32_t* value) {
|
||||
const Instruction* def = defUseMgr->GetDef(id);
|
||||
if (def == nullptr || def->opcode() != spv::Op::OpConstant) return false;
|
||||
*value = def->GetSingleWordInOperand(0);
|
||||
return true;
|
||||
};
|
||||
uint32_t localSize[3] = {0, 0, 0};
|
||||
bool haveLocalSize = false;
|
||||
if (Instruction* workgroupSize =
|
||||
FindBuiltinDefinition(irContext, spv::BuiltIn::WorkgroupSize)) {
|
||||
if (workgroupSize->opcode() == spv::Op::OpConstantComposite &&
|
||||
workgroupSize->NumInOperands() == 3) {
|
||||
haveLocalSize =
|
||||
resolveUintConstant(workgroupSize->GetSingleWordInOperand(0), &localSize[0]) &&
|
||||
resolveUintConstant(workgroupSize->GetSingleWordInOperand(1), &localSize[1]) &&
|
||||
resolveUintConstant(workgroupSize->GetSingleWordInOperand(2), &localSize[2]);
|
||||
}
|
||||
}
|
||||
if (!haveLocalSize) {
|
||||
for (const Instruction& mode : irContext->module()->execution_modes()) {
|
||||
if (mode.opcode() == spv::Op::OpExecutionMode &&
|
||||
static_cast<spv::ExecutionMode>(mode.GetSingleWordInOperand(1)) ==
|
||||
spv::ExecutionMode::LocalSize) {
|
||||
localSize[0] = mode.GetSingleWordInOperand(2);
|
||||
localSize[1] = mode.GetSingleWordInOperand(3);
|
||||
localSize[2] = mode.GetSingleWordInOperand(4);
|
||||
haveLocalSize = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!haveLocalSize || localSize[0] != kIterationRPLocalSizeX ||
|
||||
localSize[1] != kIterationRPLocalSizeY || localSize[2] != kIterationRPLocalSizeZ) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// Fingerprint 2: the reduction's subgroupInclusiveAdd on a vec2.
|
||||
bool sawVec2InclusiveAdd = false;
|
||||
for (auto& function : *irContext->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& inst : block) {
|
||||
if (inst.opcode() == spv::Op::OpGroupNonUniformFAdd &&
|
||||
static_cast<spv::GroupOperation>(inst.GetSingleWordInOperand(1)) ==
|
||||
spv::GroupOperation::InclusiveScan &&
|
||||
IsVec2Float32(irContext, inst.type_id())) {
|
||||
sawVec2InclusiveAdd = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!sawVec2InclusiveAdd) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// gl_SubgroupID, whose value range the pack's scratch size bakes in.
|
||||
const Instruction* subgroupIdVariable =
|
||||
FindBuiltinDefinition(irContext, spv::BuiltIn::SubgroupId);
|
||||
if (subgroupIdVariable == nullptr ||
|
||||
subgroupIdVariable->opcode() != spv::Op::OpVariable) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
const uint32_t subgroupIdVariableId = subgroupIdVariable->result_id();
|
||||
|
||||
// Conservative taint walk over values, and through Function/Private
|
||||
// temporaries by variable (glslang routinely spills builtin loads into
|
||||
// locals before they reach an index expression). Over-tainting is safe:
|
||||
// the candidate filter below still demands the exact vec2[32] shape.
|
||||
std::unordered_map<uint32_t, bool> valueTainted; // result id -> tainted
|
||||
std::unordered_map<uint32_t, bool> variableTainted; // variable id -> tainted
|
||||
bool changedTaint = true;
|
||||
while (changedTaint) {
|
||||
changedTaint = false;
|
||||
for (auto& function : *irContext->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& inst : block) {
|
||||
const spv::Op opcode = inst.opcode();
|
||||
if (opcode == spv::Op::OpStore) {
|
||||
if (!valueTainted.count(inst.GetSingleWordInOperand(1))) continue;
|
||||
const Instruction* root =
|
||||
RootVariable(irContext, inst.GetSingleWordInOperand(0));
|
||||
if (root == nullptr) continue;
|
||||
if (!variableTainted.count(root->result_id())) {
|
||||
variableTainted[root->result_id()] = true;
|
||||
changedTaint = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inst.result_id() == 0 || valueTainted.count(inst.result_id())) {
|
||||
continue;
|
||||
}
|
||||
bool tainted = false;
|
||||
if (opcode == spv::Op::OpLoad) {
|
||||
const uint32_t pointerId = inst.GetSingleWordInOperand(0);
|
||||
if (pointerId == subgroupIdVariableId) tainted = true;
|
||||
const Instruction* root = RootVariable(irContext, pointerId);
|
||||
if (root != nullptr && variableTainted.count(root->result_id())) {
|
||||
tainted = true;
|
||||
}
|
||||
} else {
|
||||
inst.ForEachInId([&](const uint32_t* operandId) {
|
||||
if (valueTainted.count(*operandId)) tainted = true;
|
||||
});
|
||||
}
|
||||
if (tainted) {
|
||||
valueTainted[inst.result_id()] = true;
|
||||
changedTaint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (valueTainted.empty()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// Fingerprint 3: workgroup-shared vec2[32] arrays whose access-chain
|
||||
// index depends on gl_SubgroupID - the under-declared prefixSumCache.
|
||||
std::map<uint32_t, Instruction*> candidates;
|
||||
for (auto& function : *irContext->module()) {
|
||||
for (auto& block : function) {
|
||||
for (auto& inst : block) {
|
||||
if (inst.opcode() != spv::Op::OpAccessChain &&
|
||||
inst.opcode() != spv::Op::OpInBoundsAccessChain) {
|
||||
continue;
|
||||
}
|
||||
if (inst.NumInOperands() < 2) continue;
|
||||
if (!valueTainted.count(inst.GetSingleWordInOperand(1))) continue;
|
||||
Instruction* baseVariable =
|
||||
defUseMgr->GetDef(inst.GetSingleWordInOperand(0));
|
||||
if (baseVariable == nullptr ||
|
||||
baseVariable->opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(
|
||||
baseVariable->GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Workgroup) {
|
||||
continue;
|
||||
}
|
||||
candidates.emplace(baseVariable->result_id(), baseVariable);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (candidates.empty()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
bool changedModule = false;
|
||||
for (auto& entry : candidates) {
|
||||
Instruction* variable = entry.second;
|
||||
|
||||
// The variable must be reached exclusively through access chains (plus
|
||||
// debug/decoration instructions): a whole-array load, store, or copy
|
||||
// would change type with the array and is left alone.
|
||||
bool onlyAccessChains = true;
|
||||
const uint32_t variableId = variable->result_id();
|
||||
defUseMgr->ForEachUser(variable, [&](Instruction* user) {
|
||||
switch (user->opcode()) {
|
||||
case spv::Op::OpAccessChain:
|
||||
case spv::Op::OpInBoundsAccessChain:
|
||||
if (user->GetSingleWordInOperand(0) != variableId) {
|
||||
onlyAccessChains = false;
|
||||
}
|
||||
return;
|
||||
case spv::Op::OpName:
|
||||
case spv::Op::OpDecorate:
|
||||
return;
|
||||
default:
|
||||
onlyAccessChains = false;
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (!onlyAccessChains) continue;
|
||||
if (variable->NumInOperands() > 1) continue; // initializer: leave alone
|
||||
|
||||
const Instruction* pointerType = defUseMgr->GetDef(variable->type_id());
|
||||
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) {
|
||||
continue;
|
||||
}
|
||||
const Instruction* arrayType =
|
||||
defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1));
|
||||
if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t elementTypeId = arrayType->GetSingleWordInOperand(0);
|
||||
if (!IsVec2Float32(irContext, elementTypeId)) continue;
|
||||
const Instruction* lengthConstant =
|
||||
defUseMgr->GetDef(arrayType->GetSingleWordInOperand(1));
|
||||
uint32_t currentLength = 0;
|
||||
if (lengthConstant == nullptr ||
|
||||
lengthConstant->opcode() != spv::Op::OpConstant ||
|
||||
!((currentLength = lengthConstant->GetSingleWordInOperand(0),
|
||||
currentLength == kIterationRPScratchLength))) {
|
||||
continue;
|
||||
}
|
||||
if (currentLength >= requiredLength) continue;
|
||||
|
||||
// Build the grown array type. All three new instructions are inserted
|
||||
// immediately BEFORE the variable so definition-before-use holds in the
|
||||
// module's global section (manager-created instructions append to its
|
||||
// end, after the variable). The new length constant reuses the old
|
||||
// one's integer type, whatever signedness glslang gave it (a duplicate
|
||||
// scalar constant is legal SPIR-V); the fresh array type makes the
|
||||
// pointer type unique by construction, so neither collides with an
|
||||
// existing declaration.
|
||||
const uint32_t lengthTypeId = lengthConstant->type_id();
|
||||
const uint32_t newLengthId = irContext->TakeNextId();
|
||||
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpConstant, lengthTypeId, newLengthId,
|
||||
Instruction::OperandList{{SPV_OPERAND_TYPE_TYPED_LITERAL_NUMBER,
|
||||
{requiredLength}}}));
|
||||
const uint32_t newArrayTypeId = irContext->TakeNextId();
|
||||
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpTypeArray, 0, newArrayTypeId,
|
||||
Instruction::OperandList{
|
||||
{SPV_OPERAND_TYPE_ID, {elementTypeId}},
|
||||
{SPV_OPERAND_TYPE_ID, {newLengthId}}}));
|
||||
const uint32_t newPointerTypeId = irContext->TakeNextId();
|
||||
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
irContext, spv::Op::OpTypePointer, 0, newPointerTypeId,
|
||||
Instruction::OperandList{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{static_cast<uint32_t>(spv::StorageClass::Workgroup)}},
|
||||
{SPV_OPERAND_TYPE_ID, {newArrayTypeId}}}));
|
||||
|
||||
variable->SetResultType(newPointerTypeId);
|
||||
changedModule = true;
|
||||
}
|
||||
|
||||
if (!changedModule) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken
|
||||
FixIterationRPSubgroupScratchPass::CreateFixIterationRPSubgroupScratchPass(
|
||||
const Uint32 nativeSubgroupSize) {
|
||||
return spvtools::Optimizer::PassToken(
|
||||
MakeUnique<FixIterationRPSubgroupScratchPass>(nativeSubgroupSize));
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,64 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.h
|
||||
// Copyright (c) 2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Patches ONE known shader-pack defect: iterationRP's auto-exposure reduction
|
||||
// declares `shared vec2 prefixSumCache[32]` for its 512-invocation workgroup
|
||||
// and stores per-subgroup subtotals through prefixSumCache[gl_SubgroupID].
|
||||
// The pack hard-sized that scratch for the >=16-lane subgroups desktop GL
|
||||
// drivers ship; on a narrower Vulkan device (lavapipe's 8 lanes -> 64
|
||||
// subgroups) every subgroup past entry 31 indexes shared memory out of
|
||||
// bounds - on a CPU rasterizer that is literal heap corruption. The
|
||||
// reduction ALGORITHM is width-agnostic (its combine loop is sized by
|
||||
// gl_NumSubgroups), so the faithful repair is to grow the one under-declared
|
||||
// array to ceil(512 / native width) and change nothing else. This is the
|
||||
// pack author's bug, not MobileGL's; the patch is therefore deliberately
|
||||
// NOT a general mechanism - it only rewrites modules that positively match
|
||||
// iterationRP's reduction fingerprint:
|
||||
// - GLCompute entry point with local size exactly 32x16x1;
|
||||
// - a subgroupInclusiveAdd on a vec2 (OpGroupNonUniformFAdd InclusiveScan,
|
||||
// the pack's luminance/exposure accumulator signature);
|
||||
// - a workgroup-shared array of exactly vec2[32] whose access-chain index
|
||||
// is data-dependent on gl_SubgroupID.
|
||||
// Matching at the SPIR-V level keeps the recognition robust against
|
||||
// whitespace/identifier-level drift that made the old source-text template
|
||||
// rewrite (removed in 7769156) so brittle, while still refusing to touch
|
||||
// anything that is not this pack's reduction. On devices whose native width
|
||||
// already satisfies the pack's assumption (>= 16 lanes: desktop GL, Adreno),
|
||||
// the grown length equals or undershoots the declared 32 and every module
|
||||
// passes through byte-identical.
|
||||
//
|
||||
// The pass never fails a module: anything it cannot prove is this exact
|
||||
// pattern - or cannot grow safely (a whole-array use, a spec-constant
|
||||
// length, an initializer) - is left exactly as it was.
|
||||
class FixIterationRPSubgroupScratchPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
explicit FixIterationRPSubgroupScratchPass(Uint32 nativeSubgroupSize)
|
||||
: m_nativeSubgroupSize(nativeSubgroupSize) {}
|
||||
|
||||
const char* name() const override { return "fix-iterationrp-subgroup-scratch"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateFixIterationRPSubgroupScratchPass(
|
||||
Uint32 nativeSubgroupSize);
|
||||
|
||||
private:
|
||||
Uint32 m_nativeSubgroupSize;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
Reference in New Issue
Block a user