From 5bd8ef01e51004d2a31fd789854eaa25ee93e464 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 19 Aug 2026 22:30:30 -0400 Subject: [PATCH] [Test] (MG_IntegrationTest): pin Program 203's complete golden output across desktop and Android Add a deterministic iterationRP Program 203 fixture that dispatches the original shader and compares every RG16F texel against fixed half-float golden bits. This catches both a wrong exposure result and collateral writes without retaining a serial reference shader. Make MobileGLIntegrationTest runnable as a standalone Android executable by linking the shared MobileGL library and backing EGL with an AImageReader window; desktop keeps its static-library pbuffer path. Validation: Adreno 830 passes with 0/262656 mismatches; lavapipe reproduces the current reduction defect with 1/262656 mismatches at the exposure texel. --- CMakeLists.txt | 7 + MobileGL/MG_IntegrationTest/CMakeLists.txt | 29 +- .../MG_IntegrationTest/Harness/HeadlessGL.cpp | 70 +++- .../MG_IntegrationTest/Harness/HeadlessGL.h | 10 +- MobileGL/MG_IntegrationTest/Main.cpp | 8 +- .../IterationRPProgram203Scenario.cpp | 379 ++++++++++++++++++ 6 files changed, 489 insertions(+), 14 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/IterationRPProgram203Scenario.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d0b9af6..d9873406 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -674,3 +674,10 @@ if (NOT ANDROID) add_subdirectory(tools/trace_replay) endif() endif() + +# The integration binary is also useful as a standalone adb-shell executable. +# Android cannot use the desktop-only MobileGL_s target, so its CMake module +# links libMobileGL.so and creates an AImageReader-backed window instead. +if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST) + add_subdirectory(MobileGL/MG_IntegrationTest) +endif() diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 4584ab73..881d09bc 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -24,9 +24,14 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(MGL_ITEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..) -# Only meaningful where MobileGL_s exists (i.e. not Android). -if (NOT TARGET MobileGL_s) - message(STATUS "MobileGL_s is not available; skipping the integration test module") +# Desktop links the static implementation directly. Android runs the same +# executable from adb shell and links the shipping shared library instead. +if (ANDROID) + set(MGL_ITEST_MOBILEGL_TARGET MobileGL) +elseif (TARGET MobileGL_s) + set(MGL_ITEST_MOBILEGL_TARGET MobileGL_s) +else() + message(STATUS "No MobileGL library target is available; skipping the integration test module") return() endif() @@ -69,6 +74,7 @@ add_executable(MobileGLIntegrationTest Scenarios/UniformInitializerScenario.cpp Scenarios/SwizzleAccessRoutineScenario.cpp Scenarios/IterationRPFirstReductionScenario.cpp + Scenarios/IterationRPProgram203Scenario.cpp Scenarios/IterationRPScratchFixScenario.cpp Scenarios/ProgramPipelineScenario.cpp Scenarios/ImageLoadStoreSsoScenario.cpp @@ -94,9 +100,20 @@ target_include_directories(MobileGLIntegrationTest PRIVATE # gtest, not gtest_main: Main.cpp installs the harness banner itself. target_link_libraries(MobileGLIntegrationTest PRIVATE GTest::gtest - MobileGL_s + ${MGL_ITEST_MOBILEGL_TARGET} ) +if (ANDROID) + find_library(MGL_ITEST_ANDROID_LIBRARY android REQUIRED) + find_library(MGL_ITEST_LOG_LIBRARY log REQUIRED) + find_library(MGL_ITEST_MEDIANDK_LIBRARY mediandk REQUIRED) + target_link_libraries(MobileGLIntegrationTest PRIVATE + ${MGL_ITEST_ANDROID_LIBRARY} + ${MGL_ITEST_LOG_LIBRARY} + ${MGL_ITEST_MEDIANDK_LIBRARY} + ) +endif() + if (MSVC) # Same reason as MG_Test/Backend/DirectVulkan: the GLES headers declare gl* # as dllimport on Windows, so the in-library GL entry-point definitions only @@ -105,6 +122,10 @@ if (MSVC) endif() target_compile_definitions(MobileGLIntegrationTest PRIVATE -DNOMINMAX) +if (ANDROID) + return() +endif() + # --- ctest wiring -------------------------------------------------------- # A bare libEGL on a glvnd box resolves to whatever vendor comes first, which is # usually Mesa/llvmpipe - a software rasteriser silently replacing the GPU under diff --git a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp index a745faea..04de58bd 100644 --- a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp +++ b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp @@ -18,6 +18,11 @@ #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #include +#elif defined(__ANDROID__) +#include +#include +#include +#include #endif // MobileGL's own headers, in the order MobileGL/Includes.h uses them: GL/gl.h @@ -37,7 +42,7 @@ // the only construction that is actually predictive here: MobileGL ABORTS // (MOBILEGL_ASSERT -> SIGTRAP) rather than returning an error on an unusable // platform, so nothing the parent can call in-process is allowed to be wrong. -#if !defined(_WIN32) && !defined(__APPLE__) && __has_include() +#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include() #define MGITEST_HAVE_FORK_PREFLIGHT 1 #include #include @@ -78,12 +83,58 @@ namespace MGITest { CW_USEDEFAULT, CW_USEDEFAULT, kSurfaceWidth, kSurfaceHeight, nullptr, nullptr, GetModuleHandleW(nullptr), nullptr); } +#elif defined(__ANDROID__) + AImageReader* g_imageReader = nullptr; + ANativeWindow* g_imageReaderWindow = nullptr; + + void DrainImageReader(void*, AImageReader* reader) { + AImage* image = nullptr; + if (AImageReader_acquireNextImage(reader, &image) == AMEDIA_OK && image != nullptr) { + AImage_delete(image); + } + } + + bool CreateImageReaderWindow() { + if (g_imageReaderWindow != nullptr) return true; + constexpr int kMaxImages = 4; + const media_status_t status = AImageReader_newWithUsage( + kSurfaceWidth, kSurfaceHeight, AIMAGE_FORMAT_RGBA_8888, + AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT, + kMaxImages, &g_imageReader); + if (status != AMEDIA_OK || g_imageReader == nullptr) return false; + + AImageReader_ImageListener listener = {nullptr, DrainImageReader}; + AImageReader_setImageListener(g_imageReader, &listener); + if (AImageReader_getWindow(g_imageReader, &g_imageReaderWindow) != AMEDIA_OK || + g_imageReaderWindow == nullptr) { + AImageReader_setImageListener(g_imageReader, nullptr); + AImageReader_delete(g_imageReader); + g_imageReader = nullptr; + return false; + } + ANativeWindow_acquire(g_imageReaderWindow); + return true; + } + + void DestroyImageReaderWindow() { + if (g_imageReaderWindow != nullptr) { + ANativeWindow_release(g_imageReaderWindow); + g_imageReaderWindow = nullptr; + } + if (g_imageReader != nullptr) { + AImageReader_setImageListener(g_imageReader, nullptr); + AImageReader_delete(g_imageReader); + g_imageReader = nullptr; + } + } #endif bool UseWindowSurface() { #if defined(_WIN32) const char* value = std::getenv("MOBILEGL_ITEST_WINDOW_SURFACE"); return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0; +#elif defined(__ANDROID__) + return true; #else return false; #endif @@ -123,10 +174,10 @@ namespace MGITest { // callers). surfaceless is the platform with no window-system dependency at // all; the surface this file then creates is still a pbuffer, which every // platform supports and which the amendment to this rule requires as the - // fallback shape. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a + // fallback shape on desktop. Android instead supplies an AImageReader + // ANativeWindow. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a // driver that consults them directly cannot reintroduce the dependency - // behind EGL's back. Desktop-only file: MG_IntegrationTest never builds - // for Android, so no device path is affected. + // behind EGL's back. void EnsureHeadlessPlatform() { #if defined(__linux__) && !defined(__ANDROID__) static bool done = false; @@ -214,12 +265,21 @@ namespace MGITest { return 6; } surface = eglCreateWindowSurface(display, config, g_testWindow, nullptr); +#elif defined(__ANDROID__) + if (!CreateImageReaderWindow()) { + outReason = "failed to create the Android AImageReader integration-test window"; + return 6; + } + surface = eglCreateWindowSurface(display, config, g_imageReaderWindow, nullptr); #endif } else { const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE}; surface = eglCreatePbufferSurface(display, config, pbufferAttribs); } if (surface == EGL_NO_SURFACE) { +#if defined(__ANDROID__) + DestroyImageReaderWindow(); +#endif outReason = WithEglError(useWindowSurface ? "eglCreateWindowSurface failed" : "eglCreatePbufferSurface failed"); return 6; @@ -548,6 +608,8 @@ namespace MGITest { DestroyWindow(g_testWindow); g_testWindow = nullptr; } +#elif defined(__ANDROID__) + DestroyImageReaderWindow(); #endif m_context = nullptr; m_surface = nullptr; diff --git a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.h b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.h index e3a36c3f..46e5cf84 100644 --- a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.h +++ b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.h @@ -14,11 +14,11 @@ // inspects backend state - both bugs this module pins were invisible to // state-level assertions and visible only in pixels. // -// Headless by construction, following MG_Benchmark/Driver/DriverBench.c: an EGL -// context on a PBUFFER surface. No window, no window manager, no human. Unlike -// DriverBench the scenarios do draw to the DEFAULT framebuffer (that is where -// the Y-flip lives) and do call eglSwapBuffers (that is the frame boundary the -// cross-frame scenarios need to be real). +// Headless by construction: desktop uses an EGL pbuffer and Android uses an +// AImageReader-backed ANativeWindow that needs no Activity. No window manager, +// no human. Unlike DriverBench the scenarios do draw to the DEFAULT framebuffer +// (that is where the Y-flip lives) and do call eglSwapBuffers (that is the frame +// boundary the cross-frame scenarios need to be real). // // One process is one backend: MOBILEGL_BACKEND_TYPE is latched at // initialization, so the CMake wiring runs this binary once per backend rather diff --git a/MobileGL/MG_IntegrationTest/Main.cpp b/MobileGL/MG_IntegrationTest/Main.cpp index e211b625..433a5041 100644 --- a/MobileGL/MG_IntegrationTest/Main.cpp +++ b/MobileGL/MG_IntegrationTest/Main.cpp @@ -31,8 +31,14 @@ namespace { // silently bound to a workstation's window system is a different // run from CI's and must be visible as one in the log. const char* eglPlatform = std::getenv("EGL_PLATFORM"); - std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless, EGL_PLATFORM=%s)\n", +#if defined(__ANDROID__) + constexpr const char* surfaceKind = "AImageReader window"; +#else + constexpr const char* surfaceKind = "pbuffer"; +#endif + std::fprintf(stderr, " renderer: %s\n surface: %dx%d %s (headless, EGL_PLATFORM=%s)\n", gl.RendererString().c_str(), gl.Width(), gl.Height(), + surfaceKind, eglPlatform != nullptr ? eglPlatform : ""); } else if (MGITest::RequireGpu()) { std::fprintf(stderr, diff --git a/MobileGL/MG_IntegrationTest/Scenarios/IterationRPProgram203Scenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/IterationRPProgram203Scenario.cpp new file mode 100644 index 00000000..d934b508 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/IterationRPProgram203Scenario.cpp @@ -0,0 +1,379 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IterationRPProgram203Scenario.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 +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Full iterationRP Program 203 golden input/output fixture. The original shader +// consumes deterministic complete textures and uniforms, then its complete +// 512x513 RG16F output image is compared against fixed half-float golden bits. +// This catches both a wrong exposure slot and collateral scratch corruption. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + constexpr int kSceneWidth = 854; + constexpr int kSceneHeight = 480; + constexpr int kPixelDataWidth = 512; + constexpr int kPixelDataHeight = 513; + constexpr std::size_t kSceneTexelCount = + static_cast(kSceneWidth) * kSceneHeight; + constexpr std::size_t kPixelDataTexelCount = + static_cast(kPixelDataWidth) * kPixelDataHeight; + + struct Rgba32f { + float r, g, b, a; + }; + + struct Rg16 { + std::uint16_t r, g; + }; + + static_assert(sizeof(Rgba32f) == 16); + static_assert(sizeof(Rg16) == 4); + + // Captured from the fixed fixture on Adreno 830. These are the exact + // RG16F storage bits for (0.806640625, 8.2578125), not rounded decimal + // comparisons performed by the test. + constexpr Rg16 kGoldenExposure = {0x3a74u, 0x4821u}; + + constexpr const char* kCommonSource = R"glsl( +#version 430 core +#extension GL_KHR_shader_subgroup_arithmetic : require + +uniform int frameCounter; +uniform float frameTime; +uniform float aspectRatio; +uniform vec2 pixelSize; +uniform float nightVision; +uniform float darknessLightFactor; +uniform sampler2D colortex2; +uniform sampler2D pixelData2D; +layout(rg16f) uniform image2D img_pixelData2D; + +float remapSaturate(float x, float e0, float e1) { + return clamp((x - e0) / (e1 - e0), 0.0f, 1.0f); +} + +float GetExposureValue(float luminance) { + float aeCurve = 0.65f; + aeCurve = mix(aeCurve, clamp(aeCurve * 1.2f, 0.0f, 1.0f), nightVision); + aeCurve *= remapSaturate(luminance, 2.0f, 1.0f) * 0.6f + 0.4f; + float ae = pow(luminance, -aeCurve); + ae *= 1.0f - min(darknessLightFactor * 2.0f, 0.9f); + ae *= 8.5f; + return ae; +} +)glsl"; + + constexpr const char* kOriginalMain = R"glsl( +layout(local_size_x = 32, local_size_y = 16) in; +shared vec2 prefixSumCache[32]; + +void main() { + vec2 texCoord = (vec2(gl_GlobalInvocationID.xy) + 0.5f) * vec2(1.0f / 32.0f, 1.0f / 16.0f); + vec2 sampleCoord = texCoord * (1.0f / 64.0f); + sampleCoord.x += (15.0f / 32.0f) + pixelSize.x * 12.0f; + float tileExposure = dot(textureLod(colortex2, sampleCoord, 0.0f).rgb, + vec3(0.2125f, 0.7154f, 0.0721f)); + vec2 sampleLuminance = vec2(tileExposure, 0.0f); + sampleLuminance = subgroupInclusiveAdd(sampleLuminance); + if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) + prefixSumCache[gl_SubgroupID] = sampleLuminance; + barrier(); + + uint loopLength = uint(findMSB(gl_NumSubgroups)); + loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u); + for (uint i = 0u; i < loopLength; ++i) { + if ((gl_SubgroupID & (1u << i)) > 0u) { + sampleLuminance += prefixSumCache[(gl_SubgroupID >> i << i) - 1u]; + if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) + prefixSumCache[gl_SubgroupID] = sampleLuminance; + } + barrier(); + } + if (gl_LocalInvocationIndex == 511u) + prefixSumCache[0] = sampleLuminance / 512.0f; + barrier(); + + float avg = prefixSumCache[0].x; + vec2 tileDistance = texCoord * 2.0f - 1.0f; + tileDistance.y /= aspectRatio; + float centerDistance = length(tileDistance); + float tileWeight = remapSaturate(centerDistance, 0.6f, 0.4f); + tileExposure = max(7.0E-7f, tileExposure); + float lumaWeight = avg / tileExposure; + lumaWeight = pow(lumaWeight, remapSaturate(avg, 0.02f, 0.001f) * 0.4f + 0.2f); + tileWeight *= lumaWeight; + + vec2 sampleExposure = vec2(tileExposure * tileWeight, tileWeight); + sampleExposure = subgroupInclusiveAdd(sampleExposure); + if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) + prefixSumCache[gl_SubgroupID] = sampleExposure; + barrier(); + for (uint i = 0u; i < loopLength; ++i) { + if ((gl_SubgroupID & (1u << i)) > 0u) { + sampleExposure += prefixSumCache[(gl_SubgroupID >> i << i) - 1u]; + if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) + prefixSumCache[gl_SubgroupID] = sampleExposure; + } + barrier(); + } + + if (gl_LocalInvocationIndex == 511u) { + float avgExposure = max(sampleExposure.x / sampleExposure.y * 29.3f, 1.0E-10f); + avgExposure = log2(avgExposure); + float prevAvgExposure = log2(texelFetch(pixelData2D, ivec2(0, 0), 0).x); + float frameTimeFixed = frameTime + step(frameCounter, 20) * 100.0f; + float exposureTime = clamp(frameTimeFixed * 2.0f, 0.0f, 1.0f); + avgExposure = mix(prevAvgExposure, avgExposure, exposureTime); + avgExposure = max(exp2(avgExposure), 1.0E-5f); + float exposure = GetExposureValue(avgExposure); + imageStore(img_pixelData2D, ivec2(0, 0), vec4(avgExposure, exposure, 0.0f, 0.0f)); + } +} +)glsl"; + + GLuint CompileCompute(const char* mainSource, std::string* error) { + const std::array sources = {kCommonSource, mainSource}; + const GLuint shader = glCreateShader(GL_COMPUTE_SHADER); + glShaderSource(shader, static_cast(sources.size()), sources.data(), nullptr); + glCompileShader(shader); + GLint compiled = GL_FALSE; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + if (compiled != GL_TRUE) { + std::array log{}; + glGetShaderInfoLog(shader, static_cast(log.size() - 1), nullptr, log.data()); + *error = log.data(); + 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_TRUE) { + std::array log{}; + glGetProgramInfoLog(program, static_cast(log.size() - 1), nullptr, log.data()); + *error = log.data(); + glDeleteProgram(program); + return 0; + } + return program; + } + + std::vector MakeSceneInput() { + std::vector texels(kSceneTexelCount); + for (int y = 0; y < kSceneHeight; ++y) { + for (int x = 0; x < kSceneWidth; ++x) { + std::uint32_t h = static_cast(x) * 0x9e3779b9u; + h ^= static_cast(y) * 0x85ebca6bu; + h ^= h >> 16u; + h *= 0x7feb352du; + h ^= h >> 15u; + const float noise = static_cast(h & 0xffffu) / 65535.0f; + float base = 0.0002f + noise * 0.075f; + const float dx = static_cast(x - 420); + const float dy = static_cast(y - 4); + base += 0.65f * std::exp(-(dx * dx + dy * dy) / 18.0f); + if (((x + y * 17) % 113) == 0) base += 1.75f; + texels[static_cast(y) * kSceneWidth + x] = + {base * 0.83f, base * 1.07f, base * 1.31f, 1.0f}; + } + } + return texels; + } + + std::uint16_t FloatToHalf(float value) { + const std::uint32_t bits = std::bit_cast(value); + const std::uint32_t sign = (bits >> 16u) & 0x8000u; + const std::uint32_t exponent = (bits >> 23u) & 0xffu; + std::uint32_t mantissa = bits & 0x7fffffu; + + if (exponent == 0xffu) { + return static_cast(sign | (mantissa == 0 ? 0x7c00u : 0x7e00u)); + } + int halfExponent = static_cast(exponent) - 127 + 15; + if (halfExponent >= 31) return static_cast(sign | 0x7c00u); + if (halfExponent <= 0) { + if (halfExponent < -10) return static_cast(sign); + mantissa |= 0x800000u; + const unsigned shift = static_cast(14 - halfExponent); + const std::uint32_t rounded = mantissa + ((1u << (shift - 1u)) - 1u) + + ((mantissa >> shift) & 1u); + return static_cast(sign | (rounded >> shift)); + } + mantissa += 0xfffu + ((mantissa >> 13u) & 1u); + if ((mantissa & 0x800000u) != 0) { + mantissa = 0; + if (++halfExponent >= 31) return static_cast(sign | 0x7c00u); + } + return static_cast(sign | (static_cast(halfExponent) << 10u) | + (mantissa >> 13u)); + } + + std::vector MakePixelDataInput() { + std::vector texels(kPixelDataTexelCount); + for (std::size_t i = 0; i < texels.size(); ++i) { + texels[i] = {FloatToHalf(0.35f + static_cast(i % 97u) * 0.0025f), + FloatToHalf(-0.45f + static_cast(i % 89u) * 0.01f)}; + } + texels[0] = {FloatToHalf(0.73f), FloatToHalf(1.25f)}; + return texels; + } + + std::vector MakeGoldenOutput() { + std::vector golden = MakePixelDataInput(); + golden[0] = kGoldenExposure; + return golden; + } + + GLuint MakeTexture(GLenum internalFormat, GLenum format, GLenum type, int width, int height, + const void* data) { + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, 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, static_cast(internalFormat), width, height, 0, format, + type, data); + return texture; + } + + void BindAndDispatch(GLuint program, GLuint scene, GLuint pixelData) { + glUseProgram(program); + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, scene); + glUniform1i(glGetUniformLocation(program, "colortex2"), 3); + glActiveTexture(GL_TEXTURE4); + glBindTexture(GL_TEXTURE_2D, pixelData); + glUniform1i(glGetUniformLocation(program, "pixelData2D"), 4); + glBindImageTexture(0, pixelData, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RG16F); + glUniform1i(glGetUniformLocation(program, "img_pixelData2D"), 0); + glUniform1i(glGetUniformLocation(program, "frameCounter"), 100); + glUniform1f(glGetUniformLocation(program, "frameTime"), 1.0f / 60.0f); + glUniform1f(glGetUniformLocation(program, "aspectRatio"), + static_cast(kSceneWidth) / kSceneHeight); + glUniform2f(glGetUniformLocation(program, "pixelSize"), 1.0f / kSceneWidth, 1.0f / kSceneHeight); + glUniform1f(glGetUniformLocation(program, "nightVision"), 0.23f); + glUniform1f(glGetUniformLocation(program, "darknessLightFactor"), 0.08f); + glDispatchCompute(1, 1, 1); + glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + } + + std::vector ReadWholeRgTexture(GLuint texture) { + std::vector texels(kPixelDataTexelCount); + glBindTexture(GL_TEXTURE_2D, texture); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RG, GL_HALF_FLOAT, texels.data()); + return texels; + } + + class IterationRPProgram203Scenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + + GLint stages = 0; + GLint features = 0; + GLint invocations = 0; + glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &stages); + glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &features); + glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &invocations); + const GLbitfield required = + GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR; + if ((static_cast(stages) & GL_COMPUTE_SHADER_BIT) == 0 || + (static_cast(features) & required) != required || invocations < 512) { + GTEST_SKIP() << "requires 512-invocation basic+arithmetic compute subgroups"; + } + + std::string error; + m_original = CompileCompute(kOriginalMain, &error); + ASSERT_NE(m_original, 0u) << "original Program 203: " << error; + + const std::vector scene = MakeSceneInput(); + const std::vector pixelData = MakePixelDataInput(); + m_scene = MakeTexture(GL_RGBA16F, GL_RGBA, GL_FLOAT, kSceneWidth, kSceneHeight, scene.data()); + m_originalOutput = + MakeTexture(GL_RG16F, GL_RG, GL_HALF_FLOAT, kPixelDataWidth, kPixelDataHeight, + pixelData.data()); + ASSERT_EQ(FirstGLError(), static_cast(GL_NO_ERROR)); + } + + void TearDown() override { + if (!Ready()) return; + const std::array textures = {m_scene, m_originalOutput}; + glDeleteTextures(static_cast(textures.size()), textures.data()); + if (m_original != 0) glDeleteProgram(m_original); + } + + GLuint m_original = 0; + GLuint m_scene = 0; + GLuint m_originalOutput = 0; + }; + } // namespace + + TEST_F(IterationRPProgram203Scenario, FixedCompleteInputProducesFixedCompleteGoldenOutput) { + if (!Ready()) return; + + BindAndDispatch(m_original, m_scene, m_originalOutput); + glFinish(); + const std::vector actual = ReadWholeRgTexture(m_originalOutput); + const std::vector expected = MakeGoldenOutput(); + ASSERT_EQ(FirstGLError(), static_cast(GL_NO_ERROR)); + + std::size_t mismatchTexels = 0; + std::size_t firstMismatch = actual.size(); + for (std::size_t i = 0; i < actual.size(); ++i) { + if (actual[i].r != expected[i].r || actual[i].g != expected[i].g) { + if (firstMismatch == actual.size()) firstMismatch = i; + ++mismatchTexels; + } + } + + RecordProperty("program203_output_width", kPixelDataWidth); + RecordProperty("program203_output_height", kPixelDataHeight); + RecordProperty("program203_compared_texels", static_cast(actual.size())); + RecordProperty("program203_mismatch_texels", static_cast(mismatchTexels)); + std::cout << "IterationRPProgram203Scenario complete-output actualExposureBits=(0x" << std::hex + << actual[0].r << ", 0x" << actual[0].g << ") goldenExposureBits=(0x" << expected[0].r + << ", 0x" << expected[0].g << std::dec << ") mismatches=" << mismatchTexels << '/' + << actual.size() << '\n'; + + if (firstMismatch != actual.size()) { + const std::size_t x = firstMismatch % kPixelDataWidth; + const std::size_t y = firstMismatch / kPixelDataWidth; + ADD_FAILURE() << "complete Program 203 output differs at " << x << ',' << y + << ": actual half bits=(0x" << std::hex << actual[firstMismatch].r << ", 0x" + << actual[firstMismatch].g << ") golden half bits=(0x" << expected[firstMismatch].r + << ", 0x" << expected[firstMismatch].g << std::dec << "); mismatched " + << mismatchTexels << " of " << actual.size() << " texels"; + } + EXPECT_EQ(mismatchTexels, 0u); + } +} // namespace MGITest