Files
MobileGL/MobileGL/MG_IntegrationTest/Scenarios/IterationRPFirstReductionScenario.cpp
T
swung0x48 d8576a2ed3 [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.
2026-08-19 09:48:11 -04:00

899 lines
46 KiB
C++

// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IterationRPFirstReductionScenario.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
//
// Scenario - ITERATIONRP'S FIRST SUBGROUP REDUCTION.
//
// iterationRP reduces a 32 x 16 exposure tile with a vector subgroup inclusive add,
// then a shared-memory scan of subgroup totals. The source assumes that every
// subgroup has a last lane, that there are 2..32 subgroups, and that local index
// 511 belongs to the last subgroup and its last lane. Those are source assumptions,
// not API contracts. This probe intentionally does not repair them: it records the
// observed topology and makes each handoff independently observable.
#include <algorithm>
#include <array>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <limits>
#include <sstream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr std::size_t kInvocationCount = 512;
constexpr std::size_t kScanStageCount = 6;
constexpr std::uint32_t kQuietNanBits = 0x7fc00000u;
constexpr std::size_t kNoSlot = std::numeric_limits<std::size_t>::max();
struct UVec4 {
std::uint32_t x;
std::uint32_t y;
std::uint32_t z;
std::uint32_t w;
};
struct Vec4 {
float x;
float y;
float z;
float w;
};
// Matches the std430 block exactly. uvec4/vec4 arrays have a 16-byte
// stride, floats are a dense scalar array, and the outer scan array is
// stage-major in both GLSL and C++.
struct ProbeOutput {
std::array<UVec4, kInvocationCount> invocation;
std::array<UVec4, kInvocationCount> subgroup;
std::array<Vec4, kInvocationCount> reduction;
std::array<float, kInvocationCount> finalAverage;
std::array<std::array<float, kInvocationCount>, kScanStageCount> scanAfter;
};
static_assert(sizeof(UVec4) == 16);
static_assert(sizeof(Vec4) == 16);
static_assert(std::is_standard_layout_v<ProbeOutput>);
static_assert(offsetof(ProbeOutput, invocation) == 0);
static_assert(offsetof(ProbeOutput, subgroup) == 8192);
static_assert(offsetof(ProbeOutput, reduction) == 16384);
static_assert(offsetof(ProbeOutput, finalAverage) == 24576);
static_assert(offsetof(ProbeOutput, scanAfter) == 26624);
static_assert(sizeof(ProbeOutput) == 38912);
enum class InputMode {
SampledRgba32f,
IndexedSsbo,
};
const char* InputModeName(InputMode mode) {
return mode == InputMode::SampledRgba32f ? "sampled RGBA32F" : "indexed SSBO";
}
std::uint32_t FloatBits(float value) {
return std::bit_cast<std::uint32_t>(value);
}
bool SameBits(float lhs, float rhs) {
return FloatBits(lhs) == FloatBits(rhs);
}
bool IsQuietNanSentinel(float value) {
return FloatBits(value) == kQuietNanBits;
}
bool DrainGlErrors() {
bool hadError = false;
while (glGetError() != GL_NO_ERROR) hadError = true;
return hadError;
}
bool HasExtension(const char* wanted) {
GLint extensionCount = 0;
glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);
for (GLint i = 0; i < extensionCount; ++i) {
const auto* extension = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
if (extension != nullptr && std::string(extension) == wanted) return true;
}
return false;
}
struct CapabilityInfo {
bool subgroupExtension = false;
GLint subgroupSize = 0;
GLint supportedStages = 0;
GLint supportedFeatures = 0;
GLint maxComputeStorageBlocks = 0;
GLint maxStorageBindings = 0;
GLint maxWorkGroupInvocations = 0;
std::array<GLint, 3> maxWorkGroupSize{};
bool queryHadError = false;
// iterationRP's source contract needs gl_NumSubgroups in [2, 32] for its 512
// invocations, i.e. an advertised subgroup width in [16, 256]. A device
// outside that window (lavapipe's 8-lane subgroups give 64 subgroups) cannot
// run the fixture's verbatim reduction at all, so the scenario SKIPS there -
// the pack itself replays through the FixIterationRPSubgroupScratch patch, which
// this probe deliberately does not model. The width only gates the domain;
// lane placement and group counts still come from observed values alone.
bool SubgroupWidthInSourceDomain() const {
return subgroupSize >= 16 && subgroupSize <= 256;
}
bool SupportsProbe() const {
const auto stages = static_cast<GLbitfield>(supportedStages);
const auto features = static_cast<GLbitfield>(supportedFeatures);
return !queryHadError && subgroupExtension &&
(stages & GL_COMPUTE_SHADER_BIT) != 0 &&
(features & (GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR)) ==
(GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR) &&
SubgroupWidthInSourceDomain() &&
maxComputeStorageBlocks >= 2 && maxStorageBindings >= 2 &&
maxWorkGroupInvocations >= static_cast<GLint>(kInvocationCount) && maxWorkGroupSize[0] >= 32 &&
maxWorkGroupSize[1] >= 16 && maxWorkGroupSize[2] >= 1;
}
std::string MissingRequirements() const {
std::vector<std::string> missing;
const auto stages = static_cast<GLbitfield>(supportedStages);
const auto features = static_cast<GLbitfield>(supportedFeatures);
if (queryHadError) missing.emplace_back("a subgroup/compute capability query generated GL error");
if (!subgroupExtension) missing.emplace_back("GL_KHR_shader_subgroup");
if ((stages & GL_COMPUTE_SHADER_BIT) == 0) {
missing.emplace_back("GL_COMPUTE_SHADER_BIT in GL_SUBGROUP_SUPPORTED_STAGES_KHR");
}
const auto requiredFeatures =
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
if ((features & requiredFeatures) != requiredFeatures) {
missing.emplace_back("basic|arithmetic in GL_SUBGROUP_SUPPORTED_FEATURES_KHR");
}
if (!SubgroupWidthInSourceDomain()) {
missing.emplace_back(
"GL_SUBGROUP_SIZE_KHR in [16, 256] (iterationRP's source contract needs "
"gl_NumSubgroups in [2, 32] for 512 invocations; width " +
std::to_string(subgroupSize) + " is outside the fixture's domain)");
}
if (maxComputeStorageBlocks < 2 || maxStorageBindings < 2) {
missing.emplace_back("two compute SSBO bindings");
}
if (maxWorkGroupInvocations < static_cast<GLint>(kInvocationCount) || maxWorkGroupSize[0] < 32 ||
maxWorkGroupSize[1] < 16 || maxWorkGroupSize[2] < 1) {
missing.emplace_back("a 32x16x1 / 512-invocation compute workgroup");
}
std::ostringstream message;
for (std::size_t i = 0; i < missing.size(); ++i) {
if (i != 0) message << ", ";
message << missing[i];
}
return message.str();
}
};
CapabilityInfo QueryCapabilities() {
CapabilityInfo info;
DrainGlErrors();
info.subgroupExtension = HasExtension("GL_KHR_shader_subgroup");
glGetIntegerv(GL_SUBGROUP_SIZE_KHR, &info.subgroupSize);
glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &info.supportedStages);
glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &info.supportedFeatures);
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &info.maxComputeStorageBlocks);
glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &info.maxStorageBindings);
glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &info.maxWorkGroupInvocations);
for (GLuint axis = 0; axis < info.maxWorkGroupSize.size(); ++axis) {
glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, axis, &info.maxWorkGroupSize[axis]);
}
info.queryHadError = DrainGlErrors();
return info;
}
void PrintMetadata(const CapabilityInfo& info, std::ostream& output) {
output << "IterationRPFirstReductionScenario metadata: "
<< "GL_SUBGROUP_SIZE_KHR=" << info.subgroupSize
<< ", GL_SUBGROUP_SUPPORTED_STAGES_KHR=0x" << std::hex
<< static_cast<GLbitfield>(info.supportedStages)
<< ", GL_SUBGROUP_SUPPORTED_FEATURES_KHR=0x"
<< static_cast<GLbitfield>(info.supportedFeatures) << std::dec
<< ", subgroupExtension=" << info.subgroupExtension
<< ", GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS=" << info.maxComputeStorageBlocks
<< ", GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS=" << info.maxStorageBindings
<< ", GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS=" << info.maxWorkGroupInvocations
<< ", GL_MAX_COMPUTE_WORK_GROUP_SIZE=" << info.maxWorkGroupSize[0] << 'x'
<< info.maxWorkGroupSize[1] << 'x' << info.maxWorkGroupSize[2]
<< ", queryHadError=" << info.queryHadError << '\n';
}
bool DumpRequested() {
const char* value = std::getenv("MOBILEGL_ITEST_SUBGROUP_PROBE_DUMP");
return value != nullptr && std::string(value) == "1";
}
constexpr const char* kShaderPreamble = R"(#version 430 core
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
layout(std430, binding = 1) buffer SubgroupProbeOutput {
uvec4 invocation[512];
uvec4 subgroup[512];
vec4 reduction[512];
float finalAverage[512];
float scanAfter[6][512];
} outProbe;
shared vec2 prefixSumCache[32];
)";
constexpr const char* kSampledInput = R"(
uniform sampler2D colortex2;
uniform vec2 pixelSize;
)";
constexpr const char* kIndexedInput = R"(
layout(std430, binding = 0) readonly buffer Input {
float value[512];
} inputData;
)";
// Only the expression producing tileExposure differs between the two
// tests. The remainder is the iterationRP first reduction, with stores
// placed after its existing barriers to expose each handoff.
constexpr const char* kSampledTileExposure = R"(
vec2 texCoord = (vec2(gl_GlobalInvocationID.xy) + 0.5) *
vec2(1.0 / 32.0, 1.0 / 16.0);
vec2 sampleCoord = texCoord * (1.0 / 64.0);
sampleCoord.x += (15.0 / 32.0) + pixelSize.x * 12.0;
float tileExposure = dot(
textureLod(colortex2, sampleCoord, 0.0).rgb,
vec3(0.2125, 0.7154, 0.0721));
)";
constexpr const char* kIndexedTileExposure = R"(
float tileExposure = inputData.value[gl_LocalInvocationIndex];
)";
constexpr const char* kReductionBody = R"(
vec2 sampleLuminance = vec2(tileExposure, 0.0);
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
float nativeInclusive = sampleLuminance.x;
// This is a uniform, safety-only branch: it leaves an invalid source
// contract visible without indexing past the 32-entry cache or underflowing
// loopLength - 1. It is deliberately a failure on the CPU, not a skip.
bool sourceDomain = gl_NumSubgroups >= 2u && gl_NumSubgroups <= 32u;
if (!sourceDomain) {
float qNaN = uintBitsToFloat(0x7fc00000u);
uint localIndex = gl_LocalInvocationIndex;
outProbe.invocation[localIndex] = uvec4(localIndex, gl_LocalInvocationID);
outProbe.subgroup[localIndex] = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID,
gl_SubgroupInvocationID);
outProbe.reduction[localIndex] = vec4(tileExposure, nativeInclusive, qNaN, qNaN);
outProbe.finalAverage[localIndex] = qNaN;
for (uint stage = 0u; stage < 6u; ++stage)
outProbe.scanAfter[stage][localIndex] = qNaN;
return;
}
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
barrier();
float sourceRawSubtotal = prefixSumCache[gl_SubgroupID].x;
uint loopLength = uint(findMSB(gl_NumSubgroups));
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
sampleLuminance += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
prefixSumCache[gl_SubgroupID] = sampleLuminance;
}
barrier();
outProbe.scanAfter[scanStage][gl_LocalInvocationIndex] = sampleLuminance.x;
}
float sourceMergedPrefix = sampleLuminance.x;
if (gl_LocalInvocationIndex == 511u)
prefixSumCache[0] = sampleLuminance / 512.0;
barrier();
float avg = prefixSumCache[0].x;
uint localIndex = gl_LocalInvocationIndex;
outProbe.invocation[localIndex] = uvec4(localIndex, gl_LocalInvocationID);
outProbe.subgroup[localIndex] = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID,
gl_SubgroupInvocationID);
outProbe.reduction[localIndex] = vec4(tileExposure, nativeInclusive, sourceRawSubtotal, sourceMergedPrefix);
outProbe.finalAverage[localIndex] = avg;
}
)";
std::string BuildProbeShader(InputMode mode) {
std::string source = kShaderPreamble;
source += mode == InputMode::SampledRgba32f ? kSampledInput : kIndexedInput;
source += "\nvoid main() {\n";
source += mode == InputMode::SampledRgba32f ? kSampledTileExposure : kIndexedTileExposure;
source += kReductionBody;
return source;
}
std::string FormatFloat(float value) {
std::ostringstream text;
text << std::hexfloat << value;
return text.str();
}
struct ValidationResult {
bool ok = true;
std::string phase;
std::string message;
bool scanStageMismatch = false;
int scanStage = -1;
bool ownerEvaluated = false;
bool index511IsSourceLastLaneWriter = false;
bool index511IsHighestSubgroupMember = false;
std::uint32_t highestObservedSubgroup = 0;
};
ValidationResult Failure(std::string phase, std::string message) {
ValidationResult result;
result.ok = false;
result.phase = std::move(phase);
result.message = std::move(message);
return result;
}
constexpr float kSampledLuminance = 0.2125f + 0.7154f + 0.0721f;
float ExpectedInput(InputMode mode, std::uint32_t localIndex) {
return mode == InputMode::SampledRgba32f ? kSampledLuminance : static_cast<float>(localIndex + 1u);
}
ValidationResult ValidateProbe(const ProbeOutput& output, InputMode mode) {
std::array<std::size_t, kInvocationCount> slotForLocal{};
slotForLocal.fill(kNoSlot);
// 1. Record identity. Slots are only used to locate each reported
// local index; all subgroup behavior below groups recorded IDs/lanes.
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
const std::uint32_t localIndex = output.invocation[slot].x;
if (localIndex >= kInvocationCount) {
std::ostringstream message;
message << "output slot " << slot << " reports localIndex " << localIndex << " outside [0, 511]";
return Failure("record identity", message.str());
}
if (slotForLocal[localIndex] != kNoSlot) {
std::ostringstream message;
message << "localIndex " << localIndex << " appears in output slots " << slotForLocal[localIndex]
<< " and " << slot;
return Failure("record identity", message.str());
}
slotForLocal[localIndex] = slot;
}
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
if (slotForLocal[localIndex] == kNoSlot) {
std::ostringstream message;
message << "localIndex " << localIndex << " is missing from all 512 records";
return Failure("record identity", message.str());
}
}
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const UVec4& invocation = output.invocation[slot];
const std::uint32_t expectedX = static_cast<std::uint32_t>(localIndex % 32u);
const std::uint32_t expectedY = static_cast<std::uint32_t>(localIndex / 32u);
if (invocation.y != expectedX || invocation.z != expectedY || invocation.w != 0u) {
std::ostringstream message;
message << "localIndex " << localIndex << " reports local invocation (" << invocation.y << ','
<< invocation.z << ',' << invocation.w << "), expected (" << expectedX << ',' << expectedY
<< ",0)";
return Failure("record identity", message.str());
}
const float expectedInput = ExpectedInput(mode, static_cast<std::uint32_t>(localIndex));
const float actualInput = output.reduction[slot].x;
if (!SameBits(actualInput, expectedInput)) {
std::ostringstream message;
message << "localIndex " << localIndex << " input was " << FormatFloat(actualInput) << ", expected "
<< FormatFloat(expectedInput);
return Failure("input", message.str());
}
}
// 2. Observed topology. Do not derive lanes or subgroup membership
// from local invocation indices: only the values the shader recorded
// participate in grouping.
const std::uint32_t reportedNumSubgroups = output.subgroup[slotForLocal[0]].y;
if (reportedNumSubgroups == 0u) {
return Failure("observed topology", "localIndex 0 reported gl_NumSubgroups == 0");
}
if (reportedNumSubgroups > kInvocationCount) {
std::ostringstream message;
message << "reported gl_NumSubgroups=" << reportedNumSubgroups
<< " exceeds the 512 recorded invocations, so at least one subgroup ID is missing";
return Failure("observed topology", message.str());
}
std::vector<std::vector<std::size_t>> subgroupSlots(reportedNumSubgroups);
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const UVec4& subgroup = output.subgroup[slot];
if (subgroup.x == 0u || subgroup.y == 0u) {
std::ostringstream message;
message << "localIndex " << localIndex << " reported subgroupSize=" << subgroup.x
<< ", numSubgroups=" << subgroup.y;
return Failure("observed topology", message.str());
}
if (subgroup.y != reportedNumSubgroups) {
std::ostringstream message;
message << "localIndex " << localIndex << " reported numSubgroups=" << subgroup.y
<< ", while localIndex 0 reported " << reportedNumSubgroups;
return Failure("observed topology", message.str());
}
if (subgroup.z >= reportedNumSubgroups) {
std::ostringstream message;
message << "localIndex " << localIndex << " reported subgroupID=" << subgroup.z
<< " outside [0, " << (reportedNumSubgroups - 1u) << ']';
return Failure("observed topology", message.str());
}
if (subgroup.w >= subgroup.x) {
std::ostringstream message;
message << "localIndex " << localIndex << " reported laneID=" << subgroup.w
<< " outside its subgroupSize=" << subgroup.x;
return Failure("observed topology", message.str());
}
subgroupSlots[subgroup.z].push_back(slot);
}
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
if (subgroupSlots[subgroupID].empty()) {
std::ostringstream message;
message << "reported gl_NumSubgroups=" << reportedNumSubgroups
<< " but subgroupID " << subgroupID << " has no recorded members";
return Failure("observed topology", message.str());
}
auto& members = subgroupSlots[subgroupID];
std::sort(members.begin(), members.end(), [&output](std::size_t lhs, std::size_t rhs) {
return output.subgroup[lhs].w < output.subgroup[rhs].w;
});
for (std::size_t i = 1; i < members.size(); ++i) {
if (output.subgroup[members[i - 1]].w == output.subgroup[members[i]].w) {
std::ostringstream message;
message << "subgroupID " << subgroupID << " contains duplicate laneID "
<< output.subgroup[members[i]].w;
return Failure("observed topology", message.str());
}
}
}
// 3. Native subgroup arithmetic, in the actual lane ordering emitted
// by the driver. The fixture values and all partial sums are exactly
// representable binary32 values, so compare representation, not epsilon.
std::array<float, kInvocationCount> nativePrefix{};
std::vector<float> nativeSubtotal(reportedNumSubgroups, 0.0f);
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
float inclusive = 0.0f;
for (const std::size_t slot : subgroupSlots[subgroupID]) {
const std::uint32_t localIndex = output.invocation[slot].x;
inclusive += ExpectedInput(mode, localIndex);
nativePrefix[slot] = inclusive;
const float actualNative = output.reduction[slot].y;
if (!SameBits(actualNative, inclusive)) {
std::ostringstream message;
message << "subgroupID " << subgroupID << ", laneID " << output.subgroup[slot].w
<< ", localIndex " << localIndex << " nativeInclusive was " << FormatFloat(actualNative)
<< ", expected " << FormatFloat(inclusive);
return Failure("native subgroup arithmetic", message.str());
}
}
nativeSubtotal[subgroupID] = inclusive;
}
// sourceDomain is the narrow source-side safety branch. It is checked
// after native arithmetic so an unsupported source topology still
// reports native subgroup behavior before failing explicitly.
if (reportedNumSubgroups < 2u || reportedNumSubgroups > 32u) {
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const Vec4& reduction = output.reduction[slot];
if (!IsQuietNanSentinel(reduction.z) || !IsQuietNanSentinel(reduction.w) ||
!IsQuietNanSentinel(output.finalAverage[slot])) {
std::ostringstream message;
message << "iterationRP source reduction has no valid contract for gl_NumSubgroups="
<< reportedNumSubgroups << "; localIndex " << localIndex
<< " did not preserve its qNaN source-reduction sentinel";
return Failure("source domain", message.str());
}
for (std::size_t stage = 0; stage < kScanStageCount; ++stage) {
if (!IsQuietNanSentinel(output.scanAfter[stage][slot])) {
std::ostringstream message;
message << "iterationRP source reduction has no valid contract for gl_NumSubgroups="
<< reportedNumSubgroups << "; localIndex " << localIndex << ", scan stage " << stage
<< " did not preserve its qNaN source-reduction sentinel";
return Failure("source domain", message.str());
}
}
}
std::ostringstream message;
message << "iterationRP source reduction has no valid contract for observed gl_NumSubgroups="
<< reportedNumSubgroups << " (requires 2..32); native subgroup results were recorded";
return Failure("source domain", message.str());
}
// 4. iterationRP source writer and first shared-memory handoff.
std::vector<std::size_t> sourceWriter(reportedNumSubgroups, kNoSlot);
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
std::size_t writerCount = 0;
for (const std::size_t slot : subgroupSlots[subgroupID]) {
const UVec4& subgroup = output.subgroup[slot];
if (subgroup.w == subgroup.x - 1u) {
sourceWriter[subgroupID] = slot;
++writerCount;
}
}
if (writerCount != 1u) {
std::ostringstream message;
message << "subgroupID " << subgroupID << " has " << writerCount
<< " recorded lane(s) where laneID == subgroupSize - 1; iterationRP leaves that "
"shared-cache entry unwritten";
return Failure("source writer", message.str());
}
for (const std::size_t slot : subgroupSlots[subgroupID]) {
const float actualRawSubtotal = output.reduction[slot].z;
if (!SameBits(actualRawSubtotal, nativeSubtotal[subgroupID])) {
std::ostringstream message;
message << "subgroupID " << subgroupID << ", localIndex " << output.invocation[slot].x
<< " sourceRawSubtotal was " << FormatFloat(actualRawSubtotal) << ", expected "
<< FormatFloat(nativeSubtotal[subgroupID]);
return Failure("source raw subtotal", message.str());
}
}
}
// 5. Reproduce the source loop exactly, including the redundant final
// scan iteration on power-of-two subgroup counts. Reads and writes in
// one iteration target disjoint cache entries, so update the cache at
// the CPU equivalent of the source barrier.
std::array<float, kInvocationCount> mergedPrefix = nativePrefix;
std::vector<float> cache = nativeSubtotal;
std::uint32_t loopLength = std::bit_width(reportedNumSubgroups) - 1u;
loopLength +=
static_cast<std::uint32_t>(reportedNumSubgroups - (1u << (loopLength - 1u)) > 0u);
for (std::uint32_t scanStage = 0u; scanStage < loopLength; ++scanStage) {
std::vector<float> cacheAfterStage = cache;
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
if ((subgroupID & (1u << scanStage)) == 0u) continue;
const std::uint32_t sourceCacheIndex = (subgroupID >> scanStage << scanStage) - 1u;
const float sourcePrefix = cache[sourceCacheIndex];
for (const std::size_t slot : subgroupSlots[subgroupID]) {
mergedPrefix[slot] += sourcePrefix;
}
cacheAfterStage[subgroupID] = mergedPrefix[sourceWriter[subgroupID]];
}
cache.swap(cacheAfterStage);
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const float actualAfterStage = output.scanAfter[scanStage][slot];
if (!SameBits(actualAfterStage, mergedPrefix[slot])) {
std::ostringstream message;
message << "scanStage " << scanStage << ", subgroupID " << output.subgroup[slot].z
<< ", laneID " << output.subgroup[slot].w << ", localIndex " << localIndex
<< " scanAfter was " << FormatFloat(actualAfterStage) << ", expected "
<< FormatFloat(mergedPrefix[slot]);
ValidationResult result = Failure("source scan", message.str());
result.scanStageMismatch = true;
result.scanStage = static_cast<int>(scanStage);
return result;
}
}
}
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
const std::size_t slot = slotForLocal[localIndex];
const float actualMergedPrefix = output.reduction[slot].w;
if (!SameBits(actualMergedPrefix, mergedPrefix[slot])) {
std::ostringstream message;
message << "localIndex " << localIndex << " sourceMergedPrefix was "
<< FormatFloat(actualMergedPrefix) << ", expected " << FormatFloat(mergedPrefix[slot]);
return Failure("source scan", message.str());
}
}
// 6. Final owner and average. The uniformity check is intentionally
// separate from the source's topology contract at local index 511.
const float firstAverage = output.finalAverage[slotForLocal[0]];
for (std::size_t localIndex = 1; localIndex < kInvocationCount; ++localIndex) {
const float actualAverage = output.finalAverage[slotForLocal[localIndex]];
if (!SameBits(actualAverage, firstAverage)) {
std::ostringstream message;
message << "finalAverage differs: localIndex 0 has " << FormatFloat(firstAverage)
<< ", localIndex " << localIndex << " has " << FormatFloat(actualAverage);
return Failure("final average", message.str());
}
}
ValidationResult ownerResult;
ownerResult.ownerEvaluated = true;
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
if (!subgroupSlots[subgroupID].empty()) {
ownerResult.highestObservedSubgroup = std::max(ownerResult.highestObservedSubgroup, subgroupID);
}
}
const std::size_t index511Slot = slotForLocal[kInvocationCount - 1u];
const UVec4& index511Subgroup = output.subgroup[index511Slot];
ownerResult.index511IsSourceLastLaneWriter =
index511Subgroup.w == index511Subgroup.x - 1u;
ownerResult.index511IsHighestSubgroupMember =
index511Subgroup.z == ownerResult.highestObservedSubgroup;
if (!ownerResult.index511IsSourceLastLaneWriter || !ownerResult.index511IsHighestSubgroupMember) {
std::ostringstream message;
message << "iterationRP topology incompatibility: localIndex 511 is sourceLastLaneWriter="
<< ownerResult.index511IsSourceLastLaneWriter << ", highestSubgroupMember="
<< ownerResult.index511IsHighestSubgroupMember << " (subgroupID=" << index511Subgroup.z
<< ", highest observed subgroupID=" << ownerResult.highestObservedSubgroup << ')';
ownerResult.ok = false;
ownerResult.phase = "final average";
ownerResult.message = message.str();
return ownerResult;
}
float total = 0.0f;
for (const float subtotal : nativeSubtotal) total += subtotal;
float sampledExpectedTotal = 0.0f;
for (std::size_t i = 0; i < kInvocationCount; ++i) sampledExpectedTotal += kSampledLuminance;
const float expectedTotal = mode == InputMode::IndexedSsbo ? 131328.0f : sampledExpectedTotal;
if (!SameBits(total, expectedTotal) || !SameBits(mergedPrefix[index511Slot], expectedTotal)) {
std::ostringstream message;
message << "iterationRP source total was " << FormatFloat(mergedPrefix[index511Slot])
<< " (native total " << FormatFloat(total) << "), expected " << FormatFloat(expectedTotal);
ownerResult.ok = false;
ownerResult.phase = "final average";
ownerResult.message = message.str();
return ownerResult;
}
const float expectedAverage = mode == InputMode::IndexedSsbo ? 256.5f : sampledExpectedTotal / 512.0f;
if (!SameBits(firstAverage, expectedAverage)) {
std::ostringstream message;
message << "finalAverage was " << FormatFloat(firstAverage) << ", expected "
<< FormatFloat(expectedAverage);
ownerResult.ok = false;
ownerResult.phase = "final average";
ownerResult.message = message.str();
return ownerResult;
}
return ownerResult;
}
void DumpProbe(const ProbeOutput& output, const CapabilityInfo& capabilities, const ValidationResult& validation,
bool includeScanStages) {
PrintMetadata(capabilities, std::cout);
if (validation.ok) {
std::cout << "IterationRPFirstReductionScenario firstFailure=none\n";
} else {
std::cout << "IterationRPFirstReductionScenario firstFailure=" << validation.phase << ": "
<< validation.message << '\n';
}
std::cout << "localIndex,localX,localY,localZ,subgroupSize,numSubgroups,subgroupID,laneID,input,"
"nativeInclusive,subgroupSubtotal,mergedPrefix,finalAverage\n";
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
const UVec4& invocation = output.invocation[slot];
const UVec4& subgroup = output.subgroup[slot];
const Vec4& reduction = output.reduction[slot];
std::cout << invocation.x << ',' << invocation.y << ',' << invocation.z << ',' << invocation.w << ','
<< subgroup.x << ',' << subgroup.y << ',' << subgroup.z << ',' << subgroup.w << ','
<< std::hexfloat << reduction.x << ',' << reduction.y << ',' << reduction.z << ','
<< reduction.w << ',' << output.finalAverage[slot] << std::defaultfloat << '\n';
}
if (includeScanStages) {
std::cout << "scanStage,localIndex,scanAfter\n";
for (std::size_t scanStage = 0; scanStage < kScanStageCount; ++scanStage) {
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
std::cout << scanStage << ',' << output.invocation[slot].x << ',' << std::hexfloat
<< output.scanAfter[scanStage][slot] << std::defaultfloat << '\n';
}
}
}
}
class IterationRPFirstReductionScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_capabilities = QueryCapabilities();
// GL_SUBGROUP_SIZE_KHR gates only whether the fixture's source contract
// can hold on this device (SubgroupWidthInSourceDomain); it is
// deliberately never used to infer lane placement or an expected group
// count - those come from observed values alone.
PrintMetadata(m_capabilities, std::cout);
RecordProperty("iterationrp_gl_subgroup_size_khr", std::to_string(m_capabilities.subgroupSize));
if (!m_capabilities.SupportsProbe()) {
GTEST_SKIP() << "subgroup probe requires " << m_capabilities.MissingRequirements();
}
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
if (m_texture != 0) glDeleteTextures(1, &m_texture);
if (m_inputBuffer != 0) glDeleteBuffers(1, &m_inputBuffer);
if (m_outputBuffer != 0) glDeleteBuffers(1, &m_outputBuffer);
if (m_program != 0) glDeleteProgram(m_program);
m_texture = 0;
m_inputBuffer = 0;
m_outputBuffer = 0;
m_program = 0;
}
GLuint CompileComputeProgram(const std::string& source, std::string* outError) {
const char* text = source.c_str();
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
if (shader == 0) {
*outError = "glCreateShader(GL_COMPUTE_SHADER) returned 0";
return 0;
}
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[8192] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
*outError = std::string("the subgroup probe compute shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[8192] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
*outError = std::string("the subgroup probe compute program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
bool RunProbe(InputMode mode, ProbeOutput* output, std::string* outError) {
m_program = CompileComputeProgram(BuildProbeShader(mode), outError);
if (m_program == 0) return false;
ProbeOutput poison{};
std::memset(&poison, 0xa5, sizeof(poison));
glGenBuffers(1, &m_outputBuffer);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_outputBuffer);
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(ProbeOutput), &poison, GL_DYNAMIC_COPY);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_outputBuffer);
if (mode == InputMode::IndexedSsbo) {
std::array<float, kInvocationCount> values{};
for (std::size_t i = 0; i < values.size(); ++i) values[i] = static_cast<float>(i + 1u);
glGenBuffers(1, &m_inputBuffer);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_inputBuffer);
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(values), values.data(), GL_STATIC_DRAW);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_inputBuffer);
} else {
constexpr std::array<float, 4> kOneTexel = {1.0f, 1.0f, 1.0f, 1.0f};
glGenTextures(1, &m_texture);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, m_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 1, 1, 0, GL_RGBA, GL_FLOAT, kOneTexel.data());
}
if (const GLenum error = FirstGLError(); error != GL_NO_ERROR) {
std::ostringstream message;
message << "subgroup probe resource setup left " << GLErrorName(error);
*outError = message.str();
return false;
}
glUseProgram(m_program);
if (mode == InputMode::SampledRgba32f) {
const GLint sampler = glGetUniformLocation(m_program, "colortex2");
const GLint pixelSize = glGetUniformLocation(m_program, "pixelSize");
if (sampler == -1 || pixelSize == -1) {
*outError = "the sampled probe uniforms were optimized away or not reflected";
return false;
}
glUniform1i(sampler, 3);
glUniform2f(pixelSize, 1.0f / 854.0f, 1.0f / 480.0f);
}
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_outputBuffer);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(ProbeOutput), output);
if (const GLenum error = FirstGLError(); error != GL_NO_ERROR) {
std::ostringstream message;
message << "subgroup probe dispatch/readback left " << GLErrorName(error);
*outError = message.str();
return false;
}
return true;
}
void RunAndValidate(InputMode mode) {
ProbeOutput output{};
std::string error;
ASSERT_TRUE(RunProbe(mode, &output, &error)) << InputModeName(mode) << ": " << error;
const ValidationResult validation = ValidateProbe(output, mode);
if (validation.ownerEvaluated) {
RecordProperty("iterationrp_index511_source_last_lane_writer",
validation.index511IsSourceLastLaneWriter ? "true" : "false");
RecordProperty("iterationrp_index511_highest_subgroup_member",
validation.index511IsHighestSubgroupMember ? "true" : "false");
RecordProperty("iterationrp_highest_observed_subgroup",
std::to_string(validation.highestObservedSubgroup));
std::cout << "IterationRPFirstReductionScenario owner: localIndex511 sourceLastLaneWriter="
<< validation.index511IsSourceLastLaneWriter << ", highestSubgroupMember="
<< validation.index511IsHighestSubgroupMember << ", highestObservedSubgroup="
<< validation.highestObservedSubgroup << '\n';
}
if (!validation.ok || DumpRequested()) {
DumpProbe(output, m_capabilities, validation, validation.scanStageMismatch || DumpRequested());
}
EXPECT_TRUE(validation.ok) << validation.phase << ": " << validation.message;
}
CapabilityInfo m_capabilities;
GLuint m_program = 0;
GLuint m_inputBuffer = 0;
GLuint m_outputBuffer = 0;
GLuint m_texture = 0;
};
} // namespace
TEST_F(IterationRPFirstReductionScenario, SampledRgba32fFirstAverage) {
if (!Ready() || IsSkipped()) return;
RunAndValidate(InputMode::SampledRgba32f);
}
TEST_F(IterationRPFirstReductionScenario, IndexedInputTopologyAndReduction) {
if (!Ready() || IsSkipped()) return;
RunAndValidate(InputMode::IndexedSsbo);
}
} // namespace MGITest