From baeb2fa1bc66bbef0dc71473ba9b842a039d6f71 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 10:08:02 -0400 Subject: [PATCH 01/11] [Perf] (ShaderTranspiler, Benchmark): add a per-stage stopwatch for the DirectGLES program-build chain --- MobileGL/MG_Benchmark/CMakeLists.txt | 3 +- .../MG_Benchmark/Transpile/CMakeLists.txt | 20 + .../Transpile/TranspileProfile.cpp | 1229 +++++++++++++++++ 3 files changed, 1251 insertions(+), 1 deletion(-) create mode 100644 MobileGL/MG_Benchmark/Transpile/CMakeLists.txt create mode 100644 MobileGL/MG_Benchmark/Transpile/TranspileProfile.cpp diff --git a/MobileGL/MG_Benchmark/CMakeLists.txt b/MobileGL/MG_Benchmark/CMakeLists.txt index f9ca958c..dff0d38a 100644 --- a/MobileGL/MG_Benchmark/CMakeLists.txt +++ b/MobileGL/MG_Benchmark/CMakeLists.txt @@ -43,4 +43,5 @@ set_tests_properties(SanityBench PROPERTIES LABELS benchmark) add_subdirectory(Program) add_subdirectory(Buffer) add_subdirectory(Driver) -add_subdirectory(Container) \ No newline at end of file +add_subdirectory(Container) +add_subdirectory(Transpile) \ No newline at end of file diff --git a/MobileGL/MG_Benchmark/Transpile/CMakeLists.txt b/MobileGL/MG_Benchmark/Transpile/CMakeLists.txt new file mode 100644 index 00000000..234ba26d --- /dev/null +++ b/MobileGL/MG_Benchmark/Transpile/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.24) + +# Deliberately NOT a google-benchmark target: the interesting quantity is a per-stage +# breakdown of one program build, which needs its own clock around sub-steps that share +# set-up, and a plain main() keeps the output a table this can be read straight out of. +add_executable( + TranspileProfile + TranspileProfile.cpp +) + +target_include_directories(TranspileProfile PRIVATE + ${MGL_ROOT}/include + ${MGL_ROOT}/MobileGL + ${MGL_ROOT}/3rdparty/SPIRV-Reflect +) + +target_link_libraries( + TranspileProfile PRIVATE + ${LINK_LIBRARIES} +) diff --git a/MobileGL/MG_Benchmark/Transpile/TranspileProfile.cpp b/MobileGL/MG_Benchmark/Transpile/TranspileProfile.cpp new file mode 100644 index 00000000..61930ff6 --- /dev/null +++ b/MobileGL/MG_Benchmark/Transpile/TranspileProfile.cpp @@ -0,0 +1,1229 @@ +// MobileGL - MobileGL/MG_Benchmark/Transpile/TranspileProfile.cpp +// Copyright (c) 2025-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 + +// Per-stage stopwatch for the DirectGLES program-build path. +// +// The chain a program goes through is GLSL text -> ShaderSourceProcessor -> glslang parse -> +// glslang link + mapIO -> GlslangToSpv -> SanitizeAndOptimizeBinary -> SPIRV-Reflect +// (global-UBO routing) -> a run of separate ShaderCompiler::...ForEssl optimizer round trips +// and Declares*/Probe* module parses -> SPIRV-Cross -> ESSL text passes -> the driver. +// +// Every one of those ...ForEssl entry points builds its own spvtools::Optimizer and calls +// Run(), i.e. a full SPIR-V parse + IR build + re-serialize per call, and every Declares* +// probe is another full BuildModule. This program measures each of them separately, plus a +// zero-pass Optimizer::Run and a bare BuildModule on the same binary, so the FIXED +// parse/serialize overhead can be separated from the work the passes actually do. It also +// runs the same pass set merged onto ONE Optimizer, which is the saving a merged chain +// would realise. +// +// Measurement only: it links MobileGL_s and drives the public ShaderCompiler API, so it +// needs no GL context and no driver. The driver's own glCompileShader/glLinkProgram is +// therefore NOT included here - see the report for how to measure that half on device. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Init.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "glslang/Include/PoolAlloc.h" +#include "glslang/MachineIndependent/Initialize.h" + +#include "source/opt/build_module.h" +#include "source/opt/ir_context.h" +#include "spirv-tools/optimizer.hpp" + +namespace MobileGL::MG_Util::ShaderTranspiler { + // Defined in ShaderCompiler.cpp at namespace scope, with no header declaration; the + // resource table every glslang parse is handed. + TBuiltInResource BuildTBuiltInResource(const CompileEnv* env); +} // namespace MobileGL::MG_Util::ShaderTranspiler + +using namespace MobileGL; +using namespace MobileGL::MG_Util::ShaderTranspiler; +using Clock = std::chrono::steady_clock; + +namespace { + + // ---------------------------------------------------------------- timing + + struct Stat { + double medianUs = 0.0; + double p10Us = 0.0; + double p90Us = 0.0; + double minUs = 0.0; + int reps = 0; + }; + + // Keeps a measured result observable so the optimizer cannot delete the work. + volatile unsigned long long g_sink = 0; + template + void benchmarkSink(T value) { + g_sink += static_cast(value); + } + + Stat Summarize(std::vector& samples) { + std::sort(samples.begin(), samples.end()); + Stat s; + s.reps = static_cast(samples.size()); + s.minUs = samples.front(); + s.medianUs = samples[samples.size() / 2]; + s.p10Us = samples[static_cast(samples.size() * 0.10)]; + s.p90Us = samples[static_cast(samples.size() * 0.90)]; + return s; + } + + // Runs `body` until at least kMinReps samples exist AND kBudgetMs has elapsed, capped at + // kMaxReps. Three untimed warm-up calls first, so allocator growth and first-touch page + // faults land outside the sample set. + template + Stat Measure(F&& body, const double budgetMs = 250.0, const int minReps = 15, + const int maxReps = 2000) { + for (int i = 0; i < 3; ++i) body(); + + std::vector samples; + samples.reserve(64); + const auto start = Clock::now(); + while (true) { + const auto t0 = Clock::now(); + body(); + const auto t1 = Clock::now(); + samples.push_back(std::chrono::duration(t1 - t0).count()); + if (static_cast(samples.size()) >= maxReps) break; + const double elapsedMs = std::chrono::duration(t1 - start).count(); + if (static_cast(samples.size()) >= minReps && elapsedMs >= budgetMs) break; + } + return Summarize(samples); + } + + // Same loop, but `body` returns the microseconds of the part that should be timed - for + // stages whose set-up (a fresh glslang parse, a fresh link) has to happen per iteration + // and must NOT be counted. + template + Stat MeasureInner(F&& body, const double budgetMs = 250.0, const int minReps = 15, + const int maxReps = 2000) { + for (int i = 0; i < 3; ++i) (void)body(); + + std::vector samples; + samples.reserve(64); + const auto start = Clock::now(); + while (true) { + samples.push_back(body()); + if (static_cast(samples.size()) >= maxReps) break; + const double elapsedMs = + std::chrono::duration(Clock::now() - start).count(); + if (static_cast(samples.size()) >= minReps && elapsedMs >= budgetMs) break; + } + return Summarize(samples); + } + + struct Row { + std::string corpus; + std::string stage; // "VS" / "FS" / "program" + std::string what; + std::string kind; // roundtrip / probe / frontend / crosscompile / baseline / merged + size_t moduleWords = 0; + Stat stat; + }; + + std::vector g_rows; + + void Emit(const std::string& corpus, const std::string& stage, const std::string& what, + const std::string& kind, size_t words, const Stat& s) { + g_rows.push_back(Row{corpus, stage, what, kind, words, s}); + std::printf("%-14s %-8s %-40s %-13s %7zu %10.1f %10.1f %10.1f %6d\n", corpus.c_str(), + stage.c_str(), what.c_str(), kind.c_str(), words, s.medianUs, s.p10Us, + s.p90Us, s.reps); + std::fflush(stdout); + } + + // ---------------------------------------------------------------- corpora + + // (a) The pair already in MG_Benchmark/Program/ProgramBench.cpp. + const char* kTinyVs = R"(#version 460 + +layout (location = 0) in vec4 Position; +in float fIn4; +in float fIn2; +in float fIn5; +in float fIn6; +in float fIn1; +in float fIn3; + +layout(location = 0) uniform mat4 ProjMat; +layout(location = 10) uniform mat3 TestMat3; +layout(location = 20) uniform mat2 TestMat2; +uniform vec2 InSize; +uniform vec2 OutSize; + +out vec2 texCoord; +out vec2 oneTexel; + +void main(){ + vec4 outPos = ProjMat * vec4(Position.xy, 0.0, 1.0); + gl_Position = vec4(outPos.xy, 0.2, 1.0); + + vec2 dummy2 = TestMat2[0]; + vec3 dummy3 = TestMat3[0]; + + oneTexel = (1.0 * (fIn1 * fIn2 * fIn3 * fIn4 * fIn5 * fIn6)) / InSize; + + texCoord = Position.xy / OutSize; +})"; + + const char* kTinyFs = R"(#version 460 + +uniform sampler2D InSampler; + +in vec2 texCoord; +in vec2 oneTexel; + +uniform vec2 InSize; + +layout(location = 1) uniform vec3 Gray; +uniform vec3 RedMatrix; +uniform vec3 GreenMatrix0; +uniform vec3 BlueMatrix; +uniform vec3 Offset; +uniform vec3 ColorScale; +layout(location = 6) uniform float Saturation; +uniform int AQuickFoxJumpsOverALazyDog; +uniform int intVal; + +out vec4 fragColor; + +void main() { + vec4 InTexel = texture(InSampler, texCoord); + + float RedValue = dot(InTexel.rgb, RedMatrix); + float GreenValue = dot(InTexel.rgb, GreenMatrix0); + float BlueValue = dot(InTexel.rgb, BlueMatrix); + vec3 OutColor = vec3(RedValue, GreenValue, BlueValue); + + OutColor = (OutColor * ColorScale) + Offset; + + float Luma = dot(OutColor, Gray); + vec3 Chroma = OutColor - Luma; + OutColor = (Chroma * Saturation) + Luma; + + fragColor = vec4(OutColor, float(intVal)); +})"; + + // (b) KHR-GL33.texture_swizzle.smoke_*, reproduced from the templates in + // external/openglcts/modules/gl/gl3cTextureSwizzleTests.cpp (SmokeTest::getVertexShader / + // getFragmentShader). One instantiation: source format usampler2D ("uint"/"u"), output + // format "uint", 2D target, access "texture", channel "r". prepareAndTestProgram builds + // both the fragment-tested and the vertex-tested pair, which is why both appear here; + // that is the pair the case links 2592 times. + const char* kCtsBlankVs = R"(#version 330 core + +void main() +{ + switch (gl_VertexID) + { + case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break; + case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break; + case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break; + case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break; + } +} +)"; + + const char* kCtsTestFs = R"(#version 330 core + +uniform usampler2D sampler; + +out uint out_color; + +void main() +{ + uint result = texture(sampler, vec2(0, 0)).r; + + out_color = result; +} +)"; + + const char* kCtsTestVs = R"(#version 330 core + +uniform usampler2D sampler; + +flat out uint result; + +void main() +{ + result = texture(sampler, vec2(0, 0)).r; + + switch (gl_VertexID) + { + case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break; + case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break; + case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break; + case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break; + } +} +)"; + + const char* kCtsBlankFs = R"(#version 330 core + +flat in uint result; + +out uint out_color; + +void main() +{ + out_color = result; +} +)"; + + // (c) A large, realistic deferred-lighting fragment shader in the shape Iris shader packs + // reach the transpiler in: a flat wall of default-block uniforms (so the global-UBO + // packing and the OpName traffic are representative), several samplers, helper functions, + // a shadow-filter loop, PBR, fog, and tonemapping. Synthesized rather than copied so the + // benchmark carries no third-party shader-pack source. + const char* kBigVs = R"(#version 330 core + +in vec3 vaPosition; +in vec4 vaColor; +in vec2 vaUV0; +in ivec2 vaUV2; +in vec3 vaNormal; +in vec4 at_tangent; +in vec2 mc_Entity; +in vec2 mc_midTexCoord; + +uniform mat4 modelViewMatrix; +uniform mat4 modelViewMatrixInverse; +uniform mat4 projectionMatrix; +uniform mat4 projectionMatrixInverse; +uniform mat4 gbufferModelView; +uniform mat4 gbufferModelViewInverse; +uniform mat4 gbufferProjection; +uniform mat4 gbufferProjectionInverse; +uniform mat4 shadowModelView; +uniform mat4 shadowProjection; +uniform mat4 textureMatrix; +uniform vec3 cameraPosition; +uniform vec3 previousCameraPosition; +uniform vec3 chunkOffset; +uniform float frameTimeCounter; +uniform float rainStrength; +uniform float viewWidth; +uniform float viewHeight; +uniform int worldTime; +uniform int frameCounter; + +out vec4 vColor; +out vec2 vTexCoord; +out vec2 vLightCoord; +out vec3 vNormal; +out vec3 vTangent; +out vec3 vViewPos; +out vec3 vWorldPos; +out vec3 vShadowPos; +out float vBlockId; +out float vFogDepth; + +vec3 WavePosition(vec3 worldPos, float id, float t) { + if (id < 0.5) return worldPos; + float phase = worldPos.x * 0.35 + worldPos.z * 0.27 + t * 1.7; + float amp = 0.045 * (1.0 + rainStrength); + worldPos.x += sin(phase) * amp; + worldPos.z += cos(phase * 1.13) * amp; + worldPos.y += sin(phase * 0.71) * amp * 0.5; + return worldPos; +} + +void main() { + vec3 localPos = vaPosition + chunkOffset; + vec3 worldPos = localPos + cameraPosition; + worldPos = WavePosition(worldPos, mc_Entity.x, frameTimeCounter); + localPos = worldPos - cameraPosition; + + vec4 viewPos = modelViewMatrix * vec4(localPos, 1.0); + gl_Position = projectionMatrix * viewPos; + + vColor = vaColor; + vTexCoord = (textureMatrix * vec4(vaUV0, 0.0, 1.0)).xy; + vLightCoord = clamp((vec2(vaUV2) - 8.0) / 240.0, 0.0, 1.0); + vNormal = normalize(mat3(modelViewMatrix) * vaNormal); + vTangent = normalize(mat3(modelViewMatrix) * at_tangent.xyz); + vViewPos = viewPos.xyz; + vWorldPos = worldPos; + vBlockId = mc_Entity.x; + vFogDepth = length(viewPos.xyz); + + vec4 shadowView = shadowModelView * vec4(localPos, 1.0); + vec4 shadowClip = shadowProjection * shadowView; + vShadowPos = shadowClip.xyz / max(shadowClip.w, 1e-5) * 0.5 + 0.5; +} +)"; + + const char* kBigFs = R"(#version 330 core + +uniform sampler2D gtexture; +uniform sampler2D lightmap; +uniform sampler2D normals; +uniform sampler2D specular; +uniform sampler2D noisetex; +uniform sampler2D depthtex0; +uniform sampler2D depthtex1; +uniform sampler2D colortex0; +uniform sampler2D colortex1; +uniform sampler2D colortex2; +uniform sampler2D colortex3; +uniform sampler2D colortex4; +uniform sampler2DShadow shadowtex0; +uniform sampler2DShadow shadowtex1; +uniform sampler2D shadowcolor0; + +uniform mat4 gbufferModelView; +uniform mat4 gbufferModelViewInverse; +uniform mat4 gbufferProjection; +uniform mat4 gbufferProjectionInverse; +uniform mat4 gbufferPreviousModelView; +uniform mat4 gbufferPreviousProjection; +uniform mat4 shadowModelView; +uniform mat4 shadowModelViewInverse; +uniform mat4 shadowProjection; +uniform mat4 shadowProjectionInverse; + +uniform vec3 cameraPosition; +uniform vec3 previousCameraPosition; +uniform vec3 sunPosition; +uniform vec3 moonPosition; +uniform vec3 shadowLightPosition; +uniform vec3 upPosition; +uniform vec3 fogColor; +uniform vec3 skyColor; +uniform vec4 entityColor; + +uniform float frameTimeCounter; +uniform float frameTime; +uniform float sunAngle; +uniform float shadowAngle; +uniform float rainStrength; +uniform float wetness; +uniform float aspectRatio; +uniform float viewWidth; +uniform float viewHeight; +uniform float near; +uniform float far; +uniform float nightVision; +uniform float blindness; +uniform float darknessFactor; +uniform float screenBrightness; +uniform float eyeAltitude; +uniform float centerDepthSmooth; +uniform float playerMood; + +uniform int worldTime; +uniform int worldDay; +uniform int moonPhase; +uniform int frameCounter; +uniform int heldItemId; +uniform int heldBlockLightValue; +uniform int isEyeInWater; +uniform int hideGUI; +uniform ivec2 eyeBrightness; +uniform ivec2 eyeBrightnessSmooth; + +in vec4 vColor; +in vec2 vTexCoord; +in vec2 vLightCoord; +in vec3 vNormal; +in vec3 vTangent; +in vec3 vViewPos; +in vec3 vWorldPos; +in vec3 vShadowPos; +in float vBlockId; +in float vFogDepth; + +layout(location = 0) out vec4 outColor0; +layout(location = 1) out vec4 outColor1; +layout(location = 2) out vec4 outColor2; +layout(location = 3) out vec4 outColor3; + +const float PI = 3.14159265358979323846; +const float SHADOW_BIAS = 0.0009; +const int SHADOW_SAMPLES = 12; + +const vec2 kPoisson[12] = vec2[12]( + vec2(-0.3260, -0.4058), vec2( 0.7912, -0.0421), vec2(-0.1958, -0.9018), + vec2(-0.2354, 0.5259), vec2( 0.4479, 0.5734), vec2( 0.9036, 0.4162), + vec2(-0.7935, -0.5960), vec2(-0.0344, 0.0468), vec2(-0.9174, 0.2495), + vec2( 0.4443, -0.7563), vec2(-0.5551, -0.0998), vec2( 0.1770, 0.9294) +); + +float Luminance(vec3 c) { + return dot(c, vec3(0.2125, 0.7154, 0.0721)); +} + +float LinearizeDepth(float d) { + return (2.0 * near * far) / (far + near - (d * 2.0 - 1.0) * (far - near)); +} + +vec3 ScreenToView(vec3 screenPos) { + vec4 ndc = vec4(screenPos * 2.0 - 1.0, 1.0); + vec4 view = gbufferProjectionInverse * ndc; + return view.xyz / view.w; +} + +vec3 ViewToWorld(vec3 viewPos) { + return (gbufferModelViewInverse * vec4(viewPos, 1.0)).xyz + cameraPosition; +} + +float Hash12(vec2 p) { + vec3 p3 = fract(vec3(p.xyx) * 0.1031); + p3 += dot(p3, p3.yzx + 33.33); + return fract((p3.x + p3.y) * p3.z); +} + +mat2 RotationFromAngle(float a) { + float s = sin(a); + float c = cos(a); + return mat2(c, -s, s, c); +} + +float DistributionGGX(vec3 n, vec3 h, float roughness) { + float a = roughness * roughness; + float a2 = a * a; + float ndoth = max(dot(n, h), 0.0); + float denom = ndoth * ndoth * (a2 - 1.0) + 1.0; + return a2 / max(PI * denom * denom, 1e-6); +} + +float GeometrySchlick(float ndotv, float roughness) { + float r = roughness + 1.0; + float k = (r * r) / 8.0; + return ndotv / (ndotv * (1.0 - k) + k); +} + +float GeometrySmith(vec3 n, vec3 v, vec3 l, float roughness) { + return GeometrySchlick(max(dot(n, v), 0.0), roughness) * + GeometrySchlick(max(dot(n, l), 0.0), roughness); +} + +vec3 FresnelSchlick(float cosTheta, vec3 f0) { + return f0 + (1.0 - f0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); +} + +float SampleShadow(vec3 shadowPos, float ndotl) { + if (shadowPos.x < 0.0 || shadowPos.x > 1.0 || shadowPos.y < 0.0 || shadowPos.y > 1.0) { + return 1.0; + } + float bias = SHADOW_BIAS * (1.0 + (1.0 - ndotl) * 3.0); + float angle = Hash12(gl_FragCoord.xy + float(frameCounter)) * PI * 2.0; + mat2 rot = RotationFromAngle(angle); + float radius = 1.4 / 2048.0 * (1.0 + wetness); + float sum = 0.0; + for (int i = 0; i < SHADOW_SAMPLES; ++i) { + vec2 offset = rot * kPoisson[i] * radius; + sum += texture(shadowtex1, vec3(shadowPos.xy + offset, shadowPos.z - bias)); + } + return sum / float(SHADOW_SAMPLES); +} + +vec3 SkyLight(vec3 normal, float lightLevel) { + vec3 up = normalize(mat3(gbufferModelViewInverse) * upPosition); + float hemi = clamp(dot(normal, up) * 0.5 + 0.5, 0.0, 1.0); + vec3 sky = mix(fogColor, skyColor, hemi); + return sky * lightLevel * (1.0 - rainStrength * 0.6); +} + +vec3 BlockLight(float lightLevel) { + vec3 warm = vec3(1.0, 0.62, 0.28); + float shaped = pow(lightLevel, 2.2); + return warm * shaped * (1.0 + float(heldBlockLightValue) * 0.01); +} + +vec3 ApplyFog(vec3 color, float dist, vec3 viewDir) { + float density = 0.0016 * (1.0 + rainStrength * 2.0 + float(isEyeInWater) * 12.0); + float amount = 1.0 - exp(-dist * density); + vec3 tint = mix(fogColor, skyColor, clamp(viewDir.y * 0.5 + 0.5, 0.0, 1.0)); + return mix(color, tint, clamp(amount, 0.0, 1.0)); +} + +vec3 Tonemap(vec3 c) { + c *= 1.6; + vec3 a = c * (c + 0.0245786) - 0.000090537; + vec3 b = c * (0.983729 * c + 0.4329510) + 0.238081; + c = a / b; + return pow(clamp(c, 0.0, 1.0), vec3(1.0 / 2.2)); +} + +void main() { + vec4 albedo = texture(gtexture, vTexCoord) * vColor; + if (albedo.a < 0.1) discard; + + vec4 normalTex = texture(normals, vTexCoord); + vec4 specTex = texture(specular, vTexCoord); + + vec3 n = normalize(vNormal); + vec3 t = normalize(vTangent); + vec3 b = cross(n, t); + mat3 tbn = mat3(t, b, n); + vec3 tangentNormal = normalTex.xyz * 2.0 - 1.0; + tangentNormal.z = sqrt(max(1.0 - dot(tangentNormal.xy, tangentNormal.xy), 0.0)); + vec3 normal = normalize(tbn * tangentNormal); + + float roughness = clamp(1.0 - specTex.r, 0.02, 1.0); + float metallic = clamp(specTex.g, 0.0, 1.0); + float emissive = clamp(specTex.b, 0.0, 1.0); + float porosity = clamp(specTex.a, 0.0, 1.0); + + vec3 viewDir = normalize(-vViewPos); + vec3 lightDir = normalize(shadowLightPosition); + vec3 halfDir = normalize(viewDir + lightDir); + + float ndotl = max(dot(normal, lightDir), 0.0); + float shadow = SampleShadow(vShadowPos, ndotl); + + vec3 f0 = mix(vec3(0.04), albedo.rgb, metallic); + float ndf = DistributionGGX(normal, halfDir, roughness); + float geo = GeometrySmith(normal, viewDir, lightDir, roughness); + vec3 fres = FresnelSchlick(max(dot(halfDir, viewDir), 0.0), f0); + + vec3 kd = (vec3(1.0) - fres) * (1.0 - metallic); + vec3 spec = (ndf * geo * fres) / + max(4.0 * max(dot(normal, viewDir), 0.0) * ndotl, 1e-4); + + vec3 sunColor = mix(vec3(1.0, 0.92, 0.80), vec3(0.32, 0.42, 0.66), rainStrength); + vec3 direct = (kd * albedo.rgb / PI + spec) * sunColor * ndotl * shadow; + + vec3 ambient = SkyLight(mat3(gbufferModelViewInverse) * normal, vLightCoord.y) * albedo.rgb; + vec3 blockLit = BlockLight(vLightCoord.x) * albedo.rgb; + vec3 emissiveLit = albedo.rgb * emissive * 4.0; + + vec3 color = direct + ambient + blockLit + emissiveLit; + + float wetMix = clamp(wetness * (1.0 - porosity) * vLightCoord.y, 0.0, 1.0); + color = mix(color, color * 0.72, wetMix); + + color *= 1.0 - clamp(blindness + darknessFactor * 0.5, 0.0, 1.0); + color += vec3(nightVision) * 0.06 * Luminance(albedo.rgb); + + vec3 worldViewDir = normalize(mat3(gbufferModelViewInverse) * -viewDir); + color = ApplyFog(color, vFogDepth, worldViewDir); + color = Tonemap(color); + + outColor0 = vec4(color, albedo.a); + outColor1 = vec4(normal * 0.5 + 0.5, 1.0); + outColor2 = vec4(roughness, metallic, emissive, 1.0); + outColor3 = vec4(vLightCoord, vBlockId / 255.0, shadow); +} +)"; + + // ------------------------------------------------------- shared helpers + + spvtools::MessageConsumer SilentConsumer() { + return [](spv_message_level_t, const char*, const spv_position_t&, const char*) {}; + } + + struct BuiltProgram { + std::vector types; + std::vector> raw; // straight out of GlslangToSpv + std::vector> sanitized; // after SanitizeAndOptimizeBinary + }; + + // Fresh parse of both stages. glslang mutates a TShader during link/mapIO, so anything + // that needs to be timed repeatedly has to rebuild these each iteration. + std::vector> ParseBoth(const std::string& vsPre, + const std::string& fsPre, + const CompileEnv& env) { + std::vector> out; + for (const auto& [type, src] : + {std::pair{GL_VERTEX_SHADER, &vsPre}, + std::pair{GL_FRAGMENT_SHADER, &fsPre}}) { + ShaderAttrib attrib{.shaderType = type, .sourceStr = *src, .flags = 0, .env = &env}; + auto r = ShaderCompiler::CompileShader(attrib); + if (!r) { + std::fprintf(stderr, "PARSE FAILED (%s):\n%s\n", + type == GL_VERTEX_SHADER ? "VS" : "FS", r.error().log.c_str()); + std::abort(); + } + out.push_back(r.value()); + } + return out; + } + + // ------------------------------------------------------------ the sweep + + void ProfileCorpus(const std::string& name, const char* vsSrc, const char* fsSrc) { + const CompileEnv& env = *GetDefaultCompileEnv(); + + // ---- stage 0: ShaderSourceProcessor (pure String -> String) ---- + { + Stat s = Measure([&] { + String src = vsSrc; + PreprocessShaderSource(ShaderStage::Vertex, src, env); + benchmarkSink(src.size()); + }); + Emit(name, "VS", "ShaderSourceProcessor.Preprocess", "frontend", 0, s); + } + { + Stat s = Measure([&] { + String src = fsSrc; + PreprocessShaderSource(ShaderStage::Fragment, src, env); + benchmarkSink(src.size()); + }); + Emit(name, "FS", "ShaderSourceProcessor.Preprocess", "frontend", 0, s); + } + + String vsPre = vsSrc; + PreprocessShaderSource(ShaderStage::Vertex, vsPre, env); + String fsPre = fsSrc; + PreprocessShaderSource(ShaderStage::Fragment, fsPre, env); + + // ---- stage 1: glslang parse ---- + { + Stat s = Measure([&] { + ShaderAttrib a{.shaderType = GL_VERTEX_SHADER, .sourceStr = vsPre, .flags = 0, .env = &env}; + auto r = ShaderCompiler::CompileShader(a); + benchmarkSink(r.has_value()); + }); + Emit(name, "VS", "glslang.parse", "frontend", 0, s); + } + { + Stat s = Measure([&] { + ShaderAttrib a{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fsPre, .flags = 0, .env = &env}; + auto r = ShaderCompiler::CompileShader(a); + benchmarkSink(r.has_value()); + }); + Emit(name, "FS", "glslang.parse", "frontend", 0, s); + } + + // ---- stage 2: glslang link + mapIO (fresh parses each iteration, untimed) ---- + { + Stat inner = MeasureInner([&] { + auto shaders = ParseBoth(vsPre, fsPre, env); + const auto t0 = Clock::now(); + ProgramAttrib pa{.shaders = shaders}; + auto p = ShaderCompiler::LinkProgram(pa); + const auto t1 = Clock::now(); + benchmarkSink(p.has_value()); + return std::chrono::duration(t1 - t0).count(); + }); + Emit(name, "program", "glslang.link+mapIO", "frontend", 0, inner); + } + + // ---- stage 3: GlslangToSpv ---- + { + Stat inner = MeasureInner([&] { + auto shaders = ParseBoth(vsPre, fsPre, env); + ProgramAttrib pa{.shaders = shaders}; + auto p = ShaderCompiler::LinkProgram(pa); + std::vector types{GL_VERTEX_SHADER, GL_FRAGMENT_SHADER}; + ProgramBinaryAttrib ba{.shaderTypes = types, .program = *p.value()}; + const auto t0 = Clock::now(); + auto bin = ShaderCompiler::GetSpirvBinaryFromProgram(ba); + const auto t1 = Clock::now(); + benchmarkSink(bin.has_value()); + return std::chrono::duration(t1 - t0).count(); + }); + Emit(name, "program", "glslang.GlslangToSpv (both stages)", "frontend", 0, inner); + } + + // ---- build the artefacts every later measurement runs against ---- + BuiltProgram built; + { + auto shaders = ParseBoth(vsPre, fsPre, env); + ProgramAttrib pa{.shaders = shaders}; + auto p = ShaderCompiler::LinkProgram(pa); + if (!p) { + std::fprintf(stderr, "LINK FAILED: %s\n", p.error().log.c_str()); + std::abort(); + } + built.types = {GL_VERTEX_SHADER, GL_FRAGMENT_SHADER}; + ProgramBinaryAttrib ba{.shaderTypes = built.types, .program = *p.value()}; + auto bin = ShaderCompiler::GetSpirvBinaryFromProgram(ba); + built.raw = bin.value(); + built.sanitized = built.raw; + for (auto& m : built.sanitized) { + Vector out; + if (!ShaderCompiler::SanitizeAndOptimizeBinary(m, out, true, false)) { + std::fprintf(stderr, "SANITIZE FAILED\n"); + std::abort(); + } + m = out; + } + } + + // ---- stage 4: SanitizeAndOptimizeBinary, per stage ---- + for (size_t i = 0; i < built.raw.size(); ++i) { + const char* stage = i == 0 ? "VS" : "FS"; + const Vector& in = built.raw[i]; + Stat s = Measure([&] { + Vector out; + ShaderCompiler::SanitizeAndOptimizeBinary(in, out, true, false); + benchmarkSink(out.size()); + }); + Emit(name, stage, "SanitizeAndOptimizeBinary (11 passes)", "roundtrip", in.size(), s); + + Stat sv = Measure([&] { + Vector out; + ShaderCompiler::SanitizeAndOptimizeBinary(in, out, true, true); + benchmarkSink(out.size()); + }); + Emit(name, stage, "SanitizeAndOptimizeBinary + spirv-val", "roundtrip", in.size(), sv); + + // Which of the eleven passes the time goes to: the two optimizing ones + // (PrivateToEntryLocal feeding AggressiveDCE) against the nine legality ones. + Stat pd = Measure([&] { + spvtools::Optimizer opt(SPV_ENV_VULKAN_1_1); + opt.SetMessageConsumer(SilentConsumer()); + opt.RegisterPass(PrivateToEntryLocalPass::CreatePrivateToEntryLocalPass()); + spvtools::OptimizerOptions o; + o.set_run_validator(false); + Vector out; + opt.Run(in.data(), in.size(), &out, o); + benchmarkSink(out.size()); + }); + Emit(name, stage, " Sanitize part: PrivateToEntryLocal only", "roundtrip", in.size(), pd); + + Stat ad = Measure([&] { + spvtools::Optimizer opt(SPV_ENV_VULKAN_1_1); + opt.SetMessageConsumer(SilentConsumer()); + opt.RegisterPass(spvtools::CreateAggressiveDCEPass(false)); + spvtools::OptimizerOptions o; + o.set_run_validator(false); + Vector out; + opt.Run(in.data(), in.size(), &out, o); + benchmarkSink(out.size()); + }); + Emit(name, stage, " Sanitize part: AggressiveDCE only", "roundtrip", in.size(), ad); + } + + // Everything below runs on the SANITIZED module, which is what the DirectGLES + // backend actually receives (ProgramSpirvTask stores the optimized binary). + for (size_t i = 0; i < built.sanitized.size(); ++i) { + const bool isVs = (i == 0); + const char* stage = isVs ? "VS" : "FS"; + const Vector& mod = built.sanitized[i]; + const size_t words = mod.size(); + + // ---- the two fixed-cost baselines ---- + { + Stat s = Measure([&] { + spvtools::Optimizer opt(SPV_ENV_VULKAN_1_1); + spvtools::OptimizerOptions o; + o.set_run_validator(false); + opt.SetMessageConsumer(SilentConsumer()); + Vector out; + opt.Run(mod.data(), mod.size(), &out, o); + benchmarkSink(out.size()); + }); + Emit(name, stage, "BASELINE Optimizer::Run, ZERO passes", "baseline", words, s); + } + { + Stat s = Measure([&] { + auto ctx = spvtools::BuildModule(SPV_ENV_VULKAN_1_1, SilentConsumer(), + mod.data(), mod.size()); + benchmarkSink(ctx != nullptr); + }); + Emit(name, stage, "BASELINE BuildModule only (parse, no emit)", "baseline", words, s); + } + + // ---- the gate probes ---- + { + Stat s = Measure([&] { + auto f = ShaderCompiler::ProbeSpirvGateFeatures(mod); + benchmarkSink(f.WritesViewportIndexOutput || f.DeclaresMultisampledImage); + }); + Emit(name, stage, "ProbeSpirvGateFeatures (merged 2 gates)", "probe", words, s); + } + { + Stat s = Measure([&] { + benchmarkSink(ShaderCompiler::DeclaresViewportIndexBuiltin(mod)); + }); + Emit(name, stage, "DeclaresViewportIndexBuiltin", "probe", words, s); + } + { + Stat s = Measure([&] { + benchmarkSink(ShaderCompiler::DeclaresMultisampledImage(mod)); + }); + Emit(name, stage, "DeclaresMultisampledImage", "probe", words, s); + } + { + Stat s = Measure([&] { + benchmarkSink(ShaderCompiler::DeclaresFormatlessStorageImage(mod)); + }); + Emit(name, stage, "DeclaresFormatlessStorageImage", "probe", words, s); + } + { + Stat s = Measure([&] { + benchmarkSink(ShaderCompiler::ModuleDeclaresBufferTextureSampler(mod)); + }); + Emit(name, stage, "ModuleDeclaresBufferTextureSampler", "probe", words, s); + } + { + // Early-outs on every module that declares no 1D-array storage image, i.e. + // InspectBinary's BuildModule + a full copy of the binary. + Stat s = Measure([&] { + Vector out; + ShaderCompiler::Lower1DArrayImagesForEssl(mod, out, false); + benchmarkSink(out.size()); + }); + Emit(name, stage, "Lower1DArrayImagesForEssl (gate-only path)", "probe", words, s); + } + if (!isVs) { + // Same shape: BinaryHasDynamicOutputIndexing's BuildModule + a copy. + Stat s = Measure([&] { + Vector out; + ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(mod, out, false); + benchmarkSink(out.size()); + }); + Emit(name, stage, "LegalizeFragmentOutputIndexingForEssl (gate-only)", "probe", words, s); + } + + // ---- the individual optimizer round trips ---- + if (isVs) { + Stat s = Measure([&] { + Vector out; + ShaderCompiler::LowerDrawParametersForEssl(mod, out, false); + benchmarkSink(out.size()); + }); + Emit(name, stage, "LowerDrawParametersForEssl", "roundtrip", words, s); + + Stat s2 = Measure([&] { + Vector out; + ShaderCompiler::SplitArrayVertexInputsForEssl(mod, out, false); + benchmarkSink(out.size()); + }); + Emit(name, stage, "SplitArrayVertexInputsForEssl", "roundtrip", words, s2); + + Vector split; + ShaderCompiler::SplitArrayVertexInputsForEssl(mod, split, false); + Stat s3 = Measure([&] { benchmarkSink(split != mod); }, 100.0); + Emit(name, stage, " (its 'did anything change' vector compare)", "roundtrip", words, s3); + } else { + // A pass whose Process() acquires the def-use manager BEFORE asking whether it + // has anything to do, run on a stage that can never contain its builtins. The + // delta over the zero-pass baseline is the cost of the analysis alone. + Stat s = Measure([&] { + Vector out; + ShaderCompiler::LowerDrawParametersForEssl(mod, out, false); + benchmarkSink(out.size()); + }); + Emit(name, stage, "LowerDrawParametersForEssl (no-op stage: def-use cost)", + "roundtrip", words, s); + } + { + Stat s = Measure([&] { + Vector out; + ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(mod, out, false); + benchmarkSink(out.size()); + }); + Emit(name, stage, "StripUboMemberRelaxedPrecisionForEssl", "roundtrip", words, s); + } + { + Stat s = Measure([&] { + Vector out; + ShaderCompiler::LowerRectImages(mod, out, false); + benchmarkSink(out.size()); + }); + Emit(name, stage, "LowerRectImages", "roundtrip", words, s); + } + { + // Armed on Adreno (integer multisample really is 1 while 4 is advertised). + Stat s = Measure([&] { + Vector out; + ShaderCompiler::ClampMultisampleFetchesForEssl(mod, out, 4, 1, 1, 4, false); + benchmarkSink(out.size()); + }); + Emit(name, stage, "ClampMultisampleFetchesForEssl (if armed)", "roundtrip", words, s); + } + { + // Armed on a driver without GL_OES_viewport_array (Mali). + Stat s = Measure([&] { + Vector out; + ShaderCompiler::LowerViewportIndexForEssl(mod, out, false); + benchmarkSink(out.size()); + }); + Emit(name, stage, "LowerViewportIndexForEssl (if armed)", "roundtrip", words, s); + } + { + // Armed on a driver without GL_NV_shader_noperspective_interpolation. + Stat s = Measure([&] { + Vector out; + ShaderCompiler::EmulateNoPerspectiveForEssl(mod, out, false); + benchmarkSink(out.size()); + }); + Emit(name, stage, "EmulateNoPerspectiveForEssl (if armed)", "roundtrip", words, s); + } + + // ---- SPIRV-Reflect (global-UBO routing) and SPIRV-Cross ---- + { + Stat s = Measure([&] { + SpvcSession session(mod, SessionUsageBit::Reflection); + benchmarkSink(session.ParseMetaData()); + }); + Emit(name, stage, "SPIRV-Reflect ParseMetaData (UBO routing)", "crosscompile", words, s); + } + { + Stat s = Measure([&] { + SpvcSession session(mod, SessionUsageBit::Transpile); + spvc_compiler_options options; + session.CreateOptions(&options); + spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320); + spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE); + spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, + SPVC_FALSE); + session.SetOptions(options); + const char* result = nullptr; + session.Compile(&result); + benchmarkSink(result != nullptr ? std::strlen(result) : 0); + }); + Emit(name, stage, "SPIRV-Cross parse+emit ESSL", "crosscompile", words, s); + } + + // ---- the whole backend chain, as Managers.cpp runs it on an Adreno-class + // driver: viewport gate OFF, multisample-clamp gate ON, noperspective + // supported, no XFB, no format-less image, no storage-block override. + { + Stat s = Measure([&] { + const Vector* eff = &mod; + Vector a, b, c, d, e, f; + if (isVs && ShaderCompiler::LowerDrawParametersForEssl(*eff, a, false) && !a.empty()) + eff = &a; + auto gates = ShaderCompiler::ProbeSpirvGateFeatures(*eff); + if (gates.DeclaresMultisampledImage && + ShaderCompiler::ClampMultisampleFetchesForEssl(*eff, b, 4, 1, 1, 4, false) && + !b.empty()) + eff = &b; + if (isVs && ShaderCompiler::SplitArrayVertexInputsForEssl(*eff, c, false) && + !c.empty() && c != *eff) + eff = &c; + if (ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(*eff, d, false) && !d.empty()) + eff = &d; + if (ShaderCompiler::LowerRectImages(*eff, e, false) && !e.empty()) eff = &e; + if (ShaderCompiler::Lower1DArrayImagesForEssl(*eff, f, false) && !f.empty()) eff = &f; + Vector g; + if (!isVs && ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(*eff, g, false) && + !g.empty()) + eff = &g; + benchmarkSink(eff->size()); + }); + Emit(name, stage, "TOTAL current SPIR-V chain (backend, Adreno)", "chain", words, s); + } + + // ---- the same work, merged onto ONE Optimizer ---- + { + Stat s = Measure([&] { + spvtools::Optimizer opt(SPV_ENV_VULKAN_1_1); + opt.SetMessageConsumer(SilentConsumer()); + if (isVs) { + opt.RegisterPass(LowerDrawParametersPass::CreateLowerDrawParametersPass()); + opt.RegisterPass(SplitArrayVertexInputsPass::CreateSplitArrayVertexInputsPass()); + } + opt.RegisterPass(ClampMultisampleFetchPass::CreateClampMultisampleFetchPass(4, 1, 1, 4)); + opt.RegisterPass( + StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass()); + opt.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass()); + spvtools::OptimizerOptions o; + o.set_run_validator(false); + Vector out; + opt.Run(mod.data(), mod.size(), &out, o); + benchmarkSink(out.size()); + }); + Emit(name, stage, "TOTAL merged SPIR-V chain (one Run)", "merged", words, s); + } + // Same, plus the 1D-array-image lowering registered as an ordinary pass instead of + // being reached through its own BuildModule gate. + { + Stat s = Measure([&] { + spvtools::Optimizer opt(SPV_ENV_VULKAN_1_1); + opt.SetMessageConsumer(SilentConsumer()); + if (isVs) { + opt.RegisterPass(LowerDrawParametersPass::CreateLowerDrawParametersPass()); + opt.RegisterPass(SplitArrayVertexInputsPass::CreateSplitArrayVertexInputsPass()); + } + opt.RegisterPass(ClampMultisampleFetchPass::CreateClampMultisampleFetchPass(4, 1, 1, 4)); + opt.RegisterPass( + StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass()); + opt.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass()); + opt.RegisterPass(Lower1DArrayImagesPass::CreateLower1DArrayImagesPass()); + spvtools::OptimizerOptions o; + o.set_run_validator(false); + Vector out; + opt.Run(mod.data(), mod.size(), &out, o); + benchmarkSink(out.size()); + }); + Emit(name, stage, "TOTAL merged + 1D-array pass (one Run)", "merged", words, s); + } + } + } + +} // namespace + +int main(int argc, char** argv) { + // "--floor-loop" parses the same trivial shader forever, so an external sampler can + // attribute the source-independent part of a glslang parse. + const bool floorLoop = argc > 1 && std::strcmp(argv[1], "--floor-loop") == 0; + + // Times the first glslang parse of the process separately: glslang builds its built-in + // symbol tables lazily, under a process-wide lock, on the first parse of each + // (version, spvVersion, profile, source) combination. + MobileGL::Initialize(); + + const CompileEnv& env = *GetDefaultCompileEnv(); + { + const auto t0 = Clock::now(); + String src = kCtsTestFs; + PreprocessShaderSource(ShaderStage::Fragment, src, env); + ShaderAttrib a{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = src, .flags = 0, .env = &env}; + auto r = ShaderCompiler::CompileShader(a); + const auto t1 = Clock::now(); + std::printf("# glslang FIRST parse of the process (cold built-in tables): %.1f us (ok=%d)\n", + std::chrono::duration(t1 - t0).count(), r.has_value() ? 1 : 0); + } + ShaderCompiler::PrewarmBuiltins(); + + if (floorLoop) { + static const char* kEmpty330 = "#version 330 core\nvoid main() {}\n"; + String pre = kEmpty330; + PreprocessShaderSource(ShaderStage::Vertex, pre, env); + std::printf("# floor loop; sample me\n"); + std::fflush(stdout); + for (;;) { + ShaderAttrib a{ + .shaderType = GL_VERTEX_SHADER, .sourceStr = pre, .flags = 0, .env = &env}; + auto r = ShaderCompiler::CompileShader(a); + benchmarkSink(r.has_value()); + } + } + + std::printf("\n%-14s %-8s %-40s %-13s %7s %10s %10s %10s %6s\n", "corpus", "stage", "what", + "kind", "words", "median_us", "p10_us", "p90_us", "reps"); + std::printf("%s\n", std::string(120, '-').c_str()); + + // The per-parse FLOOR. glslang caches its version/profile built-in symbol table across + // parses, but AddContextSpecificSymbols() - the resource-dependent half, gl_MaxVertexAttribs + // and friends, generated from the TBuiltInResource we hand it - is rebuilt on every single + // TShader::parse (ShaderLang.cpp: it is called unconditionally after the cached table is + // adopted). An empty shader measures exactly that, with no user code to confound it. + { + static const char* kEmpty330 = "#version 330 core\nvoid main() {}\n"; + static const char* kEmpty460 = "#version 460\nvoid main() {}\n"; + for (const auto& [label, src] : {std::pair{"330 core", kEmpty330}, + std::pair{"460", kEmpty460}}) { + String pre = src; + PreprocessShaderSource(ShaderStage::Vertex, pre, env); + Stat s = Measure([&] { + ShaderAttrib a{ + .shaderType = GL_VERTEX_SHADER, .sourceStr = pre, .flags = 0, .env = &env}; + auto r = ShaderCompiler::CompileShader(a); + benchmarkSink(r.has_value()); + }); + Emit("floor", "VS", std::string("glslang.parse of empty main(), #version ") + label, + "frontend", 0, s); + } + } + + // What that floor is made of. glslang caches the version/profile built-in symbol table + // (SharedSymbolTables) but calls AddContextSpecificSymbols() unconditionally on every + // TShader::parse (ShaderLang.cpp), and that constructs a TBuiltIns, GENERATES the + // resource-dependent built-in declarations as GLSL text from the TBuiltInResource, and + // then PARSES that text into a fresh symbol-table level. Only the generate half can be + // timed from outside; the rest of the floor is the re-parse of the text it produced. + { + const TBuiltInResource resources = BuildTBuiltInResource(&env); + glslang::SpvVersion spvVersion; + spvVersion.spv = 0x00010300; + spvVersion.vulkan = 100; + glslang::TPoolAllocator pool; + glslang::SetThreadPoolAllocator(&pool); + size_t commonBytes = 0; + size_t stageBytes = 0; + Stat s = MeasureInner( + [&]() -> double { + pool.push(); + const auto t0 = Clock::now(); + glslang::TBuiltIns builtIns; + builtIns.initialize(resources, 330, ECoreProfile, spvVersion, EShLangVertex); + const auto t1 = Clock::now(); + commonBytes = builtIns.getCommonString().size(); + stageBytes = builtIns.getStageString(EShLangVertex).size(); + const double us = std::chrono::duration(t1 - t0).count(); + pool.pop(); + return us; + }, + 150.0, 15, 400); + glslang::SetThreadPoolAllocator(nullptr); + Emit("floor", "VS", " of which: TBuiltIns::initialize (generate only)", "frontend", 0, s); + std::printf("# generated built-in declaration text: common %zu bytes, vertex stage %zu bytes\n", + commonBytes, stageBytes); + } + + // Splitting the floor further: how much of it is object set-up that never touches the + // source, and how much is MobileGL's particular glslang configuration rather than a parse + // as such. + { + static const char* kEmpty330 = "#version 330 core\nvoid main() {}\n"; + const TBuiltInResource resources = BuildTBuiltInResource(&env); + + Stat ctor = Measure([&] { + auto sh = MakeShared(EShLangVertex); + const char* src[] = {kEmpty330}; + sh->setStrings(src, 1); + sh->setNanMinMaxClamp(true); + sh->setInvertY(true); + sh->setPreamble("#undef VULKAN\n"); + sh->setEnvInput(glslang::EShSourceGlsl, EShLangVertex, glslang::EShClientVulkan, 450); + sh->setEnvClient(glslang::EShClientVulkan, glslang::EShTargetVulkan_1_1); + sh->setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_3); + sh->setEnvInputVulkanRulesRelaxed(); + sh->setAutoMapLocations(true); + sh->setAutoMapBindings(true); + sh->setGlobalUniformBlockName(GLOBAL_UBO_NAME); + benchmarkSink(sh != nullptr); + }); + Emit("floor", "VS", " TShader ctor+configure, NO parse()", "frontend", 0, ctor); + + Stat bare = Measure([&] { + auto sh = MakeShared(EShLangVertex); + const char* src[] = {kEmpty330}; + sh->setStrings(src, 1); + TBuiltInResource r = resources; + benchmarkSink(sh->parse(&r, 330, ECoreProfile, false, true, EShMsgDefault)); + }); + Emit("floor", "VS", " parse() with NO MobileGL env configuration", "frontend", 0, bare); + } + + std::printf("\n%-14s %-8s %-40s %-13s %7s %10s %10s %10s %6s\n", "corpus", "stage", "what", + "kind", "words", "median_us", "p10_us", "p90_us", "reps"); + std::printf("%s\n", std::string(120, '-').c_str()); + + ProfileCorpus("tiny", kTinyVs, kTinyFs); + ProfileCorpus("cts_fs_tested", kCtsBlankVs, kCtsTestFs); + ProfileCorpus("cts_vs_tested", kCtsTestVs, kCtsBlankFs); + ProfileCorpus("iris_large", kBigVs, kBigFs); + + return 0; +} From 52718ecf84fea0c887bcfaab009992630d75d6f3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 10:11:38 -0400 Subject: [PATCH 02/11] [Fix, Test] (GLImpl, DirectGLES, DirectVulkan): accept GL_RENDERBUFFER endpoints in glCopyImageSubData --- MobileGL/MG_Backend/BackendObject.h | 18 +- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 161 +++++++++----- MobileGL/MG_Backend/DirectGLES/DirectGLES.h | 4 +- .../MG_Backend/DirectVulkan/DirectVulkan.cpp | 8 +- .../MG_Backend/DirectVulkan/DirectVulkan.h | 4 +- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 204 ++++++++++++------ .../DirectVulkan/Renderer/VulkanRenderer.h | 5 +- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 146 +++++++++---- MobileGL/MG_Test/Texture/TextureTest.cpp | 110 +++++++++- 9 files changed, 483 insertions(+), 177 deletions(-) diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 884a59b2..5c7aafff 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -14,6 +14,7 @@ namespace MobileGL { namespace MG_State::GLState { class FramebufferObject; class ITextureObject; + class RenderbufferObject; } enum class BackendType { @@ -24,6 +25,19 @@ namespace MobileGL { }; namespace MG_Backend { + // One endpoint of a glCopyImageSubData. GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER + // alongside the ten whole-image texture targets, and a renderbuffer name lives in a + // namespace of its own - so an endpoint is a sum type, not an ITextureObject. At most + // one of the two pointers is set; neither is set when the name named nothing, which is + // the INVALID_VALUE the frontend validator reports. + struct CopyImageEndpoint { + SharedPtr Texture; + SharedPtr Renderbuffer; + + Bool IsRenderbuffer() const { return Renderbuffer != nullptr; } + Bool Exists() const { return Texture != nullptr || Renderbuffer != nullptr; } + }; + enum class FormatCapability : Uint64 { Creatable = 1ull << 0, @@ -160,9 +174,9 @@ namespace MobileGL { GLsizei height, GLint border); void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height); - void (*CopyImageSubData)(const SharedPtr& srcTexture, + void (*CopyImageSubData)(const CopyImageEndpoint& src, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, - const SharedPtr& dstTexture, + const CopyImageEndpoint& dst, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); void (*GenerateMipmap)(GLenum target); diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 72842256..f8ba1dc6 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -5708,27 +5708,83 @@ namespace MobileGL::MG_Backend::DirectGLES { // The 1D-array case is not just a rename: GL addresses its layers with y/height while the // ES 2D array that backs it addresses them with z/depth, so the two axes swap with the // target. + // + // GL_RENDERBUFFER is the exception that must NOT be translated: ES 3.2 core (and + // GL_EXT_copy_image) take it as a srcTarget/dstTarget verbatim, while + // ConvertGLEnumToTextureTarget answers Unknown for it and the translation below would hand + // the driver GL_UNKNOWN_MGL. struct GLESCopyImageEndpoint { GLenum target = GL_TEXTURE_2D; + // Exactly one of the two is set. The backend object is kept rather than its id, because + // the id is only stable until the OTHER endpoint syncs (a sync can re-mint a texture), + // so it is read at the point of use. + SharedPtr texture; + SharedPtr renderbuffer; GLint x = 0; GLint y = 0; GLint z = 0; + + Bool IsRenderbuffer() const { return renderbuffer != nullptr; } + GLuint Name() const { + if (renderbuffer) return renderbuffer->GetBackendRenderbufferId(); + return texture ? texture->GetBackendTextureId() : 0u; + } }; - static GLESCopyImageEndpoint MakeGLESCopyImageEndpoint(GLenum appTarget, GLint x, GLint y, GLint z) { - const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget); - GLESCopyImageEndpoint endpoint{}; - endpoint.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget); - if (stateTarget == TextureTarget::Texture1DArray) { - endpoint.x = x; - endpoint.y = 0; - endpoint.z = y; - return endpoint; + // The renderbuffer twin of TextureImpl::SyncTextureObjectToBackend: the same + // find-or-create-then-sync the framebuffer attachment walk does (see SyncAttachmentObject), + // reachable from a path that has a renderbuffer but no framebuffer. + static SharedPtr SyncRenderbufferObjectToBackend( + const SharedPtr& renderbufferObject) { + if (!renderbufferObject) return nullptr; + SharedPtr backendRenderbufferObject; + if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) { + backendRenderbufferObject = *slot; + } else { + auto& newSlot = RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbufferObject); + if (!newSlot) { + newSlot = MakeShared(); + } + backendRenderbufferObject = newSlot; } - endpoint.x = x; - endpoint.y = y; - endpoint.z = z; - return endpoint; + backendRenderbufferObject->SyncToBackend(renderbufferObject); + return backendRenderbufferObject; + } + + static Bool MakeGLESCopyImageEndpoint(const CopyImageEndpoint& endpoint, GLenum appTarget, GLint x, GLint y, + GLint z, GLESCopyImageEndpoint& out) { + if (endpoint.IsRenderbuffer()) { + out.renderbuffer = SyncRenderbufferObjectToBackend(endpoint.Renderbuffer); + if (!out.renderbuffer) return false; + out.target = GL_RENDERBUFFER; + out.x = x; + out.y = y; + out.z = z; + return true; + } + // BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a + // slot inside the backend texture registry, and the second call mutates that very map: + // GetOrCreate indexes it (an insert relocates entries - by rehashing, and also by + // robin-hood displacement well under the load factor), and Find drops any + // entry whose state object has expired - which, with the map open-addressed and erasing + // by shifting the probe cluster backwards, relocates entries other than the erased one. + // Either way a reference taken by the first call is stale by the time the second returns, + // and it is read four more times below. Copying the SharedPtr costs two refcount bumps on + // a path that is already doing a texture copy. + out.texture = TextureImpl::SyncTextureObjectToBackend(endpoint.Texture); + if (!out.texture) return false; + const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget); + out.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget); + if (stateTarget == TextureTarget::Texture1DArray) { + out.x = x; + out.y = 0; + out.z = y; + return true; + } + out.x = x; + out.y = y; + out.z = z; + return true; } // The region extent swaps the same two axes for a 1D array, and does so for whichever side @@ -5744,85 +5800,86 @@ namespace MobileGL::MG_Backend::DirectGLES { std::swap(height, depth); } - void CopyImageSubData(const SharedPtr& srcTexture, + static TextureInternalFormat GetCopyImageEndpointFormat(const CopyImageEndpoint& endpoint) { + if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat(); + return endpoint.Texture ? endpoint.Texture->GetFormat() : TextureInternalFormat::Unknown; + } + + void CopyImageSubData(const CopyImageEndpoint& srcEndpoint, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, - const SharedPtr& dstTexture, + const CopyImageEndpoint& dstEndpoint, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { - // BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a - // slot inside the backend texture registry, and the second call mutates that very map: - // GetOrCreate indexes it (an insert relocates entries - by rehashing, and also by - // robin-hood displacement well under the load factor), and Find drops any - // entry whose state object has expired - which, with the map open-addressed and erasing - // by shifting the probe cluster backwards, relocates entries other than the erased one. - // Either way a reference taken by the first call is stale by the time the second returns, - // and it is read four more times below. Copying the SharedPtr costs two refcount bumps on - // a path that is already doing a texture copy. - const SharedPtr srcBackendTexture = - TextureImpl::SyncTextureObjectToBackend(srcTexture); - const SharedPtr dstBackendTexture = - TextureImpl::SyncTextureObjectToBackend(dstTexture); + GLESCopyImageEndpoint src{}; + GLESCopyImageEndpoint dst{}; // The DirectVulkan half of this entry point died exactly here, on a texture whose sync // produced nothing - and it died in a release build, where the MOBILEGL_ASSERT that was - // supposed to catch it expands to nothing. The four GetBackendTextureId() calls below - // are the same dereference. The frontend validator is what keeps this unreachable and - // what reports the error the application is owed; declining is only how a future gap up - // there stops being a crash. See the level guard in VulkanRenderer::CopyImageSubData. - if (!srcBackendTexture || !dstBackendTexture) { - MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__); + // supposed to catch it expands to nothing. The four Name() calls below are the same + // dereference. The frontend validator is what keeps this unreachable and what reports + // the error the application is owed; declining is only how a future gap up there stops + // being a crash. See the level guard in VulkanRenderer::CopyImageSubData. + if (!MakeGLESCopyImageEndpoint(srcEndpoint, srcTarget, srcX, srcY, srcZ, src) || + !MakeGLESCopyImageEndpoint(dstEndpoint, dstTarget, dstX, dstY, dstZ, dst)) { + MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__); return; } - const GLESCopyImageEndpoint src = MakeGLESCopyImageEndpoint(srcTarget, srcX, srcY, srcZ); - const GLESCopyImageEndpoint dst = MakeGLESCopyImageEndpoint(dstTarget, dstX, dstY, dstZ); GLsizei copyHeight = srcHeight; GLsizei copyDepth = srcDepth; ApplyGLESCopyImageExtent(srcTarget, dstTarget, copyHeight, copyDepth); - const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcTexture->GetFormat()); - const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstTexture->GetFormat()); - const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcTexture->GetFormat()); - const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstTexture->GetFormat()); - if (srcIsDepth || dstIsDepth || srcStencil || dstStencil) { + const TextureInternalFormat srcFormat = GetCopyImageEndpointFormat(srcEndpoint); + const TextureInternalFormat dstFormat = GetCopyImageEndpointFormat(dstEndpoint); + // Both emulation fallbacks below are written against TEXTURE ids and texture targets, so + // an endpoint that is a renderbuffer takes the native ES copy - which accepts + // GL_RENDERBUFFER on both sides - and reports rather than mis-dispatches if the driver + // turns it down. + const Bool anyRenderbuffer = src.IsRenderbuffer() || dst.IsRenderbuffer(); + + const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcFormat); + const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstFormat); + const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcFormat); + const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstFormat); + if (!anyRenderbuffer && (srcIsDepth || dstIsDepth || srcStencil || dstStencil)) { MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil, "DirectGLES CopyImageSubData only supports depth-only image copies."); MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D, "DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D."); MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1, "DirectGLES depth CopyImageSubData only supports single-layer copies."); - BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight, - dstBackendTexture->GetBackendTextureId(), dstLevel, dst.x, dst.y, srcWidth, copyHeight); + BlitDepthTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight, + dst.Name(), dstLevel, dst.x, dst.y, srcWidth, copyHeight); return; } - if (srcTexture->GetFormat() == TextureInternalFormat::R32F || - dstTexture->GetFormat() == TextureInternalFormat::R32F) { + if (!anyRenderbuffer && + (srcFormat == TextureInternalFormat::R32F || dstFormat == TextureInternalFormat::R32F)) { // The single glGetError below decides the fallback dispatch, and // ErrorLopper::Clear is compiled out at the default log level - drain // with the always-live helper so a stale flag cannot misroute a // succeeded native copy into the 2D-only fallback. ClearGLErrors(); - g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z, - dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z, + g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z, + dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z, srcWidth, copyHeight, copyDepth); const GLenum copyImageError = g_GLESFuncs.glGetError(); if (copyImageError == GL_NO_ERROR) { return; } - MOBILEGL_ASSERT(IsColorOnlyFormat(srcTexture->GetFormat()) && IsColorOnlyFormat(dstTexture->GetFormat()), + MOBILEGL_ASSERT(IsColorOnlyFormat(srcFormat) && IsColorOnlyFormat(dstFormat), "DirectGLES CopyImageSubData only supports color-only or depth-only copies."); MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D, "DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D."); MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1, "DirectGLES color CopyImageSubData only supports single-layer copies."); - CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight, - dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y); + CopyR32FTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight, + dst.Name(), dst.target, dstLevel, dst.x, dst.y); return; } ClearGLErrors(); - g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z, - dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z, + g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z, + dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z, srcWidth, copyHeight, copyDepth); // Every error condition glCopyImageSubData has was already ruled out by the frontend // validator, so a driver error here is an internal invariant violation, not something diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 81e2b144..94947965 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -76,9 +76,9 @@ namespace MobileGL::MG_Backend::DirectGLES { GLsizei height, GLint border); void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height); - void CopyImageSubData(const SharedPtr& srcTexture, + void CopyImageSubData(const CopyImageEndpoint& src, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, - const SharedPtr& dstTexture, + const CopyImageEndpoint& dst, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); void GenerateMipmap(GLenum target); diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index a917113c..4bcf74f4 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -632,15 +632,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context"); pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height); } - void CopyImageSubData(const SharedPtr& srcTexture, + void CopyImageSubData(const CopyImageEndpoint& src, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, - const SharedPtr& dstTexture, + const CopyImageEndpoint& dst, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer"); MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context"); - pVulkanRenderer->CopyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, - dstTexture, dstTarget, dstLevel, dstX, dstY, dstZ, + pVulkanRenderer->CopyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ, + dst, dstTarget, dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth); } void GenerateMipmap(GLenum target) { diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h index 2f992399..74241e81 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.h @@ -82,9 +82,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { GLsizei height, GLint border); void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height); - void CopyImageSubData(const SharedPtr& srcTexture, + void CopyImageSubData(const CopyImageEndpoint& src, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, - const SharedPtr& dstTexture, + const CopyImageEndpoint& dst, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); void GenerateMipmap(GLenum target); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 4fd23b38..2de8c9d0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -8869,7 +8869,7 @@ void main() { // A mixed 2D-array <-> 3D pair is legal because maintenance1 - core since Vulkan 1.1 - // relaxed the old "layerCounts must match" rule into "the 3D side's extent.depth must // equal the array side's layerCount". - struct CopyImageEndpoint { + struct CopyImageSliceMapping { // True for a VK_IMAGE_TYPE_3D image, i.e. slices ride the z axis, not the layer axis. Bool slicesAreDepth = false; // The GL z offset, kept in whichever field this endpoint's image type reads it from. @@ -8883,13 +8883,35 @@ void main() { Int32 OffsetZ() const { return slicesAreDepth ? static_cast(baseSlice) : 0; } }; - Bool TryResolveCopyImageEndpoint(TextureTarget target, - const VkTextureManager::TextureResource& resource, Uint32 mipLevel, - GLint glZ, GLsizei glDepth, CopyImageEndpoint& outEndpoint) { + // The Vulkan image one glCopyImageSubData endpoint names, after the two object kinds GL + // 4.6 core 18.3.2 allows have been collapsed onto the fields this copy reads. A + // renderbuffer is a single-level, single-layer 2D image, so its shape answers are + // constants rather than a mip walk. `trackedLayout` points AT the owning resource's own + // layout field - both resource maps are node-based, so the pointer survives the further + // lookups the clear materialization below makes. + struct CopyImageVkImage { + Bool isRenderbuffer = false; + VkImage image = VK_NULL_HANDLE; + VkImageLayout* trackedLayout = nullptr; + VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE; + Uint32 mipLevels = 1; + VkExtent2D extent = {0, 0}; + Uint32 depth = 1; + Uint32 arrayLayers = 1; + }; + + Bool TryResolveCopyImageSliceMapping(TextureTarget target, const CopyImageVkImage& image, Uint32 mipLevel, + GLint glZ, GLsizei glDepth, CopyImageSliceMapping& outMapping) { if (glZ < 0 || glDepth <= 0) { return false; } const Uint32 baseSlice = static_cast(glZ); + if (image.isRenderbuffer) { + // A renderbuffer holds one 2D image and nothing else; GL still requires the + // z/depth pair and it can only name that one slice. + outMapping = {}; + return baseSlice == 0 && glDepth == 1; + } switch (target) { case TextureTarget::Texture1D: case TextureTarget::Texture2D: @@ -8897,12 +8919,12 @@ void main() { case TextureTarget::Texture2DMultisample: // Not layered at all: GL still requires the z/depth pair, and it can only name the // one slice these targets have. - outEndpoint = {}; + outMapping = {}; return baseSlice == 0 && glDepth == 1; case TextureTarget::Texture3D: - outEndpoint.slicesAreDepth = true; - outEndpoint.baseSlice = baseSlice; - outEndpoint.availableSlices = std::max(1u, resource.depth >> mipLevel); + outMapping.slicesAreDepth = true; + outMapping.baseSlice = baseSlice; + outMapping.availableSlices = std::max(1u, image.depth >> mipLevel); return true; case TextureTarget::Texture2DArray: case TextureTarget::Texture2DMultisampleArray: @@ -8911,9 +8933,9 @@ void main() { // A cube map is an array of six faces here (see TryResolveTextureShapeInfo), and GL // numbers its faces on the same z axis an array texture numbers its layers, so both // arrive as a plain layer range. - outEndpoint.slicesAreDepth = false; - outEndpoint.baseSlice = baseSlice; - outEndpoint.availableSlices = resource.arrayLayers; + outMapping.slicesAreDepth = false; + outMapping.baseSlice = baseSlice; + outMapping.availableSlices = image.arrayLayers; return true; default: // GL_TEXTURE_1D_ARRAY carries its layers on the Y axis (srcY/srcHeight), which @@ -8923,15 +8945,20 @@ void main() { return false; } } + + Uint CopyImageEndpointName(const CopyImageEndpoint& endpoint) { + if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetExternalIndex(); + return endpoint.Texture ? endpoint.Texture->GetExternalIndex() : 0u; + } } // namespace - void VulkanRenderer::CopyImageSubData(const SharedPtr& srcTexture, + void VulkanRenderer::CopyImageSubData(const CopyImageEndpoint& srcEndpoint, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, - const SharedPtr& dstTexture, + const CopyImageEndpoint& dstEndpoint, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { - MOBILEGL_ASSERT(srcTexture != nullptr && dstTexture != nullptr, - "CopyImageSubData requires valid source and destination textures."); + MOBILEGL_ASSERT(srcEndpoint.Exists() && dstEndpoint.Exists(), + "CopyImageSubData requires valid source and destination images."); // The frontend already declines a zero or negative extent, so anything else here is a // caller MobileGL wrote - but it still reaches vkCmdCopyImage in a release build, and a // zero extent.depth is as invalid as a zero width. @@ -8948,9 +8975,9 @@ void main() { // and an overlap check). Refused outright, and refused for real rather than through an // assertion the release build drops: recording the pair anyway is a validation error and, // on a tiler, a copy whose source has already been overwritten. - if (srcTexture.get() == dstTexture.get()) { - MGLOG_E_ONCE("%s: in-place copy on textureId=%d is not supported; declining the copy", __func__, - srcTexture->GetExternalIndex()); + if (srcEndpoint.Texture == dstEndpoint.Texture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) { + MGLOG_E_ONCE("%s: in-place copy on objectId=%u is not supported; declining the copy", __func__, + CopyImageEndpointName(srcEndpoint)); return; } @@ -8963,8 +8990,39 @@ void main() { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } - auto* srcResource = m_textureManager->SyncTextureAndGetDescriptor(*srcTexture); - auto* dstResource = m_textureManager->SyncTextureAndGetDescriptor(*dstTexture); + // One resolver for both object kinds. The texture arm is the same + // SyncTextureAndGetDescriptor the copy always used; the renderbuffer arm goes through the + // render-pass manager, which is where a renderbuffer's VkImage lives. + const auto resolveImage = [this](const CopyImageEndpoint& endpoint, CopyImageVkImage& out) { + if (endpoint.IsRenderbuffer()) { + auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(endpoint.Renderbuffer); + if (resource == nullptr) return false; + out.isRenderbuffer = true; + out.image = resource->image; + out.trackedLayout = &resource->layout; + out.aspect = resource->aspect; + out.mipLevels = 1; + out.extent = resource->extent; + out.depth = 1; + out.arrayLayers = 1; + return out.image != VK_NULL_HANDLE; + } + auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*endpoint.Texture); + if (resource == nullptr) return false; + out.isRenderbuffer = false; + out.image = resource->image; + out.trackedLayout = &resource->layout; + out.aspect = resource->aspect; + out.mipLevels = resource->mipLevels; + out.extent = resource->extent; + out.depth = resource->depth; + out.arrayLayers = resource->arrayLayers; + return true; + }; + CopyImageVkImage srcImage{}; + CopyImageVkImage dstImage{}; + const Bool srcResolved = resolveImage(srcEndpoint, srcImage); + const Bool dstResolved = resolveImage(dstEndpoint, dstImage); // Real checks, not MOBILEGL_ASSERT: the assertions this replaces compile to nothing in // a release build, which is where both observed failures happened - a null resource // dereferenced right below (lavapipe) and a mip level the VkImage does not have handed @@ -8980,29 +9038,29 @@ void main() { // The frontend validator (ValidateTextureLevelExists) is what produces the // GL_INVALID_VALUE the application is actually owed. This guard exists so the next gap // up there declines a copy instead of taking the process down. - if (srcResource == nullptr || dstResource == nullptr) { - MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__); + if (!srcResolved || !dstResolved) { + MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__); return; } - if (srcLevel < 0 || dstLevel < 0 || static_cast(srcLevel) >= srcResource->mipLevels || - static_cast(dstLevel) >= dstResource->mipLevels) { + if (srcLevel < 0 || dstLevel < 0 || static_cast(srcLevel) >= srcImage.mipLevels || + static_cast(dstLevel) >= dstImage.mipLevels) { MGLOG_E_ONCE("%s: mip level out of range (src %d of %u, dst %d of %u); declining the copy", __func__, - srcLevel, srcResource->mipLevels, dstLevel, dstResource->mipLevels); + srcLevel, srcImage.mipLevels, dstLevel, dstImage.mipLevels); return; } const VkImageAspectFlags copyAspectMask = - srcResource->aspect & dstResource->aspect & + srcImage.aspect & dstImage.aspect & (VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT); MOBILEGL_ASSERT(copyAspectMask != 0 && - (srcResource->aspect & copyAspectMask) == srcResource->aspect && - (dstResource->aspect & copyAspectMask) == dstResource->aspect, + (srcImage.aspect & copyAspectMask) == srcImage.aspect && + (dstImage.aspect & copyAspectMask) == dstImage.aspect, "CopyImageSubData source and destination aspects are incompatible."); const Uint32 srcMipLevel = static_cast(srcLevel); const Uint32 dstMipLevel = static_cast(dstLevel); - const Uint32 srcMipWidth = std::max(1u, srcResource->extent.width >> srcMipLevel); - const Uint32 srcMipHeight = std::max(1u, srcResource->extent.height >> srcMipLevel); - const Uint32 dstMipWidth = std::max(1u, dstResource->extent.width >> dstMipLevel); - const Uint32 dstMipHeight = std::max(1u, dstResource->extent.height >> dstMipLevel); + const Uint32 srcMipWidth = std::max(1u, srcImage.extent.width >> srcMipLevel); + const Uint32 srcMipHeight = std::max(1u, srcImage.extent.height >> srcMipLevel); + const Uint32 dstMipWidth = std::max(1u, dstImage.extent.width >> dstMipLevel); + const Uint32 dstMipHeight = std::max(1u, dstImage.extent.height >> dstMipLevel); // Promoted for the same reason as the level range above, and it is the same bug class: // a VkImageCopy whose region runs past the image is an out-of-bounds promise to the // driver, and the frontend does not check the region at all (there is a CTS sibling, @@ -9025,10 +9083,10 @@ void main() { // here: every target whose slices this function can address on one of the two Vulkan axes. // A refusal has to be a real decline, not an assertion - the assertion compiled to nothing // in a release build and the unsupported shape reached vkCmdCopyImage anyway. - CopyImageEndpoint srcEndpoint; - CopyImageEndpoint dstEndpoint; - if (!TryResolveCopyImageEndpoint(srcTextureTarget, *srcResource, srcMipLevel, srcZ, srcDepth, srcEndpoint) || - !TryResolveCopyImageEndpoint(dstTextureTarget, *dstResource, dstMipLevel, dstZ, srcDepth, dstEndpoint)) { + CopyImageSliceMapping srcSlices; + CopyImageSliceMapping dstSlices; + if (!TryResolveCopyImageSliceMapping(srcTextureTarget, srcImage, srcMipLevel, srcZ, srcDepth, srcSlices) || + !TryResolveCopyImageSliceMapping(dstTextureTarget, dstImage, dstMipLevel, dstZ, srcDepth, dstSlices)) { MGLOG_E_ONCE("%s: unsupported target pair src=%s dst=%s (srcZ=%d dstZ=%d depth=%d); declining the copy", __func__, MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(), MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), srcZ, dstZ, srcDepth); @@ -9039,25 +9097,31 @@ void main() { // shrinks) and a 3D texture by the selected level's depth (which every level halves), so // both come from the endpoint that resolved them. const Uint32 copySliceCount = static_cast(srcDepth); - if (srcEndpoint.baseSlice + copySliceCount > srcEndpoint.availableSlices || - dstEndpoint.baseSlice + copySliceCount > dstEndpoint.availableSlices) { + if (srcSlices.baseSlice + copySliceCount > srcSlices.availableSlices || + dstSlices.baseSlice + copySliceCount > dstSlices.availableSlices) { MGLOG_E_ONCE("%s: slice range outside image bounds (srcZ=%d of %u, dstZ=%d of %u, depth=%d); " "declining the copy", - __func__, srcZ, srcEndpoint.availableSlices, dstZ, dstEndpoint.availableSlices, srcDepth); + __func__, srcZ, srcSlices.availableSlices, dstZ, dstSlices.availableSlices, srcDepth); return; } - const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *srcTexture); - MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source textureId=%d", - __func__, srcTexture->GetExternalIndex()); + const auto materializeClear = [this, &frame](const CopyImageEndpoint& endpoint) { + if (endpoint.IsRenderbuffer()) { + return MaterializePendingClearForRenderbuffer(frame.commandBuffer, endpoint.Renderbuffer); + } + return MaterializePendingClearForTexture(frame.commandBuffer, *endpoint.Texture); + }; + const Bool clearReady = materializeClear(srcEndpoint); + MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source objectId=%u", + __func__, CopyImageEndpointName(srcEndpoint)); // A clear still parked on the destination would otherwise materialize AFTER this copy and // wipe the texels it just wrote. - const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *dstTexture); - MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination textureId=%d", - __func__, dstTexture->GetExternalIndex()); + const Bool dstClearReady = materializeClear(dstEndpoint); + MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination objectId=%u", + __func__, CopyImageEndpointName(dstEndpoint)); - const VkImageLayout srcOriginalLayout = srcResource->layout; - const VkImageLayout dstOriginalLayout = dstResource->layout; + const VkImageLayout srcOriginalLayout = *srcImage.trackedLayout; + const VkImageLayout dstOriginalLayout = *dstImage.trackedLayout; // A layout of UNDEFINED means nothing has ever been written to the image, which on the // SOURCE side is glTexStorage without an upload: legal GL, and the texels it copies are // undefined by the same spec sentence that lets the application ask. Both sides therefore @@ -9083,15 +9147,15 @@ void main() { // [baseSlice, baseSlice + depth) the slice mapping above hands the copy. if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { Bool srcReady = VkTextureManager::TransitionImageLayout( - frame.commandBuffer, srcResource->image, srcResource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, - srcResource->aspect, 0, srcResource->mipLevels); + srcImage.aspect, 0, srcImage.mipLevels); MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__); - srcCopyLayout = srcResource->layout; + srcCopyLayout = *srcImage.trackedLayout; } else { Bool srcReady = VkTextureManager::TransitionImageLayout( - frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + frame.commandBuffer, srcImage.image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1); MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__); @@ -9103,15 +9167,15 @@ void main() { VkImageLayout dstCopyLayout = dstOriginalLayout; if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { Bool dstReady = VkTextureManager::TransitionImageLayout( - frame.commandBuffer, dstResource->image, dstResource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, - dstResource->aspect, 0, dstResource->mipLevels); + dstImage.aspect, 0, dstImage.mipLevels); MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__); - dstCopyLayout = dstResource->layout; + dstCopyLayout = *dstImage.trackedLayout; } else { Bool dstReady = VkTextureManager::TransitionImageLayout( - frame.commandBuffer, dstResource->image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + frame.commandBuffer, dstImage.image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1); MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__); @@ -9121,18 +9185,18 @@ void main() { // on extent.depth as soon as either endpoint IS: a 3D image's subresource is always the // single layer (0, 1) and its slices are counted by the depth of the copy extent. With two // non-3D endpoints both layer counts carry it and extent.depth stays 1. - const Bool copyCrossesDepthAxis = srcEndpoint.slicesAreDepth || dstEndpoint.slicesAreDepth; + const Bool copyCrossesDepthAxis = srcSlices.slicesAreDepth || dstSlices.slicesAreDepth; VkImageCopy copyRegion{}; copyRegion.srcSubresource.aspectMask = copyAspectMask; copyRegion.srcSubresource.mipLevel = srcMipLevel; - copyRegion.srcSubresource.baseArrayLayer = srcEndpoint.BaseArrayLayer(); - copyRegion.srcSubresource.layerCount = srcEndpoint.slicesAreDepth ? 1u : copySliceCount; - copyRegion.srcOffset = {srcX, srcY, srcEndpoint.OffsetZ()}; + copyRegion.srcSubresource.baseArrayLayer = srcSlices.BaseArrayLayer(); + copyRegion.srcSubresource.layerCount = srcSlices.slicesAreDepth ? 1u : copySliceCount; + copyRegion.srcOffset = {srcX, srcY, srcSlices.OffsetZ()}; copyRegion.dstSubresource.aspectMask = copyAspectMask; copyRegion.dstSubresource.mipLevel = dstMipLevel; - copyRegion.dstSubresource.baseArrayLayer = dstEndpoint.BaseArrayLayer(); - copyRegion.dstSubresource.layerCount = dstEndpoint.slicesAreDepth ? 1u : copySliceCount; - copyRegion.dstOffset = {dstX, dstY, dstEndpoint.OffsetZ()}; + copyRegion.dstSubresource.baseArrayLayer = dstSlices.BaseArrayLayer(); + copyRegion.dstSubresource.layerCount = dstSlices.slicesAreDepth ? 1u : copySliceCount; + copyRegion.dstOffset = {dstX, dstY, dstSlices.OffsetZ()}; copyRegion.extent = {static_cast(srcWidth), static_cast(srcHeight), copyCrossesDepthAxis ? copySliceCount : 1u}; MGLOG_D("CopyImageSubData: src(target=%s level=%u layer=%u+%u z=%d) -> dst(target=%s level=%u layer=%u+%u " @@ -9143,8 +9207,8 @@ void main() { copyRegion.dstSubresource.baseArrayLayer, copyRegion.dstSubresource.layerCount, copyRegion.dstOffset.z, srcWidth, srcHeight, copyRegion.extent.depth); vkCmdCopyImage(frame.commandBuffer, - srcResource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - dstResource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + srcImage.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + dstImage.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Region); VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; @@ -9152,14 +9216,14 @@ void main() { GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask); if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { Bool srcRestored = VkTextureManager::TransitionImageLayout( - frame.commandBuffer, srcResource->image, srcResource->layout, srcRestoreLayout, + frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, srcRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, - srcResource->aspect, 0, srcResource->mipLevels); + srcImage.aspect, 0, srcImage.mipLevels); MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__); } else { Bool srcRestored = VkTextureManager::TransitionImageLayout( - frame.commandBuffer, srcResource->image, srcCopyLayout, srcRestoreLayout, + frame.commandBuffer, srcImage.image, srcCopyLayout, srcRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask, VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1); MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__); @@ -9170,14 +9234,14 @@ void main() { GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask); if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) { Bool dstRestored = VkTextureManager::TransitionImageLayout( - frame.commandBuffer, dstResource->image, dstResource->layout, dstRestoreLayout, + frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, dstRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, - dstResource->aspect, 0, dstResource->mipLevels); + dstImage.aspect, 0, dstImage.mipLevels); MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__); } else { Bool dstRestored = VkTextureManager::TransitionImageLayout( - frame.commandBuffer, dstResource->image, dstCopyLayout, dstRestoreLayout, + frame.commandBuffer, dstImage.image, dstCopyLayout, dstRestoreLayout, VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1); MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 4df06667..fff16041 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -23,6 +23,7 @@ #include "VkTimerQueryManager.h" #include "MG_Util/Math/VectorTypes.h" #include +#include #include #include "../VkIncludes.h" @@ -197,9 +198,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { GLbitfield mask, GLenum filter); void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height); - void CopyImageSubData(const SharedPtr& srcTexture, + void CopyImageSubData(const CopyImageEndpoint& srcEndpoint, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, - const SharedPtr& dstTexture, + const CopyImageEndpoint& dstEndpoint, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); void GenerateMipmap(GLenum target); diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index c35b527c..a9539587 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -3412,9 +3412,9 @@ namespace MobileGL::MG_Impl::GLImpl { MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height); } - void CopyImageSubData_Backend(const SharedPtr& srcTexture, + void CopyImageSubData_Backend(const MG_Backend::CopyImageEndpoint& src, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, - const SharedPtr& dstTexture, + const MG_Backend::CopyImageEndpoint& dst, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { auto copyImageSubData = MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData; @@ -3425,7 +3425,7 @@ namespace MobileGL::MG_Impl::GLImpl { "Backend does not support image-to-image copies.")); return; } - copyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel, dstX, + copyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth); } @@ -3472,9 +3472,9 @@ namespace MobileGL::MG_Impl::GLImpl { // the ~30 entry points that reach it through a BOUND object (where the name was never // in question and the fault is the binding), so this is a local rule rather than a // change to the helper. - Bool ValidateCopyImageObjectExists(const SharedPtr& textureObject, + Bool ValidateCopyImageObjectExists(const MG_Backend::CopyImageEndpoint& endpoint, const char* endpointName) { - if (textureObject) return true; + if (endpoint.Exists()) return true; MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique( @@ -3498,21 +3498,75 @@ namespace MobileGL::MG_Impl::GLImpl { MG_Util::ConvertTextureTargetToString(textureObject->GetTarget())))); return false; } - } // namespace - Bool ValidateCopyImageSubData_State(const SharedPtr& srcTexture, - GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, - const SharedPtr& dstTexture, - GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, - GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { - if (!ValidateCopyImageObjectExists(srcTexture, "source") || - !ValidateCopyImageObjectExists(dstTexture, "destination")) { + // ---- The questions ValidateCopyImageSubData_State asks of one endpoint. --------------- + // A renderbuffer answers all of them directly: it has exactly one image, no mip chain and + // no sampler state, and it carries its own internal format and extent. + + Int GetCopyImageEndpointSamples(const MG_Backend::CopyImageEndpoint& endpoint) { + if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetSamples(); + return endpoint.Texture->GetSamples(); + } + + TextureInternalFormat GetCopyImageEndpointFormat(const MG_Backend::CopyImageEndpoint& endpoint) { + if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat(); + return endpoint.Texture->GetFormat(); + } + + // A renderbuffer has level 0 and nothing else, and the failure is the same INVALID_VALUE + // ValidateTextureLevelExists records for a level a texture does not have. + Bool ValidateCopyImageEndpointLevelExists(const MG_Backend::CopyImageEndpoint& endpoint, GLint level, + const char* caller) { + if (!endpoint.IsRenderbuffer()) { + return TextureImpl::ValidateTextureLevelExists(endpoint.Texture, level, caller); + } + if (level == 0) return true; + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", caller, "A renderbuffer has only level 0.")); return false; } - const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget); - const auto dstTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(dstTarget); - if (!TextureImpl::ValidateTextureTarget(srcTextureTarget) || - !TextureImpl::ValidateTextureTarget(dstTextureTarget)) { + + Bool IsCopyImageEndpointComplete(const MG_Backend::CopyImageEndpoint& endpoint) { + // A renderbuffer is complete exactly when it has storage - there is nothing else it + // could be missing. + if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->IsAllocated(); + return endpoint.Texture && endpoint.Texture->IsComplete(); + } + + GLenum GetCopyImageEndpointCompressedFormat(const MG_Backend::CopyImageEndpoint& endpoint, + TextureUploadTarget uploadTarget, GLint level) { + if (endpoint.IsRenderbuffer()) return GL_NONE; + return GetCompressedLevelFormat(endpoint.Texture, uploadTarget, level); + } + + IntVec3 GetCopyImageEndpointLevelSize(const MG_Backend::CopyImageEndpoint& endpoint, + TextureUploadTarget uploadTarget, GLint level) { + if (endpoint.IsRenderbuffer()) { + return {endpoint.Renderbuffer->GetWidth(), endpoint.Renderbuffer->GetHeight(), 1}; + } + return GetCopyImageLevelSize(endpoint.Texture, uploadTarget, level); + } + } // namespace + + Bool ValidateCopyImageSubData_State(const MG_Backend::CopyImageEndpoint& src, + GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, + const MG_Backend::CopyImageEndpoint& dst, + GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, + GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { + if (!ValidateCopyImageObjectExists(src, "source") || + !ValidateCopyImageObjectExists(dst, "destination")) { + return false; + } + // GL_RENDERBUFFER has no TextureTarget to convert to, and it needs none: it is its own + // whole-image target, and the endpoint that carries it was resolved from the renderbuffer + // namespace, so it matches its object by construction. + const auto srcTextureTarget = + src.IsRenderbuffer() ? TextureTarget::Unknown : MG_Util::ConvertGLEnumToTextureTarget(srcTarget); + const auto dstTextureTarget = + dst.IsRenderbuffer() ? TextureTarget::Unknown : MG_Util::ConvertGLEnumToTextureTarget(dstTarget); + if ((!src.IsRenderbuffer() && !TextureImpl::ValidateTextureTarget(srcTextureTarget)) || + (!dst.IsRenderbuffer() && !TextureImpl::ValidateTextureTarget(dstTextureTarget))) { return false; } // GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but @@ -3520,8 +3574,8 @@ namespace MobileGL::MG_Impl::GLImpl { if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) { return false; } - if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") || - !ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) { + if (!ValidateCopyImageTargetMatchesObject(src.Texture, srcTextureTarget, "source") || + !ValidateCopyImageTargetMatchesObject(dst.Texture, dstTextureTarget, "destination")) { return false; } if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) || @@ -3535,8 +3589,8 @@ namespace MobileGL::MG_Impl::GLImpl { // driver as an out-of-range mip index - on Adreno that is a SIGSEGV inside // vkCmdCopyImage, which is what KHR-GL43.copy_image.non_existent_mipmap used to do to // the whole glcts process. The answer the spec asks for is GL_INVALID_VALUE. - if (!TextureImpl::ValidateTextureLevelExists(srcTexture, srcLevel, __func__) || - !TextureImpl::ValidateTextureLevelExists(dstTexture, dstLevel, __func__)) { + if (!ValidateCopyImageEndpointLevelExists(src, srcLevel, __func__) || + !ValidateCopyImageEndpointLevelExists(dst, dstLevel, __func__)) { return false; } if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) { @@ -3552,37 +3606,41 @@ namespace MobileGL::MG_Impl::GLImpl { // A multisample image can only be copied to one with the same sample count, and a // single-sample image reports zero - so this one comparison is also what rejects // copying between a multisample target and a non-multisample one. - if (srcTexture->GetSamples() != dstTexture->GetSamples()) { + const Int srcSamples = GetCopyImageEndpointSamples(src); + const Int dstSamples = GetCopyImageEndpointSamples(dst); + if (srcSamples != dstSamples) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( "MG_Impl/GLImpl", __func__, std::format("The two images have different sample counts ({} vs. {}).", - srcTexture->GetSamples(), dstTexture->GetSamples()))); + srcSamples, dstSamples))); return false; } // 18.3.2: both images must be complete. An incomplete one has no defined texels to copy // and no defined storage to copy into. - if (!srcTexture->IsComplete() || !dstTexture->IsComplete()) { + const Bool srcComplete = IsCopyImageEndpointComplete(src); + const Bool dstComplete = IsCopyImageEndpointComplete(dst); + if (!srcComplete || !dstComplete) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique( "MG_Impl/GLImpl", __func__, std::format("A copied image is incomplete (source complete: {}, destination complete: {}).", - srcTexture->IsComplete(), dstTexture->IsComplete()))); + srcComplete, dstComplete))); return false; } - const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture); - const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture); + const auto srcUploadTarget = GetPrimaryUploadTarget(src.Texture); + const auto dstUploadTarget = GetPrimaryUploadTarget(dst.Texture); const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock( - srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel)); + GetCopyImageEndpointFormat(src), GetCopyImageEndpointCompressedFormat(src, srcUploadTarget, srcLevel)); const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock( - dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel)); + GetCopyImageEndpointFormat(dst), GetCopyImageEndpointCompressedFormat(dst, dstUploadTarget, dstLevel)); if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) { return false; } - const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel); - const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel); + const IntVec3 srcLevelSize = GetCopyImageEndpointLevelSize(src, srcUploadTarget, srcLevel); + const IntVec3 dstLevelSize = GetCopyImageEndpointLevelSize(dst, dstUploadTarget, dstLevel); if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight, srcLevelSize.x(), srcLevelSize.y(), "source") || !TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight, @@ -5780,17 +5838,29 @@ namespace MobileGL::MG_Impl::GLImpl { GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { // A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is - // INVALID_OPERATION - so resolve through the plain lookup, which answers a null + // INVALID_OPERATION - so resolve through the plain lookups, which answer a null // SharedPtr, and let the validator record the error this entry point owes. - const SharedPtr srcTexture = - MG_State::pGLContext->GetTextureObject(srcName); - const SharedPtr dstTexture = - MG_State::pGLContext->GetTextureObject(dstName); - if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, srcX, srcY, dstTexture, dstTarget, + // + // The TARGET picks the namespace: GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER, and a + // renderbuffer name has nothing to do with a texture name. Resolving both through + // GetTextureObject made every renderbuffer endpoint INVALID_VALUE - or, when the number + // happened to collide with a live texture, INVALID_ENUM from the target check. + const auto resolveEndpoint = [](GLuint name, GLenum target) { + MG_Backend::CopyImageEndpoint endpoint{}; + if (target == GL_RENDERBUFFER) { + endpoint.Renderbuffer = MG_State::pGLContext->GetRenderbufferObject(name); + } else { + endpoint.Texture = MG_State::pGLContext->GetTextureObject(name); + } + return endpoint; + }; + const MG_Backend::CopyImageEndpoint src = resolveEndpoint(srcName, srcTarget); + const MG_Backend::CopyImageEndpoint dst = resolveEndpoint(dstName, dstTarget); + if (!ValidateCopyImageSubData_State(src, srcTarget, srcLevel, srcX, srcY, dst, dstTarget, dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) { return; } - CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel, + CopyImageSubData_Backend(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth); } diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 19279b0d..4658a7b3 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -4100,24 +4100,25 @@ namespace { GLint SrcZ = -1; GLint DstZ = -1; GLsizei Depth = -1; + Bool SrcIsRenderbuffer = false; + Bool DstIsRenderbuffer = false; } g_copyImageSubDataCall; - void RecordCopyImageSubData(const SharedPtr& srcTexture, GLenum srcTarget, + void RecordCopyImageSubData(const MG_Backend::CopyImageEndpoint& src, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, - const SharedPtr& dstTexture, GLenum dstTarget, + const MG_Backend::CopyImageEndpoint& dst, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { - (void)srcTexture; (void)srcLevel; (void)srcX; (void)srcY; - (void)dstTexture; (void)dstLevel; (void)dstX; (void)dstY; (void)srcWidth; (void)srcHeight; - g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ, dstZ, srcDepth}; + g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ, + dstZ, srcDepth, src.IsRenderbuffer(), dst.IsRenderbuffer()}; } // Two storage-backed 2D textures of the requested formats, so a copy between them is a legal @@ -4388,3 +4389,102 @@ TEST_F(TextureTest, CopyImageSubDataPassesTheRectangleTargetThroughUntranslated) EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast(GL_TEXTURE_RECTANGLE)); EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } + +// GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER as an endpoint target, and a renderbuffer name lives +// in its own namespace. Resolving BOTH names through the texture namespace answered a null object +// for every renderbuffer endpoint, so all 74 conformance cases that name one - the whole +// texture<->renderbuffer half of KHR-GL43.copy_image, plus its smoke test - reported +// GL_INVALID_VALUE. The endpoint is a sum type now; the target picks the namespace. +TEST_F(TextureTest, CopyImageSubDataResolvesARenderbufferEndpointInTheRenderbufferNamespace) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint texture = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture); + MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 8, 8); + GLuint renderbuffer = 0; + MG_Impl::GLImpl::CreateRenderbuffers(1, &renderbuffer); + MG_Impl::GLImpl::NamedRenderbufferStorage(renderbuffer, GL_RGBA8, 8, 8); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, + 4, 4, 1); + EXPECT_TRUE(g_copyImageSubDataCall.Called); + EXPECT_FALSE(g_copyImageSubDataCall.SrcIsRenderbuffer); + EXPECT_TRUE(g_copyImageSubDataCall.DstIsRenderbuffer); + EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast(GL_RENDERBUFFER)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // ...and back the other way, which is the second half of the conformance case's two-copy + // shape (texture -> renderbuffer -> texture). + g_copyImageSubDataCall = {}; + MG_Impl::GLImpl::CopyImageSubData(renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, texture, GL_TEXTURE_2D, 0, 0, 0, 0, + 4, 4, 1); + EXPECT_TRUE(g_copyImageSubDataCall.Called); + EXPECT_TRUE(g_copyImageSubDataCall.SrcIsRenderbuffer); + EXPECT_FALSE(g_copyImageSubDataCall.DstIsRenderbuffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// Renderbuffer to renderbuffer, the shape neither endpoint could take before, plus the negative +// that pins which table was consulted: with GL_RENDERBUFFER named, a number that is not a live +// RENDERBUFFER is INVALID_VALUE - the texture table is never asked. +TEST_F(TextureTest, CopyImageSubDataKeepsTheTwoNameNamespacesApart) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint srcRenderbuffer = 0; + GLuint dstRenderbuffer = 0; + MG_Impl::GLImpl::CreateRenderbuffers(1, &srcRenderbuffer); + MG_Impl::GLImpl::CreateRenderbuffers(1, &dstRenderbuffer); + MG_Impl::GLImpl::NamedRenderbufferStorage(srcRenderbuffer, GL_RGBA8, 8, 8); + MG_Impl::GLImpl::NamedRenderbufferStorage(dstRenderbuffer, GL_RGBA8, 8, 8); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(srcRenderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, dstRenderbuffer, + GL_RENDERBUFFER, 0, 0, 0, 0, 4, 4, 1); + EXPECT_TRUE(g_copyImageSubDataCall.Called); + EXPECT_TRUE(g_copyImageSubDataCall.SrcIsRenderbuffer); + EXPECT_TRUE(g_copyImageSubDataCall.DstIsRenderbuffer); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + g_copyImageSubDataCall = {}; + MG_Impl::GLImpl::CopyImageSubData(srcRenderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, 4243, GL_RENDERBUFFER, 0, 0, 0, + 0, 4, 4, 1); + EXPECT_FALSE(g_copyImageSubDataCall.Called); + ExpectSingleGlError(GL_INVALID_VALUE); +} + +// A renderbuffer has exactly one image, so any level above zero is the same INVALID_VALUE a +// texture gets for a level it does not have - and an unallocated one is an incomplete image, +// which 18.3.2 spells INVALID_OPERATION. +TEST_F(TextureTest, CopyImageSubDataChecksARenderbufferLevelAndStorage) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint texture = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture); + MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 8, 8); + GLuint renderbuffer = 0; + MG_Impl::GLImpl::CreateRenderbuffers(1, &renderbuffer); + MG_Impl::GLImpl::NamedRenderbufferStorage(renderbuffer, GL_RGBA8, 8, 8); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, renderbuffer, GL_RENDERBUFFER, 1, 0, 0, 0, + 4, 4, 1); + EXPECT_FALSE(g_copyImageSubDataCall.Called); + ExpectSingleGlError(GL_INVALID_VALUE); + + g_copyImageSubDataCall = {}; + GLuint emptyRenderbuffer = 0; + MG_Impl::GLImpl::CreateRenderbuffers(1, &emptyRenderbuffer); + DrainPendingGlErrors(); + + MG_Impl::GLImpl::CopyImageSubData(texture, GL_TEXTURE_2D, 0, 0, 0, 0, emptyRenderbuffer, GL_RENDERBUFFER, 0, 0, + 0, 0, 4, 4, 1); + EXPECT_FALSE(g_copyImageSubDataCall.Called); + ExpectSingleGlError(GL_INVALID_OPERATION); +} From 1c0be3e715b41bbb652bec1b0192a20e86b0ffcd Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 10:14:48 -0400 Subject: [PATCH 03/11] [Fix, Test] (DirectGLES, TextureUtil): return RGB9_E5 glGetTexImage from the stored words --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 118 ++++++++++++++++++ MobileGL/MG_Test/Texture/TextureTest.cpp | 23 ++++ .../MG_Util/Texture/PixelStoreProcessor.cpp | 8 ++ .../MG_Util/Texture/PixelStoreProcessor.h | 11 ++ 4 files changed, 160 insertions(+) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index f8ba1dc6..715ddd90 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -5805,6 +5805,92 @@ namespace MobileGL::MG_Backend::DirectGLES { return endpoint.Texture ? endpoint.Texture->GetFormat() : TextureInternalFormat::Unknown; } + // Whether this endpoint's CPU shadow can be addressed texel-exactly by the mirror below: one + // upload target (so not a cube map, whose six chains the z axis selects between) and layers on + // the z axis (GL_TEXTURE_1D_ARRAY carries them on y). + static Bool CanMirrorCopyImageShadow(const SharedPtr& texture) { + if (!texture) return false; + if (texture->GetTarget() == TextureTarget::Texture1DArray) return false; + return texture->GetUploadTargets().size() == 1; + } + + // glCopyImageSubData is defined as a raw texel-block move, so for a destination whose CPU + // shadow has to stay authoritative - a packed format with redundant encodings, where a GPU + // readback can only answer with RE-ENCODED words (see the verbatim branch in GetTexImage) - + // the same move is replayed on the shadow. Nothing is marked dirty: the driver copy already + // put these texels on the GPU, and flagging the level would only schedule a redundant upload + // back over them. + // + // Declined, leaving the shadow exactly as it was, for every shape whose bytes this cannot + // address exactly - a renderbuffer (no shadow at all), a cube or 1D-array endpoint, a level + // whose shadow is missing or not a plain texel grid, a region outside either level, or a + // self-copy within one level, where the row copies could overlap. + static void MirrorCopyImageIntoDestinationShadow(const CopyImageEndpoint& srcEndpoint, GLint srcLevel, GLint srcX, + GLint srcY, GLint srcZ, const CopyImageEndpoint& dstEndpoint, + GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, + GLsizei width, GLsizei height, GLsizei depth) { + if (!CanMirrorCopyImageShadow(srcEndpoint.Texture) || !CanMirrorCopyImageShadow(dstEndpoint.Texture)) return; + if (srcEndpoint.Texture == dstEndpoint.Texture && srcLevel == dstLevel) return; + if (width <= 0 || height <= 0 || depth <= 0) return; + if (srcLevel < 0 || dstLevel < 0 || srcX < 0 || srcY < 0 || srcZ < 0 || dstX < 0 || dstY < 0 || dstZ < 0) { + return; + } + auto* srcMipmap = MG_State::GLState::AsMipmapTexture(srcEndpoint.Texture.get()); + auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get()); + if (!srcMipmap || !dstMipmap) return; + + const auto srcUploadTarget = srcEndpoint.Texture->GetUploadTargets()[0]; + const auto dstUploadTarget = dstEndpoint.Texture->GetUploadTargets()[0]; + const IntVec3 srcSize = srcMipmap->GetMipmapTexelSize(srcUploadTarget, static_cast(srcLevel)); + const IntVec3 dstSize = dstMipmap->GetMipmapTexelSize(dstUploadTarget, static_cast(dstLevel)); + const SizeT srcSlices = static_cast(std::max(srcSize.z(), 1)); + const SizeT dstSlices = static_cast(std::max(dstSize.z(), 1)); + if (srcSize.x() <= 0 || srcSize.y() <= 0 || dstSize.x() <= 0 || dstSize.y() <= 0) return; + const SizeT srcTexels = static_cast(srcSize.x()) * static_cast(srcSize.y()) * srcSlices; + const SizeT dstTexels = static_cast(dstSize.x()) * static_cast(dstSize.y()) * dstSlices; + const SizeT srcBytes = srcMipmap->GetMipmapByteSize(srcUploadTarget, static_cast(srcLevel)); + const SizeT dstBytes = dstMipmap->GetMipmapByteSize(dstUploadTarget, static_cast(dstLevel)); + // A shadow that is not exactly texels x texelSize bytes is one this cannot index (a + // compressed blob, or a level whose allocation disagrees with its recorded extent). + const SizeT texelBytes = srcTexels == 0 ? 0 : srcBytes / srcTexels; + if (texelBytes == 0 || srcBytes != srcTexels * texelBytes || dstTexels == 0 || + dstBytes != dstTexels * texelBytes) { + return; + } + if (static_cast(srcX) + width > static_cast(srcSize.x()) || + static_cast(srcY) + height > static_cast(srcSize.y()) || + static_cast(srcZ) + depth > srcSlices || + static_cast(dstX) + width > static_cast(dstSize.x()) || + static_cast(dstY) + height > static_cast(dstSize.y()) || + static_cast(dstZ) + depth > dstSlices) { + return; + } + + const auto* srcBase = static_cast( + srcMipmap->MapMipmapData(srcUploadTarget, static_cast(srcLevel))); + auto* dstBase = static_cast(dstMipmap->MapMipmapData(dstUploadTarget, static_cast(dstLevel))); + if (!srcBase || !dstBase) return; + + const SizeT rowBytes = static_cast(width) * texelBytes; + for (GLsizei slice = 0; slice < depth; ++slice) { + for (GLsizei row = 0; row < height; ++row) { + const SizeT srcOffset = ((static_cast(srcZ + slice) * static_cast(srcSize.y()) + + static_cast(srcY + row)) * + static_cast(srcSize.x()) + + static_cast(srcX)) * + texelBytes; + const SizeT dstOffset = ((static_cast(dstZ + slice) * static_cast(dstSize.y()) + + static_cast(dstY + row)) * + static_cast(dstSize.x()) + + static_cast(dstX)) * + texelBytes; + Memcpy(dstBase + dstOffset, srcBase + srcOffset, rowBytes); + } + } + MGLOG_D("CopyImageSubData: mirrored %dx%dx%d texels into the destination's CPU shadow", width, height, + depth); + } + void CopyImageSubData(const CopyImageEndpoint& srcEndpoint, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, const CopyImageEndpoint& dstEndpoint, @@ -5895,6 +5981,14 @@ namespace MobileGL::MG_Backend::DirectGLES { MG_Util::ConvertGLEnumToString(dst.target).c_str(), MG_Util::ConvertGLEnumToString(dstTarget).c_str()); MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request."); + return; + } + // The copy landed on the GPU. For a destination whose readback cannot be bit-exact the + // CPU shadow is what glGetTexImage answers from, so it has to follow the same move - + // otherwise it hands back whatever the level held before this copy. + if (MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(dstFormat)) { + MirrorCopyImageIntoDestinationShadow(srcEndpoint, srcLevel, srcX, srcY, srcZ, dstEndpoint, dstLevel, + dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth); } } @@ -7818,6 +7912,30 @@ namespace MobileGL::MG_Backend::DirectGLES { backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY; const GLsizei sliceCount = std::max(size.z(), 1); const Bool multiSlice = size.z() > 1; + // glGetTexImage answers with the STORED texels, and for a packed format whose encoding + // is not unique the GPU route below cannot: it reads GL_RGBA/GL_FLOAT and re-encodes, + // which canonicalizes an RGB9_E5 shared exponent (0xf8fc0000 -> 0xe7e00000 - the same + // value 8064, different words), and the conformance suite compares the words + // ("CopyImageSubData modified contents of source image"). The scratch FBO does NOT + // decide this for us: Adreno reports an RGB9_E5 colour attachment complete, so the + // shadow branch further down was unreachable. Serve the verbatim-word pairs from the + // shadow first and keep the GPU attempts as the fallback for a level the shadow never + // received. Every other format still prefers the GPU, so a rendered-into texture is + // unaffected; RGB9_E5 is not colour-renderable, so its shadow stays authoritative - + // and the one path that GPU-writes it, CopyImageSubData, mirrors itself into the + // shadow for exactly this reason. + const Bool verbatimPackedShadowRead = + MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding(textureObject->GetFormat()) && + MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer( + textureObject->GetFormat(), MG_Util::ConvertGLEnumToTextureInputFormat(format), + MG_Util::ConvertGLEnumToTexturePixelDataType(type)); + if (verbatimPackedShadowRead && + GetTexImageViaShadowConversion(textureMipmapObject, + MG_Util::ConvertGLEnumToTextureUploadTarget(target), level, size.x(), + size.y(), sliceCount, format, type, pixels, applyPackImageParams)) { + MGLOG_D("GetTexImage: finished via shadow conversion (verbatim packed words)"); + return; + } // A multi-slice read used to go to the CPU shadow outright, on the grounds that the // scratch FBO can only expose one layer at a time. But the shadow only holds what was // uploaded, so every slice that was rendered to came back stale - which is exactly what diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 4658a7b3..8922040e 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -3320,6 +3320,29 @@ TEST(SharedExponentRGB9E5Test, RawPackedPixelTransferCoversOnlyIdenticalLayouts) TexturePixelDataType::UnsignedInt5999Rev)); } +TEST(SharedExponentRGB9E5Test, RedundantPackedEncodingIsRGB9E5Only) { + using MG_Util::PixelStoreProcessor::HasRedundantPackedEncoding; + + // This is the predicate that decides whether the CPU shadow has to answer glGetTexImage + // instead of a GPU readback, so it must be as narrow as the defect: only the shared exponent + // has several legal encodings of one value. + EXPECT_TRUE(HasRedundantPackedEncoding(TextureInternalFormat::RGB9E5)); + + // The other three packed 32-bit layouts round-trip through float32 bit-exactly (each field is + // either an integer or a unique float encoding), so a GPU readback still serves them - which + // matters because RGB10_A2 and R11F_G11F_B10F ARE colour-renderable and their shadow can + // legitimately be stale. + EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB10A2)); + EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB10A2UI)); + EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::R11FG11FB10F)); + + // Nothing unpacked qualifies, and neither does an unknown format. + EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGBA8)); + EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGBA32F)); + EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::RGB8)); + EXPECT_FALSE(HasRedundantPackedEncoding(TextureInternalFormat::Unknown)); +} + TEST_F(TextureTest, TexImage2DRGB9E5KeepsNonCanonicalClientWords) { // Upload direction: GL_RGB / GL_UNSIGNED_INT_5_9_9_9_REV into GL_RGB9_E5 stores the client // words untouched, including the redundant encodings the CTS generates. diff --git a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp index c52449fa..099f49b3 100644 --- a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp +++ b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp @@ -806,6 +806,14 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { return IsRawPackedPixelPair(packedInternal.kind, clientFormat, clientType); } + Bool HasRedundantPackedEncoding(TextureInternalFormat internalFormat) { + InternalPackedLayout packedInternal{}; + if (!GetInternalPackedLayout(internalFormat, packedInternal)) { + return false; + } + return packedInternal.kind == PackedInternalKind::FloatRGB9E5; + } + // assume 8 bit per channel // swizzle.size() == channel count void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector& swizzle) { diff --git a/MobileGL/MG_Util/Texture/PixelStoreProcessor.h b/MobileGL/MG_Util/Texture/PixelStoreProcessor.h index 91169739..c589f1ff 100644 --- a/MobileGL/MG_Util/Texture/PixelStoreProcessor.h +++ b/MobileGL/MG_Util/Texture/PixelStoreProcessor.h @@ -44,6 +44,17 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { Bool IsRawPackedPixelTransfer(TextureInternalFormat internalFormat, TextureInputFormat clientFormat, TexturePixelDataType clientType); + // True when a packed internal format has REDUNDANT encodings, so decoding a texel and + // re-encoding it keeps the VALUE but not the BITS. Only RGB9_E5 does: its shared exponent can + // be lowered with the mantissas shifted up to match, and the spec's encoder always emits the + // canonical form. RGB10_A2, RGB10_A2UI and R11F_G11F_B10F round-trip through float32 + // bit-exactly, so a GPU readback can answer for them. + // + // This is what decides whether the CPU shadow has to stay authoritative for a format: a + // readback of an RGB9_E5 level through a colour attachment cannot return the stored words, no + // matter how well behaved the driver is. + Bool HasRedundantPackedEncoding(TextureInternalFormat internalFormat); + // Decodes the canonical shadow-mip storage of `internalFormat` into wide RGBA texels for CPU // readback (GetTexImage of non-renderable formats). Non-integer formats fill outWide with // 4 Floats per texel; integer formats fill it with 4 Uint32/Int32 per texel and set From a9b4c47fea1b12796eb2014a82f0d62c112088d5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 10:17:55 -0400 Subject: [PATCH 04/11] [Fix, Test] (GLImpl): record the specific compressed internalformat in TexImage3D and TexStorage3D --- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 24 ++++++ MobileGL/MG_Test/Texture/TextureTest.cpp | 79 +++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index a9539587..77573173 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -2197,6 +2197,19 @@ namespace MobileGL::MG_Impl::GLImpl { } else { DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes}); + // The same specific-compressed-format tag glTexImage2D records (see TexImage2D_State): + // GL 4.6 core 8.5 commits the level to that format, so GL_TEXTURE_COMPRESSED and + // GL_TEXTURE_INTERNAL_FORMAT must report it - and, less obviously, glCopyImageSubData + // sizes the level's texel BLOCK from it. Without the tag a GL_COMPRESSED_RG_RGTC2 + // array level measured as the RG8 storage it resolved to, 2 bytes instead of 16, and + // the copy-compatibility rule refused a pairing 18.3.2 requires. AllocateStorage above + // clears the tag, so this has to follow it. + const auto compressedInfo = MG_Util::GetCompressedFormatInfo(static_cast(internalformat)); + if (compressedInfo.blockWidth != 0) { + textureMipmapObject->SetMipmapCompressedImage( + textureUploadTarget, level, static_cast(internalformat), nullptr, + MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth})); + } } if (!originalPixels) { @@ -4620,6 +4633,10 @@ namespace MobileGL::MG_Impl::GLImpl { // Array targets keep their layer count constant across levels; only true 3D // textures halve depth per level (GL 3.3 §3.9 glTexStorage3D). const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget()); + // The same specific-compressed-format tag glTexStorage2D records, for the array targets a + // compressed glTexStorage3D is legal on (GL_TEXTURE_3D was refused above). Zero width means + // a generic format, which MobileGL answers with uncompressed storage, so it is not tagged. + const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat); for (GLsizei level = 0; level < levels; ++level) { const GLsizei levelWidth = std::max(1, width >> level); const GLsizei levelHeight = std::max(1, height >> level); @@ -4629,6 +4646,13 @@ namespace MobileGL::MG_Impl::GLImpl { textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, levelHeight, levelDepth}, byteSize}); textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false); + if (compressedInfo.blockWidth != 0) { + // After AllocateStorage, which clears the tag. + textureMipmapObject->SetMipmapCompressedImage( + textureUploadTarget, static_cast(level), internalformat, nullptr, + MG_Util::CalculateCompressedTextureImageSize(compressedInfo, + {levelWidth, levelHeight, levelDepth})); + } } // See TextureStorage1D. textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast(levels)); diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 8922040e..cbdf57af 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -1651,6 +1651,56 @@ TEST_F(TextureTest, AnUncompressedRespecificationClearsTheCompressedTag) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +// The same rule for the 3D entry points, which never recorded the tag at all. Besides the two +// level queries this decides the level's texel BLOCK SIZE, which glCopyImageSubData compares +// against the other endpoint's - an untagged GL_COMPRESSED_RG_RGTC2 array level measured as the +// RG8 storage it resolves to, 2 bytes instead of 16. +TEST_F(TextureTest, TexImage3DAndTexStorage3DTagASpecificCompressedInternalFormat) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, texture); + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_COMPRESSED_RG_RGTC2, 8, 8, 2, 0, GL_RG, + GL_UNSIGNED_BYTE, nullptr); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + GLint compressed = GL_FALSE; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED, &compressed); + EXPECT_EQ(compressed, GL_TRUE); + + GLint internalFormat = 0; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat); + EXPECT_EQ(internalFormat, static_cast(GL_COMPRESSED_RG_RGTC2)); + + // 8x8 in 4x4 blocks of 16 bytes each is 64 bytes a layer, and both layers count. + GLint imageSize = 0; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize); + EXPECT_EQ(imageSize, 128); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // The texel shadow behind the tag keeps the uncompressed storage the format resolves to. + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + ASSERT_NE(textureObject, nullptr); + EXPECT_EQ(textureObject->GetFormat(), TextureInternalFormat::RG8); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0); + + // glTexStorage3D has the same gap and the same fix; immutable storage plus + // glCompressedTexSubImage3D is the modern way to upload a compressed array texture. + GLuint storageTexture = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &storageTexture); + MG_Impl::GLImpl::TextureStorage3D(storageTexture, 1, GL_COMPRESSED_RG_RGTC2, 8, 8, 2); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, storageTexture); + + compressed = GL_FALSE; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED, &compressed); + EXPECT_EQ(compressed, GL_TRUE); + imageSize = 0; + MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_2D_ARRAY, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &imageSize); + EXPECT_EQ(imageSize, 128); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0); +} + namespace { // A 16x16 RGBA8 texture with exactly `levelCount` levels, defined the way // KHR-GL43.copy_image.non_existent_mipmap defines its textures - glTexImage2D per @@ -4511,3 +4561,32 @@ TEST_F(TextureTest, CopyImageSubDataChecksARenderbufferLevelAndStorage) { EXPECT_FALSE(g_copyImageSubDataCall.Called); ExpectSingleGlError(GL_INVALID_OPERATION); } + +// A 16-byte RGTC2 block and a 16-byte RGBA32UI texel are in the same size class, so GL 4.6 core +// 18.3.2 requires this copy to succeed. It did not for an ARRAY source: glTexImage3D recorded no +// specific-compressed-format tag, so the level was measured as the 2-byte RG8 storage RGTC2 +// resolves to and the compatibility rule saw 2 against 16. +TEST_F(TextureTest, CopyImageSubDataSizesACompressedArrayLevelByItsBlock) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint compressedSource = 0; + MG_Impl::GLImpl::GenTextures(1, &compressedSource); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, compressedSource); + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_COMPRESSED_RG_RGTC2, 8, 8, 1, 0, GL_RG, + GL_UNSIGNED_BYTE, nullptr); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_BASE_LEVEL, 0); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 0); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0); + + GLuint uncompressedDestination = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &uncompressedDestination); + MG_Impl::GLImpl::TextureStorage3D(uncompressedDestination, 1, GL_RGBA32UI, 8, 8, 1); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(compressedSource, GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, uncompressedDestination, + GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, 8, 8, 1); + EXPECT_TRUE(g_copyImageSubDataCall.Called); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} From b1774e80bead7b7559e6f87ec3685debf0de9585 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 10:20:34 -0400 Subject: [PATCH 05/11] [Fix, Test] (GLImpl): make copy-image completeness mipmap-aware per GL 4.6 core 8.17 --- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 33 +++++- MobileGL/MG_Test/Texture/TextureTest.cpp | 109 ++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 77573173..3f6a021b 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -3540,11 +3540,42 @@ namespace MobileGL::MG_Impl::GLImpl { return false; } + // Targets with no mip chain have q == level_base by definition (GL 4.6 core 8.17), so no + // minification filter can make them mipmap incomplete - while the shared predicate derives + // q from the base level's size alone and would call a 16x16 multisample image incomplete. + Bool CopyImageTargetHasMipmapChain(TextureTarget target) { + switch (target) { + case TextureTarget::TextureRectangle: + case TextureTarget::TextureBuffer: + case TextureTarget::Texture2DMultisample: + case TextureTarget::Texture2DMultisampleArray: + return false; + default: + return true; + } + } + Bool IsCopyImageEndpointComplete(const MG_Backend::CopyImageEndpoint& endpoint) { // A renderbuffer is complete exactly when it has storage - there is nothing else it // could be missing. if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->IsAllocated(); - return endpoint.Texture && endpoint.Texture->IsComplete(); + const auto* texture = endpoint.Texture.get(); + if (!texture) return false; + // 18.3.2 asks for TEXTURE completeness, which GL 4.6 core 8.17 defines to include the + // MIP CHAIN whenever the minification filter samples it - and ITextureObject:: + // IsComplete() only answers the storage half (an internal format, and no zero-size + // level in the middle of the chain). A texture with level 0 alone and the default + // NEAREST_MIPMAP_LINEAR filter is incomplete, which is exactly how + // KHR-GL43.copy_image.incomplete_tex builds its subject. + // + // The filter is the texture's OWN: copy-image never goes through a texture unit, so no + // sampler object is in play. An immutable texture is unaffected - glTexStorage clamps + // TEXTURE_MAX_LEVEL to levels-1, which is what makes a single-level immutable texture + // mipmap complete under any filter. + const auto& sampler = texture->GetSamplerObject(); + const Bool mipmapped = CopyImageTargetHasMipmapChain(texture->GetTarget()) && sampler && + sampler->GetMipmapMode() != SamplerMipmapMode::None; + return MG_State::GLState::IsMipmapCompleteForFilter(texture, mipmapped); } GLenum GetCopyImageEndpointCompressedFormat(const MG_Backend::CopyImageEndpoint& endpoint, diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index cbdf57af..2182f8dc 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -4428,9 +4428,13 @@ TEST_F(TextureTest, CopyImageSubDataAcceptsAPlainMutableTexImage2DPair) { MG_Impl::GLImpl::GenTextures(1, &reusedSrc); MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedSrc); MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); MG_Impl::GLImpl::GenTextures(1, &reusedDst); MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, reusedDst); MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); MG_Impl::GLImpl::CopyImageSubData(reusedSrc, GL_TEXTURE_2D, 0, 0, 0, 0, reusedDst, GL_TEXTURE_2D, 0, 0, 0, 0, 1, @@ -4590,3 +4594,108 @@ TEST_F(TextureTest, CopyImageSubDataSizesACompressedArrayLevelByItsBlock) { EXPECT_TRUE(g_copyImageSubDataCall.Called); EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } + +// 18.3.2 requires INVALID_OPERATION when either object is an INCOMPLETE TEXTURE, and completeness +// is GL 4.6 core 8.17's - which includes the mip chain whenever the minification filter reads it. +// A mutable texture with level 0 alone still carries the default NEAREST_MIPMAP_LINEAR filter, so +// it is mipmap incomplete; the storage-only IsComplete() this used to ask called it complete and +// let the copy through, which is the whole of KHR-GL43.copy_image.incomplete_tex. +TEST_F(TextureTest, CopyImageSubDataRejectsAMipmapIncompleteTexture) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint incomplete = 0; + MG_Impl::GLImpl::GenTextures(1, &incomplete); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, incomplete); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); + GLuint complete = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &complete); + MG_Impl::GLImpl::TextureStorage2D(complete, 1, GL_RGBA8, 16, 16); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, complete, GL_TEXTURE_2D, 0, 0, 0, 0, 4, + 4, 1); + EXPECT_FALSE(g_copyImageSubDataCall.Called); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // The destination side is checked the same way. + g_copyImageSubDataCall = {}; + MG_Impl::GLImpl::CopyImageSubData(complete, GL_TEXTURE_2D, 0, 0, 0, 0, incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, 4, + 4, 1); + EXPECT_FALSE(g_copyImageSubDataCall.Called); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // Capping TEXTURE_MAX_LEVEL at the one level that exists is what the conformance suite's + // makeTextureComplete does, and it is enough to make the same object complete. + g_copyImageSubDataCall = {}; + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, incomplete); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); + DrainPendingGlErrors(); + + MG_Impl::GLImpl::CopyImageSubData(incomplete, GL_TEXTURE_2D, 0, 0, 0, 0, complete, GL_TEXTURE_2D, 0, 0, 0, 0, 4, + 4, 1); + EXPECT_TRUE(g_copyImageSubDataCall.Called); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// The targets that have no mip chain must not be dragged in: GL 4.6 core 8.17 makes q equal to +// level_base for them, so no filter can make them mipmap incomplete. A rectangle texture gets a +// non-mipmapping default filter from the object itself, so it would survive a predicate that +// trusted the sampler alone - it is here because the whole texture path is one branch and this is +// the cheap half of pinning it. +TEST_F(TextureTest, CopyImageSubDataDoesNotApplyMipmapCompletenessToRectangleTextures) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint srcRectangle = 0; + GLuint dstRectangle = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &srcRectangle); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_RECTANGLE, 1, &dstRectangle); + MG_Impl::GLImpl::TextureStorage2D(srcRectangle, 1, GL_RGBA8, 8, 8); + MG_Impl::GLImpl::TextureStorage2D(dstRectangle, 1, GL_RGBA8, 8, 8); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::CopyImageSubData(srcRectangle, GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, dstRectangle, + GL_TEXTURE_RECTANGLE, 0, 0, 0, 0, 4, 4, 1); + EXPECT_TRUE(g_copyImageSubDataCall.Called); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// The multisample half, which is the one the target guard actually exists for: a multisample +// texture keeps the shared NEAREST_MIPMAP_LINEAR default in its own sampler state (only the +// rectangle constructor overrides it), so asking the mipmap predicate about it without the target +// guard would report every 8x8 multisample image incomplete and refuse a legal copy. +TEST_F(TextureTest, CopyImageSubDataDoesNotApplyMipmapCompletenessToMultisampleTextures) { + const ScopedTextureBackendFunctionsOverride backendGuard; + MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData; + g_copyImageSubDataCall = {}; + + GLuint srcMultisample = 0; + GLuint dstMultisample = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &srcMultisample); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_MULTISAMPLE, 1, &dstMultisample); + MG_Impl::GLImpl::TextureStorage2DMultisample(srcMultisample, 1, GL_RGBA8, 8, 8, GL_FALSE); + MG_Impl::GLImpl::TextureStorage2DMultisample(dstMultisample, 1, GL_RGBA8, 8, 8, GL_FALSE); + DrainPendingGlErrors(); + + // This unit-test binary has no backend behind the renderable-format and sample-count queries, + // so the storage may not have been created at all. Checked on the state objects rather than + // assumed, so the case can only skip or test the real rule. + const auto srcObject = MG_State::pGLContext->GetTextureObject(srcMultisample); + const auto dstObject = MG_State::pGLContext->GetTextureObject(dstMultisample); + ASSERT_NE(srcObject, nullptr); + ASSERT_NE(dstObject, nullptr); + if (!srcObject->IsComplete() || !dstObject->IsComplete()) { + GTEST_SKIP() << "this context could not give the multisample textures storage"; + } + + MG_Impl::GLImpl::CopyImageSubData(srcMultisample, GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, dstMultisample, + GL_TEXTURE_2D_MULTISAMPLE, 0, 0, 0, 0, 4, 4, 1); + EXPECT_TRUE(g_copyImageSubDataCall.Called); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} From 898c39f1de7ccc4edb7f1a82f6e3e8cd9f1598d3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 10:23:03 -0400 Subject: [PATCH 06/11] [Fix] (DirectGLES, DirectVulkan): decline a null copy-image endpoint and settle a renderbuffer on its attachment layout --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 4 ++++ .../DirectVulkan/Renderer/VulkanRenderer.cpp | 22 ++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 715ddd90..7a8155f9 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -5771,6 +5771,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // Either way a reference taken by the first call is stale by the time the second returns, // and it is read four more times below. Copying the SharedPtr costs two refcount bumps on // a path that is already doing a texture copy. + // An endpoint that named nothing is the frontend validator's INVALID_VALUE and never + // reaches here - but the assertion that says so is compiled out of a release build, and + // SyncTextureObjectToBackend would register a null state object. + if (!endpoint.Texture) return false; out.texture = TextureImpl::SyncTextureObjectToBackend(endpoint.Texture); if (!out.texture) return false; const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 2de8c9d0..6d2dbf7d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -9007,6 +9007,9 @@ void main() { out.arrayLayers = 1; return out.image != VK_NULL_HANDLE; } + // An endpoint that named nothing is the frontend validator's INVALID_VALUE and never + // reaches here - but the assertion that says so is compiled out of a release build. + if (endpoint.Texture == nullptr) return false; auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*endpoint.Texture); if (resource == nullptr) return false; out.isRenderbuffer = false; @@ -9127,16 +9130,23 @@ void main() { // undefined by the same spec sentence that lets the application ask. Both sides therefore // take the same shape - transition the whole image out of UNDEFINED and settle it on a // real layout afterwards, since UNDEFINED is not a layout a barrier may transition BACK to. - const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout) { + // A renderbuffer settles on its ATTACHMENT layout instead: it is never sampled, and that is + // the layout MaterializePendingClearForRenderbuffer leaves it in. + const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout, Bool isRenderbuffer) { if (originalLayout != VK_IMAGE_LAYOUT_UNDEFINED) { return originalLayout; } - return (copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0 - ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL - : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + const Bool depthStencil = + (copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0; + if (isRenderbuffer) { + return depthStencil ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL + : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + } + return depthStencil ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL + : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; }; - const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout); - const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout); + const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout, srcImage.isRenderbuffer); + const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout, dstImage.isRenderbuffer); VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; From afebf38e90cf514366688a3b7bf02adc9d0a06df Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 10:38:29 -0400 Subject: [PATCH 07/11] [Fix, Test] (GLImpl): require only the requested level to exist in glGetTexImage --- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 69 ++++++++++++------- MobileGL/MG_Test/Texture/TextureTest.cpp | 50 ++++++++++++++ 2 files changed, 95 insertions(+), 24 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 3f6a021b..3ffea8c3 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -4200,8 +4200,14 @@ namespace MobileGL::MG_Impl::GLImpl { return false; } - // For a cube map this is exactly cube completeness: IsComplete() wants all six faces. - if (!textureObject->IsComplete()) { + // GL 4.6 core 8.11.4 names cube completeness as the only completeness a readback requires, + // and for a cube map that is exactly what IsComplete() answers (all six faces defined at + // every level). It must not speak for any other target: on a mip chain it also rejects + // "level N defined, the levels below it not", which is a perfectly readable texture at + // level N - and the shape glClearTexImage's conformance cases build, since they define + // only the level they clear. The requested level's own existence is checked below. + if ((target == TextureTarget::TextureCubeMap || target == TextureTarget::TextureCubeMapArray) && + !textureObject->IsComplete()) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", caller, "Texture is incomplete")); @@ -4260,33 +4266,48 @@ namespace MobileGL::MG_Impl::GLImpl { const auto* textureMipmapObject = static_cast(textureObject.get()); const auto& uploadTargets = textureObject->GetUploadTargets(); - if (!uploadTargets.empty() && static_cast(level) < textureMipmapObject->GetMipmapLevelCount()) { - // Tightly packed, and summed over every face because a cube map query returns all - // six. Pack pixel-store state only ever grows this, so a request rejected here - // could not have fit under any packing. - const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTargets[0], level); - const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat, - texturePixelDataType, texelSize) * - uploadTargets.size(); + // The half of the completeness gate above that GL does keep: the REQUESTED level has + // to hold an image. A name that was never given one carries no levels at all (which is + // also what an Unknown internal format answers), and a chain grown to reach level N + // leaves every level below it at {0, 0, 0}. + if (uploadTargets.empty() || static_cast(level) >= textureMipmapObject->GetMipmapLevelCount()) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", caller, "Texture level has no image to read back.")); + return false; + } + const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTargets[0], level); + if (texelSize.x() <= 0 || texelSize.y() <= 0 || texelSize.z() <= 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", caller, "Texture level has no image to read back.")); + return false; + } - if (bufSize >= 0 && static_cast(bufSize) < required) { + // Tightly packed, and summed over every face because a cube map query returns all + // six. Pack pixel-store state only ever grows this, so a request rejected here + // could not have fit under any packing. + const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat, + texturePixelDataType, texelSize) * + uploadTargets.size(); + + if (bufSize >= 0 && static_cast(bufSize) < required) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", caller, "Destination buffer is too small.")); + return false; + } + + if (pixelPackBufferObject) { + const SizeT bufferSize = pixelPackBufferObject->GetSize(); + const SizeT offset = reinterpret_cast(pixels); + if (offset > bufferSize || required > bufferSize - offset) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", caller, "Destination buffer is too small.")); + MakeUnique("MG_Impl/GLImpl", caller, + "Packing would write past the end of the pixel pack buffer.")); return false; } - - if (pixelPackBufferObject) { - const SizeT bufferSize = pixelPackBufferObject->GetSize(); - const SizeT offset = reinterpret_cast(pixels); - if (offset > bufferSize || required > bufferSize - offset) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidOperation, - MakeUnique("MG_Impl/GLImpl", caller, - "Packing would write past the end of the pixel pack buffer.")); - return false; - } - } } } diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 2182f8dc..de58330d 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -1317,6 +1317,56 @@ TEST_F(TextureTest, GetTextureImageReadsNamedObjectWithoutBinding) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +// GL 4.6 core 8.11.4 asks a readback for cube completeness and nothing else, so a mip chain whose +// levels BELOW the requested one were never defined is still readable at that level - which is +// exactly the shape ARB_clear_texture's conformance cases build (they define only the level they +// clear). The whole-chain completeness gate used to answer INVALID_OPERATION here. +TEST_F(TextureTest, GetTexImageReadsALevelWhoseLowerLevelsWereNeverDefined) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + const Uint8 pixels[] = { + 61, 62, 63, 64, + 71, 72, 73, 74, + }; + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 2, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + Uint8 output[sizeof(pixels)] = {}; + MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 2, GL_RGBA, GL_UNSIGNED_BYTE, output); + + EXPECT_EQ(std::memcmp(output, pixels, sizeof(pixels)), 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +// The other half of the same rule: loosening the chain-wide check must not let a level that holds +// no image at all through. Level 0 exists as a chain slot once level 2 is defined, but nothing ever +// gave it an image, so it stays INVALID_OPERATION - as does a level past the end of the chain and a +// texture that was never given any image whatsoever. +TEST_F(TextureTest, GetTexImageStillRejectsALevelThatHoldsNoImage) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + Uint8 output[4] = {}; + + // No image at all yet: the chain carries no levels. + MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, output); + ExpectSingleGlError(GL_INVALID_OPERATION); + + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Inside the chain, but never defined. + MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, output); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // Past the end of the chain. + MG_Impl::GLImpl::GetTexImage(GL_TEXTURE_2D, 3, GL_RGBA, GL_UNSIGNED_BYTE, output); + ExpectSingleGlError(GL_INVALID_OPERATION); +} + TEST_F(TextureTest, GetTextureSubImageReadsFullNamedLevelWithoutBinding) { GLuint texture = 0; GLuint boundTexture = 0; From 85cd6913b3981c54187c60b3a271ee02365f957b Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 10:38:52 -0400 Subject: [PATCH 08/11] [Fix] (DirectGLES): sync a texture whose mip chain only defines the upper levels --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 44 ++++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 15e6a30b..956a3377 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2427,6 +2427,26 @@ namespace MobileGL::MG_Backend::DirectGLES { return packedData.data(); } + // "Some level of this texture holds an image", which is all the sync gate below actually + // needs to know. Deliberately weaker than ITextureObject::IsComplete(): that predicate also + // answers whether the texture SAMPLES as complete, so it must keep rejecting a chain with + // undefined lower levels - but such a texture still has to be uploaded, or the level that + // IS defined never reaches the driver at all. + static Bool HasAnyDefinedMipmapLevel(const MG_State::GLState::ITextureObject* stateTextureObject) { + const auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject); + if (mipmapObject == nullptr) return false; + const auto levelCount = mipmapObject->GetMipmapLevelCount(); + for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) { + for (Uint level = 0; level < levelCount; ++level) { + const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level); + if (levelTexelSize.x() > 0 && levelTexelSize.y() > 0 && levelTexelSize.z() > 0) { + return true; + } + } + } + return false; + } + void BackendTextureObject::SyncMipmapsToBackend( const SharedPtr& stateTextureObject) { if (!stateTextureObject) { @@ -2474,8 +2494,14 @@ namespace MobileGL::MG_Backend::DirectGLES { // 3. Size changed // 4. Mipmap levels changed - if (!stateTextureObject->IsComplete()) { - MGLOG_D("Texture object with ID: %u is not complete, skipping sync.", + // IsComplete() is the sampling predicate, and it calls a chain whose lower levels are + // undefined incomplete - which is what a top-down build (upload level N, then level 0) + // and ARB_clear_texture's conformance cases both produce. Bailing out on that shape + // left the backend name with no levels whatsoever, so the level that WAS defined could + // never be sampled or read back. Sync whenever some level holds an image; the per-level + // loops below skip the degenerate ones individually. + if (!stateTextureObject->IsComplete() && !HasAnyDefinedMipmapLevel(stateTextureObject.get())) { + MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.", stateTextureObject->GetExternalIndex()); return; } @@ -2577,6 +2603,13 @@ namespace MobileGL::MG_Backend::DirectGLES { for (auto& uploadTarget : uploadTargets) { for (SizeT level = m_prevTextureInfo.mipmapLevels; level < mipmapCount; ++level) { auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level); + // A level the application never defined reads back as {0, 0, 0}; now that a + // sparse chain is synced rather than skipped whole, leave those undefined on + // the driver instead of giving the name a 0x0 image at that index. + if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 || levelTexelSize.z() <= 0) { + textureMipmapObject->MarkStorageDirty(uploadTarget, level, false); + continue; + } auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level); bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level); auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); @@ -2804,6 +2837,13 @@ namespace MobileGL::MG_Backend::DirectGLES { for (auto& uploadTarget : uploadTargets) { for (SizeT level = 0; level < mipmapCount; ++level) { auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, level); + // See the append-mips loop: an undefined level stays undefined on the + // driver rather than becoming a 0x0 image. + if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0 || + levelTexelSize.z() <= 0) { + textureMipmapObject->MarkStorageDirty(uploadTarget, level, false); + continue; + } auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level); bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level); auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget); From a8bebe1a3c2f00b369fef0ed5c73a1795d84b40c Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 10:44:52 -0400 Subject: [PATCH 09/11] [Fix, Test] (GLImpl, GLState): refuse a compressed texture in glClearTexImage/glClearTexSubImage --- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 108 +++++++++++++----- .../GLState/TextureState/MipmapStorage.cpp | 15 +++ .../GLState/TextureState/MipmapStorage.h | 13 +++ .../TextureState/MipmapUploadTargetArray.h | 10 ++ .../GLState/TextureState/TextureObject.cpp | 12 ++ .../GLState/TextureState/TextureObject.h | 12 ++ .../TextureState/TextureObject2DCube.cpp | 12 ++ .../TextureState/TextureObject2DCube.h | 4 + MobileGL/MG_Test/Texture/TextureTest.cpp | 34 ++++++ 9 files changed, 194 insertions(+), 26 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 3ffea8c3..c690d986 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -691,6 +691,34 @@ namespace MobileGL::MG_Impl::GLImpl { return textureObject; } + // Whether a raw internalformat enum names a compressed format - the question GL asks whenever an + // entry point is forbidden on a compressed image: glTexStorage3D on TEXTURE_3D (no + // block-compressed format is defined for a three-dimensional image, so it is INVALID_OPERATION + // rather than the INVALID_ENUM an unknown sized format gets - GL 4.6 core 8.19 / Khronos bug + // 11239, KHR-GLxx.texture_storage.compressed_data) and the clear-texture pair (8.19 again). + // Written against the enum ranges rather than a name list because the families are contiguous + // and MobileGL's own internal-format enum drops the ones it cannot carry, which would make this + // check silently narrower than the API surface. + static Bool IsCompressedGLInternalFormat(GLenum internalformat) { + switch (internalformat) { + case 0x8225: // GL_COMPRESSED_RED + case 0x8226: // GL_COMPRESSED_RG + case 0x84ED: // GL_COMPRESSED_RGB + case 0x84EE: // GL_COMPRESSED_RGBA + case 0x8C48: // GL_COMPRESSED_SRGB + case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA + return true; + default: + break; + } + return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT + (internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC + (internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC + (internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC + (internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR + (internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB + } + namespace { void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) { MG_State::pGLContext->RecordError( @@ -726,6 +754,21 @@ namespace MobileGL::MG_Impl::GLImpl { std::format("Texture level {} is not defined.", level)); return nullptr; } + // GL 4.6 core 8.19: a compressed internal format is INVALID_OPERATION for both clear + // entry points. Two tags to ask, because they answer different questions: the stored + // one covers a level glCompressedTexImage* or a SPECIFIC compressed internalformat + // defined, the requested one covers the six generic GL_COMPRESSED_* enums that MobileGL + // deliberately backs with uncompressed storage (see MipmapStorage) and that would + // otherwise look like an ordinary RGBA8 image by the time the clear runs. + const auto& uploadTargets = mipmapTexture->GetUploadTargets(); + if (!uploadTargets.empty() && + (mipmapTexture->GetMipmapCompressedFormat(uploadTargets[0], static_cast(level)) != GL_NONE || + mipmapTexture->GetMipmapRequestedCompressedFormat(uploadTargets[0], static_cast(level)) != + GL_NONE)) { + RecordClearTextureError(caller, ErrorCode::InvalidOperation, + "Compressed textures cannot be cleared."); + return nullptr; + } return mipmapTexture; } @@ -2210,6 +2253,13 @@ namespace MobileGL::MG_Impl::GLImpl { textureUploadTarget, level, static_cast(internalformat), nullptr, MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth})); } + // Also after AllocateStorage, which clears it. Records the generic GL_COMPRESSED_* + // enums too, which the tag above deliberately skips - glClearTexImage has to refuse + // them all (GL 4.6 core 8.19). + if (IsCompressedGLInternalFormat(static_cast(internalformat))) { + textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level, + static_cast(internalformat)); + } } if (!originalPixels) { @@ -2356,6 +2406,13 @@ namespace MobileGL::MG_Impl::GLImpl { textureUploadTarget, level, static_cast(internalformat), nullptr, MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1})); } + // Also after AllocateStorage, which clears it. Records the generic GL_COMPRESSED_* + // enums too, which the tag above deliberately skips - glClearTexImage has to refuse + // them all (GL 4.6 core 8.19). + if (IsCompressedGLInternalFormat(static_cast(internalformat))) { + textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level, + static_cast(internalformat)); + } } if (!originalPixels) { @@ -2444,6 +2501,13 @@ namespace MobileGL::MG_Impl::GLImpl { if (!isProxy) { DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes}); + // After AllocateStorage, which clears the tag. No block-compressed format has a 1D + // layout, so only the specific-format tag the 2D/3D paths record is skipped here - the + // request itself still has to be remembered for glClearTexImage (GL 4.6 core 8.19). + if (IsCompressedGLInternalFormat(static_cast(internalFormat))) { + textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level, + static_cast(internalFormat)); + } } if (!originalPixels) { @@ -4542,6 +4606,12 @@ namespace MobileGL::MG_Impl::GLImpl { const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, 1, 1); textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize}); textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false); + if (IsCompressedGLInternalFormat(internalformat)) { + // After AllocateStorage, which clears the tag. See TexImage1D_State: no compressed + // format has a 1D block layout, but glClearTexImage still has to refuse the request. + textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, + static_cast(level), internalformat); + } } // Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a // longer pre-existing chain has to be dropped explicitly. @@ -4610,6 +4680,12 @@ namespace MobileGL::MG_Impl::GLImpl { MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {levelWidth, levelHeight, 1})); } + if (IsCompressedGLInternalFormat(internalformat)) { + // Also after AllocateStorage. The generic enums land here and nowhere above, + // and glClearTexImage has to refuse them too (GL 4.6 core 8.19). + textureMipmapObject->SetMipmapRequestedCompressedFormat(uploadTarget, + static_cast(level), internalformat); + } } // See TextureStorage1D. textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast(levels)); @@ -4617,32 +4693,6 @@ namespace MobileGL::MG_Impl::GLImpl { textureObject->SetImmutableLevels(static_cast(levels)); } - // No block-compressed format is defined for a three-dimensional image, so glTexStorage3D on - // TEXTURE_3D must reject one - and with INVALID_OPERATION, not the INVALID_ENUM an unknown - // sized format gets (GL 4.6 core 8.19 / Khronos bug 11239, KHR-GLxx.texture_storage - // .compressed_data). Written against the enum ranges rather than a name list because the - // families are contiguous and MobileGL's own internal-format enum drops the ones it cannot - // carry, which would make this check silently narrower than the API surface. - static Bool IsCompressedGLInternalFormat(GLenum internalformat) { - switch (internalformat) { - case 0x8225: // GL_COMPRESSED_RED - case 0x8226: // GL_COMPRESSED_RG - case 0x84ED: // GL_COMPRESSED_RGB - case 0x84EE: // GL_COMPRESSED_RGBA - case 0x8C48: // GL_COMPRESSED_SRGB - case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA - return true; - default: - break; - } - return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT - (internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC - (internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC - (internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC - (internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR - (internalformat >= 0x93D0 && internalformat <= 0x93DD); // ASTC sRGB - } - void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth) { auto textureObject = GetTextureObjectByName(texture, __func__); @@ -4705,6 +4755,12 @@ namespace MobileGL::MG_Impl::GLImpl { MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {levelWidth, levelHeight, levelDepth})); } + if (IsCompressedGLInternalFormat(internalformat)) { + // Also after AllocateStorage. The generic enums land here and nowhere above, + // and glClearTexImage has to refuse them too (GL 4.6 core 8.19). + textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, + static_cast(level), internalformat); + } } // See TextureStorage1D. textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast(levels)); diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp index 0497edaa..58affe99 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp @@ -48,6 +48,7 @@ namespace MobileGL { m_dirtyRects.resize(requiredLevelCount); m_compressedData.resize(requiredLevelCount); m_compressedFormats.resize(requiredLevelCount, GL_NONE); + m_requestedCompressedFormats.resize(requiredLevelCount, GL_NONE); } m_texelSizes[level] = input.texelSize; @@ -79,6 +80,9 @@ namespace MobileGL { m_compressedFormats[level] = GL_NONE; m_compressedData[level].clear(); m_compressedData[level].shrink_to_fit(); + // Same story for the requested-format tag: a respecified level is whatever this + // call asked for, and the compressed entry points re-arm it right afterwards. + m_requestedCompressedFormats[level] = GL_NONE; } void MipmapStorage::SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size) { @@ -110,6 +114,16 @@ namespace MobileGL { return m_compressedData[level].data(); } + void MipmapStorage::SetRequestedCompressedFormat(Uint level, GLenum internalFormat) { + if (level >= m_requestedCompressedFormats.size()) return; + m_requestedCompressedFormats[level] = internalFormat; + } + + GLenum MipmapStorage::GetRequestedCompressedFormat(Uint level) const { + if (level >= m_requestedCompressedFormats.size()) return GL_NONE; + return m_requestedCompressedFormats[level]; + } + void MipmapStorage::TruncateToLevelCount(SizeT levelCount) { if (levelCount >= m_data.size()) return; @@ -120,6 +134,7 @@ namespace MobileGL { m_dirtyRects.resize(levelCount); m_compressedData.resize(levelCount); m_compressedFormats.resize(levelCount); + m_requestedCompressedFormats.resize(levelCount); } void MipmapStorage::UpdateSubData(Uint level, DataPtr input) { diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h index fd6b8302..fe632db4 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.h @@ -96,6 +96,18 @@ namespace MobileGL { SizeT GetCompressedByteSize(Uint level) const; const void* MapCompressedData(Uint level) const; + // The compressed internalformat the application ASKED for, which is not the same + // question as the one above: the six generic GL_COMPRESSED_* enums let the + // implementation choose, MobileGL chooses uncompressed storage, and the level is + // deliberately left untagged so GL_TEXTURE_COMPRESSED keeps answering false and + // glGetCompressedTexImage is not handed a blob nothing ever compressed. The entry + // points that must refuse a compressed image outright (glClearTexImage / + // glClearTexSubImage, GL 4.6 core 8.19) still need to know, so the request is + // recorded separately. Set right after AllocateLevel, which clears it. + void SetRequestedCompressedFormat(Uint level, GLenum internalFormat); + // GL_NONE when the level was not requested with a compressed internalformat. + GLenum GetRequestedCompressedFormat(Uint level) const; + protected: // Insert one clamped, non-empty write box, keeping the list disjoint // and bounded (see kMaxDirtyRects). @@ -115,6 +127,7 @@ namespace MobileGL { Vector> m_dirtyRects; Vector> m_compressedData; Vector m_compressedFormats; + Vector m_requestedCompressedFormats; }; } // namespace GLState } // namespace MG_State diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h b/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h index 1f6980d4..5142422c 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h +++ b/MobileGL/MG_State/GLState/TextureState/MipmapUploadTargetArray.h @@ -111,6 +111,16 @@ namespace MobileGL { return m_storage[targetIndex].MapCompressedData(level); } + void SetRequestedCompressedFormat(Uint targetIndex, Uint level, GLenum internalFormat) { + MOBILEGL_ASSERT(targetIndex < TargetCount, "SetRequestedCompressedFormat: target invalid"); + m_storage[targetIndex].SetRequestedCompressedFormat(level, internalFormat); + } + + GLenum GetRequestedCompressedFormat(Uint targetIndex, Uint level) const { + MOBILEGL_ASSERT(targetIndex < TargetCount, "GetRequestedCompressedFormat: target invalid"); + return m_storage[targetIndex].GetRequestedCompressedFormat(level); + } + protected: Array m_storage; }; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index b5bfa3f2..86c1a4a2 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -373,6 +373,18 @@ namespace MobileGL { return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); } + void TextureObjectWithOneMipmap::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, + Uint mipmapLevel, GLenum internalFormat) { + m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, + internalFormat); + } + + GLenum TextureObjectWithOneMipmap::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const { + return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), + mipmapLevel); + } + IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const { if (m_textureStorage.GetLevelCount() == 0) { return {0, 0, 0}; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index 9e346014..742e5e78 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -220,6 +220,15 @@ namespace MobileGL::MG_State::GLState { virtual GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; virtual SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; virtual const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0; + + // The compressed internalformat the level was REQUESTED with, recorded even when MobileGL + // answered it with uncompressed storage (the six generic GL_COMPRESSED_* enums) - see + // MipmapStorage. Only the entry points GL forbids on a compressed image read it. + virtual void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel, + GLenum internalFormat) = 0; + // GL_NONE when the level was not requested with a compressed internalformat. + virtual GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const = 0; }; // Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the @@ -286,6 +295,9 @@ namespace MobileGL::MG_State::GLState { GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; + void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel, + GLenum internalFormat) override; + GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; IntVec3 GetBaseSize() const override; Bool IsComplete() const override; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp index a1997e4c..765477c5 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp @@ -96,6 +96,18 @@ namespace MobileGL { return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel); } + void TextureObject2DCube::SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, + Uint mipmapLevel, GLenum internalFormat) { + m_textureStorage.SetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, + internalFormat); + } + + GLenum TextureObject2DCube::GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const { + return m_textureStorage.GetRequestedCompressedFormat(GetIndexOfTextureUploadTarget(uploadTarget), + mipmapLevel); + } + Uint TextureObject2DCube::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const { MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target && target <= TextureUploadTarget::CubeMapNegativeZ, diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h index 547ea0e9..d705db02 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.h @@ -39,6 +39,10 @@ namespace MobileGL { SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override; + void SetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel, + GLenum internalFormat) override; + GLenum GetMipmapRequestedCompressedFormat(TextureUploadTarget uploadTarget, + Uint mipmapLevel) const override; IntVec3 GetBaseSize() const override; Bool IsComplete() const override; diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index de58330d..b663b3f7 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -377,6 +377,40 @@ TEST_F(TextureTest, ClearTexImageErrorContracts) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast(GL_INVALID_ENUM)); } +// GL 4.6 core 8.19: a compressed internal format is INVALID_OPERATION for both clear entry points. +// The generic GL_COMPRESSED_* enums are the half that needs its own tag - MobileGL answers them +// with uncompressed storage on purpose, so by the time the clear runs the level looks like any +// other RGBA8 image unless the REQUEST was recorded alongside it. +TEST_F(TextureTest, ClearTexImageRejectsCompressedTextures) { + GLuint genericTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &genericTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, genericTexture); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::ClearTexImage(genericTexture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + ExpectSingleGlError(GL_INVALID_OPERATION); + MG_Impl::GLImpl::ClearTexSubImage(genericTexture, 0, 0, 0, 0, 4, 4, 1, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // A specific compressed internalformat is refused through the tag the level already carried... + GLuint specificTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &specificTexture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, specificTexture); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, + nullptr); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::ClearTexImage(specificTexture, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // ...and respecifying the level with an uncompressed format makes it clearable again, because + // AllocateStorage clears both tags. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_R8, 8, 8, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::ClearTexImage(specificTexture, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + // GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT is float state that must answer every numeric query: GetFloatv // is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default. TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) { From 042c61fb75368e6aed4eeb201605657beeab1fed Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 10:49:57 -0400 Subject: [PATCH 10/11] [Fix, Test] (TextureUtil, GLImpl): accept GL_STENCIL_INDEX as a stencil-only texture internal format --- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 4 +-- .../MG_Impl/GLImpl/Texture/Validators.cpp | 10 ++++--- MobileGL/MG_Test/Texture/TextureTest.cpp | 26 +++++++++++++++++++ .../GLToMG/TextureEnumConverter.cpp | 6 +++++ .../MGToMG/TextureEnumConverter.cpp | 3 +++ .../MG_Util/Texture/PixelStoreProcessor.cpp | 17 +++++++++++- 6 files changed, 60 insertions(+), 6 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index c690d986..9009930b 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -4303,8 +4303,8 @@ namespace MobileGL::MG_Impl::GLImpl { } // Shared format/type/internal-format matrix (packed-type pairing, depth-vs-color mismatch, - // integer-ness). Also rejects STENCIL_INDEX readback, which needs GL_ARB_texture_stencil8 - // (not advertised by MobileGL). + // integer-ness). Also rejects a STENCIL_INDEX readback of anything but stencil-only + // storage, which is the only pairing GL 4.4 / ARB_texture_stencil8 ever made legal. if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput( textureInputFormat, textureObject->GetFormat(), texturePixelDataType)) { return false; diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp index 1b1dd6f2..ec423efb 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp @@ -313,9 +313,13 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { return false; } - // TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4). - if (format == TextureInputFormat::StencilIndex) { - return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format"); + // The stencil-only transfer path arrived with GL 4.4 / ARB_texture_stencil8, and only ever + // pairs with stencil-only storage: against a depth, depth-stencil or colour internal format + // STENCIL_INDEX keeps the pre-4.4 answer (GL CTS packed_pixels feeds exactly that pairing + // and expects INVALID_OPERATION). + if (format == TextureInputFormat::StencilIndex && + internalFormat != TextureInternalFormat::StencilIndex8) { + return recordInvalidOperation("STENCIL_INDEX requires a stencil-only internal format"); } if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) { diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index b663b3f7..953a8929 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -1057,6 +1057,32 @@ TEST_F(TextureTest, TexImage2DAcceptsSpecCompliantFormatCombinations) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +// GL_STENCIL_INDEX is the unsized base format for stencil-only storage, and refusing it as an +// internal format killed the ARB_clear_texture stencil case in its own setup - before it could +// reach the calls it actually tests. The stencil-only transfer format stays paired with +// stencil-only storage in both directions, which is what keeps those clears erroring. +TEST_F(TextureTest, StencilIndexIsATextureInternalFormatPairedOnlyWithStencilStorage) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_STENCIL_INDEX, 4, 4, 0, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, + nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + ASSERT_NE(textureObject, nullptr); + EXPECT_EQ(textureObject->GetFormat(), TextureInternalFormat::StencilIndex8); + + // A colour transfer format against stencil storage is still INVALID_OPERATION, so the clear + // the conformance case makes next fails the way it is supposed to. + MG_Impl::GLImpl::ClearTexImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + ExpectSingleGlError(GL_INVALID_OPERATION); + + // ...and the other direction: GL_STENCIL_INDEX against colour storage stays illegal. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, nullptr); + ExpectSingleGlError(GL_INVALID_OPERATION); +} + // Desktop GL table 3.3 lists GREEN and BLUE as TexImage client formats (GL CTS packed_pixels // rgba8_format_green/blue upload with them and verify the readback): the single input component // feeds the named channel, the other color channels default to 0 and alpha to 1. diff --git a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp index 332a3cd4..8d6fb042 100644 --- a/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/GLToMG/TextureEnumConverter.cpp @@ -253,6 +253,12 @@ namespace MobileGL { return TextureInternalFormat::Depth32FStencil8; case GL_STENCIL_INDEX8: return TextureInternalFormat::StencilIndex8; + // The unsized stencil base format resolves to the only stencil storage there is, the + // same way the unsized colour and depth base formats below resolve to theirs. Returning + // Unknown made glTexImage2D(GL_STENCIL_INDEX) an error, which killed the negative + // clear-texture cases in their own setup before they could reach the call they test. + case GL_STENCIL_INDEX: + return TextureInternalFormat::StencilIndex8; case GL_DEPTH_COMPONENT: return TextureInternalFormat::DepthComponent; case GL_DEPTH_STENCIL: diff --git a/MobileGL/MG_Util/Converters/MGToMG/TextureEnumConverter.cpp b/MobileGL/MG_Util/Converters/MGToMG/TextureEnumConverter.cpp index 4564d155..f75bfd22 100644 --- a/MobileGL/MG_Util/Converters/MGToMG/TextureEnumConverter.cpp +++ b/MobileGL/MG_Util/Converters/MGToMG/TextureEnumConverter.cpp @@ -124,6 +124,9 @@ namespace MobileGL { case TextureInternalFormat::DepthComponent32F: case TextureInternalFormat::Depth24Stencil8: case TextureInternalFormat::Depth32FStencil8: + // Already sized: both GL_STENCIL_INDEX8 and the unsized GL_STENCIL_INDEX resolve here, + // and there is only one stencil storage to infer. + case TextureInternalFormat::StencilIndex8: return internalformat; // probably we should assume unorm here? case TextureInternalFormat::RGBA: { diff --git a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp index 099f49b3..62c633ba 100644 --- a/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp +++ b/MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp @@ -138,6 +138,11 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { case TextureInternalFormat::DepthComponent32F: out = {1, ShadowComponent::Float32, false}; return true; + // Stencil is the one single-channel INTEGER shadow that is not a colour format: eight + // bits, held as an unsigned index rather than a normalized value. + case TextureInternalFormat::StencilIndex8: + out = {1, ShadowComponent::UInt8, true}; + return true; case TextureInternalFormat::R8: case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true; @@ -336,8 +341,13 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { case TextureInputFormat::BGRAInteger: out = {{2, 1, 0, 3}, 4, true}; return true; // A depth value converts like a single normalized/float channel. case TextureInputFormat::DepthComponent: out = {{0, -1, -1, -1}, 1, false}; return true; + // A stencil index is a single INTEGER channel (GL 4.6 core 8.4.4.3). Without this the + // upload fell to the raw-memcpy branch, which copies the client element width into the + // one-byte STENCIL_INDEX8 shadow verbatim - right for GL_UNSIGNED_BYTE and wrong for + // every wider type. The state layer keeps this paired with stencil-only storage. + case TextureInputFormat::StencilIndex: out = {{0, -1, -1, -1}, 1, true}; return true; default: - return false; // stencil / packed depth-stencil / unknown + return false; // packed depth-stencil / unknown } } @@ -998,6 +1008,11 @@ namespace MobileGL::MG_Util::PixelStoreProcessor { const void* inputPixel, Vector& outputPixel) { outputPixel.clear(); + // A stencil index became a transferable format when STENCIL_INDEX8 texture storage did (see + // GetUnpackChannelMapping), but this helper serves glClearBufferData, whose internal formats + // are all colour (GL 4.6 core table 8.20): a stencil pattern would otherwise pass the size + // check and land silently in an equally-sized colour store. + if (textureInputFormat == TextureInputFormat::StencilIndex) return false; if (inputPixel == nullptr || !IsValidUnpackPixelPair(textureInputFormat, inputDataType)) return false; PixelStoreParameters params{}; From 0995dfea352f01ef95279375b996f3a6d3cc04c5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 11:06:48 -0400 Subject: [PATCH 11/11] [Test] (MG_IntegrationTest): force the iterationRP repairs on when the pinned ICD is lavapipe --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index a36a0c2c..4b8b5c61 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -247,6 +247,19 @@ endif() set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV}) if (MOBILEGL_ITEST_VK_ICD) list(APPEND MGL_ITEST_VULKAN_ENV "VK_ICD_FILENAMES=${MOBILEGL_ITEST_VK_ICD}") + # The three iterationRP repairs are tri-state quirks that default to device + # auto-detection, and lavapipe is not on any auto list - so on lavapipe the + # iterationRP scenarios run unrepaired and Program 203 misses its golden + # output. CI's integration-gpu job exports these three by hand; pinning them + # to the ICD instead means a local `ctest -L integration-gpu` measures the + # same thing the gate does, with no environment to remember. + if (MOBILEGL_ITEST_VK_ICD MATCHES "lvp_icd|lavapipe") + message(STATUS "Integration tests: lavapipe ICD - forcing the iterationRP repairs on") + list(APPEND MGL_ITEST_VULKAN_ENV + "MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1" + "MOBILEGL_DERIVE_NUM_SUBGROUPS=1" + "MOBILEGL_ITERATIONRP_FIX_BARRIER=1") + endif() endif() # The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests