mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 06:38:31 +09:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37b20f2fca | ||
|
|
ee908aab46 | ||
|
|
a4ccf3a837 |
@@ -420,9 +420,6 @@ jobs:
|
|||||||
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
||||||
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
|
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
|
||||||
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
|
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
|
||||||
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
|
||||||
MOBILEGL_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
|
||||||
MOBILEGL_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
|
||||||
run: |
|
run: |
|
||||||
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
||||||
test -f "${apk_file}"
|
test -f "${apk_file}"
|
||||||
|
|||||||
@@ -265,9 +265,6 @@ jobs:
|
|||||||
# crash stack without burning a CI round on an in-workflow debugger.
|
# crash stack without burning a CI round on an in-workflow debugger.
|
||||||
env:
|
env:
|
||||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||||
MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: "1"
|
|
||||||
MOBILEGL_DERIVE_NUM_SUBGROUPS: "1"
|
|
||||||
MOBILEGL_ITERATIONRP_FIX_BARRIER: "1"
|
|
||||||
run: |
|
run: |
|
||||||
ulimit -c unlimited
|
ulimit -c unlimited
|
||||||
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
|
||||||
@@ -642,12 +639,6 @@ jobs:
|
|||||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
|
||||||
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
|
||||||
fi
|
fi
|
||||||
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
|
|
||||||
&& [ '${{ matrix.case }}' = 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' ]; then
|
|
||||||
export MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1
|
|
||||||
export MOBILEGL_DERIVE_NUM_SUBGROUPS=1
|
|
||||||
export MOBILEGL_ITERATIONRP_FIX_BARRIER=1
|
|
||||||
fi
|
|
||||||
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
|
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
|
||||||
# runner has, so force it on for the OIT case it exists to fix. ForceOn
|
# runner has, so force it on for the OIT case it exists to fix. ForceOn
|
||||||
# bypasses only the vendor gate, so this exercises the real strip on
|
# bypasses only the vendor gate, so this exercises the real strip on
|
||||||
|
|||||||
@@ -27,4 +27,3 @@ MobileGL/MG*/cmake-build*
|
|||||||
tools/trace_replay/work/
|
tools/trace_replay/work/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
/.gradle
|
|
||||||
|
|||||||
+1
-11
@@ -279,20 +279,17 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
|
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
|
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp
|
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||||
@@ -465,7 +462,7 @@ set(MOBILEGL_INCLUDE_DIR
|
|||||||
# Header-only submodule: no add_subdirectory, no link target. Only
|
# Header-only submodule: no add_subdirectory, no link target. Only
|
||||||
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
|
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
|
||||||
# pimpl so no consumer target needs this path.
|
# pimpl so no consumer target needs this path.
|
||||||
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
|
${CMAKE_SOURCE_DIR}/3rdparty/asio/asio/include
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||||
@@ -677,10 +674,3 @@ if (NOT ANDROID)
|
|||||||
add_subdirectory(tools/trace_replay)
|
add_subdirectory(tools/trace_replay)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The integration binary is also useful as a standalone adb-shell executable.
|
|
||||||
# Android cannot use the desktop-only MobileGL_s target, so its CMake module
|
|
||||||
# links libMobileGL.so and creates an AImageReader-backed window instead.
|
|
||||||
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
|
|
||||||
add_subdirectory(MobileGL/MG_IntegrationTest)
|
|
||||||
endif()
|
|
||||||
|
|||||||
@@ -100,10 +100,6 @@ namespace MobileGL::MG_Config {
|
|||||||
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
|
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
|
||||||
// verbatim.
|
// verbatim.
|
||||||
QuirkOverride FixIterationRPSubgroupScratch = QuirkOverride::Auto;
|
QuirkOverride FixIterationRPSubgroupScratch = QuirkOverride::Auto;
|
||||||
// MOBILEGL_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
|
|
||||||
// rendezvous between its two reductions over prefixSumCache. Off by default and
|
|
||||||
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
|
|
||||||
Bool IterationRPFixBarrier = false;
|
|
||||||
// MOBILEGL_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
|
// MOBILEGL_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
|
||||||
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
|
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
|
||||||
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
|
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
|
||||||
|
|||||||
@@ -171,7 +171,6 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
|
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
|
||||||
features.FixIterationRPSubgroupScratch =
|
features.FixIterationRPSubgroupScratch =
|
||||||
QueryEnvQuirkOverride("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
QueryEnvQuirkOverride("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||||
features.IterationRPFixBarrier = QueryEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
|
|
||||||
features.DeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_DERIVE_NUM_SUBGROUPS");
|
features.DeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_DERIVE_NUM_SUBGROUPS");
|
||||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ namespace MobileGL {
|
|||||||
namespace MG_State::GLState {
|
namespace MG_State::GLState {
|
||||||
class FramebufferObject;
|
class FramebufferObject;
|
||||||
class ITextureObject;
|
class ITextureObject;
|
||||||
class RenderbufferObject;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class BackendType {
|
enum class BackendType {
|
||||||
@@ -25,19 +24,6 @@ namespace MobileGL {
|
|||||||
};
|
};
|
||||||
|
|
||||||
namespace MG_Backend {
|
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<MG_State::GLState::ITextureObject> Texture;
|
|
||||||
SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;
|
|
||||||
|
|
||||||
Bool IsRenderbuffer() const { return Renderbuffer != nullptr; }
|
|
||||||
Bool Exists() const { return Texture != nullptr || Renderbuffer != nullptr; }
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class FormatCapability : Uint64 {
|
enum class FormatCapability : Uint64 {
|
||||||
Creatable = 1ull << 0,
|
Creatable = 1ull << 0,
|
||||||
|
|
||||||
@@ -174,9 +160,9 @@ namespace MobileGL {
|
|||||||
GLsizei height, GLint border);
|
GLsizei height, GLint border);
|
||||||
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||||
GLsizei width, GLsizei height);
|
GLsizei width, GLsizei height);
|
||||||
void (*CopyImageSubData)(const CopyImageEndpoint& src,
|
void (*CopyImageSubData)(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const CopyImageEndpoint& dst,
|
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||||
void (*GenerateMipmap)(GLenum target);
|
void (*GenerateMipmap)(GLenum target);
|
||||||
@@ -250,14 +236,6 @@ namespace MobileGL {
|
|||||||
// (optional; null = frontend falls back to CPU accounting).
|
// (optional; null = frontend falls back to CPU accounting).
|
||||||
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
|
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
|
||||||
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
|
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
|
||||||
// Whether GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN should be answered from the
|
|
||||||
// frontend's own accounting wherever that accounting is exact - a capture with no
|
|
||||||
// geometry stage - instead of from the query above. Set by DirectGLES, whose result
|
|
||||||
// is whatever the ES driver's PRIMITIVES_WRITTEN counter says: Adreno reports twice
|
|
||||||
// the written count for a vertex-only capture that follows a large render pass,
|
|
||||||
// where the desktop-exact answer is the one the frontend already computed. Defaults
|
|
||||||
// to false, so a backend that never sets it keeps using its GPU result.
|
|
||||||
Bool PrefersCpuXfbPrimitiveAccounting = false;
|
|
||||||
// Transform feedback capture spans, for backends whose own GL/ES driver
|
// Transform feedback capture spans, for backends whose own GL/ES driver
|
||||||
// performs the capture (DirectGLES). Both optional; null means the backend
|
// performs the capture (DirectGLES). Both optional; null means the backend
|
||||||
// drives capture from its draw recording instead (DirectVulkan). End is
|
// drives capture from its draw recording instead (DirectVulkan). End is
|
||||||
@@ -340,22 +318,6 @@ namespace MobileGL {
|
|||||||
Int MaxVertexAttribs = 16;
|
Int MaxVertexAttribs = 16;
|
||||||
Int MaxComputeShaderStorageBlocks = 8;
|
Int MaxComputeShaderStorageBlocks = 8;
|
||||||
Int MaxCombinedShaderStorageBlocks = 32;
|
Int MaxCombinedShaderStorageBlocks = 32;
|
||||||
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Zero is a legal answer for the four
|
|
||||||
// non-compute, non-fragment stages and these defaults are the spec minimums, not
|
|
||||||
// placeholders: GL 4.6 table 23.64 and ES 3.2 table 21.44 both set the minimum for
|
|
||||||
// vertex, tessellation control, tessellation evaluation and geometry at 0, and only
|
|
||||||
// fragment (8 in GL, 4 in ES) and compute are guaranteed to have any. Every real ARM
|
|
||||||
// GLES driver takes that allowance - a Mali-G925 reports 0 for all four - so a
|
|
||||||
// backend that cannot honour a graphics-stage storage block MUST report 0 here
|
|
||||||
// rather than a hopeful number. Advertising a non-zero count the driver will refuse
|
|
||||||
// does not make the block work; it only moves the failure from an honest
|
|
||||||
// "unsupported" at query time to a backend link error the frontend never surfaces,
|
|
||||||
// after which every draw with that program silently renders nothing.
|
|
||||||
Int MaxVertexShaderStorageBlocks = 0;
|
|
||||||
Int MaxTessControlShaderStorageBlocks = 0;
|
|
||||||
Int MaxTessEvaluationShaderStorageBlocks = 0;
|
|
||||||
Int MaxGeometryShaderStorageBlocks = 0;
|
|
||||||
Int MaxFragmentShaderStorageBlocks = 8;
|
|
||||||
Int MaxComputeUniformBlocks = 12;
|
Int MaxComputeUniformBlocks = 12;
|
||||||
Int MaxComputeWorkGroupInvocations = 128;
|
Int MaxComputeWorkGroupInvocations = 128;
|
||||||
Int MaxShaderStorageBufferBindings = 8;
|
Int MaxShaderStorageBufferBindings = 8;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
#include "BackendObject_DirectGLES.h"
|
#include "BackendObject_DirectGLES.h"
|
||||||
#include "MG_Backend/BackendObject.h"
|
#include "MG_Backend/BackendObject.h"
|
||||||
#include "MG_Backend/BackendObjects.h"
|
|
||||||
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
||||||
#include <MG_Backend/DirectGLES/Managers.h>
|
#include <MG_Backend/DirectGLES/Managers.h>
|
||||||
#include <MG_Backend/DirectGLES/Utils.h>
|
#include <MG_Backend/DirectGLES/Utils.h>
|
||||||
@@ -407,12 +406,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return complete;
|
return complete;
|
||||||
}
|
}
|
||||||
|
|
||||||
// `samples` only reaches the multisample targets; every other target ignores it. The
|
|
||||||
// descending sample walk (ProbeTextureSampleCounts) reuses this whole routine rather than
|
|
||||||
// repeating the gen/bind/completeness/delete dance.
|
|
||||||
Bool ProbeTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, GLenum internalFormat,
|
Bool ProbeTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, GLenum internalFormat,
|
||||||
GLenum imageFormat, GLenum imageType, TextureInternalFormat logicalFormat,
|
GLenum imageFormat, GLenum imageType, TextureInternalFormat logicalFormat,
|
||||||
Bool* outRenderable, Int samples = 1) {
|
Bool* outRenderable) {
|
||||||
if (!IsGLESProbeTextureTarget(target) || !gl.glGenTextures || !gl.glBindTexture || !gl.glDeleteTextures) {
|
if (!IsGLESProbeTextureTarget(target) || !gl.glGenTextures || !gl.glBindTexture || !gl.glDeleteTextures) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -432,11 +428,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
const Bool isMultisample = IsGLESProbeMultisampleTarget(target);
|
const Bool isMultisample = IsGLESProbeMultisampleTarget(target);
|
||||||
if (isMultisample) {
|
if (isMultisample) {
|
||||||
const auto probeSamples = static_cast<GLsizei>(std::max(samples, 1));
|
|
||||||
if (target == TextureTarget::Texture2DMultisample && gl.glTexStorage2DMultisample) {
|
if (target == TextureTarget::Texture2DMultisample && gl.glTexStorage2DMultisample) {
|
||||||
gl.glTexStorage2DMultisample(glTarget, probeSamples, internalFormat, 1, 1, GL_TRUE);
|
gl.glTexStorage2DMultisample(glTarget, 1, internalFormat, 1, 1, GL_TRUE);
|
||||||
} else if (target == TextureTarget::Texture2DMultisampleArray && gl.glTexStorage3DMultisample) {
|
} else if (target == TextureTarget::Texture2DMultisampleArray && gl.glTexStorage3DMultisample) {
|
||||||
gl.glTexStorage3DMultisample(glTarget, probeSamples, internalFormat, 1, 1, 1, GL_TRUE);
|
gl.glTexStorage3DMultisample(glTarget, 1, internalFormat, 1, 1, 1, GL_TRUE);
|
||||||
} else {
|
} else {
|
||||||
gl.glBindTexture(glTarget, static_cast<GLuint>(previousBinding));
|
gl.glBindTexture(glTarget, static_cast<GLuint>(previousBinding));
|
||||||
gl.glDeleteTextures(1, &texture);
|
gl.glDeleteTextures(1, &texture);
|
||||||
@@ -532,29 +527,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return sampleCounts;
|
return sampleCounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The multisample TEXTURE twin of ProbeRenderbufferSampleCounts. It used to be a
|
|
||||||
// hardcoded {1}, which made glGetInternalformativ(GL_SAMPLES) claim a one-sample maximum
|
|
||||||
// for every format on the multisample targets even where glTexImage2DMultisample happily
|
|
||||||
// accepts four - GL 4.6 core 8.8 makes that query the definition of the maximum, so the
|
|
||||||
// two answers cannot both be right. Completeness is required at every count, exactly as
|
|
||||||
// the renderbuffer walk requires it; the caller only reaches here once the one-sample
|
|
||||||
// probe has already succeeded, so 1 terminates the list without being re-probed.
|
|
||||||
Vector<Int> ProbeTextureSampleCounts(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
|
|
||||||
GLenum internalFormat, GLenum imageFormat, GLenum imageType,
|
|
||||||
TextureInternalFormat logicalFormat, Int maxSamples) {
|
|
||||||
Vector<Int> sampleCounts;
|
|
||||||
for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) {
|
|
||||||
Bool renderable = false;
|
|
||||||
const Bool created = ProbeTexture(gl, target, internalFormat, imageFormat, imageType, logicalFormat,
|
|
||||||
&renderable, samples);
|
|
||||||
if (created && renderable) {
|
|
||||||
sampleCounts.push_back(samples);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sampleCounts.push_back(1);
|
|
||||||
return sampleCounts;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PopulateFormatCapabilitiesImpl(const MG_External::GLESFunctionsTable& gl,
|
void PopulateFormatCapabilitiesImpl(const MG_External::GLESFunctionsTable& gl,
|
||||||
const MG_External::GLESCapabilities& capabilities,
|
const MG_External::GLESCapabilities& capabilities,
|
||||||
FormatCapabilityCache& cache) {
|
FormatCapabilityCache& cache) {
|
||||||
@@ -655,11 +627,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
AddFullFormatCaps(cache, targetIndex, formatIndex,
|
AddFullFormatCaps(cache, targetIndex, formatIndex,
|
||||||
BuildTextureCapsFromProbe(logicalFormat, target, nativeRenderable));
|
BuildTextureCapsFromProbe(logicalFormat, target, nativeRenderable));
|
||||||
if (IsGLESProbeMultisampleTarget(target)) {
|
if (IsGLESProbeMultisampleTarget(target)) {
|
||||||
const Int maxSamples =
|
cache.SampleCounts[targetIndex][formatIndex] = {1};
|
||||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat);
|
|
||||||
cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
|
|
||||||
gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
|
|
||||||
nativeInfo.ImageType, logicalFormat, maxSamples);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
shouldProbeFallback = !nativeCreated || !nativeRenderable;
|
shouldProbeFallback = !nativeCreated || !nativeRenderable;
|
||||||
@@ -677,11 +645,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo);
|
LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo);
|
||||||
}
|
}
|
||||||
if (IsGLESProbeMultisampleTarget(target)) {
|
if (IsGLESProbeMultisampleTarget(target)) {
|
||||||
const Int maxSamples =
|
cache.SampleCounts[targetIndex][formatIndex] = {1};
|
||||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
|
|
||||||
cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
|
|
||||||
gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
|
|
||||||
fallbackInfo.ImageType, logicalFormat, maxSamples);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -783,29 +747,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
PopulateFormatCapabilitiesImpl(gl, capabilities, cache);
|
PopulateFormatCapabilitiesImpl(gl, capabilities, cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
|
|
||||||
Int samples) {
|
|
||||||
if (samples <= 1) {
|
|
||||||
return samples;
|
|
||||||
}
|
|
||||||
|
|
||||||
Int maxSamples = 0;
|
|
||||||
const SizeT formatIndex = static_cast<SizeT>(logicalFormat);
|
|
||||||
if (pActiveBackendObject && targetIndex < kFormatCapabilityTargetCount &&
|
|
||||||
formatIndex < kFormatCapabilityFormatCount) {
|
|
||||||
// Descending, so the head is the largest count this device actually allocated.
|
|
||||||
const Vector<Int>& probedCounts =
|
|
||||||
pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
|
|
||||||
if (!probedCounts.empty()) {
|
|
||||||
maxSamples = probedCounts.front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (maxSamples <= 0) {
|
|
||||||
maxSamples = GetGLESFormatMaxSamples(g_GLESCapabilities, logicalFormat, imageFormat);
|
|
||||||
}
|
|
||||||
return std::min(samples, std::max(maxSamples, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
BackendObject_DirectGLES::~BackendObject_DirectGLES() {
|
BackendObject_DirectGLES::~BackendObject_DirectGLES() {
|
||||||
DestroyEGLContext();
|
DestroyEGLContext();
|
||||||
}
|
}
|
||||||
@@ -1166,12 +1107,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// geometry shader's amplification.
|
// geometry shader's amplification.
|
||||||
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
|
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
|
||||||
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
|
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
|
||||||
// ...but where it CAN see the whole capture - no geometry stage - the frontend's
|
|
||||||
// own count is the desktop-exact one and the ES driver's is only as good as the
|
|
||||||
// vendor made it (Adreno doubles PRIMITIVES_WRITTEN for a vertex-only capture that
|
|
||||||
// follows a large render pass). The query above stays installed: it is still what
|
|
||||||
// answers an amplifying span, and PRIMITIVES_GENERATED always.
|
|
||||||
funcsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
|
|
||||||
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||||
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||||
@@ -1251,31 +1186,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
||||||
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks;
|
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks;
|
||||||
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks;
|
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks;
|
||||||
// Per-stage storage-block counts, forwarded from the host driver rather than invented.
|
|
||||||
// A stage the driver cannot serve reports 0, which is a legal answer everywhere these
|
|
||||||
// limits appear (GL 4.6 table 23.64, ES 3.2 table 21.44 - the minimum is 0 for every
|
|
||||||
// graphics stage except fragment) and is the only answer that lets an application take
|
|
||||||
// its own fallback instead of building a program the driver will refuse to link. The
|
|
||||||
// stage limit cannot exceed the combined limit or the number of binding points there
|
|
||||||
// are to bind buffers to, so clamp to both.
|
|
||||||
const auto clampStageStorageBlocks = [this](Int stageLimit) {
|
|
||||||
return std::min({std::max(stageLimit, 0), std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
|
|
||||||
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)});
|
|
||||||
};
|
|
||||||
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
|
|
||||||
m_dynamicParameters.MaxVertexShaderStorageBlocks =
|
|
||||||
clampStageStorageBlocks(m_GLESCapabilities.MaxVertexShaderStorageBlocks);
|
|
||||||
m_dynamicParameters.MaxTessControlShaderStorageBlocks =
|
|
||||||
clampStageStorageBlocks(m_GLESCapabilities.MaxTessControlShaderStorageBlocks);
|
|
||||||
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks =
|
|
||||||
clampStageStorageBlocks(m_GLESCapabilities.MaxTessEvaluationShaderStorageBlocks);
|
|
||||||
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
|
|
||||||
clampStageStorageBlocks(m_GLESCapabilities.MaxGeometryShaderStorageBlocks);
|
|
||||||
m_dynamicParameters.MaxFragmentShaderStorageBlocks =
|
|
||||||
clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks);
|
|
||||||
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
|
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
|
||||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
|
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
|
||||||
// (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.)
|
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
|
||||||
// This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
|
// This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
|
||||||
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
|
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
|
||||||
// than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says
|
// than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says
|
||||||
|
|||||||
@@ -18,16 +18,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const MG_External::GLESCapabilities& capabilities,
|
const MG_External::GLESCapabilities& capabilities,
|
||||||
FormatCapabilityCache& cache);
|
FormatCapabilityCache& cache);
|
||||||
|
|
||||||
// Clamps a requested sample count down to what the ES driver can really deliver for this
|
|
||||||
// format on this format-capability target: the probed per-format list when there is one, the
|
|
||||||
// driver's per-class GL_MAX_*_SAMPLES otherwise. The frontend deliberately validates against
|
|
||||||
// the count MobileGL advertises instead (GL_Getter's GetAdvertisedMaxSamples), which on a
|
|
||||||
// driver reporting GL_MAX_INTEGER_SAMPLES 1 is higher than the driver accepts, so every ES
|
|
||||||
// allocation call has to come through here. The shadow state keeps the requested count, so
|
|
||||||
// GL_TEXTURE_SAMPLES and framebuffer completeness still answer what the application asked for.
|
|
||||||
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
|
|
||||||
Int samples);
|
|
||||||
|
|
||||||
class BackendObject_DirectGLES : public BackendObject {
|
class BackendObject_DirectGLES : public BackendObject {
|
||||||
public:
|
public:
|
||||||
~BackendObject_DirectGLES() override;
|
~BackendObject_DirectGLES() override;
|
||||||
|
|||||||
@@ -300,32 +300,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Clear();
|
Clear();
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// Error HYGIENE is not a debugging feature: every site that brackets a risky ES call with
|
void ErrorLopper::Loop(const std::function<void(GLenum)>& func) {}
|
||||||
// Clear()/Loop() relied on these to empty the driver's queue, and compiling them to
|
void ErrorLopper::Clear() {}
|
||||||
// nothing left whatever the driver raised sitting there for an unrelated later
|
ErrorLopper::ErrorLopper() = default;
|
||||||
// `glGetError() == GL_NO_ERROR` probe to read as its own failure. The callback stays
|
ErrorLopper::~ErrorLopper() = default;
|
||||||
// unused because MGLOG_D is compiled out at this level, but the queue still gets drained.
|
|
||||||
// Bounded like DrainESErrors: a driver that never returns GL_NO_ERROR (a lost context is
|
|
||||||
// the usual way) must not spin here.
|
|
||||||
constexpr Int kMaxDrainedESErrors = 32;
|
|
||||||
|
|
||||||
void ErrorLopper::Loop(const std::function<void(GLenum)>& func) {
|
|
||||||
static_cast<void>(func);
|
|
||||||
for (Int i = 0; i < kMaxDrainedESErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ErrorLopper::Clear() {
|
|
||||||
for (Int i = 0; i < kMaxDrainedESErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorLopper::ErrorLopper() {
|
|
||||||
Clear();
|
|
||||||
}
|
|
||||||
ErrorLopper::~ErrorLopper() {
|
|
||||||
Clear();
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||||
@@ -1341,16 +1319,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// is stored as an ES 2D array (MapToBackendTextureTarget), and so is layerable; asking
|
// is stored as an ES 2D array (MapToBackendTextureTarget), and so is layerable; asking
|
||||||
// the state target instead answered "no" for it and pinned every 1D-array image binding
|
// the state target instead answered "no" for it and pinned every 1D-array image binding
|
||||||
// to layer 0, whatever the application passed.
|
// to layer 0, whatever the application passed.
|
||||||
//
|
|
||||||
// `layer` travels with the answer, because GL 4.6 core 8.26 (and ES 3.2 8.22, word for
|
|
||||||
// word) makes them one rule: "If the texture identified by texture does not have
|
|
||||||
// multiple layers or faces, the entire texture level is bound, regardless of the values
|
|
||||||
// of layered and layer." REGARDLESS means ignored - not clamped, and not an error - so
|
|
||||||
// the driver must not be handed a layer index the texture has no room for. Adreno takes
|
|
||||||
// such a request literally and leaves the image unit reading zero, which is what failed
|
|
||||||
// KHR-GL42.bind_image_texture.single_layer's layer:1 rows on GL_TEXTURE_2D and on the
|
|
||||||
// GL_TEXTURE_1D that is stored as one. Normalizing here and not in the frontend shadow
|
|
||||||
// is deliberate: GL_IMAGE_BINDING_LAYER must keep echoing what the application passed.
|
|
||||||
static Bool SupportsLayeredImageBinding(TextureTarget target) {
|
static Bool SupportsLayeredImageBinding(TextureTarget target) {
|
||||||
const TextureTarget backendTarget = TextureImpl::MapToBackendTextureTarget(target);
|
const TextureTarget backendTarget = TextureImpl::MapToBackendTextureTarget(target);
|
||||||
return backendTarget == TextureTarget::Texture3D || backendTarget == TextureTarget::TextureCubeMap ||
|
return backendTarget == TextureTarget::Texture3D || backendTarget == TextureTarget::TextureCubeMap ||
|
||||||
@@ -1406,11 +1374,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto& backendTexture = SyncTextureObjectToBackend(imageBinding.Texture, true);
|
auto& backendTexture = SyncTextureObjectToBackend(imageBinding.Texture, true);
|
||||||
const Bool layerable = SupportsLayeredImageBinding(imageBinding.Texture->GetTarget());
|
const GLboolean layered =
|
||||||
const GLboolean layered = layerable ? imageBinding.Layered : GL_FALSE;
|
SupportsLayeredImageBinding(imageBinding.Texture->GetTarget()) ? imageBinding.Layered : GL_FALSE;
|
||||||
const GLint layer = layerable ? imageBinding.Layer : 0;
|
|
||||||
g_GLESFuncs.glBindImageTexture(unit, backendTexture->GetBackendTextureId(), imageBinding.Level,
|
g_GLESFuncs.glBindImageTexture(unit, backendTexture->GetBackendTextureId(), imageBinding.Level,
|
||||||
layered, layer, imageBinding.Access, imageBinding.Format);
|
layered, imageBinding.Layer, imageBinding.Access, imageBinding.Format);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A buffer texture bound to a WRITABLE image unit is a buffer the shader is about to
|
// A buffer texture bound to a WRITABLE image unit is a buffer the shader is about to
|
||||||
@@ -3878,28 +3845,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect");
|
sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empties the ES driver's error queue, BOUNDED. A driver that never answers GL_NO_ERROR - a
|
static void DrainBlitErrors() {
|
||||||
// lost context is the usual way, and GL_CONTEXT_LOST is allowed to keep coming back - would
|
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||||
// otherwise spin an unbounded drain forever inside whichever GL entry point happened to be
|
|
||||||
// cleaning up, which is how a GPU reset reads as an unkillable process whose log simply
|
|
||||||
// stops. A healthy context cannot queue anywhere near the cap, so reaching it IS the
|
|
||||||
// diagnostic. Every drain in this backend goes through here so the bound cannot drift apart
|
|
||||||
// between them.
|
|
||||||
static constexpr Int kMaxDrainedGLErrors = 32;
|
|
||||||
|
|
||||||
static void DrainDriverErrors(const char* site) {
|
|
||||||
Int drained = 0;
|
|
||||||
while (drained < kMaxDrainedGLErrors && g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
|
||||||
++drained;
|
|
||||||
}
|
|
||||||
if (drained == kMaxDrainedGLErrors) {
|
|
||||||
MGLOG_E_ONCE("%s: the ES driver still reported errors after %d drains - the context is most likely lost",
|
|
||||||
site, kMaxDrainedGLErrors);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void DrainBlitErrors() { DrainDriverErrors("BlitFramebuffer"); }
|
|
||||||
|
|
||||||
// Sized internal format of the currently bound READ framebuffer's read colour
|
// Sized internal format of the currently bound READ framebuffer's read colour
|
||||||
// attachment, 0 when it cannot be determined.
|
// attachment, 0 when it cannot be determined.
|
||||||
static GLenum QueryReadColorAttachmentInternalFormat() {
|
static GLenum QueryReadColorAttachmentInternalFormat() {
|
||||||
@@ -4674,46 +4624,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The combined call raised an error, so by GL 4.6 2.3.1 it wrote nothing at all: BOTH
|
if (readSamples <= 0 || drawSamples > 0 || (mask & GL_COLOR_BUFFER_BIT) == 0) {
|
||||||
// aspect groups still owe their copy, and each has to be retried on its own. Re-issuing
|
return;
|
||||||
// the depth/stencil half only as a rider on a SUCCESSFUL colour resolve dropped it
|
|
||||||
// silently whenever the colour half could not be emulated - and on a framebuffer whose
|
|
||||||
// only attachment is depth it never can, because the colour emulation has no attachment
|
|
||||||
// to take a format from (KHR-GL33.framebuffer_blit's depth config test blits
|
|
||||||
// COLOR|DEPTH|STENCIL across depth-only framebuffers and kept reading the clear value).
|
|
||||||
const GLbitfield colourBit = mask & static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT);
|
|
||||||
const GLbitfield dsBits = mask & static_cast<GLbitfield>(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
|
||||||
// The colour group's one emulation is the multisample resolve that also converts format,
|
|
||||||
// which is the shape this names. It used to double as an early-out for the whole
|
|
||||||
// function, which is what cost a depth-only mask its single-aspect retry.
|
|
||||||
const Bool multisampleResolve = readSamples > 0 && drawSamples <= 0;
|
|
||||||
if (colourBit != 0) {
|
|
||||||
DrainBlitErrors();
|
|
||||||
g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, colourBit, filter);
|
|
||||||
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
|
||||||
const Bool emulated =
|
|
||||||
multisampleResolve &&
|
|
||||||
ResolveThenBlit(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, filter);
|
|
||||||
if (!emulated) {
|
|
||||||
MGLOG_E_ONCE("BlitFramebuffer: the colour aspect was dropped - the driver rejected it on its "
|
|
||||||
"own and no emulation applies");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (dsBits != 0) {
|
if (ResolveThenBlit(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, filter) &&
|
||||||
|
(mask & ~static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT)) != 0) {
|
||||||
|
DrainBlitErrors();
|
||||||
|
g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1,
|
||||||
|
mask & ~static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT), filter);
|
||||||
DrainBlitErrors();
|
DrainBlitErrors();
|
||||||
g_GLESFuncs.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, dsBits, filter);
|
|
||||||
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
|
||||||
// Nothing to fall back on yet: ResolveThenBlit is colour-only and the replicate
|
|
||||||
// pass runs in the opposite direction, so a driver that declines a multisample
|
|
||||||
// depth/stencil resolve leaves the destination holding its clear value. The log
|
|
||||||
// is the whole diagnostic - the frontend performs no validation of its own, so
|
|
||||||
// this never reaches the application as a GL error.
|
|
||||||
MGLOG_E_ONCE("BlitFramebuffer: the depth/stencil aspect was dropped - the driver rejected it on "
|
|
||||||
"its own and no emulation applies");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
DrainBlitErrors();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
||||||
@@ -5070,7 +4990,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ClearGLErrors() { DrainDriverErrors("DirectGLES"); }
|
static void ClearGLErrors() {
|
||||||
|
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {}
|
||||||
|
}
|
||||||
|
|
||||||
// Binds a guaranteed-complete 1x1 scratch framebuffer at both targets for the
|
// Binds a guaranteed-complete 1x1 scratch framebuffer at both targets for the
|
||||||
// scope (GenerateMipmap must respecify texture storage while no incomplete
|
// scope (GenerateMipmap must respecify texture storage while no incomplete
|
||||||
@@ -5708,87 +5630,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// The 1D-array case is not just a rename: GL addresses its layers with y/height while the
|
// 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
|
// ES 2D array that backs it addresses them with z/depth, so the two axes swap with the
|
||||||
// target.
|
// 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 {
|
struct GLESCopyImageEndpoint {
|
||||||
GLenum target = GL_TEXTURE_2D;
|
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<TextureImpl::BackendTextureObject> texture;
|
|
||||||
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> renderbuffer;
|
|
||||||
GLint x = 0;
|
GLint x = 0;
|
||||||
GLint y = 0;
|
GLint y = 0;
|
||||||
GLint z = 0;
|
GLint z = 0;
|
||||||
|
|
||||||
Bool IsRenderbuffer() const { return renderbuffer != nullptr; }
|
|
||||||
GLuint Name() const {
|
|
||||||
if (renderbuffer) return renderbuffer->GetBackendRenderbufferId();
|
|
||||||
return texture ? texture->GetBackendTextureId() : 0u;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The renderbuffer twin of TextureImpl::SyncTextureObjectToBackend: the same
|
static GLESCopyImageEndpoint MakeGLESCopyImageEndpoint(GLenum appTarget, GLint x, GLint y, GLint z) {
|
||||||
// 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<RenderbufferImpl::BackendRenderbufferObject> SyncRenderbufferObjectToBackend(
|
|
||||||
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {
|
|
||||||
if (!renderbufferObject) return nullptr;
|
|
||||||
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
|
|
||||||
if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
|
|
||||||
backendRenderbufferObject = *slot;
|
|
||||||
} else {
|
|
||||||
auto& newSlot = RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbufferObject);
|
|
||||||
if (!newSlot) {
|
|
||||||
newSlot = MakeShared<RenderbufferImpl::BackendRenderbufferObject>();
|
|
||||||
}
|
|
||||||
backendRenderbufferObject = newSlot;
|
|
||||||
}
|
|
||||||
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.
|
|
||||||
// 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);
|
const TextureTarget stateTarget = MG_Util::ConvertGLEnumToTextureTarget(appTarget);
|
||||||
out.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
|
GLESCopyImageEndpoint endpoint{};
|
||||||
|
endpoint.target = TextureImpl::ConvertTextureTargetToBackendGLEnum(stateTarget);
|
||||||
if (stateTarget == TextureTarget::Texture1DArray) {
|
if (stateTarget == TextureTarget::Texture1DArray) {
|
||||||
out.x = x;
|
endpoint.x = x;
|
||||||
out.y = 0;
|
endpoint.y = 0;
|
||||||
out.z = y;
|
endpoint.z = y;
|
||||||
return true;
|
return endpoint;
|
||||||
}
|
}
|
||||||
out.x = x;
|
endpoint.x = x;
|
||||||
out.y = y;
|
endpoint.y = y;
|
||||||
out.z = z;
|
endpoint.z = z;
|
||||||
return true;
|
return endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The region extent swaps the same two axes for a 1D array, and does so for whichever side
|
// The region extent swaps the same two axes for a 1D array, and does so for whichever side
|
||||||
@@ -5804,172 +5666,85 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
std::swap(height, depth);
|
std::swap(height, depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
static TextureInternalFormat GetCopyImageEndpointFormat(const CopyImageEndpoint& endpoint) {
|
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||||
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat();
|
|
||||||
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<MG_State::GLState::ITextureObject>& 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<Uint>(srcLevel));
|
|
||||||
const IntVec3 dstSize = dstMipmap->GetMipmapTexelSize(dstUploadTarget, static_cast<Uint>(dstLevel));
|
|
||||||
const SizeT srcSlices = static_cast<SizeT>(std::max(srcSize.z(), 1));
|
|
||||||
const SizeT dstSlices = static_cast<SizeT>(std::max(dstSize.z(), 1));
|
|
||||||
if (srcSize.x() <= 0 || srcSize.y() <= 0 || dstSize.x() <= 0 || dstSize.y() <= 0) return;
|
|
||||||
const SizeT srcTexels = static_cast<SizeT>(srcSize.x()) * static_cast<SizeT>(srcSize.y()) * srcSlices;
|
|
||||||
const SizeT dstTexels = static_cast<SizeT>(dstSize.x()) * static_cast<SizeT>(dstSize.y()) * dstSlices;
|
|
||||||
const SizeT srcBytes = srcMipmap->GetMipmapByteSize(srcUploadTarget, static_cast<Uint>(srcLevel));
|
|
||||||
const SizeT dstBytes = dstMipmap->GetMipmapByteSize(dstUploadTarget, static_cast<Uint>(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<SizeT>(srcX) + width > static_cast<SizeT>(srcSize.x()) ||
|
|
||||||
static_cast<SizeT>(srcY) + height > static_cast<SizeT>(srcSize.y()) ||
|
|
||||||
static_cast<SizeT>(srcZ) + depth > srcSlices ||
|
|
||||||
static_cast<SizeT>(dstX) + width > static_cast<SizeT>(dstSize.x()) ||
|
|
||||||
static_cast<SizeT>(dstY) + height > static_cast<SizeT>(dstSize.y()) ||
|
|
||||||
static_cast<SizeT>(dstZ) + depth > dstSlices) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto* srcBase = static_cast<const Uint8*>(
|
|
||||||
srcMipmap->MapMipmapData(srcUploadTarget, static_cast<Uint>(srcLevel)));
|
|
||||||
auto* dstBase = static_cast<Uint8*>(dstMipmap->MapMipmapData(dstUploadTarget, static_cast<Uint>(dstLevel)));
|
|
||||||
if (!srcBase || !dstBase) return;
|
|
||||||
|
|
||||||
const SizeT rowBytes = static_cast<SizeT>(width) * texelBytes;
|
|
||||||
for (GLsizei slice = 0; slice < depth; ++slice) {
|
|
||||||
for (GLsizei row = 0; row < height; ++row) {
|
|
||||||
const SizeT srcOffset = ((static_cast<SizeT>(srcZ + slice) * static_cast<SizeT>(srcSize.y()) +
|
|
||||||
static_cast<SizeT>(srcY + row)) *
|
|
||||||
static_cast<SizeT>(srcSize.x()) +
|
|
||||||
static_cast<SizeT>(srcX)) *
|
|
||||||
texelBytes;
|
|
||||||
const SizeT dstOffset = ((static_cast<SizeT>(dstZ + slice) * static_cast<SizeT>(dstSize.y()) +
|
|
||||||
static_cast<SizeT>(dstY + row)) *
|
|
||||||
static_cast<SizeT>(dstSize.x()) +
|
|
||||||
static_cast<SizeT>(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,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const CopyImageEndpoint& dstEndpoint,
|
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||||
GLESCopyImageEndpoint src{};
|
// BY VALUE, not by reference. SyncTextureObjectToBackend hands back a reference to a
|
||||||
GLESCopyImageEndpoint dst{};
|
// 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<TextureImpl::BackendTextureObject> srcBackendTexture =
|
||||||
|
TextureImpl::SyncTextureObjectToBackend(srcTexture);
|
||||||
|
const SharedPtr<TextureImpl::BackendTextureObject> dstBackendTexture =
|
||||||
|
TextureImpl::SyncTextureObjectToBackend(dstTexture);
|
||||||
// The DirectVulkan half of this entry point died exactly here, on a texture whose sync
|
// 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
|
// produced nothing - and it died in a release build, where the MOBILEGL_ASSERT that was
|
||||||
// supposed to catch it expands to nothing. The four Name() calls below are the same
|
// supposed to catch it expands to nothing. The four GetBackendTextureId() calls below
|
||||||
// dereference. The frontend validator is what keeps this unreachable and what reports
|
// are the same dereference. The frontend validator is what keeps this unreachable and
|
||||||
// the error the application is owed; declining is only how a future gap up there stops
|
// what reports the error the application is owed; declining is only how a future gap up
|
||||||
// being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
|
// there stops being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
|
||||||
if (!MakeGLESCopyImageEndpoint(srcEndpoint, srcTarget, srcX, srcY, srcZ, src) ||
|
if (!srcBackendTexture || !dstBackendTexture) {
|
||||||
!MakeGLESCopyImageEndpoint(dstEndpoint, dstTarget, dstX, dstY, dstZ, dst)) {
|
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__);
|
||||||
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GLESCopyImageEndpoint src = MakeGLESCopyImageEndpoint(srcTarget, srcX, srcY, srcZ);
|
||||||
|
const GLESCopyImageEndpoint dst = MakeGLESCopyImageEndpoint(dstTarget, dstX, dstY, dstZ);
|
||||||
GLsizei copyHeight = srcHeight;
|
GLsizei copyHeight = srcHeight;
|
||||||
GLsizei copyDepth = srcDepth;
|
GLsizei copyDepth = srcDepth;
|
||||||
ApplyGLESCopyImageExtent(srcTarget, dstTarget, copyHeight, copyDepth);
|
ApplyGLESCopyImageExtent(srcTarget, dstTarget, copyHeight, copyDepth);
|
||||||
|
|
||||||
const TextureInternalFormat srcFormat = GetCopyImageEndpointFormat(srcEndpoint);
|
const Bool srcIsDepth = MG_Util::IsDepthFormatInternalFormat(srcTexture->GetFormat());
|
||||||
const TextureInternalFormat dstFormat = GetCopyImageEndpointFormat(dstEndpoint);
|
const Bool dstIsDepth = MG_Util::IsDepthFormatInternalFormat(dstTexture->GetFormat());
|
||||||
// Both emulation fallbacks below are written against TEXTURE ids and texture targets, so
|
const Bool srcStencil = MG_Util::IsStencilFormatInternalFormat(srcTexture->GetFormat());
|
||||||
// an endpoint that is a renderbuffer takes the native ES copy - which accepts
|
const Bool dstStencil = MG_Util::IsStencilFormatInternalFormat(dstTexture->GetFormat());
|
||||||
// GL_RENDERBUFFER on both sides - and reports rather than mis-dispatches if the driver
|
if (srcIsDepth || dstIsDepth || srcStencil || dstStencil) {
|
||||||
// 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,
|
MOBILEGL_ASSERT(srcIsDepth && dstIsDepth && !srcStencil && !dstStencil,
|
||||||
"DirectGLES CopyImageSubData only supports depth-only image copies.");
|
"DirectGLES CopyImageSubData only supports depth-only image copies.");
|
||||||
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
|
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
|
||||||
"DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D.");
|
"DirectGLES depth CopyImageSubData only supports GL_TEXTURE_2D.");
|
||||||
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
|
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
|
||||||
"DirectGLES depth CopyImageSubData only supports single-layer copies.");
|
"DirectGLES depth CopyImageSubData only supports single-layer copies.");
|
||||||
BlitDepthTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
BlitDepthTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||||
dst.Name(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
|
dstBackendTexture->GetBackendTextureId(), dstLevel, dst.x, dst.y, srcWidth, copyHeight);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!anyRenderbuffer &&
|
if (srcTexture->GetFormat() == TextureInternalFormat::R32F ||
|
||||||
(srcFormat == TextureInternalFormat::R32F || dstFormat == TextureInternalFormat::R32F)) {
|
dstTexture->GetFormat() == TextureInternalFormat::R32F) {
|
||||||
// The single glGetError below decides the fallback dispatch, and
|
// The single glGetError below decides the fallback dispatch, and
|
||||||
// ErrorLopper::Clear is compiled out at the default log level - drain
|
// ErrorLopper::Clear is compiled out at the default log level - drain
|
||||||
// with the always-live helper so a stale flag cannot misroute a
|
// with the always-live helper so a stale flag cannot misroute a
|
||||||
// succeeded native copy into the 2D-only fallback.
|
// succeeded native copy into the 2D-only fallback.
|
||||||
ClearGLErrors();
|
ClearGLErrors();
|
||||||
g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z,
|
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
|
||||||
dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z,
|
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
|
||||||
srcWidth, copyHeight, copyDepth);
|
srcWidth, copyHeight, copyDepth);
|
||||||
const GLenum copyImageError = g_GLESFuncs.glGetError();
|
const GLenum copyImageError = g_GLESFuncs.glGetError();
|
||||||
if (copyImageError == GL_NO_ERROR) {
|
if (copyImageError == GL_NO_ERROR) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
MOBILEGL_ASSERT(IsColorOnlyFormat(srcFormat) && IsColorOnlyFormat(dstFormat),
|
MOBILEGL_ASSERT(IsColorOnlyFormat(srcTexture->GetFormat()) && IsColorOnlyFormat(dstTexture->GetFormat()),
|
||||||
"DirectGLES CopyImageSubData only supports color-only or depth-only copies.");
|
"DirectGLES CopyImageSubData only supports color-only or depth-only copies.");
|
||||||
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
|
MOBILEGL_ASSERT(src.target == GL_TEXTURE_2D && dst.target == GL_TEXTURE_2D,
|
||||||
"DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D.");
|
"DirectGLES color CopyImageSubData only supports GL_TEXTURE_2D.");
|
||||||
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
|
MOBILEGL_ASSERT(src.z == 0 && dst.z == 0 && copyDepth == 1,
|
||||||
"DirectGLES color CopyImageSubData only supports single-layer copies.");
|
"DirectGLES color CopyImageSubData only supports single-layer copies.");
|
||||||
CopyR32FTexture2D(src.Name(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
CopyR32FTexture2D(srcBackendTexture->GetBackendTextureId(), srcLevel, src.x, src.y, srcWidth, copyHeight,
|
||||||
dst.Name(), dst.target, dstLevel, dst.x, dst.y);
|
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClearGLErrors();
|
ClearGLErrors();
|
||||||
g_GLESFuncs.glCopyImageSubData(src.Name(), src.target, srcLevel, src.x, src.y, src.z,
|
g_GLESFuncs.glCopyImageSubData(srcBackendTexture->GetBackendTextureId(), src.target, srcLevel, src.x, src.y, src.z,
|
||||||
dst.Name(), dst.target, dstLevel, dst.x, dst.y, dst.z,
|
dstBackendTexture->GetBackendTextureId(), dst.target, dstLevel, dst.x, dst.y, dst.z,
|
||||||
srcWidth, copyHeight, copyDepth);
|
srcWidth, copyHeight, copyDepth);
|
||||||
// Every error condition glCopyImageSubData has was already ruled out by the frontend
|
// 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
|
// validator, so a driver error here is an internal invariant violation, not something
|
||||||
@@ -5985,14 +5760,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MG_Util::ConvertGLEnumToString(dst.target).c_str(),
|
MG_Util::ConvertGLEnumToString(dst.target).c_str(),
|
||||||
MG_Util::ConvertGLEnumToString(dstTarget).c_str());
|
MG_Util::ConvertGLEnumToString(dstTarget).c_str());
|
||||||
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request.");
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7283,7 +7050,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
data = std::move(expanded);
|
data = std::move(expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void DrainESErrors() { DrainDriverErrors("ReadPixels"); }
|
static void DrainESErrors() {
|
||||||
|
for (Int i = 0; i < 32 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++i) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static GLenum QueryReadAttachmentComponentType() {
|
static GLenum QueryReadAttachmentComponentType() {
|
||||||
GLint framebufferId = 0;
|
GLint framebufferId = 0;
|
||||||
@@ -7916,30 +7686,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY;
|
backendAttachTarget == GL_TEXTURE_CUBE_MAP_ARRAY;
|
||||||
const GLsizei sliceCount = std::max(size.z(), 1);
|
const GLsizei sliceCount = std::max(size.z(), 1);
|
||||||
const Bool multiSlice = 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
|
// 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
|
// 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
|
// uploaded, so every slice that was rendered to came back stale - which is exactly what
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
GLsizei height, GLint border);
|
GLsizei height, GLint border);
|
||||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||||
GLsizei height);
|
GLsizei height);
|
||||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const CopyImageEndpoint& dst,
|
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||||
void GenerateMipmap(GLenum target);
|
void GenerateMipmap(GLenum target);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
#include "Managers.h"
|
#include "Managers.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
#include "DirectGLES.h"
|
#include "DirectGLES.h"
|
||||||
#include "BackendObject_DirectGLES.h"
|
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
|
||||||
@@ -135,18 +134,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
[] { std::atexit(+[] { g_processTeardown = true; }); });
|
[] { std::atexit(+[] { g_processTeardown = true; }); });
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool VertexStageStorageBlockUsable(Int maxVertexShaderStorageBlocks) {
|
|
||||||
// One block is all the indirect-params view needs, so this is a >= 1 test and not a
|
|
||||||
// budget calculation. Negative is treated as unusable rather than clamped: a driver
|
|
||||||
// that leaves the out-param untouched is telling us nothing, and guessing "yes" here
|
|
||||||
// is what produces an unlinkable program.
|
|
||||||
return maxVertexShaderStorageBlocks >= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Bool CanUseVertexStageStorageBlock() {
|
|
||||||
return VertexStageStorageBlockUsable(g_GLESCapabilities.MaxVertexShaderStorageBlocks);
|
|
||||||
}
|
|
||||||
|
|
||||||
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType) {
|
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType) {
|
||||||
if (shaderType != GL_VERTEX_SHADER || source.find("gl_BaseInstance") == String::npos) {
|
if (shaderType != GL_VERTEX_SHADER || source.find("gl_BaseInstance") == String::npos) {
|
||||||
return source;
|
return source;
|
||||||
@@ -219,48 +206,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const Int paramsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings > 0
|
const Int paramsBinding = g_GLESCapabilities.MaxShaderStorageBufferBindings > 0
|
||||||
? g_GLESCapabilities.MaxShaderStorageBufferBindings - 1
|
? g_GLESCapabilities.MaxShaderStorageBufferBindings - 1
|
||||||
: 0;
|
: 0;
|
||||||
// The whole indirect half of this machinery is a storage block read from the VERTEX
|
|
||||||
// stage, and a storage block in the vertex stage is optional in both APIs: the
|
|
||||||
// minimum for GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS is 0 (GL 4.6 table 23.64, ES 3.2
|
|
||||||
// table 21.44) and ARM's GLES driver takes that allowance - a Mali-G925 reports 0.
|
|
||||||
// Emitting the block anyway does not make it work; it makes the program UNLINKABLE
|
|
||||||
// ("The number of vertex shader storage blocks (1) is greater than the maximum
|
|
||||||
// number allowed (0)"), and because the frontend's LINK_STATUS is glslang's and not
|
|
||||||
// the driver's, the application never learns: every draw with that program silently
|
|
||||||
// renders nothing. Dropping just the indirect half costs strictly less.
|
|
||||||
const Bool canReadIndirectParamsFromVertexStage = CanUseVertexStageStorageBlock();
|
|
||||||
String machinery;
|
String machinery;
|
||||||
if (source.find(String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";") == String::npos) {
|
if (source.find(String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";") == String::npos) {
|
||||||
machinery += String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";\n";
|
machinery += String("uniform highp int ") + BASE_INSTANCE_UNIFORM_NAME + ";\n";
|
||||||
}
|
}
|
||||||
if (!canReadIndirectParamsFromVertexStage) {
|
|
||||||
// Degraded, but contained and loud. gl_BaseInstance collapses to the plain
|
|
||||||
// mg_BaseInstance uniform, which the non-indirect draw entry points do set
|
|
||||||
// correctly - so ordinary instanced draws are unaffected. What is lost is the
|
|
||||||
// per-command baseInstance of an INDIRECT draw, which lives in the (possibly
|
|
||||||
// GPU-written) command buffer and can only be read through this block: those
|
|
||||||
// draws now see the last uniform value rather than their own command's. No
|
|
||||||
// alternative path is attempted, deliberately - there is nowhere else in the
|
|
||||||
// vertex stage to read a GPU-written buffer from.
|
|
||||||
//
|
|
||||||
// MGLOG_E_ONCE, not _D: this silently changes rendering for exactly the
|
|
||||||
// workloads (Create/Flywheel indirect instancing) whose bug reports are
|
|
||||||
// impossible to read without it, and once per process is bounded.
|
|
||||||
MGLOG_E_ONCE("gl_BaseInstance: this driver reports GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS = %d, so the "
|
|
||||||
"%s storage block an indirect draw's baseInstance must be read through cannot be "
|
|
||||||
"declared in the vertex stage. Dropping indirect baseInstance support: non-indirect "
|
|
||||||
"draws are correct, indirect draws will see a stale per-command baseInstance.",
|
|
||||||
g_GLESCapabilities.MaxVertexShaderStorageBlocks, INDIRECT_PARAMS_BLOCK_NAME);
|
|
||||||
if (rebaseInstanceId) {
|
|
||||||
// Without the block there is no per-command baseInstance to subtract, and
|
|
||||||
// the uniform is the same value the define below resolves to, so rebasing
|
|
||||||
// by it would cancel the base out of gl_InstanceID twice.
|
|
||||||
machinery += String("#define ") + ZERO_BASED_INSTANCE_ID_NAME + " gl_InstanceID\n";
|
|
||||||
}
|
|
||||||
machinery += String("#define ") + BASE_INSTANCE_LOWERED_NAME + " (" + BASE_INSTANCE_UNIFORM_NAME + ")";
|
|
||||||
source.replace(pos, declaration.size(), machinery);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
machinery += String("uniform highp int ") + BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ";\n";
|
machinery += String("uniform highp int ") + BASE_INSTANCE_WORD_INDEX_UNIFORM_NAME + ";\n";
|
||||||
machinery += String("layout(std430, binding = ") + std::to_string(paramsBinding) +
|
machinery += String("layout(std430, binding = ") + std::to_string(paramsBinding) +
|
||||||
") readonly buffer " + INDIRECT_PARAMS_BLOCK_NAME +
|
") readonly buffer " + INDIRECT_PARAMS_BLOCK_NAME +
|
||||||
@@ -2477,26 +2426,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return packedData.data();
|
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(
|
void BackendTextureObject::SyncMipmapsToBackend(
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
||||||
if (!stateTextureObject) {
|
if (!stateTextureObject) {
|
||||||
@@ -2544,14 +2473,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// 3. Size changed
|
// 3. Size changed
|
||||||
// 4. Mipmap levels changed
|
// 4. Mipmap levels changed
|
||||||
|
|
||||||
// IsComplete() is the sampling predicate, and it calls a chain whose lower levels are
|
if (!stateTextureObject->IsComplete()) {
|
||||||
// undefined incomplete - which is what a top-down build (upload level N, then level 0)
|
MGLOG_D("Texture object with ID: %u is not complete, skipping sync.",
|
||||||
// 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());
|
stateTextureObject->GetExternalIndex());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2653,13 +2576,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
for (auto& uploadTarget : uploadTargets) {
|
for (auto& uploadTarget : uploadTargets) {
|
||||||
for (SizeT level = m_prevTextureInfo.mipmapLevels; level < mipmapCount; ++level) {
|
for (SizeT level = m_prevTextureInfo.mipmapLevels; level < mipmapCount; ++level) {
|
||||||
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, 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);
|
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
|
||||||
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
|
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
|
||||||
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
||||||
@@ -2732,59 +2648,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
|
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
|
||||||
DebugImpl::ErrorLopper::Clear();
|
DebugImpl::ErrorLopper::Clear();
|
||||||
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
|
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
|
||||||
// The frontend validates against the count MobileGL advertises, which can
|
switch (targetInternal) {
|
||||||
// exceed what the driver takes for this format (Adreno: GL_MAX_SAMPLES 4,
|
case TextureTarget::Texture2DMultisample:
|
||||||
// GL_MAX_INTEGER_SAMPLES 1). Clamp the ES call - and only the ES call:
|
g_GLESFuncs.glTexStorage2DMultisample(
|
||||||
// stateTextureObject keeps the requested count so GL_TEXTURE_SAMPLES and
|
target, static_cast<GLsizei>(stateTextureObject->GetSamples()), glInternalFormat,
|
||||||
// framebuffer completeness still report what the application asked for.
|
static_cast<GLsizei>(baseSize.x()), static_cast<GLsizei>(baseSize.y()),
|
||||||
const auto backendSamples = static_cast<GLsizei>(ClampSamplesToBackendSupport(
|
stateTextureObject->HasFixedSampleLocations() ? GL_TRUE : GL_FALSE);
|
||||||
GetFormatCapabilityTargetIndex(targetInternal), textureMipmapObject->GetFormat(),
|
break;
|
||||||
glFormat, static_cast<Int>(stateTextureObject->GetSamples())));
|
case TextureTarget::Texture2DMultisampleArray:
|
||||||
// ES 3.1 8.19 requires width/height (and depth, for the array target) >= 1,
|
g_GLESFuncs.glTexStorage3DMultisample(
|
||||||
// so a degenerate size has nothing to allocate and must not reach the
|
target, static_cast<GLsizei>(stateTextureObject->GetSamples()), glInternalFormat,
|
||||||
// driver. The frontend deallocates such an image rather than defining it
|
static_cast<GLsizei>(baseSize.x()), static_cast<GLsizei>(baseSize.y()),
|
||||||
// (GL 4.6 core 8.8), so this is belt and braces for any path that still
|
static_cast<GLsizei>(baseSize.z()),
|
||||||
// syncs one.
|
stateTextureObject->HasFixedSampleLocations() ? GL_TRUE : GL_FALSE);
|
||||||
const Bool hasAllocatableSize =
|
break;
|
||||||
baseSize.x() >= 1 && baseSize.y() >= 1 &&
|
default:
|
||||||
(targetInternal != TextureTarget::Texture2DMultisampleArray || baseSize.z() >= 1);
|
MOBILEGL_ASSERT(false, "Unexpected multisample target: %d", static_cast<Int>(targetInternal));
|
||||||
if (!hasAllocatableSize) {
|
break;
|
||||||
MGLOG_D("Skipping multisample storage for texture %u: degenerate size (%d, %d, %d)",
|
|
||||||
m_backendTextureId, baseSize.x(), baseSize.y(), baseSize.z());
|
|
||||||
} else {
|
|
||||||
switch (targetInternal) {
|
|
||||||
case TextureTarget::Texture2DMultisample:
|
|
||||||
g_GLESFuncs.glTexStorage2DMultisample(
|
|
||||||
target, backendSamples, glInternalFormat,
|
|
||||||
static_cast<GLsizei>(baseSize.x()), static_cast<GLsizei>(baseSize.y()),
|
|
||||||
stateTextureObject->HasFixedSampleLocations() ? GL_TRUE : GL_FALSE);
|
|
||||||
break;
|
|
||||||
case TextureTarget::Texture2DMultisampleArray:
|
|
||||||
g_GLESFuncs.glTexStorage3DMultisample(
|
|
||||||
target, backendSamples, glInternalFormat,
|
|
||||||
static_cast<GLsizei>(baseSize.x()), static_cast<GLsizei>(baseSize.y()),
|
|
||||||
static_cast<GLsizei>(baseSize.z()),
|
|
||||||
stateTextureObject->HasFixedSampleLocations() ? GL_TRUE : GL_FALSE);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
MOBILEGL_ASSERT(false, "Unexpected multisample target: %d",
|
|
||||||
static_cast<Int>(targetInternal));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
m_backendStorageImmutable = true;
|
|
||||||
}
|
}
|
||||||
// The one storage branch that cleared the ES error queue without ever
|
m_backendStorageImmutable = true;
|
||||||
// draining it again, so anything this call raised was left for an
|
|
||||||
// unrelated later query to trip over. Paired with its two siblings now.
|
|
||||||
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__, target,
|
|
||||||
glInternalFormat, backendSamples](GLenum err) {
|
|
||||||
MGLOG_D("%s(%s:%d) ES error: %s. glTexStorage*Multisample: target=%s, internalformat=%s, "
|
|
||||||
"samples=%d",
|
|
||||||
func, file, line, MG_Util::ConvertGLEnumToString(err).c_str(),
|
|
||||||
MG_Util::ConvertGLEnumToString(target).c_str(),
|
|
||||||
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(),
|
|
||||||
static_cast<Int>(backendSamples));
|
|
||||||
});
|
|
||||||
for (const auto& uploadTarget : uploadTargets) {
|
for (const auto& uploadTarget : uploadTargets) {
|
||||||
for (SizeT level = 0; level < mipmapCount; ++level) {
|
for (SizeT level = 0; level < mipmapCount; ++level) {
|
||||||
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
|
textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
|
||||||
@@ -2887,13 +2769,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
for (auto& uploadTarget : uploadTargets) {
|
for (auto& uploadTarget : uploadTargets) {
|
||||||
for (SizeT level = 0; level < mipmapCount; ++level) {
|
for (SizeT level = 0; level < mipmapCount; ++level) {
|
||||||
auto levelTexelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, 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);
|
auto levelByteSize = textureMipmapObject->GetMipmapByteSize(uploadTarget, level);
|
||||||
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
|
bool levelDirty = textureMipmapObject->IsStorageDirty(uploadTarget, level);
|
||||||
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
||||||
@@ -4989,78 +4864,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
effectiveSpirv = &loweredSpirv;
|
effectiveSpirv = &loweredSpirv;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ESSL cannot express gl_ViewportIndex either, but unlike the draw parameters
|
|
||||||
// there IS an extension that provides it - so this runs only when the driver does
|
|
||||||
// NOT advertise GL_OES_viewport_array. A driver that does keeps the builtin and
|
|
||||||
// gets the `#extension` request added to the decompiled source below instead.
|
|
||||||
// Demoting the builtin costs the multi-viewport routing (every invocation lands in
|
|
||||||
// viewport 0), which is the degradation ViewportArrayScenario already documents
|
|
||||||
// for this backend; NOT demoting it costs the whole program, because the stage
|
|
||||||
// fails to compile and every draw made with it silently renders nothing.
|
|
||||||
// Gated on the module actually declaring the output, so no other stage pays an
|
|
||||||
// optimizer round trip for it.
|
|
||||||
// One parse of the module answers every armed pass gate below. The per-gate
|
|
||||||
// Declares* probes each cost a BuildModule per stage, and on a driver where both
|
|
||||||
// gates are armed (Mali: no GL_OES_viewport_array AND integer multisample
|
|
||||||
// squeezed to 1) the doubled parse made compile-heavy workloads ~10% slower.
|
|
||||||
// Probing the pre-lowering module is sound for both gates: demoting
|
|
||||||
// gl_ViewportIndex neither adds nor removes multisampled image types.
|
|
||||||
// Recomputed here rather than calling GL_Getter's GetAdvertisedMaxSamples():
|
|
||||||
// this is backend code and must not reach into the GL frontend. 4 is that
|
|
||||||
// translation unit's kFrontendMaxSamples, which is the source of truth -
|
|
||||||
// keep the two in step.
|
|
||||||
constexpr Int kFrontendMaxSamples = 4;
|
|
||||||
const Int advertisedMaxSamples =
|
|
||||||
std::max(g_GLESCapabilities.MaxSamples, kFrontendMaxSamples);
|
|
||||||
const Bool viewportLoweringArmed = !g_GLESCapabilities.SupportsViewportArray;
|
|
||||||
const Bool sampleClampArmed =
|
|
||||||
g_GLESCapabilities.MaxColorTextureSamples < advertisedMaxSamples ||
|
|
||||||
g_GLESCapabilities.MaxIntegerSamples < advertisedMaxSamples ||
|
|
||||||
g_GLESCapabilities.MaxDepthTextureSamples < advertisedMaxSamples;
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvGateFeatures spirvGates;
|
|
||||||
if (viewportLoweringArmed || sampleClampArmed) {
|
|
||||||
spirvGates = MG_Util::ShaderTranspiler::ShaderCompiler::ProbeSpirvGateFeatures(
|
|
||||||
*effectiveSpirv);
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector<unsigned int> loweredViewportSpirv;
|
|
||||||
if (viewportLoweringArmed && spirvGates.WritesViewportIndexOutput &&
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::LowerViewportIndexForEssl(
|
|
||||||
*effectiveSpirv, loweredViewportSpirv, enableSpirvValidation) &&
|
|
||||||
!loweredViewportSpirv.empty()) {
|
|
||||||
effectiveSpirv = &loweredViewportSpirv;
|
|
||||||
MGLOG_D("Program %u stage %s writes gl_ViewportIndex, which this ES driver has "
|
|
||||||
"no GL_OES_viewport_array for. The builtin was demoted to a plain "
|
|
||||||
"global; every invocation renders into viewport 0.",
|
|
||||||
m_backendProgramId,
|
|
||||||
MG_Util::ConvertGLEnumToString(glShaderType).c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so every multisample
|
|
||||||
// ceiling MobileGL advertises is floored to 4 no matter what the ES driver
|
|
||||||
// reports - but the realised allocation cannot be, and
|
|
||||||
// ClampSamplesToBackendSupport quietly gives an integer or depth multisample
|
|
||||||
// texture the ONE sample Adreno and Mali actually support for it. A shader
|
|
||||||
// written against the advertised ceiling then fetches a sample that storage does
|
|
||||||
// not have and reads garbage; KHR-GL33/40/41.texture_swizzle.functional_* and
|
|
||||||
// KHR-GLxx.texture_size_promotion.functional bake exactly that literal in. Clamp
|
|
||||||
// the Sample operand to the backend-real per-category maximum so the fetch lands
|
|
||||||
// inside the allocation. Gated on some category actually being squeezed AND the
|
|
||||||
// module actually declaring a multisampled image, so no other stage pays an
|
|
||||||
// optimizer round trip for it. DirectVulkan is deliberately not given this: it
|
|
||||||
// allocates the sample count it was asked for, so its modules are already right.
|
|
||||||
Vector<unsigned int> clampedSampleSpirv;
|
|
||||||
if (sampleClampArmed && spirvGates.DeclaresMultisampledImage &&
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::ClampMultisampleFetchesForEssl(
|
|
||||||
*effectiveSpirv, clampedSampleSpirv,
|
|
||||||
g_GLESCapabilities.MaxColorTextureSamples,
|
|
||||||
g_GLESCapabilities.MaxIntegerSamples,
|
|
||||||
g_GLESCapabilities.MaxDepthTextureSamples, advertisedMaxSamples,
|
|
||||||
enableSpirvValidation) &&
|
|
||||||
!clampedSampleSpirv.empty()) {
|
|
||||||
effectiveSpirv = &clampedSampleSpirv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GLSL ES has no ARRAY vertex inputs, and SPIRV-Cross refuses the whole module
|
// GLSL ES has no ARRAY vertex inputs, and SPIRV-Cross refuses the whole module
|
||||||
// rather than emulating them, so this has to happen before it sees the binary.
|
// rather than emulating them, so this has to happen before it sees the binary.
|
||||||
Vector<unsigned int> splitArrayInputSpirv;
|
Vector<unsigned int> splitArrayInputSpirv;
|
||||||
@@ -5242,17 +5045,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
source = RequestExtendedImageFormats(std::move(source),
|
source = RequestExtendedImageFormats(std::move(source),
|
||||||
imageFormatBake.needsExtendedImageFormats &&
|
imageFormatBake.needsExtendedImageFormats &&
|
||||||
g_GLESCapabilities.SupportsExtendedImageFormats);
|
g_GLESCapabilities.SupportsExtendedImageFormats);
|
||||||
// The third header-level rewrite, for the builtin SPIRV-Cross prints bare:
|
|
||||||
// gl_ViewportIndex is in no version of ESSL core, so without this directive the
|
|
||||||
// stage does not compile and the whole program - not just its viewport routing -
|
|
||||||
// is lost. The token probe keeps the line off every other program and the
|
|
||||||
// capability gate keeps it off drivers that would hard-error on an unadvertised
|
|
||||||
// name; a driver without the extension took the LowerViewportIndexPass fallback
|
|
||||||
// above and its source no longer names the builtin at all, so the two are mutually
|
|
||||||
// exclusive by construction. Read `source` BEFORE it is moved from.
|
|
||||||
const Bool needsViewportArrayExtension = g_GLESCapabilities.SupportsViewportArray &&
|
|
||||||
source.find("gl_ViewportIndex") != String::npos;
|
|
||||||
source = RequestViewportArrayExtension(std::move(source), needsViewportArrayExtension);
|
|
||||||
|
|
||||||
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
|
source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject);
|
||||||
// The completion half of the format bake, for the formats SPIRV-Cross throws on
|
// The completion half of the format bake, for the formats SPIRV-Cross throws on
|
||||||
@@ -5828,39 +5620,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
GLenum glInternalFormat, glType, glFormat;
|
GLenum glInternalFormat, glType, glFormat;
|
||||||
TextureImpl::GenerateRenderbufferFormatInfo(internalFormat, &glInternalFormat, &glFormat, &glType);
|
TextureImpl::GenerateRenderbufferFormatInfo(internalFormat, &glInternalFormat, &glFormat, &glType);
|
||||||
|
|
||||||
// The allocation is deferred to here, so an ES driver that refuses it (a
|
|
||||||
// multi-gigabyte renderbuffer is refused routinely) used to leave m_isInitialized
|
|
||||||
// true over a renderbuffer with no storage and say nothing at all: the attachment
|
|
||||||
// then rendered nowhere. Drain first so the check cannot pick up an unrelated stale
|
|
||||||
// flag, and report GL_OUT_OF_MEMORY to the application. The error lands on whatever
|
|
||||||
// entry point triggered the sync rather than on glRenderbufferStorage itself, which
|
|
||||||
// is where the deferred model puts it - still far better than silence.
|
|
||||||
DebugImpl::ErrorLopper::Clear();
|
|
||||||
if (samples > 0) {
|
if (samples > 0) {
|
||||||
// Same clamp as the multisample texture path: the frontend accepts the count it
|
g_GLESFuncs.glRenderbufferStorageMultisample(
|
||||||
// advertised, the driver only takes the count it supports for this format, and
|
GL_RENDERBUFFER, static_cast<GLsizei>(samples), glInternalFormat, static_cast<GLsizei>(width),
|
||||||
// the state object keeps reporting the requested one.
|
static_cast<GLsizei>(height));
|
||||||
const auto backendSamples = static_cast<GLsizei>(ClampSamplesToBackendSupport(
|
|
||||||
GetRenderbufferFormatCapabilityTargetIndex(), internalFormat, glFormat, samples));
|
|
||||||
g_GLESFuncs.glRenderbufferStorageMultisample(GL_RENDERBUFFER, backendSamples, glInternalFormat,
|
|
||||||
static_cast<GLsizei>(width),
|
|
||||||
static_cast<GLsizei>(height));
|
|
||||||
} else {
|
} else {
|
||||||
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, glInternalFormat, static_cast<GLsizei>(width),
|
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, glInternalFormat, static_cast<GLsizei>(width),
|
||||||
static_cast<GLsizei>(height));
|
static_cast<GLsizei>(height));
|
||||||
}
|
}
|
||||||
if (g_GLESFuncs.glGetError() == GL_OUT_OF_MEMORY) {
|
|
||||||
MGLOG_E_ONCE("Renderbuffer %u storage allocation ran out of memory: %dx%d, samples=%d, format=%s",
|
|
||||||
stateRBOObject->GetExternalIndex(), width, height, samples,
|
|
||||||
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
|
|
||||||
if (MG_State::pGLContext) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::OutOfMemory,
|
|
||||||
MakeUnique<GenericErrorInfo>("DirectGLES", "BackendRenderbufferObject::SyncToBackend",
|
|
||||||
"The ES driver could not allocate the renderbuffer storage."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DebugImpl::ErrorLopper::Clear();
|
|
||||||
|
|
||||||
m_cacheInternalFormat = internalFormat;
|
m_cacheInternalFormat = internalFormat;
|
||||||
m_cacheWidth = width;
|
m_cacheWidth = width;
|
||||||
|
|||||||
@@ -21,16 +21,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
|
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
|
||||||
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
|
String PromoteDrawParameterGlobalsToUniforms(String source, GLenum shaderType);
|
||||||
|
|
||||||
// Whether a vertex shader may declare a storage block at all, given what the host driver
|
|
||||||
// reports for GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS. Pure, and separated from the capability
|
|
||||||
// global purely so the decision can be tested without one.
|
|
||||||
//
|
|
||||||
// The indirect half of the gl_BaseInstance lowering in PromoteDrawParameterGlobalsToUniforms
|
|
||||||
// is the only thing that needs this, and it needs exactly one block. A driver reporting 0 is
|
|
||||||
// conformant - the minimum is 0 in GL 4.6 table 23.64 and ES 3.2 table 21.44 - and ARM's
|
|
||||||
// GLES driver does report 0, so this is a live path, not a defensive one.
|
|
||||||
Bool VertexStageStorageBlockUsable(Int maxVertexShaderStorageBlocks);
|
|
||||||
|
|
||||||
// True once the process has entered exit(): past that point the EGL library and
|
// True once the process has entered exit(): past that point the EGL library and
|
||||||
// the driver may already be unloaded, so a backend twin's destructor must not
|
// the driver may already be unloaded, so a backend twin's destructor must not
|
||||||
// call into g_GLESFuncs (the observed crash is a jump through an unmapped driver
|
// call into g_GLESFuncs (the observed crash is a jump through an unmapped driver
|
||||||
@@ -139,28 +129,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Twin creation is the moment a driver-owned id starts needing a guarded
|
// Twin creation is the moment a driver-owned id starts needing a guarded
|
||||||
// destructor; cold path, so the once-guard costs nothing per draw.
|
// destructor; cold path, so the once-guard costs nothing per draw.
|
||||||
EnsureProcessTeardownSentinel();
|
EnsureProcessTeardownSentinel();
|
||||||
// Sweep BEFORE the entry reference below exists: the map is open-addressed and an
|
|
||||||
// erase relocates the rest of the probe cluster, so collecting once that reference
|
|
||||||
// is taken would invalidate it. The sweep is therefore owed from an earlier call
|
|
||||||
// rather than triggered by this one.
|
|
||||||
if (m_creationTick >= kCreationGCInterval) {
|
|
||||||
m_creationTick = 0;
|
|
||||||
CollectGarbage();
|
|
||||||
}
|
|
||||||
const SizeT entryCountBeforeInsert = m_entries.size();
|
|
||||||
auto& entry = m_entries[stateObj.get()];
|
auto& entry = m_entries[stateObj.get()];
|
||||||
if (m_entries.size() != entryCountBeforeInsert) {
|
|
||||||
// A key the registry has never held. Nothing tells the backend that a texture or
|
|
||||||
// renderbuffer was DELETED - the twin, and the driver storage it owns, lives
|
|
||||||
// until a collection - and CollectGarbageIfNeeded is ticked only from the
|
|
||||||
// per-draw sync paths, which a CTS-shaped workload runs about ten times per
|
|
||||||
// case. 1024 of those ticks then span ~100 cases, so ~100 cases' worth of dead
|
|
||||||
// (and, for this suite, gigabyte-sized) objects stay allocated at once. Object
|
|
||||||
// CHURN rather than draw count is what makes the sweep urgent, so a twin the
|
|
||||||
// registry has never seen ticks it too - and it does so on the path that is
|
|
||||||
// about to allocate, which is exactly when the memory is needed.
|
|
||||||
++m_creationTick;
|
|
||||||
}
|
|
||||||
if (entry.stateRef.expired()) {
|
if (entry.stateRef.expired()) {
|
||||||
// The previous owner of this address is gone and the allocator handed it
|
// The previous owner of this address is gone and the allocator handed it
|
||||||
// to a new object: its twin describes ids the new state object never made.
|
// to a new object: its twin describes ids the new state object never made.
|
||||||
@@ -234,12 +203,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr Uint32 kGCInterval = 1024;
|
static constexpr Uint32 kGCInterval = 1024;
|
||||||
// Creations are far rarer than draws, so this counts in a much smaller unit than
|
|
||||||
// kGCInterval does.
|
|
||||||
static constexpr Uint32 kCreationGCInterval = 64;
|
|
||||||
BackendMap m_entries;
|
BackendMap m_entries;
|
||||||
Uint32 m_gcTick = 0;
|
Uint32 m_gcTick = 0;
|
||||||
Uint32 m_creationTick = 0;
|
|
||||||
Bool m_isCollecting = false;
|
Bool m_isCollecting = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -569,43 +569,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return glslCode;
|
return glslCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
String RequestViewportArrayExtension(String glslCode, Bool needed) {
|
|
||||||
#ifdef TRACY_ENABLE
|
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
||||||
#endif
|
|
||||||
// gl_ViewportIndex is desktop GL 4.1 core and is in ESSL only under
|
|
||||||
// GL_OES_viewport_array. SPIRV-Cross prints the identifier as-is and requests no
|
|
||||||
// extension for it - three lines away from the BuiltInLayer case, which DOES ask for
|
|
||||||
// one on ES - so an untouched decompile reaches the driver naming a builtin its core
|
|
||||||
// language has never heard of. The stage then fails to compile, the program is marked
|
|
||||||
// unusable and every draw made with it renders nothing while raising no GL error.
|
|
||||||
//
|
|
||||||
// Same `needed` contract as RequestExtendedImageFormats, and the same hard rule:
|
|
||||||
// `#extension` on a name the driver does not advertise is itself a compile error
|
|
||||||
// (ARM's compiler is strict about it), so this must never be emitted speculatively.
|
|
||||||
// A driver without the extension does not come through here at all - its module took
|
|
||||||
// the LowerViewportIndexPass fallback and the emitted source no longer names the
|
|
||||||
// builtin.
|
|
||||||
static constexpr const char* kDirective = "#extension GL_OES_viewport_array : require\n";
|
|
||||||
static constexpr const char* kExtName = "GL_OES_viewport_array";
|
|
||||||
if (!needed || glslCode.find(kExtName) != String::npos) {
|
|
||||||
return glslCode;
|
|
||||||
}
|
|
||||||
// Right after the #version line, for the reason spelled out above: it is the only
|
|
||||||
// position that must stay first, and ForceSupporterOutput's scan for the LAST
|
|
||||||
// #extension directive still finds whichever one that ends up being.
|
|
||||||
const SizeT versionPos = glslCode.find("#version");
|
|
||||||
if (versionPos == String::npos) {
|
|
||||||
return kDirective + glslCode;
|
|
||||||
}
|
|
||||||
const SizeT lineEnd = glslCode.find('\n', versionPos);
|
|
||||||
if (lineEnd == String::npos) {
|
|
||||||
return glslCode + "\n" + kDirective;
|
|
||||||
}
|
|
||||||
glslCode.insert(lineEnd + 1, kDirective);
|
|
||||||
return glslCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
String BakeImageFormatQualifiers(String glslCode,
|
String BakeImageFormatQualifiers(String glslCode,
|
||||||
const UnorderedMap<String, String>& esslFormatByUniformName) {
|
const UnorderedMap<String, String>& esslFormatByUniformName) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
|
|||||||
@@ -154,16 +154,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// extension - requesting an unadvertised extension is itself a compile error, so this is
|
// extension - requesting an unadvertised extension is itself a compile error, so this is
|
||||||
// never emitted speculatively. A no-op when not needed or already present.
|
// never emitted speculatively. A no-op when not needed or already present.
|
||||||
String RequestExtendedImageFormats(String glslCode, Bool needed);
|
String RequestExtendedImageFormats(String glslCode, Bool needed);
|
||||||
// Adds `#extension GL_OES_viewport_array : require` when the emitted ESSL names
|
|
||||||
// gl_ViewportIndex. SPIRV-Cross prints that identifier and asks for nothing (unlike
|
|
||||||
// gl_Layer, which it backs with GL_NV_viewport_array2 on ES) and ESSL has no core
|
|
||||||
// spelling for it at any version, so the request has to be made here or the stage does
|
|
||||||
// not compile - which loses the whole program, not just the multi-viewport routing.
|
|
||||||
// `needed` is the caller's answer for the same reason as above: only it knows whether the
|
|
||||||
// driver advertises the extension, and requesting an unadvertised one is itself a compile
|
|
||||||
// error, so this is never emitted speculatively. A no-op when not needed or already
|
|
||||||
// present.
|
|
||||||
String RequestViewportArrayExtension(String glslCode, Bool needed);
|
|
||||||
// Writes a format layout qualifier into the image declarations named in
|
// Writes a format layout qualifier into the image declarations named in
|
||||||
// `esslFormatByUniformName` that still have none. The completion half of the image-format
|
// `esslFormatByUniformName` that still have none. The completion half of the image-format
|
||||||
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
|
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
|
||||||
|
|||||||
@@ -847,38 +847,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_dynamicParameters.MaxShaderStorageBufferBindings =
|
m_dynamicParameters.MaxShaderStorageBufferBindings =
|
||||||
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
|
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
|
||||||
kMaxAdvertisedBufferBlocks);
|
kMaxAdvertisedBufferBlocks);
|
||||||
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Vulkan has one descriptor limit for every
|
|
||||||
// stage (maxPerStageDescriptorStorageBuffers, which is what MaxComputeShaderStorageBlocks
|
|
||||||
// carries), so the stage limits differ only by whether the stage can have blocks at all.
|
|
||||||
//
|
|
||||||
// Deliberately NOT gated on vertexPipelineStoresAndAtomics, unlike the per-stage image
|
|
||||||
// uniforms below. That gate reads as the obvious one and is wrong here in practice: a
|
|
||||||
// Mali-G925-Immortalis reports vertexPipelineStoresAndAtomics=false (supported AND
|
|
||||||
// enabled) and yet runs all 433 KHR-GL43.constant_expressions.*_tess_* cases correctly
|
|
||||||
// through this backend - those write their result through a storage block declared in a
|
|
||||||
// tessellation stage. Gating would report 0 and turn 433 passing cases into
|
|
||||||
// "unsupported", removing function that demonstrably works.
|
|
||||||
//
|
|
||||||
// The asymmetry with DirectGLES is real and is the point. There, 0 prevents a program
|
|
||||||
// the driver refuses outright at link time; the honest limit converts a silent
|
|
||||||
// wrong-render into a capability an application can route around. Here there is no such
|
|
||||||
// failure to prevent, so the limit stays at what the device can address. If a Vulkan
|
|
||||||
// device is ever found that genuinely rejects such a pipeline, the gate belongs at
|
|
||||||
// pipeline creation where the rejection is observable, not on a feature bit this driver
|
|
||||||
// reports inaccurately.
|
|
||||||
{
|
|
||||||
const Int maxPerStageStorageBlocks =
|
|
||||||
std::min(std::max(m_dynamicParameters.MaxComputeShaderStorageBlocks, 0),
|
|
||||||
std::min(std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
|
|
||||||
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)));
|
|
||||||
m_dynamicParameters.MaxVertexShaderStorageBlocks = maxPerStageStorageBlocks;
|
|
||||||
m_dynamicParameters.MaxTessControlShaderStorageBlocks = maxPerStageStorageBlocks;
|
|
||||||
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks = maxPerStageStorageBlocks;
|
|
||||||
// The one hard capability in the set: no geometry stage means no blocks in it.
|
|
||||||
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
|
|
||||||
m_vulkanCaps.SupportsGeometryShader ? maxPerStageStorageBlocks : 0;
|
|
||||||
m_dynamicParameters.MaxFragmentShaderStorageBlocks = maxPerStageStorageBlocks;
|
|
||||||
}
|
|
||||||
m_dynamicParameters.MaxTextureBufferSize = clampLimit(
|
m_dynamicParameters.MaxTextureBufferSize = clampLimit(
|
||||||
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
|
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
|
||||||
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
|
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
|
||||||
|
|||||||
@@ -632,15 +632,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context");
|
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyTexSubImage2D called with null GL context");
|
||||||
pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
pVulkanRenderer->CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
||||||
}
|
}
|
||||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const CopyImageEndpoint& dst,
|
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer");
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::CopyImageSubData called with null VulkanRenderer");
|
||||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context");
|
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::CopyImageSubData called with null GL context");
|
||||||
pVulkanRenderer->CopyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ,
|
pVulkanRenderer->CopyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ,
|
||||||
dst, dstTarget, dstLevel, dstX, dstY, dstZ,
|
dstTexture, dstTarget, dstLevel, dstX, dstY, dstZ,
|
||||||
srcWidth, srcHeight, srcDepth);
|
srcWidth, srcHeight, srcDepth);
|
||||||
}
|
}
|
||||||
void GenerateMipmap(GLenum target) {
|
void GenerateMipmap(GLenum target) {
|
||||||
|
|||||||
@@ -82,9 +82,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLsizei height, GLint border);
|
GLsizei height, GLint border);
|
||||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||||
GLsizei height);
|
GLsizei height);
|
||||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const CopyImageEndpoint& dst,
|
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||||
void GenerateMipmap(GLenum target);
|
void GenerateMipmap(GLenum target);
|
||||||
|
|||||||
@@ -3190,25 +3190,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GL_KHR_shader_subgroup handling (SubgroupSupportPolicy.h). Native subgroup
|
// GL_KHR_shader_subgroup handling (SubgroupSupportPolicy.h). Native subgroup
|
||||||
// operations execute natively; module repairs keep the GL contract intact
|
// operations execute natively; two module repairs keep the GL contract intact
|
||||||
// around them. The opt-in emulation path replaces them only on devices with no
|
// around them. The opt-in emulation path replaces them only on devices with no
|
||||||
// subgroup support at all (MOBILEGL_MAGMA_EMULATE_SUBGROUP).
|
// subgroup support at all (MOBILEGL_MAGMA_EMULATE_SUBGROUP).
|
||||||
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Compute) {
|
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Compute) {
|
||||||
// Program 203 broadcasts the first reduction through
|
|
||||||
// prefixSumCache[0], then lets the second reduction overwrite that
|
|
||||||
// scratch without first rendezvousing all readers. Patch that exact
|
|
||||||
// fingerprint before either native or emulated subgroup lowering.
|
|
||||||
if (m_subgroupPolicy.fixIterationRPBarrier) {
|
|
||||||
Vector<Uint> patchedSpirv;
|
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPBarrierForVulkan(
|
|
||||||
moduleSpirvs[i], patchedSpirv, enableSpirvValidation)) {
|
|
||||||
moduleSpirvs[i] = std::move(patchedSpirv);
|
|
||||||
} else {
|
|
||||||
MGLOG_E("ProgramFactory: iterationRP barrier patch failed for program %u; "
|
|
||||||
"Program 203 keeps its shared-scratch race",
|
|
||||||
program.GetExternalIndex());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (m_subgroupPolicy.emulateSubgroups) {
|
if (m_subgroupPolicy.emulateSubgroups) {
|
||||||
Vector<Uint> emulatedSpirv;
|
Vector<Uint> emulatedSpirv;
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::EmulateSubgroupsForVulkan(
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::EmulateSubgroupsForVulkan(
|
||||||
@@ -3228,7 +3213,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<Uint> patchedSpirv;
|
Vector<Uint> patchedSpirv;
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
|
||||||
moduleSpirvs[i], patchedSpirv, m_subgroupPolicy.nativeSubgroupSize,
|
moduleSpirvs[i], patchedSpirv, m_subgroupPolicy.nativeSubgroupSize,
|
||||||
m_subgroupPolicy.maxComputeSharedMemoryBytes,
|
|
||||||
enableSpirvValidation)) {
|
enableSpirvValidation)) {
|
||||||
moduleSpirvs[i] = std::move(patchedSpirv);
|
moduleSpirvs[i] = std::move(patchedSpirv);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -381,7 +381,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
struct SubgroupLoweringPolicy {
|
struct SubgroupLoweringPolicy {
|
||||||
Bool emulateSubgroups = false; // MOBILEGL_MAGMA_EMULATE_SUBGROUP, no-native-support devices
|
Bool emulateSubgroups = false; // MOBILEGL_MAGMA_EMULATE_SUBGROUP, no-native-support devices
|
||||||
Bool fixIterationRPSubgroupScratch = false; // patch iterationRP's under-declared scratch
|
Bool fixIterationRPSubgroupScratch = false; // patch iterationRP's under-declared scratch
|
||||||
Bool fixIterationRPBarrier = false; // repair Program 203's shared-scratch race
|
|
||||||
Bool deriveNumSubgroups = false; // repair the NumSubgroups builtin
|
Bool deriveNumSubgroups = false; // repair the NumSubgroups builtin
|
||||||
Bool requireFullSubgroups = false; // computeFullSubgroups enabled on the device
|
Bool requireFullSubgroups = false; // computeFullSubgroups enabled on the device
|
||||||
Uint32 nativeSubgroupSize = 0;
|
Uint32 nativeSubgroupSize = 0;
|
||||||
|
|||||||
@@ -1848,19 +1848,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (rounded == 0 && (supported & VK_SAMPLE_COUNT_1_BIT) != 0) {
|
|
||||||
// Nothing at two samples or above. Reachable because the frontend validates
|
|
||||||
// multisample allocations against the count MobileGL ADVERTISES (GL requires
|
|
||||||
// GL_MAX_SAMPLES >= 4) rather than against the device's per-format support, so
|
|
||||||
// a format this device cannot multisample at all now gets here instead of
|
|
||||||
// being refused up front. Keeping the unsupported count would hand
|
|
||||||
// vkCreateImage an invalid VkImageCreateInfo; one sample is at least a legal
|
|
||||||
// image, and the samples-08726 hazard above is the lesser of the two.
|
|
||||||
MGLOG_W_ONCE("Multisample texture format %d supports no count above one on this device; "
|
|
||||||
"backing it with a single sample",
|
|
||||||
static_cast<Int>(format));
|
|
||||||
rounded = static_cast<Uint32>(VK_SAMPLE_COUNT_1_BIT);
|
|
||||||
}
|
|
||||||
if (rounded != 0) {
|
if (rounded != 0) {
|
||||||
resolvedSampleCount = static_cast<VkSampleCountFlagBits>(rounded);
|
resolvedSampleCount = static_cast<VkSampleCountFlagBits>(rounded);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3063,7 +3063,6 @@ void main() {
|
|||||||
subgroupPolicy.emulateSubgroups = ShouldEmulateSubgroups(m_nativeSubgroupSupported);
|
subgroupPolicy.emulateSubgroups = ShouldEmulateSubgroups(m_nativeSubgroupSupported);
|
||||||
subgroupPolicy.fixIterationRPSubgroupScratch =
|
subgroupPolicy.fixIterationRPSubgroupScratch =
|
||||||
m_nativeSubgroupSupported && ShouldFixIterationRPSubgroupScratch();
|
m_nativeSubgroupSupported && ShouldFixIterationRPSubgroupScratch();
|
||||||
subgroupPolicy.fixIterationRPBarrier = ShouldFixIterationRPBarrier();
|
|
||||||
subgroupPolicy.deriveNumSubgroups =
|
subgroupPolicy.deriveNumSubgroups =
|
||||||
m_nativeSubgroupSupported && ShouldDeriveNumSubgroups();
|
m_nativeSubgroupSupported && ShouldDeriveNumSubgroups();
|
||||||
subgroupPolicy.requireFullSubgroups = m_computeFullSubgroupsFeatureEnabled;
|
subgroupPolicy.requireFullSubgroups = m_computeFullSubgroupsFeatureEnabled;
|
||||||
@@ -8869,7 +8868,7 @@ void main() {
|
|||||||
// A mixed 2D-array <-> 3D pair is legal because maintenance1 - core since Vulkan 1.1 -
|
// 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
|
// relaxed the old "layerCounts must match" rule into "the 3D side's extent.depth must
|
||||||
// equal the array side's layerCount".
|
// equal the array side's layerCount".
|
||||||
struct CopyImageSliceMapping {
|
struct CopyImageEndpoint {
|
||||||
// True for a VK_IMAGE_TYPE_3D image, i.e. slices ride the z axis, not the layer axis.
|
// True for a VK_IMAGE_TYPE_3D image, i.e. slices ride the z axis, not the layer axis.
|
||||||
Bool slicesAreDepth = false;
|
Bool slicesAreDepth = false;
|
||||||
// The GL z offset, kept in whichever field this endpoint's image type reads it from.
|
// The GL z offset, kept in whichever field this endpoint's image type reads it from.
|
||||||
@@ -8883,35 +8882,13 @@ void main() {
|
|||||||
Int32 OffsetZ() const { return slicesAreDepth ? static_cast<Int32>(baseSlice) : 0; }
|
Int32 OffsetZ() const { return slicesAreDepth ? static_cast<Int32>(baseSlice) : 0; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// The Vulkan image one glCopyImageSubData endpoint names, after the two object kinds GL
|
Bool TryResolveCopyImageEndpoint(TextureTarget target,
|
||||||
// 4.6 core 18.3.2 allows have been collapsed onto the fields this copy reads. A
|
const VkTextureManager::TextureResource& resource, Uint32 mipLevel,
|
||||||
// renderbuffer is a single-level, single-layer 2D image, so its shape answers are
|
GLint glZ, GLsizei glDepth, CopyImageEndpoint& outEndpoint) {
|
||||||
// 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) {
|
if (glZ < 0 || glDepth <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const Uint32 baseSlice = static_cast<Uint32>(glZ);
|
const Uint32 baseSlice = static_cast<Uint32>(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) {
|
switch (target) {
|
||||||
case TextureTarget::Texture1D:
|
case TextureTarget::Texture1D:
|
||||||
case TextureTarget::Texture2D:
|
case TextureTarget::Texture2D:
|
||||||
@@ -8919,12 +8896,12 @@ void main() {
|
|||||||
case TextureTarget::Texture2DMultisample:
|
case TextureTarget::Texture2DMultisample:
|
||||||
// Not layered at all: GL still requires the z/depth pair, and it can only name the
|
// Not layered at all: GL still requires the z/depth pair, and it can only name the
|
||||||
// one slice these targets have.
|
// one slice these targets have.
|
||||||
outMapping = {};
|
outEndpoint = {};
|
||||||
return baseSlice == 0 && glDepth == 1;
|
return baseSlice == 0 && glDepth == 1;
|
||||||
case TextureTarget::Texture3D:
|
case TextureTarget::Texture3D:
|
||||||
outMapping.slicesAreDepth = true;
|
outEndpoint.slicesAreDepth = true;
|
||||||
outMapping.baseSlice = baseSlice;
|
outEndpoint.baseSlice = baseSlice;
|
||||||
outMapping.availableSlices = std::max(1u, image.depth >> mipLevel);
|
outEndpoint.availableSlices = std::max(1u, resource.depth >> mipLevel);
|
||||||
return true;
|
return true;
|
||||||
case TextureTarget::Texture2DArray:
|
case TextureTarget::Texture2DArray:
|
||||||
case TextureTarget::Texture2DMultisampleArray:
|
case TextureTarget::Texture2DMultisampleArray:
|
||||||
@@ -8933,9 +8910,9 @@ void main() {
|
|||||||
// A cube map is an array of six faces here (see TryResolveTextureShapeInfo), and GL
|
// 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
|
// numbers its faces on the same z axis an array texture numbers its layers, so both
|
||||||
// arrive as a plain layer range.
|
// arrive as a plain layer range.
|
||||||
outMapping.slicesAreDepth = false;
|
outEndpoint.slicesAreDepth = false;
|
||||||
outMapping.baseSlice = baseSlice;
|
outEndpoint.baseSlice = baseSlice;
|
||||||
outMapping.availableSlices = image.arrayLayers;
|
outEndpoint.availableSlices = resource.arrayLayers;
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
// GL_TEXTURE_1D_ARRAY carries its layers on the Y axis (srcY/srcHeight), which
|
// GL_TEXTURE_1D_ARRAY carries its layers on the Y axis (srcY/srcHeight), which
|
||||||
@@ -8945,20 +8922,15 @@ void main() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint CopyImageEndpointName(const CopyImageEndpoint& endpoint) {
|
|
||||||
if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetExternalIndex();
|
|
||||||
return endpoint.Texture ? endpoint.Texture->GetExternalIndex() : 0u;
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void VulkanRenderer::CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
|
void VulkanRenderer::CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const CopyImageEndpoint& dstEndpoint,
|
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||||
MOBILEGL_ASSERT(srcEndpoint.Exists() && dstEndpoint.Exists(),
|
MOBILEGL_ASSERT(srcTexture != nullptr && dstTexture != nullptr,
|
||||||
"CopyImageSubData requires valid source and destination images.");
|
"CopyImageSubData requires valid source and destination textures.");
|
||||||
// The frontend already declines a zero or negative extent, so anything else here is a
|
// 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
|
// caller MobileGL wrote - but it still reaches vkCmdCopyImage in a release build, and a
|
||||||
// zero extent.depth is as invalid as a zero width.
|
// zero extent.depth is as invalid as a zero width.
|
||||||
@@ -8975,9 +8947,9 @@ void main() {
|
|||||||
// and an overlap check). Refused outright, and refused for real rather than through an
|
// 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,
|
// 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.
|
// on a tiler, a copy whose source has already been overwritten.
|
||||||
if (srcEndpoint.Texture == dstEndpoint.Texture && srcEndpoint.Renderbuffer == dstEndpoint.Renderbuffer) {
|
if (srcTexture.get() == dstTexture.get()) {
|
||||||
MGLOG_E_ONCE("%s: in-place copy on objectId=%u is not supported; declining the copy", __func__,
|
MGLOG_E_ONCE("%s: in-place copy on textureId=%d is not supported; declining the copy", __func__,
|
||||||
CopyImageEndpointName(srcEndpoint));
|
srcTexture->GetExternalIndex());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8990,42 +8962,8 @@ void main() {
|
|||||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// One resolver for both object kinds. The texture arm is the same
|
auto* srcResource = m_textureManager->SyncTextureAndGetDescriptor(*srcTexture);
|
||||||
// SyncTextureAndGetDescriptor the copy always used; the renderbuffer arm goes through the
|
auto* dstResource = m_textureManager->SyncTextureAndGetDescriptor(*dstTexture);
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
// 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;
|
|
||||||
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
|
// 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
|
// 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
|
// dereferenced right below (lavapipe) and a mip level the VkImage does not have handed
|
||||||
@@ -9041,29 +8979,29 @@ void main() {
|
|||||||
// The frontend validator (ValidateTextureLevelExists) is what produces the
|
// The frontend validator (ValidateTextureLevelExists) is what produces the
|
||||||
// GL_INVALID_VALUE the application is actually owed. This guard exists so the next gap
|
// 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.
|
// up there declines a copy instead of taking the process down.
|
||||||
if (!srcResolved || !dstResolved) {
|
if (srcResource == nullptr || dstResource == nullptr) {
|
||||||
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
|
MGLOG_E_ONCE("%s: source or destination texture failed to sync; declining the copy", __func__);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcImage.mipLevels ||
|
if (srcLevel < 0 || dstLevel < 0 || static_cast<Uint32>(srcLevel) >= srcResource->mipLevels ||
|
||||||
static_cast<Uint32>(dstLevel) >= dstImage.mipLevels) {
|
static_cast<Uint32>(dstLevel) >= dstResource->mipLevels) {
|
||||||
MGLOG_E_ONCE("%s: mip level out of range (src %d of %u, dst %d of %u); declining the copy", __func__,
|
MGLOG_E_ONCE("%s: mip level out of range (src %d of %u, dst %d of %u); declining the copy", __func__,
|
||||||
srcLevel, srcImage.mipLevels, dstLevel, dstImage.mipLevels);
|
srcLevel, srcResource->mipLevels, dstLevel, dstResource->mipLevels);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const VkImageAspectFlags copyAspectMask =
|
const VkImageAspectFlags copyAspectMask =
|
||||||
srcImage.aspect & dstImage.aspect &
|
srcResource->aspect & dstResource->aspect &
|
||||||
(VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
|
(VK_IMAGE_ASPECT_COLOR_BIT | VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
|
||||||
MOBILEGL_ASSERT(copyAspectMask != 0 &&
|
MOBILEGL_ASSERT(copyAspectMask != 0 &&
|
||||||
(srcImage.aspect & copyAspectMask) == srcImage.aspect &&
|
(srcResource->aspect & copyAspectMask) == srcResource->aspect &&
|
||||||
(dstImage.aspect & copyAspectMask) == dstImage.aspect,
|
(dstResource->aspect & copyAspectMask) == dstResource->aspect,
|
||||||
"CopyImageSubData source and destination aspects are incompatible.");
|
"CopyImageSubData source and destination aspects are incompatible.");
|
||||||
const Uint32 srcMipLevel = static_cast<Uint32>(srcLevel);
|
const Uint32 srcMipLevel = static_cast<Uint32>(srcLevel);
|
||||||
const Uint32 dstMipLevel = static_cast<Uint32>(dstLevel);
|
const Uint32 dstMipLevel = static_cast<Uint32>(dstLevel);
|
||||||
const Uint32 srcMipWidth = std::max(1u, srcImage.extent.width >> srcMipLevel);
|
const Uint32 srcMipWidth = std::max(1u, srcResource->extent.width >> srcMipLevel);
|
||||||
const Uint32 srcMipHeight = std::max(1u, srcImage.extent.height >> srcMipLevel);
|
const Uint32 srcMipHeight = std::max(1u, srcResource->extent.height >> srcMipLevel);
|
||||||
const Uint32 dstMipWidth = std::max(1u, dstImage.extent.width >> dstMipLevel);
|
const Uint32 dstMipWidth = std::max(1u, dstResource->extent.width >> dstMipLevel);
|
||||||
const Uint32 dstMipHeight = std::max(1u, dstImage.extent.height >> dstMipLevel);
|
const Uint32 dstMipHeight = std::max(1u, dstResource->extent.height >> dstMipLevel);
|
||||||
// Promoted for the same reason as the level range above, and it is the same bug class:
|
// 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
|
// 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,
|
// driver, and the frontend does not check the region at all (there is a CTS sibling,
|
||||||
@@ -9086,10 +9024,10 @@ void main() {
|
|||||||
// here: every target whose slices this function can address on one of the two Vulkan axes.
|
// 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
|
// 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.
|
// in a release build and the unsupported shape reached vkCmdCopyImage anyway.
|
||||||
CopyImageSliceMapping srcSlices;
|
CopyImageEndpoint srcEndpoint;
|
||||||
CopyImageSliceMapping dstSlices;
|
CopyImageEndpoint dstEndpoint;
|
||||||
if (!TryResolveCopyImageSliceMapping(srcTextureTarget, srcImage, srcMipLevel, srcZ, srcDepth, srcSlices) ||
|
if (!TryResolveCopyImageEndpoint(srcTextureTarget, *srcResource, srcMipLevel, srcZ, srcDepth, srcEndpoint) ||
|
||||||
!TryResolveCopyImageSliceMapping(dstTextureTarget, dstImage, dstMipLevel, dstZ, srcDepth, dstSlices)) {
|
!TryResolveCopyImageEndpoint(dstTextureTarget, *dstResource, dstMipLevel, dstZ, srcDepth, dstEndpoint)) {
|
||||||
MGLOG_E_ONCE("%s: unsupported target pair src=%s dst=%s (srcZ=%d dstZ=%d depth=%d); declining the copy",
|
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(),
|
__func__, MG_Util::ConvertTextureTargetToString(srcTextureTarget).c_str(),
|
||||||
MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), srcZ, dstZ, srcDepth);
|
MG_Util::ConvertTextureTargetToString(dstTextureTarget).c_str(), srcZ, dstZ, srcDepth);
|
||||||
@@ -9100,53 +9038,40 @@ void main() {
|
|||||||
// shrinks) and a 3D texture by the selected level's depth (which every level halves), so
|
// shrinks) and a 3D texture by the selected level's depth (which every level halves), so
|
||||||
// both come from the endpoint that resolved them.
|
// both come from the endpoint that resolved them.
|
||||||
const Uint32 copySliceCount = static_cast<Uint32>(srcDepth);
|
const Uint32 copySliceCount = static_cast<Uint32>(srcDepth);
|
||||||
if (srcSlices.baseSlice + copySliceCount > srcSlices.availableSlices ||
|
if (srcEndpoint.baseSlice + copySliceCount > srcEndpoint.availableSlices ||
|
||||||
dstSlices.baseSlice + copySliceCount > dstSlices.availableSlices) {
|
dstEndpoint.baseSlice + copySliceCount > dstEndpoint.availableSlices) {
|
||||||
MGLOG_E_ONCE("%s: slice range outside image bounds (srcZ=%d of %u, dstZ=%d of %u, depth=%d); "
|
MGLOG_E_ONCE("%s: slice range outside image bounds (srcZ=%d of %u, dstZ=%d of %u, depth=%d); "
|
||||||
"declining the copy",
|
"declining the copy",
|
||||||
__func__, srcZ, srcSlices.availableSlices, dstZ, dstSlices.availableSlices, srcDepth);
|
__func__, srcZ, srcEndpoint.availableSlices, dstZ, dstEndpoint.availableSlices, srcDepth);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto materializeClear = [this, &frame](const CopyImageEndpoint& endpoint) {
|
const Bool clearReady = MaterializePendingClearForTexture(frame.commandBuffer, *srcTexture);
|
||||||
if (endpoint.IsRenderbuffer()) {
|
MOBILEGL_ASSERT(clearReady, "%s: failed to materialize pending clear for source textureId=%d",
|
||||||
return MaterializePendingClearForRenderbuffer(frame.commandBuffer, endpoint.Renderbuffer);
|
__func__, srcTexture->GetExternalIndex());
|
||||||
}
|
|
||||||
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
|
// A clear still parked on the destination would otherwise materialize AFTER this copy and
|
||||||
// wipe the texels it just wrote.
|
// wipe the texels it just wrote.
|
||||||
const Bool dstClearReady = materializeClear(dstEndpoint);
|
const Bool dstClearReady = MaterializePendingClearForTexture(frame.commandBuffer, *dstTexture);
|
||||||
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination objectId=%u",
|
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination textureId=%d",
|
||||||
__func__, CopyImageEndpointName(dstEndpoint));
|
__func__, dstTexture->GetExternalIndex());
|
||||||
|
|
||||||
const VkImageLayout srcOriginalLayout = *srcImage.trackedLayout;
|
const VkImageLayout srcOriginalLayout = srcResource->layout;
|
||||||
const VkImageLayout dstOriginalLayout = *dstImage.trackedLayout;
|
const VkImageLayout dstOriginalLayout = dstResource->layout;
|
||||||
// A layout of UNDEFINED means nothing has ever been written to the image, which on the
|
// 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
|
// 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
|
// 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
|
// 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.
|
// real layout afterwards, since UNDEFINED is not a layout a barrier may transition BACK to.
|
||||||
// A renderbuffer settles on its ATTACHMENT layout instead: it is never sampled, and that is
|
const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout) {
|
||||||
// the layout MaterializePendingClearForRenderbuffer leaves it in.
|
|
||||||
const auto resolveRestoreLayout = [copyAspectMask](VkImageLayout originalLayout, Bool isRenderbuffer) {
|
|
||||||
if (originalLayout != VK_IMAGE_LAYOUT_UNDEFINED) {
|
if (originalLayout != VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||||
return originalLayout;
|
return originalLayout;
|
||||||
}
|
}
|
||||||
const Bool depthStencil =
|
return (copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0
|
||||||
(copyAspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0;
|
? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
|
||||||
if (isRenderbuffer) {
|
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||||
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, srcImage.isRenderbuffer);
|
const VkImageLayout srcRestoreLayout = resolveRestoreLayout(srcOriginalLayout);
|
||||||
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout, dstImage.isRenderbuffer);
|
const VkImageLayout dstRestoreLayout = resolveRestoreLayout(dstOriginalLayout);
|
||||||
|
|
||||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
VkAccessFlags srcAccessMask = 0;
|
VkAccessFlags srcAccessMask = 0;
|
||||||
@@ -9157,15 +9082,15 @@ void main() {
|
|||||||
// [baseSlice, baseSlice + depth) the slice mapping above hands the copy.
|
// [baseSlice, baseSlice + depth) the slice mapping above hands the copy.
|
||||||
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||||
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
frame.commandBuffer, srcResource->image, srcResource->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT,
|
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT,
|
||||||
srcImage.aspect, 0, srcImage.mipLevels);
|
srcResource->aspect, 0, srcResource->mipLevels);
|
||||||
MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__);
|
MOBILEGL_ASSERT(srcReady, "%s: failed to transition undefined source image", __func__);
|
||||||
srcCopyLayout = *srcImage.trackedLayout;
|
srcCopyLayout = srcResource->layout;
|
||||||
} else {
|
} else {
|
||||||
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
Bool srcReady = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, srcImage.image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
frame.commandBuffer, srcResource->image, srcCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1);
|
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, copyAspectMask, srcMipLevel, 1);
|
||||||
MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__);
|
MOBILEGL_ASSERT(srcReady, "%s: failed to transition source image", __func__);
|
||||||
@@ -9177,15 +9102,15 @@ void main() {
|
|||||||
VkImageLayout dstCopyLayout = dstOriginalLayout;
|
VkImageLayout dstCopyLayout = dstOriginalLayout;
|
||||||
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||||
Bool dstReady = VkTextureManager::TransitionImageLayout(
|
Bool dstReady = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
frame.commandBuffer, dstResource->image, dstResource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||||
dstImage.aspect, 0, dstImage.mipLevels);
|
dstResource->aspect, 0, dstResource->mipLevels);
|
||||||
MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__);
|
MOBILEGL_ASSERT(dstReady, "%s: failed to transition undefined destination image", __func__);
|
||||||
dstCopyLayout = *dstImage.trackedLayout;
|
dstCopyLayout = dstResource->layout;
|
||||||
} else {
|
} else {
|
||||||
Bool dstReady = VkTextureManager::TransitionImageLayout(
|
Bool dstReady = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, dstImage.image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
frame.commandBuffer, dstResource->image, dstCopyLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
dstStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1);
|
dstAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, copyAspectMask, dstMipLevel, 1);
|
||||||
MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__);
|
MOBILEGL_ASSERT(dstReady, "%s: failed to transition destination image", __func__);
|
||||||
@@ -9195,18 +9120,18 @@ void main() {
|
|||||||
// on extent.depth as soon as either endpoint IS: a 3D image's subresource is always the
|
// 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
|
// 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.
|
// non-3D endpoints both layer counts carry it and extent.depth stays 1.
|
||||||
const Bool copyCrossesDepthAxis = srcSlices.slicesAreDepth || dstSlices.slicesAreDepth;
|
const Bool copyCrossesDepthAxis = srcEndpoint.slicesAreDepth || dstEndpoint.slicesAreDepth;
|
||||||
VkImageCopy copyRegion{};
|
VkImageCopy copyRegion{};
|
||||||
copyRegion.srcSubresource.aspectMask = copyAspectMask;
|
copyRegion.srcSubresource.aspectMask = copyAspectMask;
|
||||||
copyRegion.srcSubresource.mipLevel = srcMipLevel;
|
copyRegion.srcSubresource.mipLevel = srcMipLevel;
|
||||||
copyRegion.srcSubresource.baseArrayLayer = srcSlices.BaseArrayLayer();
|
copyRegion.srcSubresource.baseArrayLayer = srcEndpoint.BaseArrayLayer();
|
||||||
copyRegion.srcSubresource.layerCount = srcSlices.slicesAreDepth ? 1u : copySliceCount;
|
copyRegion.srcSubresource.layerCount = srcEndpoint.slicesAreDepth ? 1u : copySliceCount;
|
||||||
copyRegion.srcOffset = {srcX, srcY, srcSlices.OffsetZ()};
|
copyRegion.srcOffset = {srcX, srcY, srcEndpoint.OffsetZ()};
|
||||||
copyRegion.dstSubresource.aspectMask = copyAspectMask;
|
copyRegion.dstSubresource.aspectMask = copyAspectMask;
|
||||||
copyRegion.dstSubresource.mipLevel = dstMipLevel;
|
copyRegion.dstSubresource.mipLevel = dstMipLevel;
|
||||||
copyRegion.dstSubresource.baseArrayLayer = dstSlices.BaseArrayLayer();
|
copyRegion.dstSubresource.baseArrayLayer = dstEndpoint.BaseArrayLayer();
|
||||||
copyRegion.dstSubresource.layerCount = dstSlices.slicesAreDepth ? 1u : copySliceCount;
|
copyRegion.dstSubresource.layerCount = dstEndpoint.slicesAreDepth ? 1u : copySliceCount;
|
||||||
copyRegion.dstOffset = {dstX, dstY, dstSlices.OffsetZ()};
|
copyRegion.dstOffset = {dstX, dstY, dstEndpoint.OffsetZ()};
|
||||||
copyRegion.extent = {static_cast<Uint32>(srcWidth), static_cast<Uint32>(srcHeight),
|
copyRegion.extent = {static_cast<Uint32>(srcWidth), static_cast<Uint32>(srcHeight),
|
||||||
copyCrossesDepthAxis ? copySliceCount : 1u};
|
copyCrossesDepthAxis ? copySliceCount : 1u};
|
||||||
MGLOG_D("CopyImageSubData: src(target=%s level=%u layer=%u+%u z=%d) -> dst(target=%s level=%u layer=%u+%u "
|
MGLOG_D("CopyImageSubData: src(target=%s level=%u layer=%u+%u z=%d) -> dst(target=%s level=%u layer=%u+%u "
|
||||||
@@ -9217,8 +9142,8 @@ void main() {
|
|||||||
copyRegion.dstSubresource.baseArrayLayer, copyRegion.dstSubresource.layerCount,
|
copyRegion.dstSubresource.baseArrayLayer, copyRegion.dstSubresource.layerCount,
|
||||||
copyRegion.dstOffset.z, srcWidth, srcHeight, copyRegion.extent.depth);
|
copyRegion.dstOffset.z, srcWidth, srcHeight, copyRegion.extent.depth);
|
||||||
vkCmdCopyImage(frame.commandBuffer,
|
vkCmdCopyImage(frame.commandBuffer,
|
||||||
srcImage.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
srcResource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
dstImage.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
dstResource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
1, ©Region);
|
1, ©Region);
|
||||||
|
|
||||||
VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
VkPipelineStageFlags srcRestoreStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
@@ -9226,14 +9151,14 @@ void main() {
|
|||||||
GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask);
|
GetImageTransitionDestinationState(srcRestoreLayout, srcRestoreStageMask, srcRestoreAccessMask);
|
||||||
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
if (srcOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||||
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, srcImage.image, *srcImage.trackedLayout, srcRestoreLayout,
|
frame.commandBuffer, srcResource->image, srcResource->layout, srcRestoreLayout,
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
||||||
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask,
|
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask,
|
||||||
srcImage.aspect, 0, srcImage.mipLevels);
|
srcResource->aspect, 0, srcResource->mipLevels);
|
||||||
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__);
|
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore undefined source image layout", __func__);
|
||||||
} else {
|
} else {
|
||||||
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
Bool srcRestored = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, srcImage.image, srcCopyLayout, srcRestoreLayout,
|
frame.commandBuffer, srcResource->image, srcCopyLayout, srcRestoreLayout,
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
VK_PIPELINE_STAGE_TRANSFER_BIT, srcRestoreStageMask,
|
||||||
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1);
|
VK_ACCESS_TRANSFER_READ_BIT, srcRestoreAccessMask, copyAspectMask, srcMipLevel, 1);
|
||||||
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__);
|
MOBILEGL_ASSERT(srcRestored, "%s: failed to restore source image layout", __func__);
|
||||||
@@ -9244,14 +9169,14 @@ void main() {
|
|||||||
GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask);
|
GetImageTransitionDestinationState(dstRestoreLayout, dstRestoreStageMask, dstRestoreAccessMask);
|
||||||
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
if (dstOriginalLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||||
Bool dstRestored = VkTextureManager::TransitionImageLayout(
|
Bool dstRestored = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, dstImage.image, *dstImage.trackedLayout, dstRestoreLayout,
|
frame.commandBuffer, dstResource->image, dstResource->layout, dstRestoreLayout,
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
|
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask,
|
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask,
|
||||||
dstImage.aspect, 0, dstImage.mipLevels);
|
dstResource->aspect, 0, dstResource->mipLevels);
|
||||||
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__);
|
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore undefined destination image layout", __func__);
|
||||||
} else {
|
} else {
|
||||||
Bool dstRestored = VkTextureManager::TransitionImageLayout(
|
Bool dstRestored = VkTextureManager::TransitionImageLayout(
|
||||||
frame.commandBuffer, dstImage.image, dstCopyLayout, dstRestoreLayout,
|
frame.commandBuffer, dstResource->image, dstCopyLayout, dstRestoreLayout,
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
|
VK_PIPELINE_STAGE_TRANSFER_BIT, dstRestoreStageMask,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1);
|
VK_ACCESS_TRANSFER_WRITE_BIT, dstRestoreAccessMask, copyAspectMask, dstMipLevel, 1);
|
||||||
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
|
MOBILEGL_ASSERT(dstRestored, "%s: failed to restore destination image layout", __func__);
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
#include "VkTimerQueryManager.h"
|
#include "VkTimerQueryManager.h"
|
||||||
#include "MG_Util/Math/VectorTypes.h"
|
#include "MG_Util/Math/VectorTypes.h"
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
#include <MG_Backend/BackendObject.h>
|
|
||||||
#include <vk_mem_alloc.h>
|
#include <vk_mem_alloc.h>
|
||||||
|
|
||||||
#include "../VkIncludes.h"
|
#include "../VkIncludes.h"
|
||||||
@@ -198,9 +197,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLbitfield mask, GLenum filter);
|
GLbitfield mask, GLenum filter);
|
||||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
|
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
|
||||||
GLint x, GLint y, GLsizei width, GLsizei height);
|
GLint x, GLint y, GLsizei width, GLsizei height);
|
||||||
void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
|
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const CopyImageEndpoint& dstEndpoint,
|
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||||
void GenerateMipmap(GLenum target);
|
void GenerateMipmap(GLenum target);
|
||||||
|
|||||||
@@ -18,11 +18,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
//
|
//
|
||||||
// Native subgroups are the implementation whenever the device has them, whatever
|
// Native subgroups are the implementation whenever the device has them, whatever
|
||||||
// their width - subgroup operations execute on the hardware paths they were made
|
// their width - subgroup operations execute on the hardware paths they were made
|
||||||
// for. Module-level repairs keep the GL contract intact around them:
|
// for. Two module-level repairs keep the GL contract intact around them:
|
||||||
// - FixIterationRPSubgroupScratchPass patches the one known pack bug: iterationRP's
|
// - FixIterationRPSubgroupScratchPass patches the one known pack bug: iterationRP's
|
||||||
// prefixSumCache[32], under-declared for sub-16-lane devices (8-lane lavapipe);
|
// prefixSumCache[32], under-declared for sub-16-lane devices (8-lane lavapipe);
|
||||||
// - FixIterationRPBarrierPass repairs Program 203's race between two reductions
|
|
||||||
// reusing that scratch, when explicitly enabled;
|
|
||||||
// - DeriveNumSubgroupsPass replaces the one builtin drivers get wrong
|
// - DeriveNumSubgroupsPass replaces the one builtin drivers get wrong
|
||||||
// (gl_NumSubgroups) with the value the rest of the topology implies.
|
// (gl_NumSubgroups) with the value the rest of the topology implies.
|
||||||
// The 32-lane shared-memory emulation (EmulateSubgroupsPass) is a LAST RESORT for
|
// The 32-lane shared-memory emulation (EmulateSubgroupsPass) is a LAST RESORT for
|
||||||
@@ -49,10 +47,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MG_Config::QuirkOverride::ForceOff;
|
MG_Config::QuirkOverride::ForceOff;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline Bool ShouldFixIterationRPBarrier() {
|
|
||||||
return MG_Config::Features.IterationRPFixBarrier;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Bool ShouldDeriveNumSubgroups() {
|
inline Bool ShouldDeriveNumSubgroups() {
|
||||||
// Auto is ON: gl_NumSubgroups must agree with the gl_SubgroupID range for the GL
|
// Auto is ON: gl_NumSubgroups must agree with the gl_SubgroupID range for the GL
|
||||||
// contract to hold, and the derived ceil() value is the one the renderer can pin
|
// contract to hold, and the derived ceil() value is the one the renderer can pin
|
||||||
|
|||||||
@@ -44,4 +44,3 @@ add_subdirectory(Program)
|
|||||||
add_subdirectory(Buffer)
|
add_subdirectory(Buffer)
|
||||||
add_subdirectory(Driver)
|
add_subdirectory(Driver)
|
||||||
add_subdirectory(Container)
|
add_subdirectory(Container)
|
||||||
add_subdirectory(Transpile)
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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}
|
|
||||||
)
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -108,12 +108,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
|
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||||
if (program != nullptr) {
|
if (program != nullptr) {
|
||||||
// A geometry stage writes what it emits, not what the draw assembled, and the
|
|
||||||
// amplification factor lives in the shader. Record that this span contained such
|
|
||||||
// a draw so the transform feedback queries keep their backend result for it.
|
|
||||||
if (program->GetShaderIndexByStage(ShaderStage::Geometry) >= 0) {
|
|
||||||
MG_State::pGLContext->AddTransformFeedbackGeometryCaptureDraw();
|
|
||||||
}
|
|
||||||
// Capacity in captured vertices = the tightest bound buffer.
|
// Capacity in captured vertices = the tightest bound buffer.
|
||||||
Uint64 capacityVertices = ~0ull;
|
Uint64 capacityVertices = ~0ull;
|
||||||
for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) {
|
for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) {
|
||||||
@@ -133,11 +127,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives);
|
MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives);
|
||||||
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
|
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
|
||||||
// Only draws that get this far are in the written counter at all. The instanced and
|
|
||||||
// indirect entry points never call this function, so a span that contains one is NOT
|
|
||||||
// fully accounted, and the queries must be able to tell: they compare this counter's
|
|
||||||
// delta against zero before standing in for the backend's own result.
|
|
||||||
MG_State::pGLContext->AddTransformFeedbackAccountedCaptureDraw();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every primitive mode a draw command accepts (GL 4.6 core table 10.1, plus
|
// Every primitive mode a draw command accepts (GL 4.6 core table 10.1, plus
|
||||||
@@ -162,23 +151,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The `mode` INVALID_ENUM in isolation, so a draw entry point can raise it BEFORE any of the
|
|
||||||
// state-dependent INVALID_OPERATIONs below. GL 4.6 core 10.4 makes a bad mode INVALID_ENUM
|
|
||||||
// unconditionally, while "no current program" is not even a spec-listed draw error - it is
|
|
||||||
// MobileGL's own null-dereference guard - so it must never shadow the enum check
|
|
||||||
// (KHR-GL31.api.coverage calls glDrawArraysInstanced/glDrawElementsInstanced with mode
|
|
||||||
// GL_POINTS-1 against a bare context and pins GL_INVALID_ENUM).
|
|
||||||
static Bool ValidatePrimitiveModeEnum(const char* functionName, GLenum mode) {
|
|
||||||
if (IsAcceptedPrimitiveMode(mode)) return true;
|
|
||||||
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidEnum,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "mode is not an accepted primitive type."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
|
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
|
||||||
if (!ValidatePrimitiveModeEnum(functionName, mode)) {
|
if (!IsAcceptedPrimitiveMode(mode)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "mode is not an accepted primitive type."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -619,14 +596,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride);
|
MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
|
MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
|
||||||
@@ -740,14 +715,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||||
const void* indices, GLint basevertex) {
|
const void* indices, GLint basevertex) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawRangeElementsBaseVertex_Backend(mode, start, end, count, type, indices, basevertex);
|
DrawRangeElementsBaseVertex_Backend(mode, start, end, count, type, indices, basevertex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
|
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawRangeElements_Backend(mode, start, end, count, type, indices);
|
DrawRangeElements_Backend(mode, start, end, count, type, indices);
|
||||||
@@ -755,7 +728,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||||
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
|
DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
|
||||||
@@ -764,7 +736,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||||
GLsizei instancecount, GLint basevertex) {
|
GLsizei instancecount, GLint basevertex) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex);
|
DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex);
|
||||||
@@ -772,21 +743,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||||
GLsizei instancecount, GLuint baseinstance) {
|
GLsizei instancecount, GLuint baseinstance) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance);
|
DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
|
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawElementsInstanced_Backend(mode, count, type, indices, instancecount);
|
DrawElementsInstanced_Backend(mode, count, type, indices, instancecount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||||
@@ -796,21 +764,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||||
GLuint baseinstance) {
|
GLuint baseinstance) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance);
|
DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
|
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
DrawArraysInstanced_Backend(mode, first, count, instancecount);
|
DrawArraysInstanced_Backend(mode, first, count, instancecount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
|
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
|
||||||
@@ -818,7 +783,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
|
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
AccountTransformFeedbackPrimitives(mode, count);
|
AccountTransformFeedbackPrimitives(mode, count);
|
||||||
@@ -826,7 +790,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
AccountTransformFeedbackPrimitives(mode, count);
|
AccountTransformFeedbackPrimitives(mode, count);
|
||||||
@@ -834,7 +797,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
if (drawcount < 0) {
|
if (drawcount < 0) {
|
||||||
@@ -848,7 +810,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
||||||
GLsizei drawcount) {
|
GLsizei drawcount) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
MultiDrawElements_Backend(mode, count, type, indices, drawcount);
|
MultiDrawElements_Backend(mode, count, type, indices, drawcount);
|
||||||
@@ -856,7 +817,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
|
||||||
GLsizei drawcount, const GLint* basevertex) {
|
GLsizei drawcount, const GLint* basevertex) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex);
|
MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex);
|
||||||
@@ -867,7 +827,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||||
if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
|
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
AccountTransformFeedbackPrimitives(mode, count);
|
AccountTransformFeedbackPrimitives(mode, count);
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||||
#include <MG_Impl/GLImpl/Texture/Validators.h>
|
#include <MG_Impl/GLImpl/Texture/Validators.h>
|
||||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
|
||||||
#include <MG_State/GLState/ErrorState/Error.h>
|
#include <MG_State/GLState/ErrorState/Error.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||||
@@ -618,17 +617,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||||
return std::numeric_limits<Int>::max();
|
return std::numeric_limits<Int>::max();
|
||||||
}
|
}
|
||||||
return GetAdvertisedMaxSamples();
|
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own
|
// GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own, lower
|
||||||
// (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
|
// one (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION.
|
||||||
// The multisample TEXTURE path resolves the limit per format the same way
|
// The multisample TEXTURE path already resolves the limit per format
|
||||||
// (GL_Texture.cpp, GetMaxSupportedTextureSamples). Both are floored to the value MobileGL
|
// (GL_Texture.cpp, GetMaxTextureSamplesForFormat); renderbuffers only ever compared
|
||||||
// advertises: on a driver where the two differ - Adreno reports GL_MAX_SAMPLES 4 and
|
// against GL_MAX_SAMPLES, so on a driver where the two differ - Adreno reports
|
||||||
// GL_MAX_INTEGER_SAMPLES 1 - rejecting the advertised count here only moves the failure
|
// GL_MAX_SAMPLES 4 and GL_MAX_INTEGER_SAMPLES 1 - an integer renderbuffer accepted a
|
||||||
// from the driver into MobileGL, so the frontend accepts it and the backend clamps the
|
// sample count the format cannot deliver, and said GL_NO_ERROR about it.
|
||||||
// count it actually hands the driver.
|
|
||||||
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
|
Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) {
|
||||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||||
return std::numeric_limits<Int>::max();
|
return std::numeric_limits<Int>::max();
|
||||||
@@ -647,10 +645,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!isIntegerFormat) {
|
if (!isIntegerFormat) {
|
||||||
return GetMaxRenderbufferSamples_State();
|
return GetMaxRenderbufferSamples_State();
|
||||||
}
|
}
|
||||||
// Per-format still, but never below the ceiling glGetIntegerv(GL_MAX_SAMPLES) promised:
|
return std::max(dynamicParameters.MaxIntegerSamples, 1);
|
||||||
// the driver's raw GL_MAX_INTEGER_SAMPLES stays the *backend* limit and the backend
|
|
||||||
// clamps to it, while the frontend honours what it advertised.
|
|
||||||
return std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
|
Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) {
|
||||||
@@ -3153,55 +3148,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GetNamedFramebufferAttachmentParameteriv_State(framebuffer, attachment, pname, params);
|
GetNamedFramebufferAttachmentParameteriv_State(framebuffer, attachment, pname, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The three argument errors GL 4.6 core 18.3.1 asks a blit for. They have to be raised here,
|
|
||||||
// in the backend-independent frontend: DirectGLES drains the driver's error queue around the
|
|
||||||
// blit on purpose (that is how the resolve fallback probes the driver), so an ES-side
|
|
||||||
// rejection never reaches the application and glGetError() answered GL_NO_ERROR for a call
|
|
||||||
// the spec requires to fail (KHR-GL30.api.coverage's glBlitFramebuffer sub-check). DirectVulkan
|
|
||||||
// already dropped the bad-filter and LINEAR-with-depth/stencil calls on the floor with a log
|
|
||||||
// line (VulkanRenderer::BlitFramebuffer), so the only thing that changes for it is that the
|
|
||||||
// error is now visible where the spec says it should be.
|
|
||||||
static Bool ValidateBlitMaskAndFilter(const char* functionName, GLbitfield mask, GLenum filter) {
|
|
||||||
constexpr GLbitfield kBlitMaskBits = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
|
|
||||||
if ((mask & ~kBlitMaskBits) != 0) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
|
||||||
"mask contains bits other than GL_COLOR_BUFFER_BIT, "
|
|
||||||
"GL_DEPTH_BUFFER_BIT and GL_STENCIL_BUFFER_BIT."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (filter != GL_NEAREST && filter != GL_LINEAR) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidEnum,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
|
||||||
"filter must be GL_NEAREST or GL_LINEAR."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Depth and stencil have no meaningful interpolation, so GL_LINEAR is rejected outright
|
|
||||||
// rather than downgraded - even when the mask also carries the colour bit.
|
|
||||||
if (filter == GL_LINEAR && (mask & (GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT)) != 0) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
|
||||||
"GL_LINEAR filtering is not allowed when mask includes "
|
|
||||||
"GL_DEPTH_BUFFER_BIT or GL_STENCIL_BUFFER_BIT."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BlitNamedFramebuffer(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1,
|
void BlitNamedFramebuffer(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1,
|
||||||
GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask,
|
GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask,
|
||||||
GLenum filter) {
|
GLenum filter) {
|
||||||
if (!ValidateBlitMaskAndFilter(__func__, mask, filter)) return;
|
|
||||||
BlitNamedFramebuffer_State(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
BlitNamedFramebuffer_State(readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1,
|
||||||
dstY1, mask, filter);
|
dstY1, mask, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
|
||||||
GLint dstY1, GLbitfield mask, GLenum filter) {
|
GLint dstY1, GLbitfield mask, GLenum filter) {
|
||||||
if (!ValidateBlitMaskAndFilter(__func__, mask, filter)) return;
|
|
||||||
BlitFramebuffer_Backend(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
BlitFramebuffer_Backend(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -213,23 +213,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return ClampBlockCountToBindingPoints(blockCount, BufferTarget::ShaderStorage);
|
return ClampBlockCountToBindingPoints(blockCount, BufferTarget::ShaderStorage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS answers. Backend-derived, and NOT a
|
|
||||||
// constant to be "restored" - these used to return a flat 16 for vertex, geometry and
|
|
||||||
// both tessellation stages, which is wrong on any host that does not serve storage
|
|
||||||
// blocks in those stages. Zero is a legal answer: GL 4.6 table 23.64 and ES 3.2 table
|
|
||||||
// 21.44 both set the minimum at 0 for every graphics stage except fragment, which is
|
|
||||||
// why the conformance suite gates each such test on the query instead of assuming it.
|
|
||||||
// ARM's GLES driver reports 0 for all four (a Mali-G925 does), and advertising 16 there
|
|
||||||
// bought nothing: the program still failed to link inside the backend, the frontend
|
|
||||||
// still reported LINK_STATUS as true, and every draw with it silently rendered nothing.
|
|
||||||
GLint StageStorageBlockCount(Int MG_Backend::DynamicBackendParameters::*stageLimit) {
|
|
||||||
static const MG_Backend::DynamicBackendParameters kBackendlessDefaults{};
|
|
||||||
const MG_Backend::DynamicBackendParameters& parameters =
|
|
||||||
MG_Backend::pActiveBackendObject ? MG_Backend::pActiveBackendObject->GetDynamicParameters()
|
|
||||||
: kBackendlessDefaults;
|
|
||||||
return ClampStorageBlockCount(static_cast<GLint>(parameters.*stageLimit));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) {
|
bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) {
|
||||||
if (pname == GL_DRAW_BUFFER) {
|
if (pname == GL_DRAW_BUFFER) {
|
||||||
drawBufferIndex = 0;
|
drawBufferIndex = 0;
|
||||||
@@ -439,18 +422,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so the driver's value is floored
|
|
||||||
// before it is advertised. Every other multisample ceiling MobileGL advertises has to be
|
|
||||||
// floored the same way: promising 4 samples globally while answering GL_MAX_INTEGER_SAMPLES
|
|
||||||
// 1 - which is exactly what Adreno reports - makes the frontend reject the very count it
|
|
||||||
// just told the application to use. The backends clamp the realised count instead.
|
|
||||||
GLint GetAdvertisedMaxSamples() {
|
|
||||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
|
||||||
return kFrontendMaxSamples;
|
|
||||||
}
|
|
||||||
return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, kFrontendMaxSamples);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
||||||
const GLubyte* GetString(GLenum name) {
|
const GLubyte* GetString(GLenum name) {
|
||||||
static String vendorString;
|
static String vendorString;
|
||||||
@@ -1565,7 +1536,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = kFrontendMaxFragmentAtomicCounters;
|
*params = kFrontendMaxFragmentAtomicCounters;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
|
||||||
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxFragmentShaderStorageBlocks);
|
*params = ClampStorageBlockCount(16); // TODO
|
||||||
return;
|
return;
|
||||||
case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
|
case GL_MAX_FRAGMENT_INPUT_COMPONENTS:
|
||||||
*params = kFrontendMaxFragmentInputComponents;
|
*params = kFrontendMaxFragmentInputComponents;
|
||||||
@@ -1591,7 +1562,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = kFrontendMaxGeometryAtomicCounterBuffers;
|
*params = kFrontendMaxGeometryAtomicCounterBuffers;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
||||||
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxGeometryShaderStorageBlocks);
|
*params = ClampStorageBlockCount(16); // TODO
|
||||||
return;
|
return;
|
||||||
case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
|
case GL_MAX_GEOMETRY_INPUT_COMPONENTS:
|
||||||
*params = kFrontendMaxGeometryInputComponents;
|
*params = kFrontendMaxGeometryInputComponents;
|
||||||
@@ -1662,11 +1633,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = 0;
|
*params = 0;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
|
||||||
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessControlShaderStorageBlocks);
|
*params = ClampStorageBlockCount(16); // TODO
|
||||||
return;
|
return;
|
||||||
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
|
||||||
*params =
|
*params = ClampStorageBlockCount(16); // TODO
|
||||||
StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks);
|
|
||||||
return;
|
return;
|
||||||
case GL_MAX_TEXTURE_LOD_BIAS:
|
case GL_MAX_TEXTURE_LOD_BIAS:
|
||||||
*params = 15; // TODO
|
*params = 15; // TODO
|
||||||
@@ -1692,7 +1662,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
|
: MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||||
*params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxVertexShaderStorageBlocks);
|
*params = ClampStorageBlockCount(16); // TODO
|
||||||
return;
|
return;
|
||||||
case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
|
case GL_MAX_VERTEX_UNIFORM_COMPONENTS:
|
||||||
*params = kFrontendMaxVertexUniformComponents;
|
*params = kFrontendMaxVertexUniformComponents;
|
||||||
@@ -2147,7 +2117,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = dynamicParameters.MaxClipDistances;
|
*params = dynamicParameters.MaxClipDistances;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_COLOR_TEXTURE_SAMPLES:
|
case GL_MAX_COLOR_TEXTURE_SAMPLES:
|
||||||
*params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples());
|
*params = dynamicParameters.MaxColorTextureSamples;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
|
case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
|
||||||
*params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents,
|
*params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents,
|
||||||
@@ -2177,7 +2147,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = dynamicParameters.MaxCubeMapTextureSize;
|
*params = dynamicParameters.MaxCubeMapTextureSize;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
|
case GL_MAX_DEPTH_TEXTURE_SAMPLES:
|
||||||
*params = std::max(dynamicParameters.MaxDepthTextureSamples, GetAdvertisedMaxSamples());
|
*params = dynamicParameters.MaxDepthTextureSamples;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_FRAMEBUFFER_WIDTH:
|
case GL_MAX_FRAMEBUFFER_WIDTH:
|
||||||
*params = dynamicParameters.MaxFramebufferWidth;
|
*params = dynamicParameters.MaxFramebufferWidth;
|
||||||
@@ -2204,7 +2174,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = dynamicParameters.MaxComputeImageUniforms;
|
*params = dynamicParameters.MaxComputeImageUniforms;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_INTEGER_SAMPLES:
|
case GL_MAX_INTEGER_SAMPLES:
|
||||||
*params = std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples());
|
*params = dynamicParameters.MaxIntegerSamples;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_RENDERBUFFER_SIZE:
|
case GL_MAX_RENDERBUFFER_SIZE:
|
||||||
*params = dynamicParameters.MaxRenderbufferSize;
|
*params = dynamicParameters.MaxRenderbufferSize;
|
||||||
@@ -2370,7 +2340,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
: dynamicParameters.MaxDrawBuffers;
|
: dynamicParameters.MaxDrawBuffers;
|
||||||
break;
|
break;
|
||||||
case GL_MAX_SAMPLES:
|
case GL_MAX_SAMPLES:
|
||||||
*params = GetAdvertisedMaxSamples();
|
*params = std::max(dynamicParameters.MaxSamples, kFrontendMaxSamples);
|
||||||
break;
|
break;
|
||||||
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
|
case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||||
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2.
|
// Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2.
|
||||||
|
|||||||
@@ -24,8 +24,4 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||||
GLenum GetError();
|
GLenum GetError();
|
||||||
GLenum GetGraphicsResetStatus();
|
GLenum GetGraphicsResetStatus();
|
||||||
// The GL_MAX_SAMPLES value MobileGL advertises, i.e. the driver's value floored to the GL
|
|
||||||
// core minimum. Frontend multisample validators have to honour this ceiling for every
|
|
||||||
// format, otherwise MobileGL rejects a sample count it advertised itself.
|
|
||||||
GLint GetAdvertisedMaxSamples();
|
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -31,15 +31,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
Bool ended = false;
|
Bool ended = false;
|
||||||
Bool resultCached = false;
|
Bool resultCached = false;
|
||||||
Uint64 cachedResult = 0;
|
Uint64 cachedResult = 0;
|
||||||
// The transform feedback primitive counter matching this query's target, at
|
// Transform feedback primitive counter at BeginQuery time.
|
||||||
// BeginQuery time.
|
|
||||||
Uint64 counterSnapshot = 0;
|
Uint64 counterSnapshot = 0;
|
||||||
// Capture-draw counters at BeginQuery time: how many capture draws the CPU
|
|
||||||
// accounting had reproduced exactly, and how many of those it could not (a
|
|
||||||
// geometry stage amplifies). Their deltas decide whether the CPU result may
|
|
||||||
// stand in for the backend's.
|
|
||||||
Uint64 accountedCaptureDrawSnapshot = 0;
|
|
||||||
Uint64 geometryCaptureDrawSnapshot = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Query calls may arrive from any thread (launchers migrate the context
|
// Query calls may arrive from any thread (launchers migrate the context
|
||||||
@@ -129,46 +122,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
g_activeTimeElapsedQueryId = 0;
|
g_activeTimeElapsedQueryId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The CPU accounting counter a transform feedback query target reads: what the capture
|
|
||||||
// buffers took for GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, and everything the capture
|
|
||||||
// stage assembled - a paused span included - for GL_PRIMITIVES_GENERATED. One counter
|
|
||||||
// for both targets would report the clamped written count as the generated one.
|
|
||||||
Uint64 TransformFeedbackCounterForTarget(GLenum target) {
|
|
||||||
return target == GL_PRIMITIVES_GENERATED
|
|
||||||
? MG_State::pGLContext->GetTransformFeedbackGeneratedCounter()
|
|
||||||
: MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The span's CPU accounting delta. Saturating: a snapshot left above its counter (a
|
|
||||||
// context switch between Begin and End, a counter that never moved) would otherwise
|
|
||||||
// wrap to 2^64-1, which GetQueryObjectuiv hands the app as 4294967295.
|
|
||||||
Uint64 TransformFeedbackCpuResult(const QueryObject* queryObject) {
|
|
||||||
const Uint64 counter = TransformFeedbackCounterForTarget(queryObject->target);
|
|
||||||
return counter > queryObject->counterSnapshot ? counter - queryObject->counterSnapshot : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whether this ended span's result should come from the CPU accounting rather than from
|
|
||||||
// the backend query it also ran. Three conditions, all necessary:
|
|
||||||
// * the backend asked for it (DirectGLES, whose ES driver counter is the unreliable
|
|
||||||
// one; DirectVulkan never sets the bit and so is untouched by any of this);
|
|
||||||
// * the target is PRIMITIVES_WRITTEN. GL_PRIMITIVES_GENERATED counts primitives
|
|
||||||
// whether or not a capture is active, and the accounting only ever sees capture
|
|
||||||
// draws, so the backend's counter is the more complete answer there;
|
|
||||||
// * the span was fully accounted: at least one capture draw reached the accounting
|
|
||||||
// (the instanced, indirect and multi-draw entry points do not call it at all, so a
|
|
||||||
// span made of those is invisible to it) and none of them amplified through a
|
|
||||||
// geometry stage, which the CPU cannot model.
|
|
||||||
Bool PrefersCpuTransformFeedbackResult(const QueryObject* queryObject) {
|
|
||||||
if (!MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting) return false;
|
|
||||||
if (queryObject->target != GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN) return false;
|
|
||||||
if (MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws() !=
|
|
||||||
queryObject->geometryCaptureDrawSnapshot) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws() !=
|
|
||||||
queryObject->accountedCaptureDrawSnapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shared GetQueryObject* implementation. Returns false when an error
|
// Shared GetQueryObject* implementation. Returns false when an error
|
||||||
// was recorded and no value should be written back. `outValueProduced`, when given,
|
// was recorded and no value should be written back. `outValueProduced`, when given,
|
||||||
// additionally distinguishes "succeeded with a value" from "succeeded but the result is not
|
// additionally distinguishes "succeeded with a value" from "succeeded but the result is not
|
||||||
@@ -454,11 +407,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
|
||||||
queryObject->backendHandle =
|
queryObject->backendHandle =
|
||||||
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
|
||||||
queryObject->counterSnapshot = TransformFeedbackCounterForTarget(target);
|
queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
|
||||||
queryObject->accountedCaptureDrawSnapshot =
|
|
||||||
MG_State::pGLContext->GetTransformFeedbackAccountedCaptureDraws();
|
|
||||||
queryObject->geometryCaptureDrawSnapshot =
|
|
||||||
MG_State::pGLContext->GetTransformFeedbackGeometryCaptureDraws();
|
|
||||||
} else if (isOcclusionQuery) {
|
} else if (isOcclusionQuery) {
|
||||||
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
|
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
|
||||||
} else {
|
} else {
|
||||||
@@ -499,21 +448,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
|
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
|
||||||
endXfbPrimitivesQuery(queryObject->backendHandle);
|
endXfbPrimitivesQuery(queryObject->backendHandle);
|
||||||
}
|
}
|
||||||
}
|
// Result comes from the GPU query at read time.
|
||||||
// A backend query that is not going to be read is released here, not left to be
|
} else {
|
||||||
// collected later: the span is over, the driver object has nothing left to say.
|
queryObject->cachedResult =
|
||||||
// Ending it first is what makes that legal.
|
MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot;
|
||||||
if (!queryObject->backendHandle || PrefersCpuTransformFeedbackResult(queryObject)) {
|
|
||||||
if (queryObject->backendHandle) {
|
|
||||||
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
|
||||||
deleteBackendQuery(queryObject->backendHandle);
|
|
||||||
}
|
|
||||||
queryObject->backendHandle = nullptr;
|
|
||||||
}
|
|
||||||
queryObject->cachedResult = TransformFeedbackCpuResult(queryObject);
|
|
||||||
queryObject->resultCached = true;
|
queryObject->resultCached = true;
|
||||||
}
|
}
|
||||||
// Otherwise the result comes from the GPU query at read time.
|
|
||||||
queryObject->active = false;
|
queryObject->active = false;
|
||||||
queryObject->ended = true;
|
queryObject->ended = true;
|
||||||
activeQueryId = 0;
|
activeQueryId = 0;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
#include "GL_Sync.h"
|
#include "GL_Sync.h"
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -36,22 +35,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
GLsync FenceSync(GLenum condition, GLbitfield flags) {
|
GLsync FenceSync(GLenum condition, GLbitfield flags) {
|
||||||
// GL 4.6 core 4.1.2: GL_SYNC_GPU_COMMANDS_COMPLETE is the only condition and the only
|
|
||||||
// legal flags value is zero; both violations return 0 rather than a handle. A caller that
|
|
||||||
// then hands the 0 back to glDeleteSync hits the glDeleteSync(0) no-op below.
|
|
||||||
if (condition != GL_SYNC_GPU_COMMANDS_COMPLETE) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidEnum,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
|
||||||
"condition must be GL_SYNC_GPU_COMMANDS_COMPLETE."));
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
if (flags != 0) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "flags must be zero."));
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
auto* syncObject = new SyncObject;
|
auto* syncObject = new SyncObject;
|
||||||
syncObject->condition = condition;
|
syncObject->condition = condition;
|
||||||
syncObject->flags = flags;
|
syncObject->flags = flags;
|
||||||
@@ -81,18 +64,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout) {
|
||||||
// GL 4.6 core 4.1.2: the server-side wait takes no flags and no finite timeout - both
|
|
||||||
// arguments exist only to be forward-compatible, and anything else is INVALID_VALUE.
|
|
||||||
// Neither backend ever honored a nonzero timeout (DirectGLES hard-codes
|
|
||||||
// 0/GL_TIMEOUT_IGNORED, DirectVulkan's queue ordering makes the wait implicit), so
|
|
||||||
// rejecting the call loses no wait that used to happen.
|
|
||||||
if (flags != 0 || timeout != GL_TIMEOUT_IGNORED) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
|
||||||
"flags must be zero and timeout must be GL_TIMEOUT_IGNORED."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto* syncObject = FindSyncObject(sync);
|
const auto* syncObject = FindSyncObject(sync);
|
||||||
if (!syncObject) {
|
if (!syncObject) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -474,48 +474,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
target == TextureTarget::Texture2DMultisampleArray;
|
target == TextureTarget::Texture2DMultisampleArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The largest count the backend actually probed for this format on this target, or 0 when
|
Int GetMaxSupportedTextureSamples(TextureInternalFormat textureInternalFormat) {
|
||||||
// it has no answer for the pair. Both backends build the list in descending order.
|
|
||||||
Int GetProbedMaxTextureSamples(TextureTarget textureTarget, TextureInternalFormat textureInternalFormat) {
|
|
||||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const SizeT targetIndex = MG_Backend::GetFormatCapabilityTargetIndex(textureTarget);
|
|
||||||
const SizeT formatIndex = static_cast<SizeT>(textureInternalFormat);
|
|
||||||
if (targetIndex >= MG_Backend::kFormatCapabilityTargetCount ||
|
|
||||||
formatIndex >= MG_Backend::kFormatCapabilityFormatCount) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const auto& sampleCounts =
|
|
||||||
MG_Backend::pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
|
|
||||||
return sampleCounts.empty() ? 0 : sampleCounts.front();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The ceiling the frontend enforces, which must never be lower than the one MobileGL
|
|
||||||
// advertises: the CTS - and real applications - read GL_MAX_SAMPLES once and hand that
|
|
||||||
// exact count to glTexImage*Multisample for every format. Answering 4 there and then
|
|
||||||
// rejecting 4 here because the ES driver reports GL_MAX_INTEGER_SAMPLES 1 (Adreno) is a
|
|
||||||
// self-inconsistency, not a spec-mandated error. The backends clamp the count they hand
|
|
||||||
// the driver; the shadow state keeps reporting what the application asked for.
|
|
||||||
Int GetMaxSupportedTextureSamples(TextureTarget textureTarget,
|
|
||||||
TextureInternalFormat textureInternalFormat) {
|
|
||||||
if (MG_Backend::pActiveBackendObject == nullptr) {
|
if (MG_Backend::pActiveBackendObject == nullptr) {
|
||||||
return std::numeric_limits<Int>::max();
|
return std::numeric_limits<Int>::max();
|
||||||
}
|
}
|
||||||
|
|
||||||
const Int advertisedMaxSamples = GetAdvertisedMaxSamples();
|
|
||||||
// glGetInternalformativ(GL_SAMPLES) is answered from this very list (GetInternalformativ
|
|
||||||
// below), and GL 4.6 core 8.8 makes that query the definition of the per-format
|
|
||||||
// maximum - validating against anything else is how the two answers drifted apart.
|
|
||||||
const Int probedMaxSamples = GetProbedMaxTextureSamples(textureTarget, textureInternalFormat);
|
|
||||||
if (probedMaxSamples > 0) {
|
|
||||||
return std::max(probedMaxSamples, advertisedMaxSamples);
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||||
if (MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) ||
|
if (MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) ||
|
||||||
MG_Util::IsStencilFormatInternalFormat(textureInternalFormat)) {
|
MG_Util::IsStencilFormatInternalFormat(textureInternalFormat)) {
|
||||||
return std::max(dynamicParameters.MaxDepthTextureSamples, advertisedMaxSamples);
|
return std::max(dynamicParameters.MaxDepthTextureSamples, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat);
|
GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat);
|
||||||
@@ -528,7 +495,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
|
normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
|
||||||
return std::max(isIntegerFormat ? dynamicParameters.MaxIntegerSamples
|
return std::max(isIntegerFormat ? dynamicParameters.MaxIntegerSamples
|
||||||
: dynamicParameters.MaxColorTextureSamples,
|
: dynamicParameters.MaxColorTextureSamples,
|
||||||
advertisedMaxSamples);
|
1);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateTextureMultisampleStorage(TextureTarget textureTarget, GLsizei samples, GLsizei width,
|
Bool ValidateTextureMultisampleStorage(TextureTarget textureTarget, GLsizei samples, GLsizei width,
|
||||||
@@ -565,7 +532,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// dimensions, and GL CTS's per-case state reset (gluStateReset) clears the default
|
// dimensions, and GL CTS's per-case state reset (gluStateReset) clears the default
|
||||||
// GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with glTexImage3DMultisample(..., 0, 0, 0).
|
// GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with glTexImage3DMultisample(..., 0, 0, 0).
|
||||||
|
|
||||||
const Int maxSamples = GetMaxSupportedTextureSamples(textureTarget, textureInternalFormat);
|
const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat);
|
||||||
if (samples > maxSamples) {
|
if (samples > maxSamples) {
|
||||||
// GL specifies INVALID_OPERATION - not INVALID_VALUE - when the sample count
|
// GL specifies INVALID_OPERATION - not INVALID_VALUE - when the sample count
|
||||||
// exceeds what the format supports, and the native Adreno driver agrees.
|
// exceeds what the format supports, and the native Adreno driver agrees.
|
||||||
@@ -590,20 +557,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"AllocateMultisampleTextureStorage requires mipmap-backed storage");
|
"AllocateMultisampleTextureStorage requires mipmap-backed storage");
|
||||||
|
|
||||||
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||||
// GL 4.6 core 8.8: a zero-sized image DEALLOCATES the image rather than defining an
|
|
||||||
// empty one. Only the multisample pair cares, and it cares a great deal: the CTS's
|
|
||||||
// per-case state reset clears both DEFAULT multisample textures this way on every
|
|
||||||
// texture unit, and a "defined" 0x0 default texture stops being skipped by
|
|
||||||
// IsUndefinedDefaultTexture - it then joins the per-draw sync and bind passes on
|
|
||||||
// every unit the reset touched, and reaches an ES glTexStorage*Multisample(..., 0, 0)
|
|
||||||
// that ES 3.1 8.19 makes INVALID_VALUE on every driver there is. A proxy target holds
|
|
||||||
// no image at all, only the query result, so it keeps recording what was asked for.
|
|
||||||
if ((width <= 0 || height <= 0 || depth <= 0) &&
|
|
||||||
!TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
|
|
||||||
textureObject->SetInternalFormat(TextureInternalFormat::Unknown);
|
|
||||||
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
textureObject->SetInternalFormat(textureInternalFormat);
|
textureObject->SetInternalFormat(textureInternalFormat);
|
||||||
textureObject->SetSamples(samples);
|
textureObject->SetSamples(samples);
|
||||||
textureObject->SetFixedSampleLocations(fixedsamplelocations == GL_TRUE);
|
textureObject->SetFixedSampleLocations(fixedsamplelocations == GL_TRUE);
|
||||||
@@ -691,34 +644,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return textureObject;
|
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 {
|
namespace {
|
||||||
void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) {
|
void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -754,21 +679,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
std::format("Texture level {} is not defined.", level));
|
std::format("Texture level {} is not defined.", level));
|
||||||
return nullptr;
|
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<Uint>(level)) != GL_NONE ||
|
|
||||||
mipmapTexture->GetMipmapRequestedCompressedFormat(uploadTargets[0], static_cast<Uint>(level)) !=
|
|
||||||
GL_NONE)) {
|
|
||||||
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
|
|
||||||
"Compressed textures cannot be cleared.");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return mipmapTexture;
|
return mipmapTexture;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2240,26 +2150,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
} else {
|
} else {
|
||||||
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
|
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<GLenum>(internalformat));
|
|
||||||
if (compressedInfo.blockWidth != 0) {
|
|
||||||
textureMipmapObject->SetMipmapCompressedImage(
|
|
||||||
textureUploadTarget, level, static_cast<GLenum>(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<GLenum>(internalformat))) {
|
|
||||||
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
|
|
||||||
static_cast<GLenum>(internalformat));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!originalPixels) {
|
if (!originalPixels) {
|
||||||
@@ -2406,13 +2296,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
|
textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
|
||||||
MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1}));
|
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<GLenum>(internalformat))) {
|
|
||||||
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
|
|
||||||
static_cast<GLenum>(internalformat));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!originalPixels) {
|
if (!originalPixels) {
|
||||||
@@ -2501,13 +2384,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!isProxy) {
|
if (!isProxy) {
|
||||||
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes});
|
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<GLenum>(internalFormat))) {
|
|
||||||
textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
|
|
||||||
static_cast<GLenum>(internalFormat));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!originalPixels) {
|
if (!originalPixels) {
|
||||||
@@ -3489,9 +3365,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CopyImageSubData_Backend(const MG_Backend::CopyImageEndpoint& src,
|
void CopyImageSubData_Backend(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const MG_Backend::CopyImageEndpoint& dst,
|
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||||
auto copyImageSubData = MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData;
|
auto copyImageSubData = MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData;
|
||||||
@@ -3502,7 +3378,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"Backend does not support image-to-image copies."));
|
"Backend does not support image-to-image copies."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
copyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel, dstX,
|
copyImageSubData(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel, dstX,
|
||||||
dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3549,9 +3425,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// the ~30 entry points that reach it through a BOUND object (where the name was never
|
// 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
|
// in question and the fault is the binding), so this is a local rule rather than a
|
||||||
// change to the helper.
|
// change to the helper.
|
||||||
Bool ValidateCopyImageObjectExists(const MG_Backend::CopyImageEndpoint& endpoint,
|
Bool ValidateCopyImageObjectExists(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||||
const char* endpointName) {
|
const char* endpointName) {
|
||||||
if (endpoint.Exists()) return true;
|
if (textureObject) return true;
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
@@ -3575,106 +3451,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
|
MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 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<GenericErrorInfo>("MG_Impl/GLImpl", caller, "A renderbuffer has only level 0."));
|
|
||||||
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();
|
|
||||||
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,
|
|
||||||
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
|
} // namespace
|
||||||
|
|
||||||
Bool ValidateCopyImageSubData_State(const MG_Backend::CopyImageEndpoint& src,
|
Bool ValidateCopyImageSubData_State(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY,
|
||||||
const MG_Backend::CopyImageEndpoint& dst,
|
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||||
if (!ValidateCopyImageObjectExists(src, "source") ||
|
if (!ValidateCopyImageObjectExists(srcTexture, "source") ||
|
||||||
!ValidateCopyImageObjectExists(dst, "destination")) {
|
!ValidateCopyImageObjectExists(dstTexture, "destination")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// GL_RENDERBUFFER has no TextureTarget to convert to, and it needs none: it is its own
|
const auto srcTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
|
||||||
// whole-image target, and the endpoint that carries it was resolved from the renderbuffer
|
const auto dstTextureTarget = MG_Util::ConvertGLEnumToTextureTarget(dstTarget);
|
||||||
// namespace, so it matches its object by construction.
|
if (!TextureImpl::ValidateTextureTarget(srcTextureTarget) ||
|
||||||
const auto srcTextureTarget =
|
!TextureImpl::ValidateTextureTarget(dstTextureTarget)) {
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
|
// GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
|
||||||
@@ -3682,8 +3473,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
|
if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ValidateCopyImageTargetMatchesObject(src.Texture, srcTextureTarget, "source") ||
|
if (!ValidateCopyImageTargetMatchesObject(srcTexture, srcTextureTarget, "source") ||
|
||||||
!ValidateCopyImageTargetMatchesObject(dst.Texture, dstTextureTarget, "destination")) {
|
!ValidateCopyImageTargetMatchesObject(dstTexture, dstTextureTarget, "destination")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
|
if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
|
||||||
@@ -3697,8 +3488,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// driver as an out-of-range mip index - on Adreno that is a SIGSEGV inside
|
// 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
|
// 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.
|
// the whole glcts process. The answer the spec asks for is GL_INVALID_VALUE.
|
||||||
if (!ValidateCopyImageEndpointLevelExists(src, srcLevel, __func__) ||
|
if (!TextureImpl::ValidateTextureLevelExists(srcTexture, srcLevel, __func__) ||
|
||||||
!ValidateCopyImageEndpointLevelExists(dst, dstLevel, __func__)) {
|
!TextureImpl::ValidateTextureLevelExists(dstTexture, dstLevel, __func__)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) {
|
if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) {
|
||||||
@@ -3714,41 +3505,37 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// A multisample image can only be copied to one with the same sample count, and a
|
// 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
|
// single-sample image reports zero - so this one comparison is also what rejects
|
||||||
// copying between a multisample target and a non-multisample one.
|
// copying between a multisample target and a non-multisample one.
|
||||||
const Int srcSamples = GetCopyImageEndpointSamples(src);
|
if (srcTexture->GetSamples() != dstTexture->GetSamples()) {
|
||||||
const Int dstSamples = GetCopyImageEndpointSamples(dst);
|
|
||||||
if (srcSamples != dstSamples) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
"MG_Impl/GLImpl", __func__,
|
"MG_Impl/GLImpl", __func__,
|
||||||
std::format("The two images have different sample counts ({} vs. {}).",
|
std::format("The two images have different sample counts ({} vs. {}).",
|
||||||
srcSamples, dstSamples)));
|
srcTexture->GetSamples(), dstTexture->GetSamples())));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 18.3.2: both images must be complete. An incomplete one has no defined texels to copy
|
// 18.3.2: both images must be complete. An incomplete one has no defined texels to copy
|
||||||
// and no defined storage to copy into.
|
// and no defined storage to copy into.
|
||||||
const Bool srcComplete = IsCopyImageEndpointComplete(src);
|
if (!srcTexture->IsComplete() || !dstTexture->IsComplete()) {
|
||||||
const Bool dstComplete = IsCopyImageEndpointComplete(dst);
|
|
||||||
if (!srcComplete || !dstComplete) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
"MG_Impl/GLImpl", __func__,
|
"MG_Impl/GLImpl", __func__,
|
||||||
std::format("A copied image is incomplete (source complete: {}, destination complete: {}).",
|
std::format("A copied image is incomplete (source complete: {}, destination complete: {}).",
|
||||||
srcComplete, dstComplete)));
|
srcTexture->IsComplete(), dstTexture->IsComplete())));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto srcUploadTarget = GetPrimaryUploadTarget(src.Texture);
|
const auto srcUploadTarget = GetPrimaryUploadTarget(srcTexture);
|
||||||
const auto dstUploadTarget = GetPrimaryUploadTarget(dst.Texture);
|
const auto dstUploadTarget = GetPrimaryUploadTarget(dstTexture);
|
||||||
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||||
GetCopyImageEndpointFormat(src), GetCopyImageEndpointCompressedFormat(src, srcUploadTarget, srcLevel));
|
srcTexture->GetFormat(), GetCompressedLevelFormat(srcTexture, srcUploadTarget, srcLevel));
|
||||||
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
|
||||||
GetCopyImageEndpointFormat(dst), GetCopyImageEndpointCompressedFormat(dst, dstUploadTarget, dstLevel));
|
dstTexture->GetFormat(), GetCompressedLevelFormat(dstTexture, dstUploadTarget, dstLevel));
|
||||||
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
|
if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const IntVec3 srcLevelSize = GetCopyImageEndpointLevelSize(src, srcUploadTarget, srcLevel);
|
const IntVec3 srcLevelSize = GetCopyImageLevelSize(srcTexture, srcUploadTarget, srcLevel);
|
||||||
const IntVec3 dstLevelSize = GetCopyImageEndpointLevelSize(dst, dstUploadTarget, dstLevel);
|
const IntVec3 dstLevelSize = GetCopyImageLevelSize(dstTexture, dstUploadTarget, dstLevel);
|
||||||
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
|
if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
|
||||||
srcLevelSize.x(), srcLevelSize.y(), "source") ||
|
srcLevelSize.x(), srcLevelSize.y(), "source") ||
|
||||||
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
|
!TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
|
||||||
@@ -4264,14 +4051,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL 4.6 core 8.11.4 names cube completeness as the only completeness a readback requires,
|
// For a cube map this is exactly cube completeness: IsComplete() wants all six faces.
|
||||||
// and for a cube map that is exactly what IsComplete() answers (all six faces defined at
|
if (!textureObject->IsComplete()) {
|
||||||
// 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(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture is incomplete"));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture is incomplete"));
|
||||||
@@ -4303,8 +4084,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Shared format/type/internal-format matrix (packed-type pairing, depth-vs-color mismatch,
|
// Shared format/type/internal-format matrix (packed-type pairing, depth-vs-color mismatch,
|
||||||
// integer-ness). Also rejects a STENCIL_INDEX readback of anything but stencil-only
|
// integer-ness). Also rejects STENCIL_INDEX readback, which needs GL_ARB_texture_stencil8
|
||||||
// storage, which is the only pairing GL 4.4 / ARB_texture_stencil8 ever made legal.
|
// (not advertised by MobileGL).
|
||||||
if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
|
if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
|
||||||
textureInputFormat, textureObject->GetFormat(), texturePixelDataType)) {
|
textureInputFormat, textureObject->GetFormat(), texturePixelDataType)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -4330,48 +4111,33 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const auto* textureMipmapObject =
|
const auto* textureMipmapObject =
|
||||||
static_cast<const MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
static_cast<const MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||||
const auto& uploadTargets = textureObject->GetUploadTargets();
|
const auto& uploadTargets = textureObject->GetUploadTargets();
|
||||||
// The half of the completeness gate above that GL does keep: the REQUESTED level has
|
if (!uploadTargets.empty() && static_cast<Uint>(level) < textureMipmapObject->GetMipmapLevelCount()) {
|
||||||
// to hold an image. A name that was never given one carries no levels at all (which is
|
// Tightly packed, and summed over every face because a cube map query returns all
|
||||||
// also what an Unknown internal format answers), and a chain grown to reach level N
|
// six. Pack pixel-store state only ever grows this, so a request rejected here
|
||||||
// leaves every level below it at {0, 0, 0}.
|
// could not have fit under any packing.
|
||||||
if (uploadTargets.empty() || static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
|
const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTargets[0], level);
|
||||||
MG_State::pGLContext->RecordError(
|
const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat,
|
||||||
ErrorCode::InvalidOperation,
|
texturePixelDataType, texelSize) *
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture level has no image to read back."));
|
uploadTargets.size();
|
||||||
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<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture level has no image to read back."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tightly packed, and summed over every face because a cube map query returns all
|
if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
|
||||||
// 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<SizeT>(bufSize) < required) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pixelPackBufferObject) {
|
|
||||||
const SizeT bufferSize = pixelPackBufferObject->GetSize();
|
|
||||||
const SizeT offset = reinterpret_cast<SizeT>(pixels);
|
|
||||||
if (offset > bufferSize || required > bufferSize - offset) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
|
||||||
"Packing would write past the end of the pixel pack buffer."));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pixelPackBufferObject) {
|
||||||
|
const SizeT bufferSize = pixelPackBufferObject->GetSize();
|
||||||
|
const SizeT offset = reinterpret_cast<SizeT>(pixels);
|
||||||
|
if (offset > bufferSize || required > bufferSize - offset) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
|
"Packing would write past the end of the pixel pack buffer."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4606,12 +4372,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, 1, 1);
|
const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, 1, 1);
|
||||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize});
|
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize});
|
||||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
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<Uint>(level), internalformat);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a
|
// Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a
|
||||||
// longer pre-existing chain has to be dropped explicitly.
|
// longer pre-existing chain has to be dropped explicitly.
|
||||||
@@ -4680,12 +4440,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
|
MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
|
||||||
{levelWidth, levelHeight, 1}));
|
{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<Uint>(level), internalformat);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// See TextureStorage1D.
|
// See TextureStorage1D.
|
||||||
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
|
textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
|
||||||
@@ -4693,6 +4447,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
textureObject->SetImmutableLevels(static_cast<Uint>(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,
|
void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
|
||||||
GLsizei depth) {
|
GLsizei depth) {
|
||||||
auto textureObject = GetTextureObjectByName(texture, __func__);
|
auto textureObject = GetTextureObjectByName(texture, __func__);
|
||||||
@@ -4735,10 +4515,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// Array targets keep their layer count constant across levels; only true 3D
|
// Array targets keep their layer count constant across levels; only true 3D
|
||||||
// textures halve depth per level (GL 3.3 §3.9 glTexStorage3D).
|
// textures halve depth per level (GL 3.3 §3.9 glTexStorage3D).
|
||||||
const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget());
|
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) {
|
for (GLsizei level = 0; level < levels; ++level) {
|
||||||
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
|
const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
|
||||||
const GLsizei levelHeight = std::max<GLsizei>(1, height >> level);
|
const GLsizei levelHeight = std::max<GLsizei>(1, height >> level);
|
||||||
@@ -4748,19 +4524,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
|
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
|
||||||
{{levelWidth, levelHeight, levelDepth}, byteSize});
|
{{levelWidth, levelHeight, levelDepth}, byteSize});
|
||||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
||||||
if (compressedInfo.blockWidth != 0) {
|
|
||||||
// After AllocateStorage, which clears the tag.
|
|
||||||
textureMipmapObject->SetMipmapCompressedImage(
|
|
||||||
textureUploadTarget, static_cast<Uint>(level), internalformat, nullptr,
|
|
||||||
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<Uint>(level), internalformat);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// See TextureStorage1D.
|
// See TextureStorage1D.
|
||||||
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
|
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
|
||||||
@@ -4921,22 +4684,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
TextureStorage3D(textureObject->GetExternalIndex(), levels, internalformat, width, height, depth);
|
TextureStorage3D(textureObject->GetExternalIndex(), levels, internalformat, width, height, depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unlike glTexImage*Multisample, where a zero-sized image is a legal deallocation (see
|
|
||||||
// AllocateMultisampleTextureStorage), the immutable forms take a strictly positive size: GL
|
|
||||||
// 4.6 core 8.19 makes width, height or depth < 1 INVALID_VALUE. Without this the shared
|
|
||||||
// _State helper would deallocate the image and TexStorageMultisample_State would then freeze
|
|
||||||
// the now-imageless texture as immutable.
|
|
||||||
static Bool ValidateTexStorageMultisampleSize(GLsizei width, GLsizei height, GLsizei depth, const char* caller) {
|
|
||||||
if (width >= 1 && height >= 1 && depth >= 1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
|
||||||
"Immutable multisample storage requires width, height and depth >= 1."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The multisample storage forms allocate exactly what the glTexImage*Multisample ones do, and
|
// The multisample storage forms allocate exactly what the glTexImage*Multisample ones do, and
|
||||||
// then freeze it: TEXTURE_IMMUTABLE_FORMAT becomes TRUE and a second call is INVALID_OPERATION
|
// then freeze it: TEXTURE_IMMUTABLE_FORMAT becomes TRUE and a second call is INVALID_OPERATION
|
||||||
// (GL 4.6 core 8.19). Only the allocation was shared before, so a multisample texture stayed
|
// (GL 4.6 core 8.19). Only the allocation was shared before, so a multisample texture stayed
|
||||||
@@ -4957,7 +4704,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
const TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||||
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||||
if (!ValidateTextureMutable(activeUnit.GetBindingSlot(textureTarget).GetBoundObject(), __func__)) return;
|
if (!ValidateTextureMutable(activeUnit.GetBindingSlot(textureTarget).GetBoundObject(), __func__)) return;
|
||||||
if (!ValidateTexStorageMultisampleSize(width, height, 1, __func__)) return;
|
|
||||||
TexStorageMultisample_State(
|
TexStorageMultisample_State(
|
||||||
target, TexImage2DMultisample_State(target, samples, internalformat, width, height, fixedsamplelocations),
|
target, TexImage2DMultisample_State(target, samples, internalformat, width, height, fixedsamplelocations),
|
||||||
__func__);
|
__func__);
|
||||||
@@ -4968,7 +4714,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
const TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
|
||||||
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||||
if (!ValidateTextureMutable(activeUnit.GetBindingSlot(textureTarget).GetBoundObject(), __func__)) return;
|
if (!ValidateTextureMutable(activeUnit.GetBindingSlot(textureTarget).GetBoundObject(), __func__)) return;
|
||||||
if (!ValidateTexStorageMultisampleSize(width, height, depth, __func__)) return;
|
|
||||||
TexStorageMultisample_State(target,
|
TexStorageMultisample_State(target,
|
||||||
TexImage3DMultisample_State(target, samples, internalformat, width, height, depth,
|
TexImage3DMultisample_State(target, samples, internalformat, width, height, depth,
|
||||||
fixedsamplelocations),
|
fixedsamplelocations),
|
||||||
@@ -5970,29 +5715,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||||
// A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is
|
// A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is
|
||||||
// INVALID_OPERATION - so resolve through the plain lookups, which answer a null
|
// INVALID_OPERATION - so resolve through the plain lookup, which answers a null
|
||||||
// SharedPtr, and let the validator record the error this entry point owes.
|
// SharedPtr, and let the validator record the error this entry point owes.
|
||||||
//
|
const SharedPtr<MG_State::GLState::ITextureObject> srcTexture =
|
||||||
// The TARGET picks the namespace: GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER, and a
|
MG_State::pGLContext->GetTextureObject(srcName);
|
||||||
// renderbuffer name has nothing to do with a texture name. Resolving both through
|
const SharedPtr<MG_State::GLState::ITextureObject> dstTexture =
|
||||||
// GetTextureObject made every renderbuffer endpoint INVALID_VALUE - or, when the number
|
MG_State::pGLContext->GetTextureObject(dstName);
|
||||||
// happened to collide with a live texture, INVALID_ENUM from the target check.
|
if (!ValidateCopyImageSubData_State(srcTexture, srcTarget, srcLevel, srcX, srcY, dstTexture, dstTarget,
|
||||||
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)) {
|
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
CopyImageSubData_Backend(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel,
|
CopyImageSubData_Backend(srcTexture, srcTarget, srcLevel, srcX, srcY, srcZ, dstTexture, dstTarget, dstLevel,
|
||||||
dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -313,13 +313,9 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The stencil-only transfer path arrived with GL 4.4 / ARB_texture_stencil8, and only ever
|
// TexImage in core 3.3 has no stencil-only upload path (that arrived with GL 4.4).
|
||||||
// pairs with stencil-only storage: against a depth, depth-stencil or colour internal format
|
if (format == TextureInputFormat::StencilIndex) {
|
||||||
// STENCIL_INDEX keeps the pre-4.4 answer (GL CTS packed_pixels feeds exactly that pairing
|
return recordInvalidOperation("STENCIL_INDEX is not a valid texture upload format");
|
||||||
// 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)) {
|
if (IsDepthLikeInputFormat(format) != IsDepthLikeInternalFormat(internalFormat)) {
|
||||||
|
|||||||
@@ -24,14 +24,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
|
|
||||||
set(MGL_ITEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..)
|
set(MGL_ITEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..)
|
||||||
|
|
||||||
# Desktop links the static implementation directly. Android runs the same
|
# Only meaningful where MobileGL_s exists (i.e. not Android).
|
||||||
# executable from adb shell and links the shipping shared library instead.
|
if (NOT TARGET MobileGL_s)
|
||||||
if (ANDROID)
|
message(STATUS "MobileGL_s is not available; skipping the integration test module")
|
||||||
set(MGL_ITEST_MOBILEGL_TARGET MobileGL)
|
|
||||||
elseif (TARGET MobileGL_s)
|
|
||||||
set(MGL_ITEST_MOBILEGL_TARGET MobileGL_s)
|
|
||||||
else()
|
|
||||||
message(STATUS "No MobileGL library target is available; skipping the integration test module")
|
|
||||||
return()
|
return()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -74,7 +69,6 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/UniformInitializerScenario.cpp
|
Scenarios/UniformInitializerScenario.cpp
|
||||||
Scenarios/SwizzleAccessRoutineScenario.cpp
|
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||||
Scenarios/IterationRPFirstReductionScenario.cpp
|
Scenarios/IterationRPFirstReductionScenario.cpp
|
||||||
Scenarios/IterationRPProgram203Scenario.cpp
|
|
||||||
Scenarios/IterationRPScratchFixScenario.cpp
|
Scenarios/IterationRPScratchFixScenario.cpp
|
||||||
Scenarios/ProgramPipelineScenario.cpp
|
Scenarios/ProgramPipelineScenario.cpp
|
||||||
Scenarios/ImageLoadStoreSsoScenario.cpp
|
Scenarios/ImageLoadStoreSsoScenario.cpp
|
||||||
@@ -86,7 +80,6 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/BufferTextureScenario.cpp
|
Scenarios/BufferTextureScenario.cpp
|
||||||
Scenarios/VertexAttribBindingScenario.cpp
|
Scenarios/VertexAttribBindingScenario.cpp
|
||||||
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
||||||
Scenarios/XfbPrimitiveQueryScenario.cpp
|
|
||||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||||
Scenarios/CopyImageLayeredScenario.cpp
|
Scenarios/CopyImageLayeredScenario.cpp
|
||||||
@@ -101,20 +94,9 @@ target_include_directories(MobileGLIntegrationTest PRIVATE
|
|||||||
# gtest, not gtest_main: Main.cpp installs the harness banner itself.
|
# gtest, not gtest_main: Main.cpp installs the harness banner itself.
|
||||||
target_link_libraries(MobileGLIntegrationTest PRIVATE
|
target_link_libraries(MobileGLIntegrationTest PRIVATE
|
||||||
GTest::gtest
|
GTest::gtest
|
||||||
${MGL_ITEST_MOBILEGL_TARGET}
|
MobileGL_s
|
||||||
)
|
)
|
||||||
|
|
||||||
if (ANDROID)
|
|
||||||
find_library(MGL_ITEST_ANDROID_LIBRARY android REQUIRED)
|
|
||||||
find_library(MGL_ITEST_LOG_LIBRARY log REQUIRED)
|
|
||||||
find_library(MGL_ITEST_MEDIANDK_LIBRARY mediandk REQUIRED)
|
|
||||||
target_link_libraries(MobileGLIntegrationTest PRIVATE
|
|
||||||
${MGL_ITEST_ANDROID_LIBRARY}
|
|
||||||
${MGL_ITEST_LOG_LIBRARY}
|
|
||||||
${MGL_ITEST_MEDIANDK_LIBRARY}
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if (MSVC)
|
if (MSVC)
|
||||||
# Same reason as MG_Test/Backend/DirectVulkan: the GLES headers declare gl*
|
# Same reason as MG_Test/Backend/DirectVulkan: the GLES headers declare gl*
|
||||||
# as dllimport on Windows, so the in-library GL entry-point definitions only
|
# as dllimport on Windows, so the in-library GL entry-point definitions only
|
||||||
@@ -123,10 +105,6 @@ if (MSVC)
|
|||||||
endif()
|
endif()
|
||||||
target_compile_definitions(MobileGLIntegrationTest PRIVATE -DNOMINMAX)
|
target_compile_definitions(MobileGLIntegrationTest PRIVATE -DNOMINMAX)
|
||||||
|
|
||||||
if (ANDROID)
|
|
||||||
return()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# --- ctest wiring --------------------------------------------------------
|
# --- ctest wiring --------------------------------------------------------
|
||||||
# A bare libEGL on a glvnd box resolves to whatever vendor comes first, which is
|
# A bare libEGL on a glvnd box resolves to whatever vendor comes first, which is
|
||||||
# usually Mesa/llvmpipe - a software rasteriser silently replacing the GPU under
|
# usually Mesa/llvmpipe - a software rasteriser silently replacing the GPU under
|
||||||
@@ -247,19 +225,6 @@ endif()
|
|||||||
set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV})
|
set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV})
|
||||||
if (MOBILEGL_ITEST_VK_ICD)
|
if (MOBILEGL_ITEST_VK_ICD)
|
||||||
list(APPEND MGL_ITEST_VULKAN_ENV "VK_ICD_FILENAMES=${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()
|
endif()
|
||||||
|
|
||||||
# The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests
|
# The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests
|
||||||
|
|||||||
@@ -18,11 +18,6 @@
|
|||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
#define WIN32_LEAN_AND_MEAN
|
#define WIN32_LEAN_AND_MEAN
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#elif defined(__ANDROID__)
|
|
||||||
#include <android/hardware_buffer.h>
|
|
||||||
#include <android/native_window.h>
|
|
||||||
#include <media/NdkImage.h>
|
|
||||||
#include <media/NdkImageReader.h>
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// MobileGL's own headers, in the order MobileGL/Includes.h uses them: GL/gl.h
|
// MobileGL's own headers, in the order MobileGL/Includes.h uses them: GL/gl.h
|
||||||
@@ -42,7 +37,7 @@
|
|||||||
// the only construction that is actually predictive here: MobileGL ABORTS
|
// the only construction that is actually predictive here: MobileGL ABORTS
|
||||||
// (MOBILEGL_ASSERT -> SIGTRAP) rather than returning an error on an unusable
|
// (MOBILEGL_ASSERT -> SIGTRAP) rather than returning an error on an unusable
|
||||||
// platform, so nothing the parent can call in-process is allowed to be wrong.
|
// platform, so nothing the parent can call in-process is allowed to be wrong.
|
||||||
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
|
#if !defined(_WIN32) && !defined(__APPLE__) && __has_include(<sys/wait.h>)
|
||||||
#define MGITEST_HAVE_FORK_PREFLIGHT 1
|
#define MGITEST_HAVE_FORK_PREFLIGHT 1
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
@@ -83,58 +78,12 @@ namespace MGITest {
|
|||||||
CW_USEDEFAULT, CW_USEDEFAULT, kSurfaceWidth, kSurfaceHeight, nullptr, nullptr,
|
CW_USEDEFAULT, CW_USEDEFAULT, kSurfaceWidth, kSurfaceHeight, nullptr, nullptr,
|
||||||
GetModuleHandleW(nullptr), nullptr);
|
GetModuleHandleW(nullptr), nullptr);
|
||||||
}
|
}
|
||||||
#elif defined(__ANDROID__)
|
|
||||||
AImageReader* g_imageReader = nullptr;
|
|
||||||
ANativeWindow* g_imageReaderWindow = nullptr;
|
|
||||||
|
|
||||||
void DrainImageReader(void*, AImageReader* reader) {
|
|
||||||
AImage* image = nullptr;
|
|
||||||
if (AImageReader_acquireNextImage(reader, &image) == AMEDIA_OK && image != nullptr) {
|
|
||||||
AImage_delete(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CreateImageReaderWindow() {
|
|
||||||
if (g_imageReaderWindow != nullptr) return true;
|
|
||||||
constexpr int kMaxImages = 4;
|
|
||||||
const media_status_t status = AImageReader_newWithUsage(
|
|
||||||
kSurfaceWidth, kSurfaceHeight, AIMAGE_FORMAT_RGBA_8888,
|
|
||||||
AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT,
|
|
||||||
kMaxImages, &g_imageReader);
|
|
||||||
if (status != AMEDIA_OK || g_imageReader == nullptr) return false;
|
|
||||||
|
|
||||||
AImageReader_ImageListener listener = {nullptr, DrainImageReader};
|
|
||||||
AImageReader_setImageListener(g_imageReader, &listener);
|
|
||||||
if (AImageReader_getWindow(g_imageReader, &g_imageReaderWindow) != AMEDIA_OK ||
|
|
||||||
g_imageReaderWindow == nullptr) {
|
|
||||||
AImageReader_setImageListener(g_imageReader, nullptr);
|
|
||||||
AImageReader_delete(g_imageReader);
|
|
||||||
g_imageReader = nullptr;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ANativeWindow_acquire(g_imageReaderWindow);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DestroyImageReaderWindow() {
|
|
||||||
if (g_imageReaderWindow != nullptr) {
|
|
||||||
ANativeWindow_release(g_imageReaderWindow);
|
|
||||||
g_imageReaderWindow = nullptr;
|
|
||||||
}
|
|
||||||
if (g_imageReader != nullptr) {
|
|
||||||
AImageReader_setImageListener(g_imageReader, nullptr);
|
|
||||||
AImageReader_delete(g_imageReader);
|
|
||||||
g_imageReader = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool UseWindowSurface() {
|
bool UseWindowSurface() {
|
||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
const char* value = std::getenv("MOBILEGL_ITEST_WINDOW_SURFACE");
|
const char* value = std::getenv("MOBILEGL_ITEST_WINDOW_SURFACE");
|
||||||
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
||||||
#elif defined(__ANDROID__)
|
|
||||||
return true;
|
|
||||||
#else
|
#else
|
||||||
return false;
|
return false;
|
||||||
#endif
|
#endif
|
||||||
@@ -174,10 +123,10 @@ namespace MGITest {
|
|||||||
// callers). surfaceless is the platform with no window-system dependency at
|
// callers). surfaceless is the platform with no window-system dependency at
|
||||||
// all; the surface this file then creates is still a pbuffer, which every
|
// all; the surface this file then creates is still a pbuffer, which every
|
||||||
// platform supports and which the amendment to this rule requires as the
|
// platform supports and which the amendment to this rule requires as the
|
||||||
// fallback shape on desktop. Android instead supplies an AImageReader
|
// fallback shape. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
|
||||||
// ANativeWindow. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a
|
|
||||||
// driver that consults them directly cannot reintroduce the dependency
|
// driver that consults them directly cannot reintroduce the dependency
|
||||||
// behind EGL's back.
|
// behind EGL's back. Desktop-only file: MG_IntegrationTest never builds
|
||||||
|
// for Android, so no device path is affected.
|
||||||
void EnsureHeadlessPlatform() {
|
void EnsureHeadlessPlatform() {
|
||||||
#if defined(__linux__) && !defined(__ANDROID__)
|
#if defined(__linux__) && !defined(__ANDROID__)
|
||||||
static bool done = false;
|
static bool done = false;
|
||||||
@@ -265,21 +214,12 @@ namespace MGITest {
|
|||||||
return 6;
|
return 6;
|
||||||
}
|
}
|
||||||
surface = eglCreateWindowSurface(display, config, g_testWindow, nullptr);
|
surface = eglCreateWindowSurface(display, config, g_testWindow, nullptr);
|
||||||
#elif defined(__ANDROID__)
|
|
||||||
if (!CreateImageReaderWindow()) {
|
|
||||||
outReason = "failed to create the Android AImageReader integration-test window";
|
|
||||||
return 6;
|
|
||||||
}
|
|
||||||
surface = eglCreateWindowSurface(display, config, g_imageReaderWindow, nullptr);
|
|
||||||
#endif
|
#endif
|
||||||
} else {
|
} else {
|
||||||
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
|
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
|
||||||
surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
|
surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
|
||||||
}
|
}
|
||||||
if (surface == EGL_NO_SURFACE) {
|
if (surface == EGL_NO_SURFACE) {
|
||||||
#if defined(__ANDROID__)
|
|
||||||
DestroyImageReaderWindow();
|
|
||||||
#endif
|
|
||||||
outReason = WithEglError(useWindowSurface ? "eglCreateWindowSurface failed"
|
outReason = WithEglError(useWindowSurface ? "eglCreateWindowSurface failed"
|
||||||
: "eglCreatePbufferSurface failed");
|
: "eglCreatePbufferSurface failed");
|
||||||
return 6;
|
return 6;
|
||||||
@@ -608,8 +548,6 @@ namespace MGITest {
|
|||||||
DestroyWindow(g_testWindow);
|
DestroyWindow(g_testWindow);
|
||||||
g_testWindow = nullptr;
|
g_testWindow = nullptr;
|
||||||
}
|
}
|
||||||
#elif defined(__ANDROID__)
|
|
||||||
DestroyImageReaderWindow();
|
|
||||||
#endif
|
#endif
|
||||||
m_context = nullptr;
|
m_context = nullptr;
|
||||||
m_surface = nullptr;
|
m_surface = nullptr;
|
||||||
|
|||||||
@@ -14,11 +14,11 @@
|
|||||||
// inspects backend state - both bugs this module pins were invisible to
|
// inspects backend state - both bugs this module pins were invisible to
|
||||||
// state-level assertions and visible only in pixels.
|
// state-level assertions and visible only in pixels.
|
||||||
//
|
//
|
||||||
// Headless by construction: desktop uses an EGL pbuffer and Android uses an
|
// Headless by construction, following MG_Benchmark/Driver/DriverBench.c: an EGL
|
||||||
// AImageReader-backed ANativeWindow that needs no Activity. No window manager,
|
// context on a PBUFFER surface. No window, no window manager, no human. Unlike
|
||||||
// no human. Unlike DriverBench the scenarios do draw to the DEFAULT framebuffer
|
// DriverBench the scenarios do draw to the DEFAULT framebuffer (that is where
|
||||||
// (that is where the Y-flip lives) and do call eglSwapBuffers (that is the frame
|
// the Y-flip lives) and do call eglSwapBuffers (that is the frame boundary the
|
||||||
// boundary the cross-frame scenarios need to be real).
|
// cross-frame scenarios need to be real).
|
||||||
//
|
//
|
||||||
// One process is one backend: MOBILEGL_BACKEND_TYPE is latched at
|
// One process is one backend: MOBILEGL_BACKEND_TYPE is latched at
|
||||||
// initialization, so the CMake wiring runs this binary once per backend rather
|
// initialization, so the CMake wiring runs this binary once per backend rather
|
||||||
|
|||||||
@@ -31,14 +31,8 @@ namespace {
|
|||||||
// silently bound to a workstation's window system is a different
|
// silently bound to a workstation's window system is a different
|
||||||
// run from CI's and must be visible as one in the log.
|
// run from CI's and must be visible as one in the log.
|
||||||
const char* eglPlatform = std::getenv("EGL_PLATFORM");
|
const char* eglPlatform = std::getenv("EGL_PLATFORM");
|
||||||
#if defined(__ANDROID__)
|
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless, EGL_PLATFORM=%s)\n",
|
||||||
constexpr const char* surfaceKind = "AImageReader window";
|
|
||||||
#else
|
|
||||||
constexpr const char* surfaceKind = "pbuffer";
|
|
||||||
#endif
|
|
||||||
std::fprintf(stderr, " renderer: %s\n surface: %dx%d %s (headless, EGL_PLATFORM=%s)\n",
|
|
||||||
gl.RendererString().c_str(), gl.Width(), gl.Height(),
|
gl.RendererString().c_str(), gl.Width(), gl.Height(),
|
||||||
surfaceKind,
|
|
||||||
eglPlatform != nullptr ? eglPlatform : "<unset>");
|
eglPlatform != nullptr ? eglPlatform : "<unset>");
|
||||||
} else if (MGITest::RequireGpu()) {
|
} else if (MGITest::RequireGpu()) {
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
|
|||||||
@@ -374,86 +374,6 @@ namespace MGITest {
|
|||||||
glUseProgram(0);
|
glUseProgram(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The same texture, bound four times over, varying nothing but `layered` and `layer`.
|
|
||||||
//
|
|
||||||
// GL 4.6 core 8.26 (and ES 3.2 8.22, word for word): "If the texture identified by
|
|
||||||
// texture does not have multiple layers or faces, the entire texture level is bound,
|
|
||||||
// regardless of the values of layered and layer." REGARDLESS means ignored - not
|
|
||||||
// clamped, and not an error - so every one of the four rows has to read the same texel
|
|
||||||
// out of a target that has no layers, including the two rows that name layer 1 on a
|
|
||||||
// texture whose only layer is 0. DirectGLES used to normalize `layered` and forward
|
|
||||||
// `layer` verbatim; Adreno honours the bogus layer by leaving the image unit reading
|
|
||||||
// zero, which is exactly the two rows KHR-GL42.bind_image_texture.single_layer failed.
|
|
||||||
//
|
|
||||||
// The bindings are checked back as well, because the fix depends on WHERE the
|
|
||||||
// normalization happens: the frontend shadow must keep echoing the application's own
|
|
||||||
// values (gl4cShaderImageLoadStoreTests' CheckBinding compares them exactly), and only
|
|
||||||
// the backend's driver call may drop the layer.
|
|
||||||
void RunNonLayerableLayerSweepCase(const TargetKind& kind) {
|
|
||||||
const GLuint program = MakeComputeProgram(SingleLoadSource(kind));
|
|
||||||
if (program == 0) return;
|
|
||||||
const GLuint texture = MakeTexture(kind, true);
|
|
||||||
if (texture == 0) return;
|
|
||||||
|
|
||||||
// A multisample texture has no TexSubImage, so MakeTexture leaves it unwritten and
|
|
||||||
// it is seeded the way the store cases do it - through a dispatch of its own.
|
|
||||||
const GLuint expected = kind.multisample ? kStoredValue : kFilledValue;
|
|
||||||
if (kind.multisample) {
|
|
||||||
const GLuint storeProgram = MakeComputeProgram(SingleStoreSource(kind));
|
|
||||||
if (storeProgram == 0) return;
|
|
||||||
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_WRITE, GL_R32UI);
|
|
||||||
glUseProgram(storeProgram);
|
|
||||||
glUniform1i(0, 0);
|
|
||||||
glDispatchCompute(1, 1, 1);
|
|
||||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": seeding the multisample texture errored";
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint ssbo = MakeResultBuffer();
|
|
||||||
glUseProgram(program);
|
|
||||||
glUniform1i(0, 0);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": assigning the image unit errored";
|
|
||||||
|
|
||||||
// glcBindImageTextureTests' own four rows, in its own order.
|
|
||||||
struct LayerRow {
|
|
||||||
GLboolean layered;
|
|
||||||
GLint layer;
|
|
||||||
};
|
|
||||||
static constexpr LayerRow kRows[] = {{GL_TRUE, 1}, {GL_TRUE, 0}, {GL_FALSE, 1}, {GL_FALSE, 0}};
|
|
||||||
|
|
||||||
for (const LayerRow& row : kRows) {
|
|
||||||
const std::string where = std::string(kind.name) +
|
|
||||||
": layered=" + (row.layered == GL_TRUE ? "TRUE" : "FALSE") +
|
|
||||||
" layer=" + std::to_string(row.layer);
|
|
||||||
// Re-zeroed per row, so a row whose binding reads nothing cannot pass on the
|
|
||||||
// previous row's answer.
|
|
||||||
const GLuint zero = 0u;
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
|
|
||||||
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(GLuint), &zero);
|
|
||||||
|
|
||||||
glBindImageTexture(0, texture, 0, row.layered, row.layer, GL_READ_ONLY, GL_R32UI);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << where << ": glBindImageTexture errored";
|
|
||||||
|
|
||||||
GLint reportedLayered = -1;
|
|
||||||
GLint reportedLayer = -1;
|
|
||||||
glGetIntegeri_v(GL_IMAGE_BINDING_LAYERED, 0, &reportedLayered);
|
|
||||||
glGetIntegeri_v(GL_IMAGE_BINDING_LAYER, 0, &reportedLayer);
|
|
||||||
EXPECT_EQ(reportedLayered, row.layered == GL_TRUE ? 1 : 0)
|
|
||||||
<< where << ": GL_IMAGE_BINDING_LAYERED stopped reporting the application's value";
|
|
||||||
EXPECT_EQ(reportedLayer, row.layer)
|
|
||||||
<< where << ": GL_IMAGE_BINDING_LAYER stopped reporting the application's value";
|
|
||||||
|
|
||||||
glDispatchCompute(1, 1, 1);
|
|
||||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << where << ": the dispatch leaked a GL error";
|
|
||||||
EXPECT_EQ(ReadResult(ssbo), expected)
|
|
||||||
<< where
|
|
||||||
<< ": the texel did not come back, so the binding named a layer the texture "
|
|
||||||
"does not have instead of the whole level";
|
|
||||||
}
|
|
||||||
glUseProgram(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<GLuint> m_programs;
|
std::vector<GLuint> m_programs;
|
||||||
std::vector<GLuint> m_textures;
|
std::vector<GLuint> m_textures;
|
||||||
std::vector<GLuint> m_buffers;
|
std::vector<GLuint> m_buffers;
|
||||||
@@ -515,31 +435,6 @@ namespace MGITest {
|
|||||||
#undef MGL_DEFINE_LOAD_CASE
|
#undef MGL_DEFINE_LOAD_CASE
|
||||||
#undef MGL_DEFINE_STORE_CASE
|
#undef MGL_DEFINE_STORE_CASE
|
||||||
|
|
||||||
// ---- and the same texture bound four times, varying only layered/layer ---
|
|
||||||
//
|
|
||||||
// KHR-GL42.bind_image_texture.single_layer's sweep, on the kinds whose backend target has
|
|
||||||
// neither layers nor faces. Two of its four rows name layer 1 on a single-layer texture,
|
|
||||||
// which the spec says is to be ignored outright rather than honoured or rejected - and
|
|
||||||
// which DirectGLES used to forward to the ES driver as written.
|
|
||||||
|
|
||||||
#define MGL_DEFINE_LAYER_SWEEP_CASE(CaseName, Kind) \
|
|
||||||
TEST_F(ImageTargetKindScenario, IgnoresLayerFor##CaseName) { \
|
|
||||||
if (!Ready()) return; \
|
|
||||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
|
|
||||||
if ((Kind).multisample && !MultisampleImagesAreUsable()) { \
|
|
||||||
GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image " \
|
|
||||||
"here and never asks for a multisample one"; \
|
|
||||||
} \
|
|
||||||
RunNonLayerableLayerSweepCase(Kind); \
|
|
||||||
}
|
|
||||||
|
|
||||||
MGL_DEFINE_LAYER_SWEEP_CASE(Texture2D, kKind2D)
|
|
||||||
MGL_DEFINE_LAYER_SWEEP_CASE(Texture1D, kKind1D)
|
|
||||||
MGL_DEFINE_LAYER_SWEEP_CASE(TextureRectangle, kKindRect)
|
|
||||||
MGL_DEFINE_LAYER_SWEEP_CASE(Texture2DMultisample, kKind2DMS)
|
|
||||||
|
|
||||||
#undef MGL_DEFINE_LAYER_SWEEP_CASE
|
|
||||||
|
|
||||||
// ---- and all of them at once -------------------------------------------
|
// ---- and all of them at once -------------------------------------------
|
||||||
//
|
//
|
||||||
// The conformance case's actual shape. The single-kind cases above cannot see a defect that
|
// The conformance case's actual shape. The single-kind cases above cannot see a defect that
|
||||||
|
|||||||
@@ -1,379 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IterationRPProgram203Scenario.cpp
|
|
||||||
// Copyright (c) 2026 MobileGL-Dev
|
|
||||||
// Licensed under the GNU Lesser General Public License v3.0:
|
|
||||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// End of Source File Header
|
|
||||||
//
|
|
||||||
// Full iterationRP Program 203 golden input/output fixture. The original shader
|
|
||||||
// consumes deterministic complete textures and uniforms, then its complete
|
|
||||||
// 512x513 RG16F output image is compared against fixed half-float golden bits.
|
|
||||||
// This catches both a wrong exposure slot and collateral scratch corruption.
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <bit>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <iostream>
|
|
||||||
#include <sstream>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "../Harness/HeadlessGL.h"
|
|
||||||
#include "../Harness/ScenarioFixture.h"
|
|
||||||
|
|
||||||
#ifdef GLAPI
|
|
||||||
#undef GLAPI
|
|
||||||
#endif
|
|
||||||
#define GL_GLEXT_PROTOTYPES
|
|
||||||
#include <GL/gl.h>
|
|
||||||
#include <GL/glcorearb.h>
|
|
||||||
#undef GL_GLEXT_PROTOTYPES
|
|
||||||
|
|
||||||
namespace MGITest {
|
|
||||||
namespace {
|
|
||||||
constexpr int kSceneWidth = 854;
|
|
||||||
constexpr int kSceneHeight = 480;
|
|
||||||
constexpr int kPixelDataWidth = 512;
|
|
||||||
constexpr int kPixelDataHeight = 513;
|
|
||||||
constexpr std::size_t kSceneTexelCount =
|
|
||||||
static_cast<std::size_t>(kSceneWidth) * kSceneHeight;
|
|
||||||
constexpr std::size_t kPixelDataTexelCount =
|
|
||||||
static_cast<std::size_t>(kPixelDataWidth) * kPixelDataHeight;
|
|
||||||
|
|
||||||
struct Rgba32f {
|
|
||||||
float r, g, b, a;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Rg16 {
|
|
||||||
std::uint16_t r, g;
|
|
||||||
};
|
|
||||||
|
|
||||||
static_assert(sizeof(Rgba32f) == 16);
|
|
||||||
static_assert(sizeof(Rg16) == 4);
|
|
||||||
|
|
||||||
// Captured from the fixed fixture on Adreno 830. These are the exact
|
|
||||||
// RG16F storage bits for (0.806640625, 8.2578125), not rounded decimal
|
|
||||||
// comparisons performed by the test.
|
|
||||||
constexpr Rg16 kGoldenExposure = {0x3a74u, 0x4821u};
|
|
||||||
|
|
||||||
constexpr const char* kCommonSource = R"glsl(
|
|
||||||
#version 430 core
|
|
||||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
|
||||||
|
|
||||||
uniform int frameCounter;
|
|
||||||
uniform float frameTime;
|
|
||||||
uniform float aspectRatio;
|
|
||||||
uniform vec2 pixelSize;
|
|
||||||
uniform float nightVision;
|
|
||||||
uniform float darknessLightFactor;
|
|
||||||
uniform sampler2D colortex2;
|
|
||||||
uniform sampler2D pixelData2D;
|
|
||||||
layout(rg16f) uniform image2D img_pixelData2D;
|
|
||||||
|
|
||||||
float remapSaturate(float x, float e0, float e1) {
|
|
||||||
return clamp((x - e0) / (e1 - e0), 0.0f, 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
float GetExposureValue(float luminance) {
|
|
||||||
float aeCurve = 0.65f;
|
|
||||||
aeCurve = mix(aeCurve, clamp(aeCurve * 1.2f, 0.0f, 1.0f), nightVision);
|
|
||||||
aeCurve *= remapSaturate(luminance, 2.0f, 1.0f) * 0.6f + 0.4f;
|
|
||||||
float ae = pow(luminance, -aeCurve);
|
|
||||||
ae *= 1.0f - min(darknessLightFactor * 2.0f, 0.9f);
|
|
||||||
ae *= 8.5f;
|
|
||||||
return ae;
|
|
||||||
}
|
|
||||||
)glsl";
|
|
||||||
|
|
||||||
constexpr const char* kOriginalMain = R"glsl(
|
|
||||||
layout(local_size_x = 32, local_size_y = 16) in;
|
|
||||||
shared vec2 prefixSumCache[32];
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec2 texCoord = (vec2(gl_GlobalInvocationID.xy) + 0.5f) * vec2(1.0f / 32.0f, 1.0f / 16.0f);
|
|
||||||
vec2 sampleCoord = texCoord * (1.0f / 64.0f);
|
|
||||||
sampleCoord.x += (15.0f / 32.0f) + pixelSize.x * 12.0f;
|
|
||||||
float tileExposure = dot(textureLod(colortex2, sampleCoord, 0.0f).rgb,
|
|
||||||
vec3(0.2125f, 0.7154f, 0.0721f));
|
|
||||||
vec2 sampleLuminance = vec2(tileExposure, 0.0f);
|
|
||||||
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
|
||||||
barrier();
|
|
||||||
|
|
||||||
uint loopLength = uint(findMSB(gl_NumSubgroups));
|
|
||||||
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
|
|
||||||
for (uint i = 0u; i < loopLength; ++i) {
|
|
||||||
if ((gl_SubgroupID & (1u << i)) > 0u) {
|
|
||||||
sampleLuminance += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
|
||||||
}
|
|
||||||
barrier();
|
|
||||||
}
|
|
||||||
if (gl_LocalInvocationIndex == 511u)
|
|
||||||
prefixSumCache[0] = sampleLuminance / 512.0f;
|
|
||||||
barrier();
|
|
||||||
|
|
||||||
float avg = prefixSumCache[0].x;
|
|
||||||
vec2 tileDistance = texCoord * 2.0f - 1.0f;
|
|
||||||
tileDistance.y /= aspectRatio;
|
|
||||||
float centerDistance = length(tileDistance);
|
|
||||||
float tileWeight = remapSaturate(centerDistance, 0.6f, 0.4f);
|
|
||||||
tileExposure = max(7.0E-7f, tileExposure);
|
|
||||||
float lumaWeight = avg / tileExposure;
|
|
||||||
lumaWeight = pow(lumaWeight, remapSaturate(avg, 0.02f, 0.001f) * 0.4f + 0.2f);
|
|
||||||
tileWeight *= lumaWeight;
|
|
||||||
|
|
||||||
vec2 sampleExposure = vec2(tileExposure * tileWeight, tileWeight);
|
|
||||||
sampleExposure = subgroupInclusiveAdd(sampleExposure);
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = sampleExposure;
|
|
||||||
barrier();
|
|
||||||
for (uint i = 0u; i < loopLength; ++i) {
|
|
||||||
if ((gl_SubgroupID & (1u << i)) > 0u) {
|
|
||||||
sampleExposure += prefixSumCache[(gl_SubgroupID >> i << i) - 1u];
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = sampleExposure;
|
|
||||||
}
|
|
||||||
barrier();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gl_LocalInvocationIndex == 511u) {
|
|
||||||
float avgExposure = max(sampleExposure.x / sampleExposure.y * 29.3f, 1.0E-10f);
|
|
||||||
avgExposure = log2(avgExposure);
|
|
||||||
float prevAvgExposure = log2(texelFetch(pixelData2D, ivec2(0, 0), 0).x);
|
|
||||||
float frameTimeFixed = frameTime + step(frameCounter, 20) * 100.0f;
|
|
||||||
float exposureTime = clamp(frameTimeFixed * 2.0f, 0.0f, 1.0f);
|
|
||||||
avgExposure = mix(prevAvgExposure, avgExposure, exposureTime);
|
|
||||||
avgExposure = max(exp2(avgExposure), 1.0E-5f);
|
|
||||||
float exposure = GetExposureValue(avgExposure);
|
|
||||||
imageStore(img_pixelData2D, ivec2(0, 0), vec4(avgExposure, exposure, 0.0f, 0.0f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)glsl";
|
|
||||||
|
|
||||||
GLuint CompileCompute(const char* mainSource, std::string* error) {
|
|
||||||
const std::array<const GLchar*, 2> sources = {kCommonSource, mainSource};
|
|
||||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
|
||||||
glShaderSource(shader, static_cast<GLsizei>(sources.size()), sources.data(), nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
GLint compiled = GL_FALSE;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
||||||
if (compiled != GL_TRUE) {
|
|
||||||
std::array<char, 8192> log{};
|
|
||||||
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size() - 1), nullptr, log.data());
|
|
||||||
*error = log.data();
|
|
||||||
glDeleteShader(shader);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
glAttachShader(program, shader);
|
|
||||||
glLinkProgram(program);
|
|
||||||
glDeleteShader(shader);
|
|
||||||
GLint linked = GL_FALSE;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
if (linked != GL_TRUE) {
|
|
||||||
std::array<char, 8192> log{};
|
|
||||||
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size() - 1), nullptr, log.data());
|
|
||||||
*error = log.data();
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Rgba32f> MakeSceneInput() {
|
|
||||||
std::vector<Rgba32f> texels(kSceneTexelCount);
|
|
||||||
for (int y = 0; y < kSceneHeight; ++y) {
|
|
||||||
for (int x = 0; x < kSceneWidth; ++x) {
|
|
||||||
std::uint32_t h = static_cast<std::uint32_t>(x) * 0x9e3779b9u;
|
|
||||||
h ^= static_cast<std::uint32_t>(y) * 0x85ebca6bu;
|
|
||||||
h ^= h >> 16u;
|
|
||||||
h *= 0x7feb352du;
|
|
||||||
h ^= h >> 15u;
|
|
||||||
const float noise = static_cast<float>(h & 0xffffu) / 65535.0f;
|
|
||||||
float base = 0.0002f + noise * 0.075f;
|
|
||||||
const float dx = static_cast<float>(x - 420);
|
|
||||||
const float dy = static_cast<float>(y - 4);
|
|
||||||
base += 0.65f * std::exp(-(dx * dx + dy * dy) / 18.0f);
|
|
||||||
if (((x + y * 17) % 113) == 0) base += 1.75f;
|
|
||||||
texels[static_cast<std::size_t>(y) * kSceneWidth + x] =
|
|
||||||
{base * 0.83f, base * 1.07f, base * 1.31f, 1.0f};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return texels;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::uint16_t FloatToHalf(float value) {
|
|
||||||
const std::uint32_t bits = std::bit_cast<std::uint32_t>(value);
|
|
||||||
const std::uint32_t sign = (bits >> 16u) & 0x8000u;
|
|
||||||
const std::uint32_t exponent = (bits >> 23u) & 0xffu;
|
|
||||||
std::uint32_t mantissa = bits & 0x7fffffu;
|
|
||||||
|
|
||||||
if (exponent == 0xffu) {
|
|
||||||
return static_cast<std::uint16_t>(sign | (mantissa == 0 ? 0x7c00u : 0x7e00u));
|
|
||||||
}
|
|
||||||
int halfExponent = static_cast<int>(exponent) - 127 + 15;
|
|
||||||
if (halfExponent >= 31) return static_cast<std::uint16_t>(sign | 0x7c00u);
|
|
||||||
if (halfExponent <= 0) {
|
|
||||||
if (halfExponent < -10) return static_cast<std::uint16_t>(sign);
|
|
||||||
mantissa |= 0x800000u;
|
|
||||||
const unsigned shift = static_cast<unsigned>(14 - halfExponent);
|
|
||||||
const std::uint32_t rounded = mantissa + ((1u << (shift - 1u)) - 1u) +
|
|
||||||
((mantissa >> shift) & 1u);
|
|
||||||
return static_cast<std::uint16_t>(sign | (rounded >> shift));
|
|
||||||
}
|
|
||||||
mantissa += 0xfffu + ((mantissa >> 13u) & 1u);
|
|
||||||
if ((mantissa & 0x800000u) != 0) {
|
|
||||||
mantissa = 0;
|
|
||||||
if (++halfExponent >= 31) return static_cast<std::uint16_t>(sign | 0x7c00u);
|
|
||||||
}
|
|
||||||
return static_cast<std::uint16_t>(sign | (static_cast<std::uint32_t>(halfExponent) << 10u) |
|
|
||||||
(mantissa >> 13u));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Rg16> MakePixelDataInput() {
|
|
||||||
std::vector<Rg16> texels(kPixelDataTexelCount);
|
|
||||||
for (std::size_t i = 0; i < texels.size(); ++i) {
|
|
||||||
texels[i] = {FloatToHalf(0.35f + static_cast<float>(i % 97u) * 0.0025f),
|
|
||||||
FloatToHalf(-0.45f + static_cast<float>(i % 89u) * 0.01f)};
|
|
||||||
}
|
|
||||||
texels[0] = {FloatToHalf(0.73f), FloatToHalf(1.25f)};
|
|
||||||
return texels;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Rg16> MakeGoldenOutput() {
|
|
||||||
std::vector<Rg16> golden = MakePixelDataInput();
|
|
||||||
golden[0] = kGoldenExposure;
|
|
||||||
return golden;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint MakeTexture(GLenum internalFormat, GLenum format, GLenum type, int width, int height,
|
|
||||||
const void* data) {
|
|
||||||
GLuint texture = 0;
|
|
||||||
glGenTextures(1, &texture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, texture);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(internalFormat), width, height, 0, format,
|
|
||||||
type, data);
|
|
||||||
return texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BindAndDispatch(GLuint program, GLuint scene, GLuint pixelData) {
|
|
||||||
glUseProgram(program);
|
|
||||||
glActiveTexture(GL_TEXTURE3);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, scene);
|
|
||||||
glUniform1i(glGetUniformLocation(program, "colortex2"), 3);
|
|
||||||
glActiveTexture(GL_TEXTURE4);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, pixelData);
|
|
||||||
glUniform1i(glGetUniformLocation(program, "pixelData2D"), 4);
|
|
||||||
glBindImageTexture(0, pixelData, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RG16F);
|
|
||||||
glUniform1i(glGetUniformLocation(program, "img_pixelData2D"), 0);
|
|
||||||
glUniform1i(glGetUniformLocation(program, "frameCounter"), 100);
|
|
||||||
glUniform1f(glGetUniformLocation(program, "frameTime"), 1.0f / 60.0f);
|
|
||||||
glUniform1f(glGetUniformLocation(program, "aspectRatio"),
|
|
||||||
static_cast<float>(kSceneWidth) / kSceneHeight);
|
|
||||||
glUniform2f(glGetUniformLocation(program, "pixelSize"), 1.0f / kSceneWidth, 1.0f / kSceneHeight);
|
|
||||||
glUniform1f(glGetUniformLocation(program, "nightVision"), 0.23f);
|
|
||||||
glUniform1f(glGetUniformLocation(program, "darknessLightFactor"), 0.08f);
|
|
||||||
glDispatchCompute(1, 1, 1);
|
|
||||||
glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Rg16> ReadWholeRgTexture(GLuint texture) {
|
|
||||||
std::vector<Rg16> texels(kPixelDataTexelCount);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, texture);
|
|
||||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_RG, GL_HALF_FLOAT, texels.data());
|
|
||||||
return texels;
|
|
||||||
}
|
|
||||||
|
|
||||||
class IterationRPProgram203Scenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
GLint stages = 0;
|
|
||||||
GLint features = 0;
|
|
||||||
GLint invocations = 0;
|
|
||||||
glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &stages);
|
|
||||||
glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &features);
|
|
||||||
glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &invocations);
|
|
||||||
const GLbitfield required =
|
|
||||||
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
|
|
||||||
if ((static_cast<GLbitfield>(stages) & GL_COMPUTE_SHADER_BIT) == 0 ||
|
|
||||||
(static_cast<GLbitfield>(features) & required) != required || invocations < 512) {
|
|
||||||
GTEST_SKIP() << "requires 512-invocation basic+arithmetic compute subgroups";
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string error;
|
|
||||||
m_original = CompileCompute(kOriginalMain, &error);
|
|
||||||
ASSERT_NE(m_original, 0u) << "original Program 203: " << error;
|
|
||||||
|
|
||||||
const std::vector<Rgba32f> scene = MakeSceneInput();
|
|
||||||
const std::vector<Rg16> pixelData = MakePixelDataInput();
|
|
||||||
m_scene = MakeTexture(GL_RGBA16F, GL_RGBA, GL_FLOAT, kSceneWidth, kSceneHeight, scene.data());
|
|
||||||
m_originalOutput =
|
|
||||||
MakeTexture(GL_RG16F, GL_RG, GL_HALF_FLOAT, kPixelDataWidth, kPixelDataHeight,
|
|
||||||
pixelData.data());
|
|
||||||
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
const std::array<GLuint, 2> textures = {m_scene, m_originalOutput};
|
|
||||||
glDeleteTextures(static_cast<GLsizei>(textures.size()), textures.data());
|
|
||||||
if (m_original != 0) glDeleteProgram(m_original);
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint m_original = 0;
|
|
||||||
GLuint m_scene = 0;
|
|
||||||
GLuint m_originalOutput = 0;
|
|
||||||
};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_F(IterationRPProgram203Scenario, FixedCompleteInputProducesFixedCompleteGoldenOutput) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
BindAndDispatch(m_original, m_scene, m_originalOutput);
|
|
||||||
glFinish();
|
|
||||||
const std::vector<Rg16> actual = ReadWholeRgTexture(m_originalOutput);
|
|
||||||
const std::vector<Rg16> expected = MakeGoldenOutput();
|
|
||||||
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
|
|
||||||
|
|
||||||
std::size_t mismatchTexels = 0;
|
|
||||||
std::size_t firstMismatch = actual.size();
|
|
||||||
for (std::size_t i = 0; i < actual.size(); ++i) {
|
|
||||||
if (actual[i].r != expected[i].r || actual[i].g != expected[i].g) {
|
|
||||||
if (firstMismatch == actual.size()) firstMismatch = i;
|
|
||||||
++mismatchTexels;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RecordProperty("program203_output_width", kPixelDataWidth);
|
|
||||||
RecordProperty("program203_output_height", kPixelDataHeight);
|
|
||||||
RecordProperty("program203_compared_texels", static_cast<long long>(actual.size()));
|
|
||||||
RecordProperty("program203_mismatch_texels", static_cast<long long>(mismatchTexels));
|
|
||||||
std::cout << "IterationRPProgram203Scenario complete-output actualExposureBits=(0x" << std::hex
|
|
||||||
<< actual[0].r << ", 0x" << actual[0].g << ") goldenExposureBits=(0x" << expected[0].r
|
|
||||||
<< ", 0x" << expected[0].g << std::dec << ") mismatches=" << mismatchTexels << '/'
|
|
||||||
<< actual.size() << '\n';
|
|
||||||
|
|
||||||
if (firstMismatch != actual.size()) {
|
|
||||||
const std::size_t x = firstMismatch % kPixelDataWidth;
|
|
||||||
const std::size_t y = firstMismatch / kPixelDataWidth;
|
|
||||||
ADD_FAILURE() << "complete Program 203 output differs at " << x << ',' << y
|
|
||||||
<< ": actual half bits=(0x" << std::hex << actual[firstMismatch].r << ", 0x"
|
|
||||||
<< actual[firstMismatch].g << ") golden half bits=(0x" << expected[firstMismatch].r
|
|
||||||
<< ", 0x" << expected[firstMismatch].g << std::dec << "); mismatched "
|
|
||||||
<< mismatchTexels << " of " << actual.size() << " texels";
|
|
||||||
}
|
|
||||||
EXPECT_EQ(mismatchTexels, 0u);
|
|
||||||
}
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -8,13 +8,11 @@
|
|||||||
//
|
//
|
||||||
// Scenario - THE FIXTURE-SHAPED SUBGROUP REDUCTION, ON WHATEVER WIDTH THE DEVICE HAS.
|
// Scenario - THE FIXTURE-SHAPED SUBGROUP REDUCTION, ON WHATEVER WIDTH THE DEVICE HAS.
|
||||||
//
|
//
|
||||||
// iterationRP hard-sizes the scratch its subgroup prefix scans write through
|
// iterationRP's auto-exposure pass declares `shared vec2 prefixSumCache[32]` for a
|
||||||
// prefixSumCache[gl_SubgroupID], and ships that idiom twice: the auto-exposure pass
|
// 512-invocation workgroup and combines per-subgroup subtotals through
|
||||||
// declares `shared vec2 prefixSumCache[32]` for a 512-invocation workgroup, and the
|
// prefixSumCache[gl_SubgroupID]. The algorithm is width-agnostic; only the static 32
|
||||||
// RTW importance warp declares `shared float prefixSumCache[64]` for a 1024-invocation
|
// bakes in "at most 32 subgroups", which every desktop capture satisfies and an 8-lane
|
||||||
// one. Both algorithms are width-agnostic; only the static lengths bake in "at most 32
|
// device (lavapipe: 64 subgroups) does not. DirectVulkan patches exactly that with
|
||||||
// (respectively 64) subgroups", which every desktop capture satisfies and an 8-lane
|
|
||||||
// device (lavapipe: 64 and 128 subgroups) does not. DirectVulkan patches exactly that with
|
|
||||||
// FixIterationRPSubgroupScratchPass, growing the array to ceil(invocations / native
|
// FixIterationRPSubgroupScratchPass, growing the array to ceil(invocations / native
|
||||||
// width) on the modules that match the pack's reduction fingerprint.
|
// width) on the modules that match the pack's reduction fingerprint.
|
||||||
//
|
//
|
||||||
@@ -49,10 +47,6 @@ namespace MGITest {
|
|||||||
constexpr std::uint32_t kInvocationCount = 512u;
|
constexpr std::uint32_t kInvocationCount = 512u;
|
||||||
// sum of 0..511, exactly representable and associativity-proof in fp32.
|
// sum of 0..511, exactly representable and associativity-proof in fp32.
|
||||||
constexpr float kExpectedTotal = 130816.0f;
|
constexpr float kExpectedTotal = 130816.0f;
|
||||||
// The RTW warp's shape: 1024 invocations into a 64-entry float scratch.
|
|
||||||
constexpr std::uint32_t kWideInvocationCount = 1024u;
|
|
||||||
// sum of 0..1023, likewise exact in fp32.
|
|
||||||
constexpr float kWideExpectedTotal = 523776.0f;
|
|
||||||
|
|
||||||
constexpr const char* kComputeSource = R"(#version 430 core
|
constexpr const char* kComputeSource = R"(#version 430 core
|
||||||
#extension GL_KHR_shader_subgroup_basic : require
|
#extension GL_KHR_shader_subgroup_basic : require
|
||||||
@@ -93,50 +87,6 @@ void main() {
|
|||||||
}
|
}
|
||||||
atomicMax(outputData.maxSubgroupId, gl_SubgroupID);
|
atomicMax(outputData.maxSubgroupId, gl_SubgroupID);
|
||||||
}
|
}
|
||||||
)";
|
|
||||||
|
|
||||||
// The RTW importance warp's shape: a plain float scan over 1024 invocations
|
|
||||||
// into a 64-entry scratch. Same idiom, different dimensions - which is exactly
|
|
||||||
// what a fingerprint pinned to the exposure pass's shape walks past.
|
|
||||||
constexpr const char* kWideComputeSource = R"(#version 430 core
|
|
||||||
#extension GL_KHR_shader_subgroup_basic : require
|
|
||||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
|
||||||
|
|
||||||
layout(local_size_x = 1024) in;
|
|
||||||
|
|
||||||
layout(std430, binding = 0) buffer Output {
|
|
||||||
float total;
|
|
||||||
uint numSubgroups;
|
|
||||||
uint maxSubgroupId;
|
|
||||||
} outputData;
|
|
||||||
|
|
||||||
shared float prefixSumCache[64];
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
float importance = float(gl_LocalInvocationID.x);
|
|
||||||
float prefixSum = subgroupInclusiveAdd(importance);
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = prefixSum;
|
|
||||||
barrier();
|
|
||||||
|
|
||||||
uint loopLength = uint(findMSB(gl_NumSubgroups));
|
|
||||||
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
|
|
||||||
|
|
||||||
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
|
|
||||||
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
|
|
||||||
prefixSum += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = prefixSum;
|
|
||||||
}
|
|
||||||
barrier();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gl_LocalInvocationID.x == 1023u) {
|
|
||||||
outputData.total = prefixSum;
|
|
||||||
outputData.numSubgroups = gl_NumSubgroups;
|
|
||||||
}
|
|
||||||
atomicMax(outputData.maxSubgroupId, gl_SubgroupID);
|
|
||||||
}
|
|
||||||
)";
|
)";
|
||||||
|
|
||||||
struct OutputBlock {
|
struct OutputBlock {
|
||||||
@@ -180,7 +130,8 @@ void main() {
|
|||||||
"512-invocation workgroup";
|
"512-invocation workgroup";
|
||||||
}
|
}
|
||||||
|
|
||||||
m_maxInvocations = static_cast<std::uint32_t>(invocations);
|
m_program = CompileComputeProgram(kComputeSource);
|
||||||
|
ASSERT_NE(m_program, 0u) << m_buildLog;
|
||||||
|
|
||||||
glGenBuffers(1, &m_output);
|
glGenBuffers(1, &m_output);
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
||||||
@@ -230,14 +181,7 @@ void main() {
|
|||||||
return program;
|
return program;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-poisons the block, compiles the shape under test and runs it once.
|
OutputBlock Dispatch() {
|
||||||
OutputBlock Dispatch(const char* source) {
|
|
||||||
const OutputBlock poison{-1.0f, 0xa5a5a5a5u, 0u};
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
|
||||||
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(OutputBlock), &poison);
|
|
||||||
m_program = CompileComputeProgram(source);
|
|
||||||
EXPECT_NE(m_program, 0u) << m_buildLog;
|
|
||||||
if (m_program == 0u) return OutputBlock{};
|
|
||||||
glUseProgram(m_program);
|
glUseProgram(m_program);
|
||||||
glDispatchCompute(1, 1, 1);
|
glDispatchCompute(1, 1, 1);
|
||||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||||
@@ -249,13 +193,12 @@ void main() {
|
|||||||
|
|
||||||
GLuint m_program = 0;
|
GLuint m_program = 0;
|
||||||
GLuint m_output = 0;
|
GLuint m_output = 0;
|
||||||
std::uint32_t m_maxInvocations = 0;
|
|
||||||
std::string m_buildLog;
|
std::string m_buildLog;
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST_F(IterationRPScratchFixScenario, FixtureShapedReductionSumsEveryInvocation) {
|
TEST_F(IterationRPScratchFixScenario, FixtureShapedReductionSumsEveryInvocation) {
|
||||||
const OutputBlock block = Dispatch(kComputeSource);
|
const OutputBlock block = Dispatch();
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||||
|
|
||||||
// The topology diagnostics catch the failure modes by name before the sum does:
|
// The topology diagnostics catch the failure modes by name before the sum does:
|
||||||
@@ -276,27 +219,4 @@ void main() {
|
|||||||
<< "workgroup reduction produced " << block.total << " with gl_NumSubgroups="
|
<< "workgroup reduction produced " << block.total << " with gl_NumSubgroups="
|
||||||
<< block.numSubgroups;
|
<< block.numSubgroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pack's second instance of the same bug, and the one that kept the CI
|
|
||||||
// retrace red after the exposure pass alone was patched.
|
|
||||||
TEST_F(IterationRPScratchFixScenario, WideFixtureShapedReductionSumsEveryInvocation) {
|
|
||||||
if (m_maxInvocations < kWideInvocationCount) {
|
|
||||||
GTEST_SKIP() << "needs a " << kWideInvocationCount << "-invocation workgroup";
|
|
||||||
}
|
|
||||||
const OutputBlock block = Dispatch(kWideComputeSource);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
|
||||||
|
|
||||||
ASSERT_NE(block.numSubgroups, 0xa5a5a5a5u) << "invocation 1023 never reached its store";
|
|
||||||
EXPECT_GE(block.numSubgroups, 1u);
|
|
||||||
EXPECT_LE(block.numSubgroups, kWideInvocationCount);
|
|
||||||
EXPECT_LT(block.maxSubgroupId, block.numSubgroups)
|
|
||||||
<< "gl_SubgroupID exceeds gl_NumSubgroups - the inconsistency "
|
|
||||||
"DeriveNumSubgroupsPass exists to repair";
|
|
||||||
|
|
||||||
// Without the patch an 8-lane device writes prefixSumCache[64..127] out of
|
|
||||||
// bounds and this comparison fails.
|
|
||||||
EXPECT_EQ(block.total, kWideExpectedTotal)
|
|
||||||
<< "workgroup reduction produced " << block.total << " with gl_NumSubgroups="
|
|
||||||
<< block.numSubgroups;
|
|
||||||
}
|
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -1,268 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/XfbPrimitiveQueryScenario.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
|
|
||||||
//
|
|
||||||
// What the two transform feedback queries report for a VERTEX-ONLY capture that
|
|
||||||
// OVERFLOWS its buffer - the shape of KHR-GL30.transform_feedback.query_vertex_*,
|
|
||||||
// and the one place where the two targets must disagree:
|
|
||||||
//
|
|
||||||
// * GL_PRIMITIVES_GENERATED counts what the capture stage assembled: 4 points.
|
|
||||||
// * GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN counts what the capture buffers
|
|
||||||
// took. With room for three vertices, a full buffer stops recording whole
|
|
||||||
// primitives (GL 4.6 core 13.2.2), so the answer is 3, not 4 and not 6.
|
|
||||||
//
|
|
||||||
// Both numbers came from the backend's own GPU counter until the driver underneath
|
|
||||||
// DirectGLES was caught reporting exactly twice the written count for this shape
|
|
||||||
// (Adreno 830, vertex-only capture issued right after a large render pass). The
|
|
||||||
// frontend already computes the desktop-exact number for a capture with no geometry
|
|
||||||
// stage, so that is what answers PRIMITIVES_WRITTEN there now - and this scenario is
|
|
||||||
// what pins the value, on every backend, without a device.
|
|
||||||
//
|
|
||||||
// The non-overflowing case is the negative control: with room for all four points
|
|
||||||
// the two targets must AGREE at 4, so a "written" that silently reports the
|
|
||||||
// generated count cannot pass both cases at once.
|
|
||||||
|
|
||||||
#include <cmath>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "../Harness/HeadlessGL.h"
|
|
||||||
#include "../Harness/ScenarioFixture.h"
|
|
||||||
|
|
||||||
#ifdef GLAPI
|
|
||||||
#undef GLAPI
|
|
||||||
#endif
|
|
||||||
#define GL_GLEXT_PROTOTYPES
|
|
||||||
#include <GL/gl.h>
|
|
||||||
#include <GL/glcorearb.h>
|
|
||||||
#undef GL_GLEXT_PROTOTYPES
|
|
||||||
|
|
||||||
namespace MGITest {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
constexpr float kPoison = -1234.0f;
|
|
||||||
// One vec4 per captured point.
|
|
||||||
constexpr std::size_t kFloatsPerVertex = 4;
|
|
||||||
constexpr std::size_t kBytesPerVertex = kFloatsPerVertex * sizeof(float);
|
|
||||||
// The draw: four points, whichever way the capture buffer is sized.
|
|
||||||
constexpr GLsizei kDrawnPoints = 4;
|
|
||||||
|
|
||||||
GLuint CompileShader(GLenum type, const std::string& source, std::string* log) {
|
|
||||||
const GLuint shader = glCreateShader(type);
|
|
||||||
const char* text = source.c_str();
|
|
||||||
glShaderSource(shader, 1, &text, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
GLint status = GL_FALSE;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
|
||||||
if (status == GL_FALSE) {
|
|
||||||
GLint length = 0;
|
|
||||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
|
||||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
|
||||||
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
|
|
||||||
if (log != nullptr) *log = buffer.data();
|
|
||||||
glDeleteShader(shader);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return shader;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vertex-only capture program - no geometry stage, so nothing amplifies and the
|
|
||||||
// primitives written are the primitives drawn (up to the buffer's capacity).
|
|
||||||
GLuint BuildCaptureProgram(std::string* log) {
|
|
||||||
const std::string vertexSource = R"(#version 430 core
|
|
||||||
layout(location = 0) in vec4 vs_in_value;
|
|
||||||
out vec4 vs_out_value;
|
|
||||||
void main() {
|
|
||||||
vs_out_value = vs_in_value;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log);
|
|
||||||
if (vertexShader == 0) return 0;
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
glAttachShader(program, vertexShader);
|
|
||||||
const char* varying = "vs_out_value";
|
|
||||||
glTransformFeedbackVaryings(program, 1, &varying, GL_INTERLEAVED_ATTRIBS);
|
|
||||||
glLinkProgram(program);
|
|
||||||
glDeleteShader(vertexShader);
|
|
||||||
GLint status = GL_FALSE;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
|
||||||
if (status == GL_FALSE) {
|
|
||||||
GLint length = 0;
|
|
||||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
|
||||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
|
||||||
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
|
||||||
if (log != nullptr) *log = buffer.data();
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
class XfbPrimitiveQueryScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
std::string log;
|
|
||||||
m_program = BuildCaptureProgram(&log);
|
|
||||||
ASSERT_NE(m_program, 0u) << "capture program failed to build: " << log;
|
|
||||||
|
|
||||||
glGenVertexArrays(1, &m_vao);
|
|
||||||
glBindVertexArray(m_vao);
|
|
||||||
glGenBuffers(1, &m_vbo);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
|
||||||
// Vertex i is (i, i+1, i+2, i+3), so a record that landed in the wrong slot
|
|
||||||
// is as visible as one that never landed at all.
|
|
||||||
float vertices[kDrawnPoints * kFloatsPerVertex] = {};
|
|
||||||
for (int point = 0; point < kDrawnPoints; ++point) {
|
|
||||||
for (std::size_t component = 0; component < kFloatsPerVertex; ++component) {
|
|
||||||
vertices[static_cast<std::size_t>(point) * kFloatsPerVertex + component] =
|
|
||||||
static_cast<float>(point) + static_cast<float>(component);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
|
||||||
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
|
|
||||||
glEnableVertexAttribArray(0);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
|
||||||
|
|
||||||
glGenQueries(2, m_queries);
|
|
||||||
ASSERT_NE(m_queries[0], 0u);
|
|
||||||
ASSERT_NE(m_queries[1], 0u);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
glDeleteQueries(2, m_queries);
|
|
||||||
glBindVertexArray(0);
|
|
||||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
|
||||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
||||||
if (m_program != 0) glDeleteProgram(m_program);
|
|
||||||
glUseProgram(0);
|
|
||||||
ScenarioTest::TearDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
// A capture buffer with room for exactly `vertexCapacity` records, poisoned so
|
|
||||||
// that "captured nothing" is legible, bound to capture point 0.
|
|
||||||
GLuint MakeCaptureBuffer(std::size_t vertexCapacity) {
|
|
||||||
GLuint buffer = 0;
|
|
||||||
glGenBuffers(1, &buffer);
|
|
||||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer);
|
|
||||||
const std::vector<float> poison(vertexCapacity * kFloatsPerVertex, kPoison);
|
|
||||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER,
|
|
||||||
static_cast<GLsizeiptr>(vertexCapacity * kBytesPerVertex), poison.data(),
|
|
||||||
GL_DYNAMIC_DRAW);
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ONE capture span, four points, with both query targets open across it - the
|
|
||||||
// order KHR-GL30.transform_feedback.query_vertex_interleaved_test uses: the
|
|
||||||
// queries wrap the whole span, never the other way round.
|
|
||||||
void RunQueriedSpan(GLuint* written, GLuint* generated) {
|
|
||||||
glEnable(GL_RASTERIZER_DISCARD);
|
|
||||||
glUseProgram(m_program);
|
|
||||||
glBindVertexArray(m_vao);
|
|
||||||
|
|
||||||
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, m_queries[0]);
|
|
||||||
glBeginQuery(GL_PRIMITIVES_GENERATED, m_queries[1]);
|
|
||||||
glBeginTransformFeedback(GL_POINTS);
|
|
||||||
glDrawArrays(GL_POINTS, 0, kDrawnPoints);
|
|
||||||
glEndTransformFeedback();
|
|
||||||
glEndQuery(GL_PRIMITIVES_GENERATED);
|
|
||||||
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
|
|
||||||
|
|
||||||
glDisable(GL_RASTERIZER_DISCARD);
|
|
||||||
glUseProgram(0);
|
|
||||||
|
|
||||||
*written = 0xFFFFFFFFu;
|
|
||||||
*generated = 0xFFFFFFFFu;
|
|
||||||
glGetQueryObjectuiv(m_queries[0], GL_QUERY_RESULT, written);
|
|
||||||
glGetQueryObjectuiv(m_queries[1], GL_QUERY_RESULT, generated);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The capture record at slot `point` must be the vertex the draw fetched there.
|
|
||||||
static ::testing::AssertionResult CapturedVertexIs(const float* record, int point) {
|
|
||||||
for (std::size_t component = 0; component < kFloatsPerVertex; ++component) {
|
|
||||||
const float expected = static_cast<float>(point) + static_cast<float>(component);
|
|
||||||
const float got = record[component];
|
|
||||||
// isfinite first: every ordered comparison against a NaN is false, so a
|
|
||||||
// pair of one-sided range tests REPORTS SUCCESS for uninitialised storage
|
|
||||||
// that happens to read as NaN.
|
|
||||||
if (!std::isfinite(got) || std::fabs(got - expected) > 0.01f) {
|
|
||||||
return ::testing::AssertionFailure()
|
|
||||||
<< "point " << point << " component " << component << " is " << got << ", expected "
|
|
||||||
<< expected << (got == kPoison ? " (the capture never reached these bytes)" : "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ::testing::AssertionSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint m_program = 0;
|
|
||||||
GLuint m_vao = 0;
|
|
||||||
GLuint m_vbo = 0;
|
|
||||||
GLuint m_queries[2] = {0, 0};
|
|
||||||
};
|
|
||||||
|
|
||||||
// The negative control: the buffer holds every point the draw produces, so both
|
|
||||||
// targets must report the same 4. A "written" that is really the generated count
|
|
||||||
// passes this case and fails the next one; a "written" that is really zero fails
|
|
||||||
// this one.
|
|
||||||
TEST_F(XfbPrimitiveQueryScenario, ACaptureThatFitsReportsEveryPrimitiveOnBothTargets) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
|
|
||||||
const GLuint captureBuffer = MakeCaptureBuffer(kDrawnPoints);
|
|
||||||
GLuint written = 0;
|
|
||||||
GLuint generated = 0;
|
|
||||||
RunQueriedSpan(&written, &generated);
|
|
||||||
|
|
||||||
EXPECT_EQ(written, 4u);
|
|
||||||
EXPECT_EQ(generated, 4u);
|
|
||||||
|
|
||||||
std::vector<float> readback(kDrawnPoints * kFloatsPerVertex, kPoison);
|
|
||||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
|
||||||
static_cast<GLsizeiptr>(kDrawnPoints * kBytesPerVertex), readback.data());
|
|
||||||
for (int point = 0; point < kDrawnPoints; ++point) {
|
|
||||||
EXPECT_TRUE(CapturedVertexIs(readback.data() + static_cast<std::size_t>(point) * kFloatsPerVertex,
|
|
||||||
point));
|
|
||||||
}
|
|
||||||
|
|
||||||
glDeleteBuffers(1, &captureBuffer);
|
|
||||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The pin: four points into a buffer sized for three. The fourth is not written, so
|
|
||||||
// the two targets part ways at 3 and 4 - the exact pair
|
|
||||||
// KHR-GL30.transform_feedback.query_vertex_interleaved_test checks, and the pair the
|
|
||||||
// Adreno driver counter got wrong (it answered 6).
|
|
||||||
TEST_F(XfbPrimitiveQueryScenario, AnOverflowingVertexOnlyCaptureStopsWritingAtTheBufferCapacity) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
|
|
||||||
constexpr std::size_t kCapacityVertices = 3;
|
|
||||||
const GLuint captureBuffer = MakeCaptureBuffer(kCapacityVertices);
|
|
||||||
GLuint written = 0;
|
|
||||||
GLuint generated = 0;
|
|
||||||
RunQueriedSpan(&written, &generated);
|
|
||||||
|
|
||||||
EXPECT_EQ(written, 3u) << "the capture buffer holds " << kCapacityVertices << " points";
|
|
||||||
EXPECT_EQ(generated, 4u) << "every point the draw assembled is generated, capacity or not";
|
|
||||||
|
|
||||||
// The three records that DID fit are the first three points, in order: an
|
|
||||||
// overflow truncates the capture, it does not scramble or drop what preceded it.
|
|
||||||
std::vector<float> readback(kCapacityVertices * kFloatsPerVertex, kPoison);
|
|
||||||
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
|
||||||
static_cast<GLsizeiptr>(kCapacityVertices * kBytesPerVertex), readback.data());
|
|
||||||
for (int point = 0; point < static_cast<int>(kCapacityVertices); ++point) {
|
|
||||||
EXPECT_TRUE(CapturedVertexIs(readback.data() + static_cast<std::size_t>(point) * kFloatsPerVertex,
|
|
||||||
point));
|
|
||||||
}
|
|
||||||
|
|
||||||
glDeleteBuffers(1, &captureBuffer);
|
|
||||||
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -328,7 +328,6 @@ namespace MobileGL {
|
|||||||
// transform feedback counter cannot see them - nothing was being captured.
|
// transform feedback counter cannot see them - nothing was being captured.
|
||||||
void AddTransformFeedbackPausedPrimitives(Uint64 primitives) {
|
void AddTransformFeedbackPausedPrimitives(Uint64 primitives) {
|
||||||
m_transformFeedbackPausedPrimitiveCounter += primitives;
|
m_transformFeedbackPausedPrimitiveCounter += primitives;
|
||||||
m_transformFeedbackGeneratedPrimitiveCounter += primitives;
|
|
||||||
}
|
}
|
||||||
Uint64 GetTransformFeedbackPausedPrimitiveCounter() const {
|
Uint64 GetTransformFeedbackPausedPrimitiveCounter() const {
|
||||||
return m_transformFeedbackPausedPrimitiveCounter;
|
return m_transformFeedbackPausedPrimitiveCounter;
|
||||||
@@ -343,30 +342,8 @@ namespace MobileGL {
|
|||||||
// (pre-clamp; drives the GS strip capture-order fixup at EndTF).
|
// (pre-clamp; drives the GS strip capture-order fixup at EndTF).
|
||||||
void AddTransformFeedbackInputPrimitives(Uint64 primitives) {
|
void AddTransformFeedbackInputPrimitives(Uint64 primitives) {
|
||||||
m_transformFeedbackInputPrimitives += primitives;
|
m_transformFeedbackInputPrimitives += primitives;
|
||||||
m_transformFeedbackGeneratedPrimitiveCounter += primitives;
|
|
||||||
}
|
}
|
||||||
Uint64 GetTransformFeedbackInputPrimitives() const { return m_transformFeedbackInputPrimitives; }
|
Uint64 GetTransformFeedbackInputPrimitives() const { return m_transformFeedbackInputPrimitives; }
|
||||||
// What a GL_PRIMITIVES_GENERATED query counts over its span: every primitive the
|
|
||||||
// capture stage assembled, including the ones a paused span discarded (those are
|
|
||||||
// generated but never written). Kept as its own running total rather than derived
|
|
||||||
// from the input counter above, which BeginTransformFeedback resets per span while
|
|
||||||
// a query may cover several of them.
|
|
||||||
Uint64 GetTransformFeedbackGeneratedCounter() const {
|
|
||||||
return m_transformFeedbackGeneratedPrimitiveCounter;
|
|
||||||
}
|
|
||||||
// Capture draws whose written-primitive count the CPU accounting reproduced
|
|
||||||
// exactly, and the subset it could not: a program with a geometry stage amplifies
|
|
||||||
// by whatever the shader emits, which only the driver's own counter knows. The
|
|
||||||
// transform feedback queries diff both over their span to decide whether the CPU
|
|
||||||
// delta may stand in for the backend's GPU result (GL_Query.cpp).
|
|
||||||
void AddTransformFeedbackAccountedCaptureDraw() { ++m_transformFeedbackAccountedCaptureDraws; }
|
|
||||||
Uint64 GetTransformFeedbackAccountedCaptureDraws() const {
|
|
||||||
return m_transformFeedbackAccountedCaptureDraws;
|
|
||||||
}
|
|
||||||
void AddTransformFeedbackGeometryCaptureDraw() { ++m_transformFeedbackGeometryCaptureDraws; }
|
|
||||||
Uint64 GetTransformFeedbackGeometryCaptureDraws() const {
|
|
||||||
return m_transformFeedbackGeometryCaptureDraws;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform feedback objects (ARB_transform_feedback2 / GL 4.0 core).
|
// Transform feedback objects (ARB_transform_feedback2 / GL 4.0 core).
|
||||||
// The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER
|
// The capture state above and the indexed GL_TRANSFORM_FEEDBACK_BUFFER
|
||||||
@@ -462,9 +439,6 @@ namespace MobileGL {
|
|||||||
Uint64 m_transformFeedbackPausedPrimitiveCounter = 0;
|
Uint64 m_transformFeedbackPausedPrimitiveCounter = 0;
|
||||||
Uint64 m_transformFeedbackCapturedVertices = 0;
|
Uint64 m_transformFeedbackCapturedVertices = 0;
|
||||||
Uint64 m_transformFeedbackInputPrimitives = 0;
|
Uint64 m_transformFeedbackInputPrimitives = 0;
|
||||||
Uint64 m_transformFeedbackGeneratedPrimitiveCounter = 0;
|
|
||||||
Uint64 m_transformFeedbackAccountedCaptureDraws = 0;
|
|
||||||
Uint64 m_transformFeedbackGeometryCaptureDraws = 0;
|
|
||||||
|
|
||||||
// Everything a transform feedback object owns while it is NOT the bound one.
|
// Everything a transform feedback object owns while it is NOT the bound one.
|
||||||
struct TransformFeedbackObjectState {
|
struct TransformFeedbackObjectState {
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ namespace MobileGL {
|
|||||||
m_dirtyRects.resize(requiredLevelCount);
|
m_dirtyRects.resize(requiredLevelCount);
|
||||||
m_compressedData.resize(requiredLevelCount);
|
m_compressedData.resize(requiredLevelCount);
|
||||||
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
|
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
|
||||||
m_requestedCompressedFormats.resize(requiredLevelCount, GL_NONE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_texelSizes[level] = input.texelSize;
|
m_texelSizes[level] = input.texelSize;
|
||||||
@@ -80,9 +79,6 @@ namespace MobileGL {
|
|||||||
m_compressedFormats[level] = GL_NONE;
|
m_compressedFormats[level] = GL_NONE;
|
||||||
m_compressedData[level].clear();
|
m_compressedData[level].clear();
|
||||||
m_compressedData[level].shrink_to_fit();
|
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) {
|
void MipmapStorage::SetCompressedImage(Uint level, GLenum internalFormat, const void* data, SizeT size) {
|
||||||
@@ -114,16 +110,6 @@ namespace MobileGL {
|
|||||||
return m_compressedData[level].data();
|
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) {
|
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
|
||||||
if (levelCount >= m_data.size()) return;
|
if (levelCount >= m_data.size()) return;
|
||||||
|
|
||||||
@@ -134,7 +120,6 @@ namespace MobileGL {
|
|||||||
m_dirtyRects.resize(levelCount);
|
m_dirtyRects.resize(levelCount);
|
||||||
m_compressedData.resize(levelCount);
|
m_compressedData.resize(levelCount);
|
||||||
m_compressedFormats.resize(levelCount);
|
m_compressedFormats.resize(levelCount);
|
||||||
m_requestedCompressedFormats.resize(levelCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
|
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
|
||||||
|
|||||||
@@ -96,18 +96,6 @@ namespace MobileGL {
|
|||||||
SizeT GetCompressedByteSize(Uint level) const;
|
SizeT GetCompressedByteSize(Uint level) const;
|
||||||
const void* MapCompressedData(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:
|
protected:
|
||||||
// Insert one clamped, non-empty write box, keeping the list disjoint
|
// Insert one clamped, non-empty write box, keeping the list disjoint
|
||||||
// and bounded (see kMaxDirtyRects).
|
// and bounded (see kMaxDirtyRects).
|
||||||
@@ -127,7 +115,6 @@ namespace MobileGL {
|
|||||||
Vector<Vector<MipmapDirtyRegion>> m_dirtyRects;
|
Vector<Vector<MipmapDirtyRegion>> m_dirtyRects;
|
||||||
Vector<Vector<Uint8>> m_compressedData;
|
Vector<Vector<Uint8>> m_compressedData;
|
||||||
Vector<GLenum> m_compressedFormats;
|
Vector<GLenum> m_compressedFormats;
|
||||||
Vector<GLenum> m_requestedCompressedFormats;
|
|
||||||
};
|
};
|
||||||
} // namespace GLState
|
} // namespace GLState
|
||||||
} // namespace MG_State
|
} // namespace MG_State
|
||||||
|
|||||||
@@ -111,16 +111,6 @@ namespace MobileGL {
|
|||||||
return m_storage[targetIndex].MapCompressedData(level);
|
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:
|
protected:
|
||||||
Array<MipmapStorage, TargetCount> m_storage;
|
Array<MipmapStorage, TargetCount> m_storage;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -373,18 +373,6 @@ namespace MobileGL {
|
|||||||
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
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 {
|
IntVec3 TextureObjectWithOneMipmap::GetBaseSize() const {
|
||||||
if (m_textureStorage.GetLevelCount() == 0) {
|
if (m_textureStorage.GetLevelCount() == 0) {
|
||||||
return {0, 0, 0};
|
return {0, 0, 0};
|
||||||
|
|||||||
@@ -220,15 +220,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
virtual GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
|
virtual GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
|
||||||
virtual SizeT GetMipmapCompressedByteSize(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;
|
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
|
// Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the
|
||||||
@@ -295,9 +286,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||||
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||||
const void* MapMipmapCompressedImage(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;
|
IntVec3 GetBaseSize() const override;
|
||||||
Bool IsComplete() const override;
|
Bool IsComplete() const override;
|
||||||
|
|||||||
@@ -96,18 +96,6 @@ namespace MobileGL {
|
|||||||
return m_textureStorage.MapCompressedData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
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 {
|
Uint TextureObject2DCube::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const {
|
||||||
MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target &&
|
MOBILEGL_ASSERT(TextureUploadTarget::CubeMapPositiveX <= target &&
|
||||||
target <= TextureUploadTarget::CubeMapNegativeZ,
|
target <= TextureUploadTarget::CubeMapNegativeZ,
|
||||||
|
|||||||
@@ -39,10 +39,6 @@ namespace MobileGL {
|
|||||||
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
SizeT GetMipmapCompressedByteSize(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||||
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
|
const void* MapMipmapCompressedImage(TextureUploadTarget uploadTarget,
|
||||||
Uint mipmapLevel) const override;
|
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;
|
IntVec3 GetBaseSize() const override;
|
||||||
Bool IsComplete() const override;
|
Bool IsComplete() const override;
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Test/Backend/DirectGLES/BaseInstanceInjectionTest.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
|
|
||||||
//
|
|
||||||
// The gate on the gl_BaseInstance indirect lowering in
|
|
||||||
// MG_Backend/DirectGLES/Managers.cpp. That lowering declares a std430 storage block in the
|
|
||||||
// VERTEX stage, and a vertex-stage storage block is optional in both APIs: the minimum for
|
|
||||||
// GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS is 0 (GL 4.6 table 23.64, ES 3.2 table 21.44), and ARM's
|
|
||||||
// GLES driver takes that allowance - a Mali-G925-Immortalis reports 0 for it and for all three
|
|
||||||
// other graphics stages.
|
|
||||||
//
|
|
||||||
// Emitting the block on such a driver does not make it work. The driver refuses the program at
|
|
||||||
// link time ("The number of vertex shader storage blocks (1) is greater than the maximum number
|
|
||||||
// allowed (0)"), and because MobileGL's frontend GL_LINK_STATUS is glslang's rather than the
|
|
||||||
// driver's, the application is told the program linked and then every draw with it renders
|
|
||||||
// nothing. Dropping the indirect half instead keeps ordinary draws working and costs only the
|
|
||||||
// per-command baseInstance of an indirect draw.
|
|
||||||
//
|
|
||||||
// No GL context and no driver: the lowering is a pure String -> String pass over one capability.
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
|
||||||
|
|
||||||
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
|
||||||
#include <MG_Backend/DirectGLES/Managers.h>
|
|
||||||
|
|
||||||
using MobileGL::Bool;
|
|
||||||
using MobileGL::String;
|
|
||||||
using MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
|
|
||||||
using MobileGL::MG_Backend::DirectGLES::PromoteDrawParameterGlobalsToUniforms;
|
|
||||||
using MobileGL::MG_Backend::DirectGLES::VertexStageStorageBlockUsable;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// The capability block is a process-global the backend fills in at init; restore whatever
|
|
||||||
// was there so ordering between this suite and any other that touches it cannot matter.
|
|
||||||
struct ScopedGLESCapabilitiesOverride {
|
|
||||||
ScopedGLESCapabilitiesOverride(): saved(g_GLESCapabilities) {}
|
|
||||||
~ScopedGLESCapabilitiesOverride() { g_GLESCapabilities = saved; }
|
|
||||||
ScopedGLESCapabilitiesOverride(const ScopedGLESCapabilitiesOverride&) = delete;
|
|
||||||
ScopedGLESCapabilitiesOverride& operator=(const ScopedGLESCapabilitiesOverride&) = delete;
|
|
||||||
|
|
||||||
MobileGL::MG_External::GLESCapabilities saved;
|
|
||||||
};
|
|
||||||
|
|
||||||
Bool Contains(const String& haystack, const String& needle) {
|
|
||||||
return haystack.find(needle) != String::npos;
|
|
||||||
}
|
|
||||||
|
|
||||||
// What SPIRV-Cross hands the backend after LowerDrawParametersPass has demoted
|
|
||||||
// gl_BaseInstance to a Private global.
|
|
||||||
constexpr const char* kLoweredBaseInstanceVertexShader = R"(#version 310 es
|
|
||||||
highp int mg_BaseInstanceLowered;
|
|
||||||
void main() {
|
|
||||||
int instance = gl_InstanceID + mg_BaseInstanceLowered;
|
|
||||||
gl_Position = vec4(float(instance));
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// One block is all the indirect view needs, so the predicate is a >= 1 test.
|
|
||||||
TEST(VertexStageStorageBlockUsableTest, RequiresAtLeastOneBlock) {
|
|
||||||
EXPECT_FALSE(VertexStageStorageBlockUsable(0));
|
|
||||||
EXPECT_TRUE(VertexStageStorageBlockUsable(1));
|
|
||||||
EXPECT_TRUE(VertexStageStorageBlockUsable(16));
|
|
||||||
}
|
|
||||||
|
|
||||||
// A driver that leaves the out-param untouched tells us nothing, and guessing "yes" is exactly
|
|
||||||
// what produces the unlinkable program. Unusable, not clamped up to one.
|
|
||||||
TEST(VertexStageStorageBlockUsableTest, ANegativeCountIsUnusableRatherThanClamped) {
|
|
||||||
EXPECT_FALSE(VertexStageStorageBlockUsable(-1));
|
|
||||||
EXPECT_FALSE(VertexStageStorageBlockUsable(-2147483647 - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(BaseInstanceInjectionGate, DriverWithVertexStorageBlocksGetsTheIndirectView) {
|
|
||||||
const ScopedGLESCapabilitiesOverride capsGuard;
|
|
||||||
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
|
||||||
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
|
||||||
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 1;
|
|
||||||
|
|
||||||
const String rewritten =
|
|
||||||
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
|
|
||||||
|
|
||||||
EXPECT_TRUE(Contains(rewritten, "layout(std430, binding = 12) readonly buffer mg_IndirectParams"));
|
|
||||||
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseInstanceWordIndex;"));
|
|
||||||
EXPECT_TRUE(Contains(rewritten, "#define mg_BaseInstanceLowered ((mg_BaseInstanceWordIndex > 0) ? "
|
|
||||||
"int(mg_indirectWords[uint(mg_BaseInstanceWordIndex - 1)]) : mg_BaseInstance)"))
|
|
||||||
<< rewritten;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The bug this gate exists for. The block must not appear at all - not at a different binding,
|
|
||||||
// not behind a preprocessor guard: a declaration the driver counts is a declaration that makes
|
|
||||||
// the whole program unlinkable, and the frontend never surfaces that failure.
|
|
||||||
TEST(BaseInstanceInjectionGate, DriverWithoutVertexStorageBlocksDeclaresNoBlockAtAll) {
|
|
||||||
const ScopedGLESCapabilitiesOverride capsGuard;
|
|
||||||
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
|
||||||
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
|
||||||
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
|
|
||||||
|
|
||||||
const String rewritten =
|
|
||||||
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
|
|
||||||
|
|
||||||
EXPECT_FALSE(Contains(rewritten, "mg_IndirectParams")) << rewritten;
|
|
||||||
EXPECT_FALSE(Contains(rewritten, "buffer"));
|
|
||||||
EXPECT_FALSE(Contains(rewritten, "mg_indirectWords"));
|
|
||||||
// Nothing reads the word index any more, so nothing may declare it either - its presence is
|
|
||||||
// what BackendProgramObjectImpl uses to decide whether to bind an indirect params buffer.
|
|
||||||
EXPECT_FALSE(Contains(rewritten, "mg_BaseInstanceWordIndex"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Degraded, but still correct for every non-indirect draw: the plain mg_BaseInstance uniform is
|
|
||||||
// what the non-indirect draw entry points already write.
|
|
||||||
TEST(BaseInstanceInjectionGate, WithoutTheBlockBaseInstanceFallsBackToThePlainUniform) {
|
|
||||||
const ScopedGLESCapabilitiesOverride capsGuard;
|
|
||||||
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
|
||||||
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
|
||||||
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
|
|
||||||
|
|
||||||
const String rewritten =
|
|
||||||
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
|
|
||||||
|
|
||||||
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseInstance;")) << rewritten;
|
|
||||||
EXPECT_TRUE(Contains(rewritten, "#define mg_BaseInstanceLowered (mg_BaseInstance)")) << rewritten;
|
|
||||||
// The global declaration must be gone; leaving it would shadow the define.
|
|
||||||
EXPECT_FALSE(Contains(rewritten, "highp int mg_BaseInstanceLowered;\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// On a driver that both leaks baseInstance into gl_InstanceID and has no vertex storage block,
|
|
||||||
// the rebase has nothing to subtract. Subtracting the uniform instead would remove the base
|
|
||||||
// twice from every non-indirect draw, which is worse than not rebasing at all.
|
|
||||||
TEST(BaseInstanceInjectionGate, WithoutTheBlockInstanceIdRebaseCollapsesToIdentity) {
|
|
||||||
const ScopedGLESCapabilitiesOverride capsGuard;
|
|
||||||
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = true;
|
|
||||||
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
|
||||||
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
|
|
||||||
|
|
||||||
const String rewritten =
|
|
||||||
PromoteDrawParameterGlobalsToUniforms(kLoweredBaseInstanceVertexShader, GL_VERTEX_SHADER);
|
|
||||||
|
|
||||||
EXPECT_TRUE(Contains(rewritten, "#define mg_ZeroBasedInstanceID gl_InstanceID")) << rewritten;
|
|
||||||
EXPECT_FALSE(Contains(rewritten, "gl_InstanceID - ("));
|
|
||||||
EXPECT_FALSE(Contains(rewritten, "mg_indirectWords"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The gate is scoped to the block, not to the whole pass: mg_DrawID and mg_BaseVertex are plain
|
|
||||||
// uniforms with no storage block behind them and must still be promoted on such a driver.
|
|
||||||
TEST(BaseInstanceInjectionGate, DrawIdAndBaseVertexArePromotedRegardless) {
|
|
||||||
const ScopedGLESCapabilitiesOverride capsGuard;
|
|
||||||
g_GLESCapabilities.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
|
||||||
g_GLESCapabilities.MaxShaderStorageBufferBindings = 13;
|
|
||||||
g_GLESCapabilities.MaxVertexShaderStorageBlocks = 0;
|
|
||||||
|
|
||||||
const String source = R"(#version 310 es
|
|
||||||
highp int mg_DrawID;
|
|
||||||
highp int mg_BaseVertex;
|
|
||||||
void main() {
|
|
||||||
gl_Position = vec4(float(mg_DrawID + mg_BaseVertex));
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const String rewritten = PromoteDrawParameterGlobalsToUniforms(source, GL_VERTEX_SHADER);
|
|
||||||
|
|
||||||
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_DrawID;")) << rewritten;
|
|
||||||
EXPECT_TRUE(Contains(rewritten, "uniform highp int mg_BaseVertex;")) << rewritten;
|
|
||||||
}
|
|
||||||
@@ -16,22 +16,5 @@ target_link_libraries(
|
|||||||
${LINK_LIBRARIES}
|
${LINK_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(
|
|
||||||
BaseInstanceInjectionTest
|
|
||||||
BaseInstanceInjectionTest.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(BaseInstanceInjectionTest PRIVATE
|
|
||||||
${MGL_ROOT}/include
|
|
||||||
${MGL_ROOT}/MobileGL
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
BaseInstanceInjectionTest PRIVATE
|
|
||||||
GTest::gtest_main
|
|
||||||
${LINK_LIBRARIES}
|
|
||||||
)
|
|
||||||
|
|
||||||
include(GoogleTest)
|
include(GoogleTest)
|
||||||
gtest_discover_tests(EsslShaderPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(EsslShaderPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
gtest_discover_tests(BaseInstanceInjectionTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
|
|||||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
|
||||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding;
|
||||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats;
|
||||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestViewportArrayExtension;
|
|
||||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms;
|
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -551,55 +550,3 @@ void main() { imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
|||||||
EXPECT_EQ(out, source);
|
EXPECT_EQ(out, source);
|
||||||
EXPECT_EQ(CountOf(out, "GL_NV_image_formats"), 1u) << out;
|
EXPECT_EQ(CountOf(out, "GL_NV_image_formats"), 1u) << out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- GL_OES_viewport_array directive -------------------------------------------------------------
|
|
||||||
|
|
||||||
// SPIRV-Cross prints gl_ViewportIndex bare and requests nothing for it, and ESSL has no core
|
|
||||||
// spelling at any version - so without this directive the stage fails to compile, the program is
|
|
||||||
// marked unusable and every draw made with it silently renders nothing.
|
|
||||||
TEST(RequestViewportArrayExtensionTest, TheDirectiveGoesRightAfterTheVersionLine) {
|
|
||||||
const String source = R"(#version 320 es
|
|
||||||
layout(points) in;
|
|
||||||
layout(points, max_vertices = 1) out;
|
|
||||||
void main() { gl_ViewportIndex = gl_InvocationID; EmitVertex(); }
|
|
||||||
)";
|
|
||||||
const String out = RequestViewportArrayExtension(source, true);
|
|
||||||
EXPECT_TRUE(Contains(out, "#version 320 es\n#extension GL_OES_viewport_array : require\n")) << out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Never speculatively: ARM's compiler hard-errors on an `#extension` naming a string the driver
|
|
||||||
// does not advertise, so the caller's "not needed" answer has to be honoured exactly. A driver
|
|
||||||
// without the extension gets the LowerViewportIndexPass fallback instead.
|
|
||||||
TEST(RequestViewportArrayExtensionTest, NotNeededMeansNotEmitted) {
|
|
||||||
const String source = R"(#version 320 es
|
|
||||||
layout(points) in;
|
|
||||||
layout(points, max_vertices = 1) out;
|
|
||||||
void main() { gl_ViewportIndex = gl_InvocationID; EmitVertex(); }
|
|
||||||
)";
|
|
||||||
EXPECT_EQ(RequestViewportArrayExtension(source, false), source);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(RequestViewportArrayExtensionTest, AnAlreadyPresentDirectiveIsNotDuplicated) {
|
|
||||||
const String source = R"(#version 320 es
|
|
||||||
#extension GL_OES_viewport_array : require
|
|
||||||
layout(points) in;
|
|
||||||
layout(points, max_vertices = 1) out;
|
|
||||||
void main() { gl_ViewportIndex = gl_InvocationID; EmitVertex(); }
|
|
||||||
)";
|
|
||||||
const String out = RequestViewportArrayExtension(source, true);
|
|
||||||
EXPECT_EQ(out, source);
|
|
||||||
EXPECT_EQ(CountOf(out, "GL_OES_viewport_array"), 1u) << out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The two image directives and this one share the insertion point, so a shader that needs both
|
|
||||||
// must end up with both - and with #version still first.
|
|
||||||
TEST(RequestViewportArrayExtensionTest, CoexistsWithTheImageFormatDirective) {
|
|
||||||
const String source = R"(#version 320 es
|
|
||||||
layout(r8ui, binding = 1) uniform writeonly highp uimage2D uni_image;
|
|
||||||
void main() { gl_ViewportIndex = 1; imageStore(uni_image, ivec2(0), uvec4(1u)); }
|
|
||||||
)";
|
|
||||||
const String out = RequestViewportArrayExtension(RequestExtendedImageFormats(source, true), true);
|
|
||||||
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
|
|
||||||
EXPECT_TRUE(Contains(out, "#extension GL_NV_image_formats : require\n")) << out;
|
|
||||||
EXPECT_TRUE(Contains(out, "#extension GL_OES_viewport_array : require\n")) << out;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,17 +34,6 @@ namespace {
|
|||||||
GLint maxFragmentImageUniforms = 4;
|
GLint maxFragmentImageUniforms = 4;
|
||||||
GLint maxComputeImageUniforms = 5;
|
GLint maxComputeImageUniforms = 5;
|
||||||
bool maxGeometryImageUniformsQueried = false;
|
bool maxGeometryImageUniformsQueried = false;
|
||||||
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. The vertex and fragment pnames are ES 3.1,
|
|
||||||
// but the tessellation and geometry ones only exist from ES 3.2 on, so asking for them
|
|
||||||
// on an older context raises GL_INVALID_ENUM - the same shape as the buffer-texture and
|
|
||||||
// anisotropy probes. The "queried" flags are what pin that gating; the "raises error"
|
|
||||||
// knob is what pins the drain.
|
|
||||||
GLint maxTessControlSsboBlocks = 6;
|
|
||||||
GLint maxTessEvaluationSsboBlocks = 7;
|
|
||||||
GLint maxGeometrySsboBlocks = 8;
|
|
||||||
GLint maxFragmentSsboBlocks = 9;
|
|
||||||
bool tessAndGeometrySsboBlocksQueried = false;
|
|
||||||
bool perStageSsboBlockQueryRaisesError = false;
|
|
||||||
GLfloat minFragmentInterpolationOffset = -0.75f;
|
GLfloat minFragmentInterpolationOffset = -0.75f;
|
||||||
GLfloat maxFragmentInterpolationOffset = 0.625f;
|
GLfloat maxFragmentInterpolationOffset = 0.625f;
|
||||||
GLint fragmentInterpolationOffsetBits = 6;
|
GLint fragmentInterpolationOffsetBits = 6;
|
||||||
@@ -122,30 +111,7 @@ namespace {
|
|||||||
funcs.glGetIntegerv = [](GLenum pname, GLint* data) {
|
funcs.glGetIntegerv = [](GLenum pname, GLint* data) {
|
||||||
switch (pname) {
|
switch (pname) {
|
||||||
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS:
|
||||||
if (g_fake.perStageSsboBlockQueryRaisesError) {
|
*data = g_fake.maxVertexSsboBlocks;
|
||||||
g_fake.pendingError = GL_INVALID_ENUM;
|
|
||||||
} else {
|
|
||||||
*data = g_fake.maxVertexSsboBlocks;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS:
|
|
||||||
if (g_fake.perStageSsboBlockQueryRaisesError) {
|
|
||||||
g_fake.pendingError = GL_INVALID_ENUM;
|
|
||||||
} else {
|
|
||||||
*data = g_fake.maxFragmentSsboBlocks;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS:
|
|
||||||
g_fake.tessAndGeometrySsboBlocksQueried = true;
|
|
||||||
*data = g_fake.maxTessControlSsboBlocks;
|
|
||||||
break;
|
|
||||||
case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS:
|
|
||||||
g_fake.tessAndGeometrySsboBlocksQueried = true;
|
|
||||||
*data = g_fake.maxTessEvaluationSsboBlocks;
|
|
||||||
break;
|
|
||||||
case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS:
|
|
||||||
g_fake.tessAndGeometrySsboBlocksQueried = true;
|
|
||||||
*data = g_fake.maxGeometrySsboBlocks;
|
|
||||||
break;
|
break;
|
||||||
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
case GL_MAX_VERTEX_IMAGE_UNIFORMS:
|
||||||
*data = g_fake.maxVertexImageUniforms;
|
*data = g_fake.maxVertexImageUniforms;
|
||||||
@@ -434,10 +400,6 @@ namespace {
|
|||||||
MobileGL::MG_External::GLESCapabilities MakeEs31Capabilities() {
|
MobileGL::MG_External::GLESCapabilities MakeEs31Capabilities() {
|
||||||
MobileGL::MG_External::GLESCapabilities caps;
|
MobileGL::MG_External::GLESCapabilities caps;
|
||||||
caps.GLESVersion = {3, 1, 0};
|
caps.GLESVersion = {3, 1, 0};
|
||||||
// The probe reads its vertex storage-block gate from caps rather than re-querying the
|
|
||||||
// driver (FillInGLESCapabilities resolves the per-stage limits before calling it), so a
|
|
||||||
// caps struct handed to the probe directly has to carry what the fake reports.
|
|
||||||
caps.MaxVertexShaderStorageBlocks = g_fake.maxVertexSsboBlocks;
|
|
||||||
return caps;
|
return caps;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,83 +527,6 @@ TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGe
|
|||||||
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
|
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS probes. These decide whether an application is
|
|
||||||
// told it may declare a storage block in a graphics stage, and on a driver that cannot serve one
|
|
||||||
// a wrong answer is not a cosmetic mis-report: the program is built, the driver refuses it at
|
|
||||||
// link time, the frontend reports LINK_STATUS true anyway, and every draw with it renders
|
|
||||||
// nothing. A Mali-G925-Immortalis reports 0 for vertex, both tessellation stages and geometry.
|
|
||||||
TEST(PerStageStorageBlockCapabilities, TakesTheDriverValuesAndGatesTessAndGeometryOnEs32) {
|
|
||||||
const auto funcs = MakeFakeGLESFunctions();
|
|
||||||
|
|
||||||
// ES 3.1: the tessellation and geometry pnames do not exist, so they must not be asked for
|
|
||||||
// and the stages must report the spec minimum of 0 rather than a hopeful driver number.
|
|
||||||
ResetFakeDriver();
|
|
||||||
g_fake.maxVertexSsboBlocks = 3;
|
|
||||||
MobileGL::MG_External::GLESCapabilities es31Caps;
|
|
||||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es31Caps, funcs));
|
|
||||||
EXPECT_EQ(es31Caps.MaxVertexShaderStorageBlocks, 3);
|
|
||||||
EXPECT_EQ(es31Caps.MaxFragmentShaderStorageBlocks, g_fake.maxFragmentSsboBlocks);
|
|
||||||
EXPECT_EQ(es31Caps.MaxTessControlShaderStorageBlocks, 0);
|
|
||||||
EXPECT_EQ(es31Caps.MaxTessEvaluationShaderStorageBlocks, 0);
|
|
||||||
EXPECT_EQ(es31Caps.MaxGeometryShaderStorageBlocks, 0);
|
|
||||||
EXPECT_FALSE(g_fake.tessAndGeometrySsboBlocksQueried);
|
|
||||||
|
|
||||||
// ES 3.2: all five are real pnames and all five driver values must come through verbatim.
|
|
||||||
ResetFakeDriver();
|
|
||||||
g_fake.maxVertexSsboBlocks = 3;
|
|
||||||
g_fake.glesMinorVersion = 2;
|
|
||||||
MobileGL::MG_External::GLESCapabilities es32Caps;
|
|
||||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es32Caps, funcs));
|
|
||||||
EXPECT_EQ(es32Caps.MaxVertexShaderStorageBlocks, 3);
|
|
||||||
EXPECT_EQ(es32Caps.MaxTessControlShaderStorageBlocks, g_fake.maxTessControlSsboBlocks);
|
|
||||||
EXPECT_EQ(es32Caps.MaxTessEvaluationShaderStorageBlocks, g_fake.maxTessEvaluationSsboBlocks);
|
|
||||||
EXPECT_EQ(es32Caps.MaxGeometryShaderStorageBlocks, g_fake.maxGeometrySsboBlocks);
|
|
||||||
EXPECT_EQ(es32Caps.MaxFragmentShaderStorageBlocks, g_fake.maxFragmentSsboBlocks);
|
|
||||||
EXPECT_TRUE(g_fake.tessAndGeometrySsboBlocksQueried);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zero has to survive the round trip intact. It is the answer that matters most - it is what
|
|
||||||
// ARM's driver actually reports - so a probe that silently substituted a floor would put the
|
|
||||||
// bug straight back.
|
|
||||||
TEST(PerStageStorageBlockCapabilities, AZeroFromTheDriverIsReportedAsZero) {
|
|
||||||
const auto funcs = MakeFakeGLESFunctions();
|
|
||||||
|
|
||||||
ResetFakeDriver();
|
|
||||||
g_fake.glesMinorVersion = 2;
|
|
||||||
g_fake.maxVertexSsboBlocks = 0;
|
|
||||||
g_fake.maxTessControlSsboBlocks = 0;
|
|
||||||
g_fake.maxTessEvaluationSsboBlocks = 0;
|
|
||||||
g_fake.maxGeometrySsboBlocks = 0;
|
|
||||||
g_fake.maxFragmentSsboBlocks = 16;
|
|
||||||
|
|
||||||
MobileGL::MG_External::GLESCapabilities maliLikeCaps;
|
|
||||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(maliLikeCaps, funcs));
|
|
||||||
|
|
||||||
EXPECT_EQ(maliLikeCaps.MaxVertexShaderStorageBlocks, 0);
|
|
||||||
EXPECT_EQ(maliLikeCaps.MaxTessControlShaderStorageBlocks, 0);
|
|
||||||
EXPECT_EQ(maliLikeCaps.MaxTessEvaluationShaderStorageBlocks, 0);
|
|
||||||
EXPECT_EQ(maliLikeCaps.MaxGeometryShaderStorageBlocks, 0);
|
|
||||||
EXPECT_EQ(maliLikeCaps.MaxFragmentShaderStorageBlocks, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A rejected query must leave no error behind for the application's first glGetError to find,
|
|
||||||
// and must fall back to the spec minimums rather than to whatever the untouched out-param held.
|
|
||||||
TEST(PerStageStorageBlockCapabilities, ARejectedQueryIsDrainedAndFallsBackToTheSpecMinimums) {
|
|
||||||
const auto funcs = MakeFakeGLESFunctions();
|
|
||||||
|
|
||||||
ResetFakeDriver();
|
|
||||||
g_fake.perStageSsboBlockQueryRaisesError = true;
|
|
||||||
g_fake.maxVertexSsboBlocks = 12;
|
|
||||||
g_fake.maxFragmentSsboBlocks = 12;
|
|
||||||
|
|
||||||
MobileGL::MG_External::GLESCapabilities caps;
|
|
||||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
|
|
||||||
|
|
||||||
EXPECT_EQ(caps.MaxVertexShaderStorageBlocks, 0);
|
|
||||||
EXPECT_EQ(caps.MaxFragmentShaderStorageBlocks, 4);
|
|
||||||
EXPECT_EQ(g_fake.pendingError, static_cast<GLenum>(GL_NO_ERROR));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) {
|
TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) {
|
||||||
const auto funcs = MakeFakeGLESFunctions();
|
const auto funcs = MakeFakeGLESFunctions();
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||||
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
|
||||||
|
|
||||||
using namespace MobileGL;
|
using namespace MobileGL;
|
||||||
|
|
||||||
@@ -120,56 +119,6 @@ namespace {
|
|||||||
g_stubResultObtainable = true;
|
g_stubResultObtainable = true;
|
||||||
g_stubResultNs = 0;
|
g_stubResultNs = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stub backend transform feedback primitive queries. g_stubXfbQuerySupported = false
|
|
||||||
// models a backend with no GPU counter at all (null handle), which is what leaves the
|
|
||||||
// frontend's CPU accounting as the only source; g_stubResultNs is what the "driver"
|
|
||||||
// would answer when its query IS read, deliberately set to a value the CPU accounting
|
|
||||||
// never produces so the two sources are told apart.
|
|
||||||
Int g_stubXfbBeginCount = 0;
|
|
||||||
Int g_stubXfbEndCount = 0;
|
|
||||||
Bool g_stubXfbQuerySupported = true;
|
|
||||||
|
|
||||||
MG_Backend::BackendQueryHandle StubBeginXfbPrimitivesQuery(Bool) {
|
|
||||||
if (!g_stubXfbQuerySupported) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
++g_stubXfbBeginCount;
|
|
||||||
return reinterpret_cast<MG_Backend::BackendQueryHandle>(static_cast<uintptr_t>(0x53));
|
|
||||||
}
|
|
||||||
|
|
||||||
void StubEndXfbPrimitivesQuery(MG_Backend::BackendQueryHandle) { ++g_stubXfbEndCount; }
|
|
||||||
|
|
||||||
void InstallStubBackendXfbQueries() {
|
|
||||||
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
|
|
||||||
backendGL.BeginXfbPrimitivesQuery = StubBeginXfbPrimitivesQuery;
|
|
||||||
backendGL.EndXfbPrimitivesQuery = StubEndXfbPrimitivesQuery;
|
|
||||||
backendGL.IsQueryResultAvailable = StubIsQueryResultAvailable;
|
|
||||||
backendGL.GetQueryResult64 = StubGetQueryResult64;
|
|
||||||
backendGL.DeleteBackendQuery = StubDeleteBackendQuery;
|
|
||||||
// Off by default: the tests that exercise the DirectGLES preference turn it on.
|
|
||||||
backendGL.PrefersCpuXfbPrimitiveAccounting = false;
|
|
||||||
g_stubXfbBeginCount = 0;
|
|
||||||
g_stubXfbEndCount = 0;
|
|
||||||
g_stubXfbQuerySupported = true;
|
|
||||||
g_stubDeleteCount = 0;
|
|
||||||
g_stubResultAvailable = true;
|
|
||||||
g_stubResultObtainable = true;
|
|
||||||
g_stubResultNs = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// What AccountTransformFeedbackPrimitives (GL_Drawing.cpp) records for one captured
|
|
||||||
// draw, without needing a draw: `assembled` primitives came out of the vertex stage
|
|
||||||
// and `written` of them fitted in the capture buffers (they differ once the buffers
|
|
||||||
// overflow, which is the whole point of PRIMITIVES_WRITTEN).
|
|
||||||
void SimulateAccountedCaptureDraw(Uint64 assembled, Uint64 written, Bool throughGeometryStage = false) {
|
|
||||||
MG_State::pGLContext->AddTransformFeedbackInputPrimitives(assembled);
|
|
||||||
if (throughGeometryStage) {
|
|
||||||
MG_State::pGLContext->AddTransformFeedbackGeometryCaptureDraw();
|
|
||||||
}
|
|
||||||
MG_State::pGLContext->AddTransformFeedbackPrimitives(written);
|
|
||||||
MG_State::pGLContext->AddTransformFeedbackAccountedCaptureDraw();
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
class QueryTest : public ::testing::Test {
|
class QueryTest : public ::testing::Test {
|
||||||
@@ -499,178 +448,6 @@ TEST_F(QueryTest, BackendResultsPropagateThroughFrontend) {
|
|||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The two transform feedback targets count different things and must therefore read
|
|
||||||
// different counters: PRIMITIVES_WRITTEN what the capture buffers took, PRIMITIVES_GENERATED
|
|
||||||
// every primitive the capture stage assembled - including the ones a paused span threw away,
|
|
||||||
// which are generated but never written. Answering both from the written counter (as the
|
|
||||||
// fallback used to) reports the clamped number as the generated one.
|
|
||||||
TEST_F(QueryTest, TransformFeedbackQueryTargetsReadTheirOwnCounter) {
|
|
||||||
const ScopedBackendFunctionsOverride backendGuard;
|
|
||||||
InstallStubBackendXfbQueries();
|
|
||||||
g_stubXfbQuerySupported = false; // no GPU counter: the CPU accounting is the only source
|
|
||||||
|
|
||||||
GLuint ids[2] = {0, 0};
|
|
||||||
MG_Impl::GLImpl::GenQueries(2, ids);
|
|
||||||
ASSERT_NE(ids[0], 0u);
|
|
||||||
ASSERT_NE(ids[1], 0u);
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, ids[0]);
|
|
||||||
MG_Impl::GLImpl::BeginQuery(GL_PRIMITIVES_GENERATED, ids[1]);
|
|
||||||
// Four points assembled into a buffer with room for three.
|
|
||||||
SimulateAccountedCaptureDraw(/*assembled=*/4, /*written=*/3);
|
|
||||||
// ...and two more points assembled while the span was paused: generated, never written.
|
|
||||||
MG_State::pGLContext->AddTransformFeedbackPausedPrimitives(2);
|
|
||||||
MG_Impl::GLImpl::EndQuery(GL_PRIMITIVES_GENERATED);
|
|
||||||
MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
|
|
||||||
|
|
||||||
GLuint written = 0;
|
|
||||||
GLuint generated = 0;
|
|
||||||
MG_Impl::GLImpl::GetQueryObjectuiv(ids[0], GL_QUERY_RESULT, &written);
|
|
||||||
MG_Impl::GLImpl::GetQueryObjectuiv(ids[1], GL_QUERY_RESULT, &generated);
|
|
||||||
EXPECT_EQ(written, 3u);
|
|
||||||
EXPECT_EQ(generated, 6u);
|
|
||||||
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
MG_Impl::GLImpl::DeleteQueries(2, ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A query span that captured nothing at all reads zero from the CPU accounting rather than
|
|
||||||
// the unsigned wrap-around a bare End-minus-Begin subtraction produces the moment the
|
|
||||||
// snapshot is not below the counter (GetQueryObjectuiv would hand the app 4294967295).
|
|
||||||
TEST_F(QueryTest, AnEmptyTransformFeedbackSpanReadsZero) {
|
|
||||||
const ScopedBackendFunctionsOverride backendGuard;
|
|
||||||
InstallStubBackendXfbQueries();
|
|
||||||
g_stubXfbQuerySupported = false;
|
|
||||||
|
|
||||||
GLuint id = 0;
|
|
||||||
MG_Impl::GLImpl::GenQueries(1, &id);
|
|
||||||
ASSERT_NE(id, 0u);
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, id);
|
|
||||||
MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
|
|
||||||
|
|
||||||
GLuint result = 123u;
|
|
||||||
MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result);
|
|
||||||
EXPECT_EQ(result, 0u);
|
|
||||||
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
MG_Impl::GLImpl::DeleteQueries(1, &id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The DirectGLES preference: for a capture the frontend counted exactly - every draw
|
|
||||||
// accounted, none of them amplified by a geometry stage - the CPU number is the
|
|
||||||
// desktop-exact one and the ES driver's PRIMITIVES_WRITTEN counter is not consulted, even
|
|
||||||
// though the backend query ran. The backend query object is released at EndQuery instead of
|
|
||||||
// being left to a result read that will never come.
|
|
||||||
TEST_F(QueryTest, VertexOnlyCaptureSpansPreferTheCpuPrimitiveAccounting) {
|
|
||||||
const ScopedBackendFunctionsOverride backendGuard;
|
|
||||||
InstallStubBackendXfbQueries();
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
|
|
||||||
g_stubResultNs = 6; // what the driver's counter would have said - twice the truth
|
|
||||||
|
|
||||||
GLuint id = 0;
|
|
||||||
MG_Impl::GLImpl::GenQueries(1, &id);
|
|
||||||
ASSERT_NE(id, 0u);
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, id);
|
|
||||||
SimulateAccountedCaptureDraw(/*assembled=*/4, /*written=*/3);
|
|
||||||
MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
|
|
||||||
EXPECT_EQ(g_stubXfbBeginCount, 1);
|
|
||||||
EXPECT_EQ(g_stubXfbEndCount, 1);
|
|
||||||
EXPECT_EQ(g_stubDeleteCount, 1); // ended, then released - not leaked
|
|
||||||
|
|
||||||
GLint available = -1;
|
|
||||||
MG_Impl::GLImpl::GetQueryObjectiv(id, GL_QUERY_RESULT_AVAILABLE, &available);
|
|
||||||
EXPECT_EQ(available, 1);
|
|
||||||
|
|
||||||
GLuint result = 0;
|
|
||||||
MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result);
|
|
||||||
EXPECT_EQ(result, 3u);
|
|
||||||
EXPECT_EQ(g_stubDeleteCount, 1); // the read had no handle left to release
|
|
||||||
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
MG_Impl::GLImpl::DeleteQueries(1, &id);
|
|
||||||
EXPECT_EQ(g_stubDeleteCount, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The regression gate for that preference: a capture fed by a geometry stage writes whatever
|
|
||||||
// the shader emits, which the CPU accounting cannot model, so the backend's counter stays the
|
|
||||||
// answer and its handle survives EndQuery to be read later.
|
|
||||||
TEST_F(QueryTest, AGeometryStageCaptureKeepsTheBackendPrimitiveResult) {
|
|
||||||
const ScopedBackendFunctionsOverride backendGuard;
|
|
||||||
InstallStubBackendXfbQueries();
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
|
|
||||||
g_stubResultNs = 9; // the amplified count only the driver knows
|
|
||||||
|
|
||||||
GLuint id = 0;
|
|
||||||
MG_Impl::GLImpl::GenQueries(1, &id);
|
|
||||||
ASSERT_NE(id, 0u);
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, id);
|
|
||||||
SimulateAccountedCaptureDraw(/*assembled=*/1, /*written=*/1, /*throughGeometryStage=*/true);
|
|
||||||
MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
|
|
||||||
EXPECT_EQ(g_stubDeleteCount, 0); // still to be read
|
|
||||||
|
|
||||||
GLuint result = 0;
|
|
||||||
MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result);
|
|
||||||
EXPECT_EQ(result, 9u);
|
|
||||||
EXPECT_EQ(g_stubDeleteCount, 1);
|
|
||||||
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
MG_Impl::GLImpl::DeleteQueries(1, &id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The other half of that gate: the instanced, indirect and multi-draw entry points never
|
|
||||||
// reach the CPU accounting, so a span made of those moves no counter at all. Its delta would
|
|
||||||
// be zero, which is not "nothing was written" - it is "nothing was counted" - and the
|
|
||||||
// backend's result has to stand.
|
|
||||||
TEST_F(QueryTest, ACaptureSpanTheAccountingNeverSawKeepsTheBackendResult) {
|
|
||||||
const ScopedBackendFunctionsOverride backendGuard;
|
|
||||||
InstallStubBackendXfbQueries();
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
|
|
||||||
g_stubResultNs = 12;
|
|
||||||
|
|
||||||
GLuint id = 0;
|
|
||||||
MG_Impl::GLImpl::GenQueries(1, &id);
|
|
||||||
ASSERT_NE(id, 0u);
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::BeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, id);
|
|
||||||
MG_Impl::GLImpl::EndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
|
|
||||||
|
|
||||||
GLuint result = 0;
|
|
||||||
MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result);
|
|
||||||
EXPECT_EQ(result, 12u);
|
|
||||||
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
MG_Impl::GLImpl::DeleteQueries(1, &id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GL_PRIMITIVES_GENERATED counts primitives whether or not a capture is active, while the
|
|
||||||
// CPU accounting only ever sees capture draws - so the preference above deliberately does
|
|
||||||
// not extend to that target, whatever the backend asked for.
|
|
||||||
TEST_F(QueryTest, PrimitivesGeneratedKeepsTheBackendResultUnderTheCpuPreference) {
|
|
||||||
const ScopedBackendFunctionsOverride backendGuard;
|
|
||||||
InstallStubBackendXfbQueries();
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
|
|
||||||
g_stubResultNs = 7;
|
|
||||||
|
|
||||||
GLuint id = 0;
|
|
||||||
MG_Impl::GLImpl::GenQueries(1, &id);
|
|
||||||
ASSERT_NE(id, 0u);
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::BeginQuery(GL_PRIMITIVES_GENERATED, id);
|
|
||||||
SimulateAccountedCaptureDraw(/*assembled=*/4, /*written=*/3);
|
|
||||||
MG_Impl::GLImpl::EndQuery(GL_PRIMITIVES_GENERATED);
|
|
||||||
EXPECT_EQ(g_stubDeleteCount, 0);
|
|
||||||
|
|
||||||
GLuint result = 0;
|
|
||||||
MG_Impl::GLImpl::GetQueryObjectuiv(id, GL_QUERY_RESULT, &result);
|
|
||||||
EXPECT_EQ(result, 7u);
|
|
||||||
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
MG_Impl::GLImpl::DeleteQueries(1, &id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Environment-agnostic property test for the env -> ConfigLoader -> Features
|
// Environment-agnostic property test for the env -> ConfigLoader -> Features
|
||||||
// chain: whatever MOBILEGL_DISABLE_TIMERQUERY is set to in the environment of
|
// chain: whatever MOBILEGL_DISABLE_TIMERQUERY is set to in the environment of
|
||||||
// this test process, MG_ConfigLoader::Init must have parsed it with the
|
// this test process, MG_ConfigLoader::Init must have parsed it with the
|
||||||
|
|||||||
@@ -365,13 +365,6 @@ TEST(DirectGLESSanity, RebasesInstanceIdWhenIndirectDrawsLeakBaseInstance) {
|
|||||||
// MaxShaderStorageBufferBindings - 1 = 12, so a regression that stops reading the
|
// MaxShaderStorageBufferBindings - 1 = 12, so a regression that stops reading the
|
||||||
// probed cap and falls back to the struct default would surface as "binding = 7".
|
// probed cap and falls back to the struct default would surface as "binding = 7".
|
||||||
caps.MaxShaderStorageBufferBindings = 13;
|
caps.MaxShaderStorageBufferBindings = 13;
|
||||||
// The indirect lowering reads its baseInstance through a storage block declared in the
|
|
||||||
// VERTEX stage, which is optional in both APIs and which the GLESCapabilities default
|
|
||||||
// (0, the spec minimum) therefore denies. This suite is pinning the shape of that
|
|
||||||
// lowering, so it has to describe a driver that can actually have it - see
|
|
||||||
// VertexStageStorageBlockUsable and the BaseInstanceInjectionGate suite for the
|
|
||||||
// zero case.
|
|
||||||
caps.MaxVertexShaderStorageBlocks = 1;
|
|
||||||
|
|
||||||
const MobileGL::String source = R"(#version 310 es
|
const MobileGL::String source = R"(#version 310 es
|
||||||
highp int mg_BaseInstanceLowered;
|
highp int mg_BaseInstanceLowered;
|
||||||
@@ -410,9 +403,6 @@ TEST(DirectGLESSanity, TheIndirectWordIndexIsOneBasedSoItsUnwrittenValueMeansNot
|
|||||||
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
|
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
|
||||||
caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
||||||
caps.MaxShaderStorageBufferBindings = 13;
|
caps.MaxShaderStorageBufferBindings = 13;
|
||||||
// See RebasesInstanceIdWhenIndirectDrawsLeakBaseInstance: without a vertex-stage
|
|
||||||
// storage block there is no word index to be one-based about.
|
|
||||||
caps.MaxVertexShaderStorageBlocks = 1;
|
|
||||||
|
|
||||||
const MobileGL::String source = R"(#version 310 es
|
const MobileGL::String source = R"(#version 310 es
|
||||||
highp int mg_BaseInstanceLowered;
|
highp int mg_BaseInstanceLowered;
|
||||||
@@ -438,10 +428,6 @@ TEST(DirectGLESSanity, KeepsInstanceIdWhenIndirectDrawsAreConforming) {
|
|||||||
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
|
auto& caps = MobileGL::MG_Backend::DirectGLES::g_GLESCapabilities;
|
||||||
caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
caps.IndirectDrawInstanceIdIncludesBaseInstance = false;
|
||||||
caps.MaxShaderStorageBufferBindings = 13;
|
caps.MaxShaderStorageBufferBindings = 13;
|
||||||
// Set explicitly even though the assertions below would also hold on the degraded path:
|
|
||||||
// this case is about a CONFORMING driver leaving gl_InstanceID alone, and it would be a
|
|
||||||
// silent weakening for it to be exercising the no-storage-block fallback instead.
|
|
||||||
caps.MaxVertexShaderStorageBlocks = 1;
|
|
||||||
|
|
||||||
const MobileGL::String source = R"(#version 310 es
|
const MobileGL::String source = R"(#version 310 es
|
||||||
highp int mg_BaseInstanceLowered;
|
highp int mg_BaseInstanceLowered;
|
||||||
@@ -456,8 +442,6 @@ void main() {
|
|||||||
|
|
||||||
EXPECT_EQ(rewritten.find("mg_ZeroBasedInstanceID"), MobileGL::String::npos);
|
EXPECT_EQ(rewritten.find("mg_ZeroBasedInstanceID"), MobileGL::String::npos);
|
||||||
EXPECT_NE(rewritten.find("int instance = gl_InstanceID + mg_BaseInstanceLowered;"), MobileGL::String::npos);
|
EXPECT_NE(rewritten.find("int instance = gl_InstanceID + mg_BaseInstanceLowered;"), MobileGL::String::npos);
|
||||||
// The indirect view is present on this driver, so the fallback must NOT have fired.
|
|
||||||
EXPECT_NE(rewritten.find("buffer mg_IndirectParams"), MobileGL::String::npos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DirectGLESSanity, LeavesDrawParameterGlobalsAloneOutsideVertexShaders) {
|
TEST(DirectGLESSanity, LeavesDrawParameterGlobalsAloneOutsideVertexShaders) {
|
||||||
|
|||||||
@@ -4,13 +4,10 @@ add_executable(
|
|||||||
SpirvPassTest
|
SpirvPassTest
|
||||||
SpirvPassTest.cpp
|
SpirvPassTest.cpp
|
||||||
DeriveNumSubgroupsTest.cpp
|
DeriveNumSubgroupsTest.cpp
|
||||||
FixIterationRPBarrierTest.cpp
|
|
||||||
FixIterationRPSubgroupScratchTest.cpp
|
FixIterationRPSubgroupScratchTest.cpp
|
||||||
EmulateSubgroupsTest.cpp
|
EmulateSubgroupsTest.cpp
|
||||||
DemoteFloat64Test.cpp
|
DemoteFloat64Test.cpp
|
||||||
FlattenXfbInterfaceBlocksTest.cpp
|
FlattenXfbInterfaceBlocksTest.cpp
|
||||||
LowerViewportIndexTest.cpp
|
|
||||||
ClampMultisampleFetchTest.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(SpirvPassTest PRIVATE
|
target_include_directories(SpirvPassTest PRIVATE
|
||||||
|
|||||||
@@ -1,383 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/ClampMultisampleFetchTest.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
|
|
||||||
//
|
|
||||||
// ClampMultisampleFetchPass exists because MobileGL advertises one multisample ceiling and the ES
|
|
||||||
// driver underneath delivers another. GL 4.6 core table 23.53 forces GL_MAX_SAMPLES and
|
|
||||||
// GL_MAX_INTEGER_SAMPLES up to 4; Adreno and Mali back an integer multisample texture with ONE
|
|
||||||
// sample, and DirectGLES quietly allocates that (ClampSamplesToBackendSupport). A CTS shader that
|
|
||||||
// bakes in `texelFetch(usampler2DMS, coord, 3)` - which is what
|
|
||||||
// KHR-GL33/40/41.texture_swizzle.functional_* and KHR-GLxx.texture_size_promotion.functional do -
|
|
||||||
// then reads a sample the storage does not have.
|
|
||||||
//
|
|
||||||
// So what has to hold is per-fetch and per-category at once: the squeezed category's Sample
|
|
||||||
// operand must come back in range, a category that is not squeezed must be untouched, a module
|
|
||||||
// with no multisampled image at all must come out byte for byte as it went in, and every result
|
|
||||||
// must still be a valid module. Real GLSL through the same glslang path the backends use, for the
|
|
||||||
// same reason LowerViewportIndexTest.cpp does it: what matters is what glslang actually emits.
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
|
||||||
|
|
||||||
#define SPV_ENABLE_UTILITY_CODE
|
|
||||||
#include "glslang/SPIRV/spirv.hpp11"
|
|
||||||
#undef SPV_ENABLE_UTILITY_CODE
|
|
||||||
|
|
||||||
#include "Includes.h"
|
|
||||||
#include "Init.h"
|
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
|
||||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
|
||||||
|
|
||||||
#include <spirv-tools/libspirv.hpp>
|
|
||||||
|
|
||||||
#include <map>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
using namespace MobileGL;
|
|
||||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// GLSL.std.450 instruction number (see 3rdparty/glslang/SPIRV/GLSL.std.450.h). The signed
|
|
||||||
// minimum, which is what a GLSL `int` sample index asks for.
|
|
||||||
constexpr Uint32 kGlslStd450SMin = 39u;
|
|
||||||
|
|
||||||
// What MobileGL tells the application GL_MAX_SAMPLES / GL_MAX_INTEGER_SAMPLES are, i.e.
|
|
||||||
// GL_Getter's kFrontendMaxSamples floor. Each test supplies its own backend-real ceilings
|
|
||||||
// against it; Adreno and Mali's Immortalis-G925 both really answer 1 for integer formats.
|
|
||||||
constexpr Int32 kAdvertisedMaxSamples = 4;
|
|
||||||
|
|
||||||
constexpr SizeT kSpirvHeaderWordCount = 5u;
|
|
||||||
|
|
||||||
template <typename Visitor>
|
|
||||||
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
|
|
||||||
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
|
|
||||||
const Uint32 wordCount = spirv[offset] >> 16u;
|
|
||||||
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
|
|
||||||
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
|
|
||||||
offset += wordCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector<Uint32> CompileFragment(const String& source) {
|
|
||||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
|
||||||
ShaderAttrib shaderAttrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
|
||||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
|
||||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
|
||||||
if (!shaderResult) return {};
|
|
||||||
|
|
||||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
|
||||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
|
||||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
|
||||||
if (!programResult) return {};
|
|
||||||
|
|
||||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER},
|
|
||||||
.program = *programResult.value()};
|
|
||||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
|
||||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
|
||||||
if (!binaryResult || binaryResult->empty()) return {};
|
|
||||||
return binaryResult->front();
|
|
||||||
}
|
|
||||||
|
|
||||||
String Disassemble(const Vector<Uint32>& spirv) {
|
|
||||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
|
||||||
String text;
|
|
||||||
tools.Disassemble(spirv, &text);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Validates(const Vector<Uint32>& spirv) {
|
|
||||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
|
||||||
tools.SetMessageConsumer(
|
|
||||||
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
|
|
||||||
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
|
|
||||||
});
|
|
||||||
return tools.Validate(spirv);
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpImageFetch words: 0 opcode/count, 1 result type, 2 result id, 3 image, 4 coordinate,
|
|
||||||
// 5 the optional image-operands mask, 6.. the ids that mask asks for.
|
|
||||||
struct ImageFetch {
|
|
||||||
Uint32 resultId = 0u;
|
|
||||||
Uint32 imageId = 0u;
|
|
||||||
Uint32 mask = 0u;
|
|
||||||
Vector<Uint32> maskOperandIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
Vector<ImageFetch> CollectImageFetches(const Vector<Uint32>& spirv) {
|
|
||||||
Vector<ImageFetch> fetches;
|
|
||||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
|
||||||
if (opcode != spv::Op::OpImageFetch || wordCount < 5u) return;
|
|
||||||
ImageFetch fetch{};
|
|
||||||
fetch.resultId = words[2];
|
|
||||||
fetch.imageId = words[3];
|
|
||||||
if (wordCount > 5u) {
|
|
||||||
fetch.mask = words[5];
|
|
||||||
for (Uint32 word = 6u; word < wordCount; ++word) {
|
|
||||||
fetch.maskOperandIds.push_back(words[word]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetches.push_back(fetch);
|
|
||||||
});
|
|
||||||
return fetches;
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpExtInst words: 0 opcode/count, 1 result type, 2 result id, 3 set, 4 instruction number,
|
|
||||||
// 5.. the operand ids.
|
|
||||||
struct ExtInst {
|
|
||||||
Uint32 resultId = 0u;
|
|
||||||
Uint32 instructionNumber = 0u;
|
|
||||||
Vector<Uint32> operandIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
Vector<ExtInst> CollectExtInsts(const Vector<Uint32>& spirv) {
|
|
||||||
Vector<ExtInst> extInsts;
|
|
||||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
|
||||||
if (opcode != spv::Op::OpExtInst || wordCount < 5u) return;
|
|
||||||
ExtInst extInst{};
|
|
||||||
extInst.resultId = words[2];
|
|
||||||
extInst.instructionNumber = words[4];
|
|
||||||
for (Uint32 word = 5u; word < wordCount; ++word) {
|
|
||||||
extInst.operandIds.push_back(words[word]);
|
|
||||||
}
|
|
||||||
extInsts.push_back(extInst);
|
|
||||||
});
|
|
||||||
return extInsts;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::map<Uint32, Uint32> CollectScalarConstants(const Vector<Uint32>& spirv) {
|
|
||||||
std::map<Uint32, Uint32> values;
|
|
||||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
|
||||||
if (opcode == spv::Op::OpConstant && wordCount == 4u) values[words[2]] = words[3];
|
|
||||||
});
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The one fetch carrying an explicit Sample operand. glslang emits Sample on its own for a
|
|
||||||
// multisample texelFetch - there is no texelFetchOffset for a multisampled sampler - so the
|
|
||||||
// sample id is the mask's first and only operand.
|
|
||||||
const ImageFetch* FindSampleCarryingFetch(const Vector<ImageFetch>& fetches) {
|
|
||||||
for (const ImageFetch& fetch : fetches) {
|
|
||||||
if ((fetch.mask & static_cast<Uint32>(spv::ImageOperandsMask::Sample)) != 0u) {
|
|
||||||
return &fetch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ImageFetch* FindLodCarryingFetch(const Vector<ImageFetch>& fetches) {
|
|
||||||
for (const ImageFetch& fetch : fetches) {
|
|
||||||
if ((fetch.mask & static_cast<Uint32>(spv::ImageOperandsMask::Lod)) != 0u) {
|
|
||||||
return &fetch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// KHR-GL4x.texture_swizzle.functional's integer multisample read in miniature: the sample
|
|
||||||
// index is the advertised GL_MAX_INTEGER_SAMPLES - 1, baked in as a literal, which is exactly
|
|
||||||
// the value the one-sample allocation underneath cannot answer. The plain sampler2D fetch is
|
|
||||||
// the negative control - a NON-multisampled image whose Lod operand this pass must not touch.
|
|
||||||
const char* const kIntegerMultisampleFetch = R"(#version 410 core
|
|
||||||
uniform usampler2DMS uintMs;
|
|
||||||
uniform sampler2D plain;
|
|
||||||
out vec4 fragColor;
|
|
||||||
void main() {
|
|
||||||
uvec4 texel = texelFetch(uintMs, ivec2(gl_FragCoord.xy), 3);
|
|
||||||
vec4 other = texelFetch(plain, ivec2(gl_FragCoord.xy), 0);
|
|
||||||
fragColor = vec4(texel) * 0.5 + other;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// The colour class, which real devices squeeze to something above 1 rather than to 1.
|
|
||||||
const char* const kColorMultisampleFetch = R"(#version 410 core
|
|
||||||
uniform sampler2DMS colorMs;
|
|
||||||
out vec4 fragColor;
|
|
||||||
void main() {
|
|
||||||
fragColor = texelFetch(colorMs, ivec2(gl_FragCoord.xy), 3);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// Every stage on a squeezed device goes through the probe, so the one that declares no
|
|
||||||
// multisampled image has to come back untouched.
|
|
||||||
const char* const kNoMultisampleFetch = R"(#version 410 core
|
|
||||||
uniform sampler2D plain;
|
|
||||||
out vec4 fragColor;
|
|
||||||
void main() {
|
|
||||||
fragColor = texelFetch(plain, ivec2(gl_FragCoord.xy), 0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
class ClampMultisampleFetchTest : public ::testing::Test {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
MobileGL::Initialize();
|
|
||||||
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
|
|
||||||
<< "the clamped module did not survive spirv-val";
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint64 m_validationFailuresAtStart = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The probe is the gate that keeps every ordinary stage off an optimizer round trip, so it has to
|
|
||||||
// answer no for a shader that never reads a multisample texture - and yes for the ones that do.
|
|
||||||
TEST_F(ClampMultisampleFetchTest, TheProbeAnswersOnlyForAMultisampledImage) {
|
|
||||||
const Vector<Uint32> plain = CompileFragment(kNoMultisampleFetch);
|
|
||||||
ASSERT_FALSE(plain.empty());
|
|
||||||
EXPECT_FALSE(ShaderCompiler::DeclaresMultisampledImage(plain));
|
|
||||||
|
|
||||||
const Vector<Uint32> integerMs = CompileFragment(kIntegerMultisampleFetch);
|
|
||||||
ASSERT_FALSE(integerMs.empty());
|
|
||||||
EXPECT_TRUE(ShaderCompiler::DeclaresMultisampledImage(integerMs));
|
|
||||||
|
|
||||||
const Vector<Uint32> colorMs = CompileFragment(kColorMultisampleFetch);
|
|
||||||
ASSERT_FALSE(colorMs.empty());
|
|
||||||
EXPECT_TRUE(ShaderCompiler::DeclaresMultisampledImage(colorMs));
|
|
||||||
|
|
||||||
// Runs on every stage of every program on a squeezed device, so it must survive a stage that
|
|
||||||
// produced no SPIR-V rather than pushing a parse diagnostic for it.
|
|
||||||
EXPECT_FALSE(ShaderCompiler::DeclaresMultisampledImage({}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The combined probe answers both gate questions from one parse; it must agree with the
|
|
||||||
// per-gate probes on the same modules and stay quiet for an empty stage.
|
|
||||||
TEST_F(ClampMultisampleFetchTest, TheCombinedProbeAgreesWithThePerGateOnes) {
|
|
||||||
const Vector<Uint32> integerMs = CompileFragment(kIntegerMultisampleFetch);
|
|
||||||
ASSERT_FALSE(integerMs.empty());
|
|
||||||
const auto msFeatures = ShaderCompiler::ProbeSpirvGateFeatures(integerMs);
|
|
||||||
EXPECT_TRUE(msFeatures.DeclaresMultisampledImage);
|
|
||||||
EXPECT_FALSE(msFeatures.WritesViewportIndexOutput);
|
|
||||||
|
|
||||||
const Vector<Uint32> plain = CompileFragment(kNoMultisampleFetch);
|
|
||||||
ASSERT_FALSE(plain.empty());
|
|
||||||
const auto plainFeatures = ShaderCompiler::ProbeSpirvGateFeatures(plain);
|
|
||||||
EXPECT_FALSE(plainFeatures.DeclaresMultisampledImage);
|
|
||||||
EXPECT_FALSE(plainFeatures.WritesViewportIndexOutput);
|
|
||||||
|
|
||||||
const auto emptyFeatures = ShaderCompiler::ProbeSpirvGateFeatures({});
|
|
||||||
EXPECT_FALSE(emptyFeatures.DeclaresMultisampledImage);
|
|
||||||
EXPECT_FALSE(emptyFeatures.WritesViewportIndexOutput);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The overwhelming majority of modules. Behind the probe they never reach the pass at all, but the
|
|
||||||
// pass has to be inert for them on its own, or a future caller that forgets the gate silently
|
|
||||||
// re-serialises every shader in the program.
|
|
||||||
TEST_F(ClampMultisampleFetchTest, LeavesAModuleWithoutAMultisampledImageUntouched) {
|
|
||||||
const Vector<Uint32> input = CompileFragment(kNoMultisampleFetch);
|
|
||||||
ASSERT_FALSE(input.empty());
|
|
||||||
|
|
||||||
Vector<Uint32> output;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
|
|
||||||
input, output, /*maxColorSamples=*/4, /*maxIntegerSamples=*/1, /*maxDepthSamples=*/4,
|
|
||||||
kAdvertisedMaxSamples, true));
|
|
||||||
EXPECT_EQ(output, input) << Disassemble(output);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The bug itself. GL_MAX_INTEGER_SAMPLES says 4, the texture has one sample, and the shader asks
|
|
||||||
// for sample 3.
|
|
||||||
TEST_F(ClampMultisampleFetchTest, ReplacesAnOutOfRangeIntegerSampleWithZero) {
|
|
||||||
const Vector<Uint32> input = CompileFragment(kIntegerMultisampleFetch);
|
|
||||||
ASSERT_FALSE(input.empty());
|
|
||||||
|
|
||||||
const Vector<ImageFetch> before = CollectImageFetches(input);
|
|
||||||
ASSERT_EQ(before.size(), 2u) << Disassemble(input);
|
|
||||||
const ImageFetch* sampleBefore = FindSampleCarryingFetch(before);
|
|
||||||
const ImageFetch* lodBefore = FindLodCarryingFetch(before);
|
|
||||||
ASSERT_NE(sampleBefore, nullptr) << Disassemble(input);
|
|
||||||
ASSERT_NE(lodBefore, nullptr) << Disassemble(input);
|
|
||||||
ASSERT_EQ(sampleBefore->maskOperandIds.size(), 1u);
|
|
||||||
const std::map<Uint32, Uint32> constantsBefore = CollectScalarConstants(input);
|
|
||||||
ASSERT_EQ(constantsBefore.count(sampleBefore->maskOperandIds.front()), 1u);
|
|
||||||
EXPECT_EQ(constantsBefore.at(sampleBefore->maskOperandIds.front()), 3u);
|
|
||||||
|
|
||||||
Vector<Uint32> output;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
|
|
||||||
input, output, /*maxColorSamples=*/4, /*maxIntegerSamples=*/1, /*maxDepthSamples=*/4,
|
|
||||||
kAdvertisedMaxSamples, true));
|
|
||||||
ASSERT_FALSE(output.empty());
|
|
||||||
const String dis = Disassemble(output);
|
|
||||||
ASSERT_TRUE(Validates(output)) << dis;
|
|
||||||
|
|
||||||
const Vector<ImageFetch> after = CollectImageFetches(output);
|
|
||||||
ASSERT_EQ(after.size(), 2u) << dis;
|
|
||||||
const ImageFetch* sampleAfter = FindSampleCarryingFetch(after);
|
|
||||||
ASSERT_NE(sampleAfter, nullptr) << dis;
|
|
||||||
ASSERT_EQ(sampleAfter->maskOperandIds.size(), 1u) << dis;
|
|
||||||
|
|
||||||
// Sample 0 is the only one a one-sample allocation has - and it is a CONSTANT, not a computed
|
|
||||||
// minimum: at K == 1 there is nothing to compare against. An id that resolves in the constant
|
|
||||||
// table cannot also be some OpExtInst's result.
|
|
||||||
const std::map<Uint32, Uint32> constantsAfter = CollectScalarConstants(output);
|
|
||||||
ASSERT_EQ(constantsAfter.count(sampleAfter->maskOperandIds.front()), 1u) << dis;
|
|
||||||
EXPECT_EQ(constantsAfter.at(sampleAfter->maskOperandIds.front()), 0u) << dis;
|
|
||||||
|
|
||||||
// The float sampler2D in the same module is not multisampled, so its Lod fetch has to come
|
|
||||||
// through with the same image, the same mask and the same operand.
|
|
||||||
const ImageFetch* lodAfter = FindLodCarryingFetch(after);
|
|
||||||
ASSERT_NE(lodAfter, nullptr) << dis;
|
|
||||||
EXPECT_EQ(lodAfter->imageId, lodBefore->imageId) << dis;
|
|
||||||
EXPECT_EQ(lodAfter->mask, lodBefore->mask) << dis;
|
|
||||||
EXPECT_EQ(lodAfter->maskOperandIds, lodBefore->maskOperandIds) << dis;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The same shader on a device whose integer ceiling really is what MobileGL advertises. Nothing is
|
|
||||||
// out of range, so nothing may be rewritten - and the module must not even be re-serialised.
|
|
||||||
TEST_F(ClampMultisampleFetchTest, LeavesTheFetchAloneWhenTheCategoryReachesTheAdvertisedMaximum) {
|
|
||||||
const Vector<Uint32> input = CompileFragment(kIntegerMultisampleFetch);
|
|
||||||
ASSERT_FALSE(input.empty());
|
|
||||||
|
|
||||||
Vector<Uint32> output;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
|
|
||||||
input, output, /*maxColorSamples=*/4, /*maxIntegerSamples=*/4, /*maxDepthSamples=*/4,
|
|
||||||
kAdvertisedMaxSamples, true));
|
|
||||||
EXPECT_EQ(output, input) << Disassemble(output);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A category squeezed to something above 1 cannot be answered with a constant: an index the
|
|
||||||
// allocation does have must survive, so only the upper bound moves.
|
|
||||||
TEST_F(ClampMultisampleFetchTest, ClampsAColorSampleWithAMinimum) {
|
|
||||||
const Vector<Uint32> input = CompileFragment(kColorMultisampleFetch);
|
|
||||||
ASSERT_FALSE(input.empty());
|
|
||||||
|
|
||||||
const Vector<ImageFetch> before = CollectImageFetches(input);
|
|
||||||
ASSERT_EQ(before.size(), 1u) << Disassemble(input);
|
|
||||||
ASSERT_EQ(before.front().maskOperandIds.size(), 1u);
|
|
||||||
const Uint32 originalSampleId = before.front().maskOperandIds.front();
|
|
||||||
|
|
||||||
Vector<Uint32> output;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::ClampMultisampleFetchesForEssl(
|
|
||||||
input, output, /*maxColorSamples=*/2, /*maxIntegerSamples=*/4, /*maxDepthSamples=*/4,
|
|
||||||
kAdvertisedMaxSamples, true));
|
|
||||||
ASSERT_FALSE(output.empty());
|
|
||||||
const String dis = Disassemble(output);
|
|
||||||
ASSERT_TRUE(Validates(output)) << dis;
|
|
||||||
|
|
||||||
const Vector<ImageFetch> after = CollectImageFetches(output);
|
|
||||||
ASSERT_EQ(after.size(), 1u) << dis;
|
|
||||||
ASSERT_EQ(after.front().maskOperandIds.size(), 1u) << dis;
|
|
||||||
const Uint32 clampedSampleId = after.front().maskOperandIds.front();
|
|
||||||
EXPECT_NE(clampedSampleId, originalSampleId) << dis;
|
|
||||||
|
|
||||||
const Vector<ExtInst> extInsts = CollectExtInsts(output);
|
|
||||||
const ExtInst* minimum = nullptr;
|
|
||||||
for (const ExtInst& extInst : extInsts) {
|
|
||||||
if (extInst.resultId == clampedSampleId) minimum = &extInst;
|
|
||||||
}
|
|
||||||
ASSERT_NE(minimum, nullptr) << dis;
|
|
||||||
EXPECT_EQ(minimum->instructionNumber, kGlslStd450SMin) << dis;
|
|
||||||
ASSERT_EQ(minimum->operandIds.size(), 2u) << dis;
|
|
||||||
EXPECT_EQ(minimum->operandIds[0], originalSampleId) << dis;
|
|
||||||
|
|
||||||
// min(sample, K - 1), i.e. the last sample a two-sample allocation has.
|
|
||||||
const std::map<Uint32, Uint32> constants = CollectScalarConstants(output);
|
|
||||||
ASSERT_EQ(constants.count(minimum->operandIds[1]), 1u) << dis;
|
|
||||||
EXPECT_EQ(constants.at(minimum->operandIds[1]), 1u) << dis;
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include <sstream>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -485,165 +484,52 @@ TEST_F(DemoteFloat64Test, RejectsGarbageInput) {
|
|||||||
EXPECT_FALSE(ShaderCompiler::DemoteFloat64ToFloat32(notSpirv, output, true));
|
EXPECT_FALSE(ShaderCompiler::DemoteFloat64ToFloat32(notSpirv, output, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
// EliminateFloatEqualsZeroPass re-spells a comparison against 0.0 through GLSL.std.450 FAbs, so
|
// EliminateFloatEqualsZeroPass turns a comparison against 0.0 into an epsilon test, a
|
||||||
// that no float-equality instruction reaches a driver that gets one wrong. Deciding WHICH
|
// workaround for drivers whose exact float compare misbehaves. Deciding WHICH constants are
|
||||||
// constants are zero used to read every float constant as though it were 32 bits wide, and on a
|
// zero used to read every float constant as though it were 32 bits wide, and on a 64-bit
|
||||||
// 64-bit constant that reads the LOW half of the mantissa - which is zero for 1.0lf, 2.0lf, 0.5lf
|
// constant that reads the LOW half of the mantissa - which is zero for 1.0lf, 2.0lf, 0.5lf and
|
||||||
// and every other round double a shader is likely to spell. Each of those was mistaken for 0.0, so
|
// every other round double a shader is likely to spell. Each of those was mistaken for 0.0, so
|
||||||
// a comparison against 1.0lf became a test against ZERO, and came out true for a uniform holding
|
// a comparison against 1.0lf became an epsilon test against ZERO, and came out true for a
|
||||||
// exactly 1.0. That is the whole of KHR-GL43.compute_shader.fp64-case2.
|
// uniform holding exactly 1.0. That is the whole of KHR-GL43.compute_shader.fp64-case2.
|
||||||
//
|
|
||||||
// The replacement itself used to be an epsilon ball, `abs(x) < 1e-4`, which called any legitimately
|
|
||||||
// small value zero: KHR-GL3x.buffer_objects.triangles computes a specular term of ~6e-5 at a large
|
|
||||||
// render target and rendered black. It is exact now - `abs(x) <= 0.0` / `abs(x) > 0.0` against the
|
|
||||||
// module's own zero constant - and the tests below pin both halves of that: only a genuine 0.0 is
|
|
||||||
// matched, and what the compare tests against is the constant the source itself spelled.
|
|
||||||
//
|
//
|
||||||
// Asserted on the optimized module rather than through a driver, because that is where the
|
// Asserted on the optimized module rather than through a driver, because that is where the
|
||||||
// rewrite happens and its fingerprint there is unambiguous: the rewrite introduces a
|
// rewrite happens and its fingerprint there is unambiguous: the epsilon form introduces a
|
||||||
// GLSL.std.450 FAbs, and nothing else in these shaders would.
|
// GLSL.std.450 FAbs, and nothing else in these shaders would.
|
||||||
namespace {
|
namespace {
|
||||||
String OptimizedDisassembly(const String& source) {
|
Bool RewritesToAnEpsilonTest(const String& source) {
|
||||||
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
|
const Vector<Uint32> input = CompileToSpirv(GL_COMPUTE_SHADER, source);
|
||||||
EXPECT_FALSE(input.empty());
|
EXPECT_FALSE(input.empty());
|
||||||
if (input.empty()) return {};
|
if (input.empty()) return false;
|
||||||
Vector<Uint32> output;
|
Vector<Uint32> output;
|
||||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
|
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(input, output, true, true));
|
||||||
return Disassemble(output);
|
return Disassemble(output).find("FAbs") != String::npos;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool RewritesToAnAbsoluteValueTest(const String& source) {
|
String CompareAgainst(const String& type, const String& literal) {
|
||||||
return OptimizedDisassembly(source).find("FAbs") != String::npos;
|
|
||||||
}
|
|
||||||
|
|
||||||
String CompareAgainstUsing(const String& type, const String& op, const String& literal) {
|
|
||||||
return "#version 430 core\n"
|
return "#version 430 core\n"
|
||||||
"layout(local_size_x = 1) in;\n"
|
"layout(local_size_x = 1) in;\n"
|
||||||
"buffer Result { int g_result; };\n"
|
"buffer Result { int g_result; };\n"
|
||||||
"uniform " + type + " g_0;\n"
|
"uniform " + type + " g_0;\n"
|
||||||
"void main() {\n"
|
"void main() {\n"
|
||||||
" g_result = 0;\n"
|
" g_result = 0;\n"
|
||||||
" if (g_0 " + op + " " + literal + ") g_result = 1;\n"
|
" if (g_0 != " + literal + ") g_result = 1;\n"
|
||||||
"}\n";
|
"}\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
String CompareAgainst(const String& type, const String& literal) {
|
|
||||||
return CompareAgainstUsing(type, "!=", literal);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every instruction of a disassembly, split into whitespace-separated tokens, so an operand can
|
|
||||||
// be identified by position instead of by a substring another opcode might also contain -
|
|
||||||
// `OpFOrdLessThan` is a prefix of `OpFOrdLessThanEqual`, and those two are the whole difference
|
|
||||||
// between the epsilon rewrite and the exact one.
|
|
||||||
Vector<Vector<String>> TokenizedInstructions(const String& disassembly) {
|
|
||||||
Vector<Vector<String>> instructions;
|
|
||||||
StringStream lines(disassembly);
|
|
||||||
String line;
|
|
||||||
while (std::getline(lines, line)) {
|
|
||||||
Vector<String> tokens;
|
|
||||||
StringStream words(line);
|
|
||||||
String word;
|
|
||||||
while (words >> word) tokens.push_back(word);
|
|
||||||
instructions.push_back(tokens);
|
|
||||||
}
|
|
||||||
return instructions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The compare the rewrite leaves behind, e.g. `%22 = OpFOrdLessThanEqual %bool %21 %float_0`,
|
|
||||||
// or an empty vector if the module has none. These four opcodes are the only ones the pass
|
|
||||||
// emits and nothing else in these shaders produces one.
|
|
||||||
Vector<String> FindRewrittenCompare(const String& disassembly) {
|
|
||||||
for (const Vector<String>& tokens : TokenizedInstructions(disassembly)) {
|
|
||||||
if (tokens.size() < 6 || tokens[1] != "=") continue;
|
|
||||||
if (tokens[2] == "OpFOrdLessThanEqual" || tokens[2] == "OpFUnordLessThanEqual" ||
|
|
||||||
tokens[2] == "OpFOrdGreaterThan" || tokens[2] == "OpFUnordGreaterThan") {
|
|
||||||
return tokens;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Result id of the module's 0.0 constant of the type FAbs produces - the constant the source
|
|
||||||
// itself spelled - found without assuming what the disassembler names it or how it prints the
|
|
||||||
// literal.
|
|
||||||
String FindZeroConstantId(const String& disassembly) {
|
|
||||||
const Vector<Vector<String>> instructions = TokenizedInstructions(disassembly);
|
|
||||||
String floatTypeId;
|
|
||||||
for (const Vector<String>& tokens : instructions) {
|
|
||||||
if (tokens.size() >= 7 && tokens[2] == "OpExtInst" && tokens[5] == "FAbs") {
|
|
||||||
floatTypeId = tokens[3];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (floatTypeId.empty()) return {};
|
|
||||||
|
|
||||||
for (const Vector<String>& tokens : instructions) {
|
|
||||||
if (tokens.size() < 5 || tokens[2] != "OpConstant" || tokens[3] != floatTypeId) continue;
|
|
||||||
char* end = nullptr;
|
|
||||||
const double value = std::strtod(tokens[4].c_str(), &end);
|
|
||||||
if (end != nullptr && *end == '\0' && value == 0.0) return tokens[0];
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// The shape the pass promises: the given opcode (either NaN half of it), tested against the
|
|
||||||
// module's own zero constant rather than against anything this pass invented.
|
|
||||||
void ExpectComparedAgainstModuleZero(const String& source, const String& orderedOpcode,
|
|
||||||
const String& unorderedOpcode) {
|
|
||||||
const String disassembly = OptimizedDisassembly(source);
|
|
||||||
const Vector<String> compare = FindRewrittenCompare(disassembly);
|
|
||||||
ASSERT_FALSE(compare.empty()) << "no rewritten compare in the optimized module\n"
|
|
||||||
<< disassembly;
|
|
||||||
EXPECT_TRUE(compare[2] == orderedOpcode || compare[2] == unorderedOpcode)
|
|
||||||
<< "expected " << orderedOpcode << " (or its unordered twin), got " << compare[2] << "\n"
|
|
||||||
<< disassembly;
|
|
||||||
|
|
||||||
const String zeroId = FindZeroConstantId(disassembly);
|
|
||||||
ASSERT_FALSE(zeroId.empty()) << "the module has no 0.0 constant of the abs() type\n"
|
|
||||||
<< disassembly;
|
|
||||||
EXPECT_EQ(compare.back(), zeroId)
|
|
||||||
<< "the rewrite compares against " << compare.back()
|
|
||||||
<< " instead of the module's own zero; a synthesized threshold is the epsilon bug\n"
|
|
||||||
<< disassembly;
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST_F(DemoteFloat64Test, AComparisonAgainstANonZeroDoubleIsLeftAlone) {
|
TEST_F(DemoteFloat64Test, AComparisonAgainstANonZeroDoubleIsLeftAlone) {
|
||||||
EXPECT_FALSE(RewritesToAnAbsoluteValueTest(CompareAgainst("double", "1.0LF")))
|
EXPECT_FALSE(RewritesToAnEpsilonTest(CompareAgainst("double", "1.0LF")))
|
||||||
<< "a double compared against 1.0lf was rewritten into a test against zero";
|
<< "a double compared against 1.0lf was rewritten into an epsilon test against zero";
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DemoteFloat64Test, AComparisonAgainstZeroIsStillRewritten) {
|
TEST_F(DemoteFloat64Test, AComparisonAgainstZeroIsStillRewritten) {
|
||||||
EXPECT_TRUE(RewritesToAnAbsoluteValueTest(CompareAgainst("double", "0.0LF")))
|
EXPECT_TRUE(RewritesToAnEpsilonTest(CompareAgainst("double", "0.0LF")))
|
||||||
<< "the rewrite must still fire for a genuine comparison against zero";
|
<< "the rewrite must still fire for a genuine comparison against zero";
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DemoteFloat64Test, TheThirtyTwoBitBehaviourIsUnchanged) {
|
TEST_F(DemoteFloat64Test, TheThirtyTwoBitBehaviourIsUnchanged) {
|
||||||
EXPECT_FALSE(RewritesToAnAbsoluteValueTest(CompareAgainst("float", "1.0")))
|
EXPECT_FALSE(RewritesToAnEpsilonTest(CompareAgainst("float", "1.0")))
|
||||||
<< "a float compared against 1.0 must not be rewritten";
|
<< "a float compared against 1.0 must not be rewritten";
|
||||||
EXPECT_TRUE(RewritesToAnAbsoluteValueTest(CompareAgainst("float", "0.0")))
|
EXPECT_TRUE(RewritesToAnEpsilonTest(CompareAgainst("float", "0.0")))
|
||||||
<< "the 32-bit behaviour this pass shipped with must be preserved exactly";
|
<< "the 32-bit behaviour this pass shipped with must be preserved exactly";
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pass matches ZERO, not "small". The old constant-is-zero test was `fabs(v) <= 1e-4`, so a
|
|
||||||
// float compared against exactly 1e-4 was declared a comparison against zero and rewritten into
|
|
||||||
// `abs(x) >= 1e-4` - a different question from the one the shader asked, against a constant that
|
|
||||||
// was never zero to begin with.
|
|
||||||
TEST_F(DemoteFloat64Test, AComparisonAgainstASmallNonZeroLiteralIsLeftAlone) {
|
|
||||||
EXPECT_FALSE(RewritesToAnAbsoluteValueTest(CompareAgainst("float", "0.0001")))
|
|
||||||
<< "a float compared against 1e-4 was treated as a comparison against zero";
|
|
||||||
EXPECT_FALSE(RewritesToAnAbsoluteValueTest(CompareAgainst("double", "0.0001LF")))
|
|
||||||
<< "the 64-bit accessor must judge the constant just as exactly as the 32-bit one";
|
|
||||||
}
|
|
||||||
|
|
||||||
// What replaces the compare, not just that something did. Both properties here are what makes the
|
|
||||||
// rewrite exact rather than a tolerance, and neither is visible in the FAbs fingerprint above.
|
|
||||||
TEST_F(DemoteFloat64Test, TheRewriteComparesAbsAgainstTheModulesOwnZero) {
|
|
||||||
// `x == 0.0` -> `abs(x) <= 0.0`. The equality has to be INSIDE the replacement: with a strict
|
|
||||||
// `<` and no epsilon left to hide behind, +/-0 would stop comparing equal to zero.
|
|
||||||
ExpectComparedAgainstModuleZero(CompareAgainstUsing("float", "==", "0.0"),
|
|
||||||
"OpFOrdLessThanEqual", "OpFUnordLessThanEqual");
|
|
||||||
// `x != 0.0` -> `abs(x) > 0.0`, the strict complement of the above.
|
|
||||||
ExpectComparedAgainstModuleZero(CompareAgainstUsing("float", "!=", "0.0"), "OpFOrdGreaterThan",
|
|
||||||
"OpFUnordGreaterThan");
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/FixIterationRPBarrierTest.cpp
|
|
||||||
// Copyright (c) 2026 MobileGL-Dev
|
|
||||||
// Licensed under the GNU Lesser General Public License v3.0:
|
|
||||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
||||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// End of Source File Header
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
|
||||||
|
|
||||||
#define SPV_ENABLE_UTILITY_CODE
|
|
||||||
#include "glslang/SPIRV/spirv.hpp11"
|
|
||||||
#undef SPV_ENABLE_UTILITY_CODE
|
|
||||||
|
|
||||||
#include "Includes.h"
|
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
|
||||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
|
||||||
|
|
||||||
#include <spirv-tools/libspirv.hpp>
|
|
||||||
|
|
||||||
#include <map>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
using namespace MobileGL;
|
|
||||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr SizeT kSpirvHeaderWordCount = 5u;
|
|
||||||
|
|
||||||
template <typename Visitor>
|
|
||||||
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
|
|
||||||
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
|
|
||||||
const Uint32 wordCount = spirv[offset] >> 16u;
|
|
||||||
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
|
|
||||||
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
|
|
||||||
offset += wordCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector<Uint32> CompileCompute(const String& source) {
|
|
||||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
|
||||||
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
|
|
||||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
|
||||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
|
||||||
if (!shaderResult) return {};
|
|
||||||
|
|
||||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
|
||||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
|
||||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
|
||||||
if (!programResult) return {};
|
|
||||||
|
|
||||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
|
|
||||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
|
||||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
|
||||||
if (!binaryResult || binaryResult->empty()) return {};
|
|
||||||
return binaryResult->front();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Validates(const Vector<Uint32>& spirv) {
|
|
||||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
|
||||||
tools.SetMessageConsumer(
|
|
||||||
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
|
|
||||||
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
|
|
||||||
});
|
|
||||||
return tools.Validate(spirv);
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
|
|
||||||
Uint32 count = 0u;
|
|
||||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
|
|
||||||
if (opcode == wanted) ++count;
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HasWorkgroupBarrierImmediatelyBeforeSecondScan(const Vector<Uint32>& spirv) {
|
|
||||||
std::map<Uint32, Uint32> uintConstants;
|
|
||||||
spv::Op previous = spv::Op::OpNop;
|
|
||||||
Uint32 scanCount = 0u;
|
|
||||||
bool found = false;
|
|
||||||
const Uint32* previousWords = nullptr;
|
|
||||||
Uint32 previousWordCount = 0u;
|
|
||||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
|
||||||
if (opcode == spv::Op::OpConstant && wordCount >= 4u) {
|
|
||||||
uintConstants[words[2]] = words[3];
|
|
||||||
}
|
|
||||||
if (opcode == spv::Op::OpGroupNonUniformFAdd && wordCount >= 6u &&
|
|
||||||
static_cast<spv::GroupOperation>(words[4]) == spv::GroupOperation::InclusiveScan && ++scanCount == 2u &&
|
|
||||||
previous == spv::Op::OpControlBarrier && previousWordCount == 4u) {
|
|
||||||
found =
|
|
||||||
uintConstants[previousWords[1]] == static_cast<Uint32>(spv::Scope::Workgroup) &&
|
|
||||||
uintConstants[previousWords[2]] == static_cast<Uint32>(spv::Scope::Workgroup) &&
|
|
||||||
uintConstants[previousWords[3]] == (static_cast<Uint32>(spv::MemorySemanticsMask::AcquireRelease) |
|
|
||||||
static_cast<Uint32>(spv::MemorySemanticsMask::WorkgroupMemory));
|
|
||||||
}
|
|
||||||
previous = opcode;
|
|
||||||
previousWords = words;
|
|
||||||
previousWordCount = wordCount;
|
|
||||||
});
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr const char* kProgram203RaceShape = R"(#version 450 core
|
|
||||||
#extension GL_KHR_shader_subgroup_basic : require
|
|
||||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
|
||||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
|
||||||
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
|
|
||||||
shared vec2 prefixSumCache[32];
|
|
||||||
void main() {
|
|
||||||
vec2 sampleLuminance = subgroupInclusiveAdd(
|
|
||||||
vec2(float(gl_LocalInvocationIndex), 1.0));
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
|
||||||
barrier();
|
|
||||||
if (gl_LocalInvocationIndex == 511u)
|
|
||||||
prefixSumCache[0] = sampleLuminance / 512.0;
|
|
||||||
barrier();
|
|
||||||
|
|
||||||
float avg = prefixSumCache[0].x;
|
|
||||||
float weight = avg > 0.0 ? float(gl_LocalInvocationIndex + 1u) / avg : 0.0;
|
|
||||||
vec2 sampleExposure = subgroupInclusiveAdd(vec2(weight, 1.0));
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = sampleExposure;
|
|
||||||
barrier();
|
|
||||||
if (gl_LocalInvocationIndex == 511u)
|
|
||||||
outputData.value = sampleExposure;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr const char* kAlreadySynchronizedShape = R"(#version 450 core
|
|
||||||
#extension GL_KHR_shader_subgroup_basic : require
|
|
||||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
|
||||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
|
||||||
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
|
|
||||||
shared vec2 prefixSumCache[32];
|
|
||||||
void main() {
|
|
||||||
vec2 first = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 1.0));
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = first;
|
|
||||||
barrier();
|
|
||||||
if (gl_LocalInvocationIndex == 511u) prefixSumCache[0] = first / 512.0;
|
|
||||||
barrier();
|
|
||||||
float avg = prefixSumCache[0].x;
|
|
||||||
barrier();
|
|
||||||
vec2 second = subgroupInclusiveAdd(vec2(avg, 1.0));
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = second;
|
|
||||||
barrier();
|
|
||||||
if (gl_LocalInvocationIndex == 511u) outputData.value = second;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr const char* kForeignSingleScanShape = R"(#version 450 core
|
|
||||||
#extension GL_KHR_shader_subgroup_basic : require
|
|
||||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
|
||||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
|
||||||
layout(std430, binding = 0) buffer Output { vec2 value; } outputData;
|
|
||||||
shared vec2 prefixSumCache[32];
|
|
||||||
void main() {
|
|
||||||
vec2 value = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 1.0));
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = value;
|
|
||||||
barrier();
|
|
||||||
if (gl_LocalInvocationIndex == 0u) outputData.value = prefixSumCache[0];
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST(FixIterationRPBarrierPass, InsertsWorkgroupBarrierBeforeSecondReduction) {
|
|
||||||
const Vector<Uint32> input = CompileCompute(kProgram203RaceShape);
|
|
||||||
ASSERT_FALSE(input.empty());
|
|
||||||
const Uint32 inputBarrierCount = CountOpcode(input, spv::Op::OpControlBarrier);
|
|
||||||
EXPECT_FALSE(HasWorkgroupBarrierImmediatelyBeforeSecondScan(input));
|
|
||||||
|
|
||||||
Vector<Uint32> output;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
|
|
||||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpControlBarrier), inputBarrierCount + 1u);
|
|
||||||
EXPECT_TRUE(HasWorkgroupBarrierImmediatelyBeforeSecondScan(output));
|
|
||||||
EXPECT_TRUE(Validates(output));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(FixIterationRPBarrierPass, LeavesOtherShapesByteIdentical) {
|
|
||||||
const Vector<Uint32> input = CompileCompute(kForeignSingleScanShape);
|
|
||||||
ASSERT_FALSE(input.empty());
|
|
||||||
Vector<Uint32> output;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
|
|
||||||
EXPECT_EQ(output, input);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(FixIterationRPBarrierPass, LeavesAnAlreadySynchronizedShaderByteIdentical) {
|
|
||||||
const Vector<Uint32> input = CompileCompute(kAlreadySynchronizedShape);
|
|
||||||
ASSERT_FALSE(input.empty());
|
|
||||||
Vector<Uint32> output;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, output, true));
|
|
||||||
EXPECT_EQ(output, input);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(FixIterationRPBarrierPass, IsIdempotent) {
|
|
||||||
const Vector<Uint32> input = CompileCompute(kProgram203RaceShape);
|
|
||||||
ASSERT_FALSE(input.empty());
|
|
||||||
Vector<Uint32> once;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(input, once, true));
|
|
||||||
Vector<Uint32> twice;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPBarrierForVulkan(once, twice, true));
|
|
||||||
EXPECT_EQ(twice, once);
|
|
||||||
}
|
|
||||||
@@ -109,11 +109,10 @@ namespace {
|
|||||||
return tools.Validate(spirv);
|
return tools.Validate(spirv);
|
||||||
}
|
}
|
||||||
|
|
||||||
// iterationRP's exposure reduction, as the pack ships it: 32x16 (512
|
// iterationRP's reduction fingerprint: 32x16x1, subgroupInclusiveAdd on a
|
||||||
// invocations), subgroupInclusiveAdd on a vec2, and a 32-entry
|
// vec2, and the pack's own 32-entry gl_SubgroupID-indexed scratch. A second,
|
||||||
// gl_SubgroupID-indexed scratch. A second, plainly indexed array rides along
|
// plainly indexed array rides along to prove the patch is surgical.
|
||||||
// to prove the patch is surgical.
|
constexpr const char* kIterationRPShapedSource = R"(#version 450 core
|
||||||
constexpr const char* kExposureShapedSource = R"(#version 450 core
|
|
||||||
#extension GL_KHR_shader_subgroup_basic : require
|
#extension GL_KHR_shader_subgroup_basic : require
|
||||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
#extension GL_KHR_shader_subgroup_arithmetic : require
|
||||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
||||||
@@ -142,195 +141,85 @@ void main() {
|
|||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
// The pack's OTHER instance of the same bug, which a fingerprint pinned to the
|
// Same scratch idiom, different workgroup shape - NOT iterationRP, so the
|
||||||
// exposure pass's dimensions walks straight past: the RTW importance warp
|
// fingerprint must refuse it even though it would break identically.
|
||||||
// scans a plain float across 1024 invocations into a 64-entry scratch.
|
constexpr const char* kWrongWorkgroupShapeSource = R"(#version 450 core
|
||||||
constexpr const char* kRtwWarpShapedSource = R"(#version 450 core
|
|
||||||
#extension GL_KHR_shader_subgroup_basic : require
|
#extension GL_KHR_shader_subgroup_basic : require
|
||||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
#extension GL_KHR_shader_subgroup_arithmetic : require
|
||||||
layout(local_size_x = 1024) in;
|
layout(local_size_x = 64, local_size_y = 8, local_size_z = 1) in;
|
||||||
layout(std430, binding = 0) buffer Output { float value; } outputData;
|
layout(std430, binding = 0) buffer Output { float value; } outputData;
|
||||||
shared float prefixSumCache[64];
|
shared vec2 prefixSumCache[32];
|
||||||
void main() {
|
void main() {
|
||||||
float importance = float(gl_LocalInvocationID.x) * 0.5;
|
vec2 v = subgroupInclusiveAdd(vec2(1.0, 0.0));
|
||||||
float prefixSum = subgroupInclusiveAdd(importance);
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||||
prefixSumCache[gl_SubgroupID] = prefixSum;
|
prefixSumCache[gl_SubgroupID] = v;
|
||||||
barrier();
|
barrier();
|
||||||
uint loopLength = uint(findMSB(gl_NumSubgroups));
|
if (gl_LocalInvocationIndex == 0u)
|
||||||
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
|
outputData.value = prefixSumCache[0].x;
|
||||||
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
|
|
||||||
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
|
|
||||||
prefixSum += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = prefixSum;
|
|
||||||
}
|
|
||||||
barrier();
|
|
||||||
}
|
|
||||||
if (gl_LocalInvocationID.x == 1023u) outputData.value = prefixSumCache[0];
|
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
// A subgroup scan, but the scratch is indexed per invocation rather than per
|
// Right shape, but a float scan and a float[32] scratch - not the pack's
|
||||||
// subgroup: its size is not a subgroup-count assumption, so it is not ours.
|
// vec2 accumulator signature.
|
||||||
constexpr const char* kInvocationIndexedSource = R"(#version 450 core
|
constexpr const char* kWrongElementTypeSource = R"(#version 450 core
|
||||||
#extension GL_KHR_shader_subgroup_basic : require
|
#extension GL_KHR_shader_subgroup_basic : require
|
||||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
#extension GL_KHR_shader_subgroup_arithmetic : require
|
||||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
||||||
layout(std430, binding = 0) buffer Output { float value; } outputData;
|
layout(std430, binding = 0) buffer Output { float value; } outputData;
|
||||||
shared vec2 perInvocation[32];
|
shared float cache[32];
|
||||||
void main() {
|
void main() {
|
||||||
vec2 v = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 0.0));
|
float v = subgroupInclusiveAdd(float(gl_LocalInvocationIndex));
|
||||||
perInvocation[gl_LocalInvocationIndex & 31u] = v;
|
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
||||||
|
cache[gl_SubgroupID] = v;
|
||||||
barrier();
|
barrier();
|
||||||
if (gl_LocalInvocationIndex == 0u) outputData.value = perInvocation[0].x;
|
if (gl_LocalInvocationIndex == 0u)
|
||||||
}
|
outputData.value = cache[0];
|
||||||
)";
|
|
||||||
|
|
||||||
// gl_SubgroupID-indexed, but no subgroup scan feeds it and the element type is
|
|
||||||
// not the pack's float accumulator.
|
|
||||||
constexpr const char* kNonFloatScratchSource = R"(#version 450 core
|
|
||||||
#extension GL_KHR_shader_subgroup_basic : require
|
|
||||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
|
||||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
|
||||||
layout(std430, binding = 0) buffer Output { uint value; } outputData;
|
|
||||||
shared uint tally[32];
|
|
||||||
void main() {
|
|
||||||
float scan = subgroupInclusiveAdd(float(gl_LocalInvocationIndex));
|
|
||||||
tally[gl_SubgroupID] = uint(scan);
|
|
||||||
barrier();
|
|
||||||
if (gl_LocalInvocationIndex == 0u) outputData.value = tally[0];
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// gl_SubgroupID-indexed, but masked into range: the declaration is bounded by
|
|
||||||
// construction, not a subgroup-count assumption, so it is not the pack's bug.
|
|
||||||
constexpr const char* kMaskedSubgroupIndexSource = R"(#version 450 core
|
|
||||||
#extension GL_KHR_shader_subgroup_basic : require
|
|
||||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
|
||||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
|
||||||
layout(std430, binding = 0) buffer Output { float value; } outputData;
|
|
||||||
shared vec2 bounded[8];
|
|
||||||
void main() {
|
|
||||||
vec2 v = subgroupInclusiveAdd(vec2(float(gl_LocalInvocationIndex), 0.0));
|
|
||||||
bounded[gl_SubgroupID & 7u] = v;
|
|
||||||
barrier();
|
|
||||||
if (gl_LocalInvocationIndex == 0u) outputData.value = bounded[0].x;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// Neither of the pack's shapes: a small per-subgroup array in a 256-invocation
|
|
||||||
// workgroup, used to prove the width gate keeps EVERY module inert at >= 16 lanes.
|
|
||||||
constexpr const char* kForeignShapeSource = R"(#version 450 core
|
|
||||||
#extension GL_KHR_shader_subgroup_basic : require
|
|
||||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
|
||||||
layout(local_size_x = 256) in;
|
|
||||||
layout(std430, binding = 0) buffer Output { float value; } outputData;
|
|
||||||
shared float partial[4];
|
|
||||||
void main() {
|
|
||||||
float v = subgroupInclusiveAdd(float(gl_LocalInvocationID.x));
|
|
||||||
if (gl_SubgroupID < 4u) partial[gl_SubgroupID] = v;
|
|
||||||
barrier();
|
|
||||||
if (gl_LocalInvocationID.x == 0u) outputData.value = partial[0];
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// No subgroup construct at all.
|
|
||||||
constexpr const char* kSubgroupFreeSource = R"(#version 450 core
|
|
||||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
|
||||||
layout(std430, binding = 0) buffer Output { float value; } outputData;
|
|
||||||
shared vec2 scratch[32];
|
|
||||||
void main() {
|
|
||||||
scratch[gl_LocalInvocationIndex & 31u] = vec2(float(gl_LocalInvocationIndex), 0.0);
|
|
||||||
barrier();
|
|
||||||
if (gl_LocalInvocationIndex == 0u) outputData.value = scratch[0].x;
|
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST(FixIterationRPSubgroupScratchPass, GrowsTheExposureScratchForNarrowSubgroups) {
|
TEST(FixIterationRPSubgroupScratchPass, GrowsThePacksScratchForNarrowSubgroups) {
|
||||||
const Vector<Uint32> input = CompileCompute(kExposureShapedSource);
|
const Vector<Uint32> input = CompileCompute(kIterationRPShapedSource);
|
||||||
ASSERT_FALSE(input.empty());
|
ASSERT_FALSE(input.empty());
|
||||||
ASSERT_EQ(WorkgroupArrayLengths(input), (std::vector<Uint32>{4u, 32u}));
|
ASSERT_EQ(WorkgroupArrayLengths(input), (std::vector<Uint32>{4u, 32u}));
|
||||||
|
|
||||||
// lavapipe: 8-lane subgroups over 512 invocations need 64 entries; the
|
// lavapipe: 8-lane subgroups over 512 invocations need 64 entries; the
|
||||||
// plainly indexed neighbour must keep its 4.
|
// plainly indexed neighbour must keep its 4.
|
||||||
Vector<Uint32> output;
|
Vector<Uint32> output;
|
||||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 32768u, true));
|
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, true));
|
||||||
EXPECT_EQ(WorkgroupArrayLengths(output), (std::vector<Uint32>{4u, 64u}));
|
EXPECT_EQ(WorkgroupArrayLengths(output), (std::vector<Uint32>{4u, 64u}));
|
||||||
EXPECT_TRUE(Validates(output));
|
EXPECT_TRUE(Validates(output));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The regression the CI retrace caught: patching only the exposure pass leaves
|
|
||||||
// this one writing 128 subgroups into 64 entries, and the frame stays wrong.
|
|
||||||
TEST(FixIterationRPSubgroupScratchPass, GrowsTheRtwWarpScratchForNarrowSubgroups) {
|
|
||||||
const Vector<Uint32> input = CompileCompute(kRtwWarpShapedSource);
|
|
||||||
ASSERT_FALSE(input.empty());
|
|
||||||
ASSERT_EQ(WorkgroupArrayLengths(input), (std::vector<Uint32>{64u}));
|
|
||||||
|
|
||||||
Vector<Uint32> output;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 32768u, true));
|
|
||||||
EXPECT_EQ(WorkgroupArrayLengths(output), (std::vector<Uint32>{128u}));
|
|
||||||
EXPECT_TRUE(Validates(output));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(FixIterationRPSubgroupScratchPass, LeavesPackWidthAssumptionsAloneOnWideDevices) {
|
TEST(FixIterationRPSubgroupScratchPass, LeavesPackWidthAssumptionsAloneOnWideDevices) {
|
||||||
// Both shapes are sized for >= 16 lanes (512/16 = 32, 1024/16 = 64), so on
|
const Vector<Uint32> input = CompileCompute(kIterationRPShapedSource);
|
||||||
// every such device the modules must pass through byte-identical.
|
|
||||||
for (const char* source : {kExposureShapedSource, kRtwWarpShapedSource}) {
|
|
||||||
const Vector<Uint32> input = CompileCompute(source);
|
|
||||||
ASSERT_FALSE(input.empty());
|
|
||||||
for (const Uint32 nativeSize : {16u, 32u, 64u, 128u}) {
|
|
||||||
Vector<Uint32> output;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
|
|
||||||
input, output, nativeSize, 32768u, true));
|
|
||||||
EXPECT_EQ(output, input) << "native width " << nativeSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST(FixIterationRPSubgroupScratchPass, RefusesAModuleOutsideTheIdiom) {
|
|
||||||
for (const char* source : {kInvocationIndexedSource, kNonFloatScratchSource,
|
|
||||||
kSubgroupFreeSource, kMaskedSubgroupIndexSource}) {
|
|
||||||
const Vector<Uint32> input = CompileCompute(source);
|
|
||||||
ASSERT_FALSE(input.empty());
|
|
||||||
Vector<Uint32> output;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 32768u, true));
|
|
||||||
EXPECT_EQ(output, input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A grown array that would not fit the device's shared memory is left alone:
|
|
||||||
// a pipeline that cannot be created is worse than the pack's own overrun.
|
|
||||||
// The width gate is what keeps unrelated shaders untouched on the devices the pack
|
|
||||||
// was written for: at >= 16 lanes nothing is rewritten, whatever its shape.
|
|
||||||
TEST(FixIterationRPSubgroupScratchPass, LeavesEveryModuleAloneAtThePacksAssumedWidth) {
|
|
||||||
const Vector<Uint32> input = CompileCompute(kForeignShapeSource);
|
|
||||||
ASSERT_FALSE(input.empty());
|
ASSERT_FALSE(input.empty());
|
||||||
for (const Uint32 nativeSize : {16u, 32u, 64u}) {
|
|
||||||
|
// >= 16 lanes means at most 32 subgroups: the pack's declared size holds and
|
||||||
|
// the module must pass through byte-identical.
|
||||||
|
for (const Uint32 nativeSize : {16u, 32u, 64u, 128u}) {
|
||||||
Vector<Uint32> output;
|
Vector<Uint32> output;
|
||||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
|
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, nativeSize, true));
|
||||||
input, output, nativeSize, 32768u, true));
|
|
||||||
EXPECT_EQ(output, input) << "native width " << nativeSize;
|
EXPECT_EQ(output, input) << "native width " << nativeSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(FixIterationRPSubgroupScratchPass, RefusesGrowthThatWouldNotFitSharedMemory) {
|
TEST(FixIterationRPSubgroupScratchPass, RefusesAModuleOutsideTheFingerprint) {
|
||||||
const Vector<Uint32> input = CompileCompute(kRtwWarpShapedSource);
|
for (const char* source : {kWrongWorkgroupShapeSource, kWrongElementTypeSource}) {
|
||||||
ASSERT_FALSE(input.empty());
|
const Vector<Uint32> input = CompileCompute(source);
|
||||||
Vector<Uint32> output;
|
ASSERT_FALSE(input.empty());
|
||||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, 256u, true));
|
Vector<Uint32> output;
|
||||||
EXPECT_EQ(output, input);
|
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, output, 8u, true));
|
||||||
|
EXPECT_EQ(output, input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(FixIterationRPSubgroupScratchPass, IsIdempotent) {
|
TEST(FixIterationRPSubgroupScratchPass, IsIdempotent) {
|
||||||
for (const char* source : {kExposureShapedSource, kRtwWarpShapedSource}) {
|
const Vector<Uint32> input = CompileCompute(kIterationRPShapedSource);
|
||||||
const Vector<Uint32> input = CompileCompute(source);
|
ASSERT_FALSE(input.empty());
|
||||||
ASSERT_FALSE(input.empty());
|
Vector<Uint32> once;
|
||||||
Vector<Uint32> once;
|
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, once, 8u, true));
|
||||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(input, once, 8u, 32768u, true));
|
Vector<Uint32> twice;
|
||||||
Vector<Uint32> twice;
|
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(once, twice, 8u, true));
|
||||||
ASSERT_TRUE(ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(once, twice, 8u, 32768u, true));
|
EXPECT_EQ(twice, once);
|
||||||
EXPECT_EQ(twice, once);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,237 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/LowerViewportIndexTest.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
|
|
||||||
//
|
|
||||||
// LowerViewportIndexPass is the DirectGLES fallback for a driver with no GL_OES_viewport_array.
|
|
||||||
// The thing it prevents is not a wrong pixel but a missing program: ESSL has no core
|
|
||||||
// gl_ViewportIndex at any version, SPIRV-Cross prints the identifier bare, and the driver rejects
|
|
||||||
// the stage - after which DirectGLES binds program 0 and every draw renders nothing while
|
|
||||||
// GL_LINK_STATUS still answers TRUE. So what has to hold is textual and structural at once: the
|
|
||||||
// emitted ESSL must stop naming the builtin, the module must stay valid, and gl_Layer - which IS
|
|
||||||
// core in ESSL 3.20 geometry shaders - must come through untouched.
|
|
||||||
//
|
|
||||||
// Real GLSL through the same glslang path the backends use, rather than hand-assembled words, for
|
|
||||||
// the same reason MG_Test/Pipeline/ViewportIndexReflectionTest.cpp does it: what matters is what
|
|
||||||
// glslang actually emits for these shaders.
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "Includes.h"
|
|
||||||
#include "Init.h"
|
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
|
||||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
|
||||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
|
||||||
|
|
||||||
#include <spirv-tools/libspirv.hpp>
|
|
||||||
|
|
||||||
using namespace MobileGL;
|
|
||||||
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
|
|
||||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
|
||||||
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
|
||||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
|
||||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
|
||||||
if (!shaderResult) return {};
|
|
||||||
|
|
||||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
|
||||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
|
||||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
|
||||||
if (!programResult) return {};
|
|
||||||
|
|
||||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
|
|
||||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
|
||||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
|
||||||
if (!binaryResult || binaryResult->empty()) return {};
|
|
||||||
return binaryResult->front();
|
|
||||||
}
|
|
||||||
|
|
||||||
String Disassemble(const Vector<Uint32>& spirv) {
|
|
||||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
|
||||||
String text;
|
|
||||||
tools.Disassemble(spirv, &text);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ESSL 320, i.e. exactly what the DirectGLES transpile asks SPIRV-Cross for.
|
|
||||||
String Transpile(const Vector<Uint32>& spirv) {
|
|
||||||
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
|
||||||
auto essl = ShaderCompiler::DecompileShader(session);
|
|
||||||
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
|
|
||||||
return essl ? essl.value() : String{};
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool Contains(const String& haystack, const String& needle) {
|
|
||||||
return haystack.find(needle) != String::npos;
|
|
||||||
}
|
|
||||||
|
|
||||||
// KHR-GL4x.viewport_array.draw_to_single_layer_with_multiple_viewports' geometry stage in
|
|
||||||
// miniature: sixteen invocations, each routing its primitive to its own viewport. This is the
|
|
||||||
// shape that today loses the whole program on a driver without GL_OES_viewport_array.
|
|
||||||
const char* const kGeometryWritesViewportIndex = R"(#version 410 core
|
|
||||||
layout(points, invocations = 16) in;
|
|
||||||
layout(triangle_strip, max_vertices = 4) out;
|
|
||||||
void main() {
|
|
||||||
gl_ViewportIndex = gl_InvocationID;
|
|
||||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
EndPrimitive();
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// Layered rendering, not viewport routing. gl_Layer IS core in ESSL 3.20 geometry shaders, so
|
|
||||||
// demoting it would break a Minecraft-style cubemap pass that works today.
|
|
||||||
const char* const kGeometryWritesLayerOnly = R"(#version 410 core
|
|
||||||
layout(points, invocations = 6) in;
|
|
||||||
layout(triangle_strip, max_vertices = 4) out;
|
|
||||||
void main() {
|
|
||||||
gl_Layer = gl_InvocationID;
|
|
||||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
EndPrimitive();
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// Both at once, which is the case that separates "lowers the right builtin" from "lowers every
|
|
||||||
// builtin it can reach": KHR-GL4x.viewport_array.draw_multiple_layers writes both.
|
|
||||||
const char* const kGeometryWritesBoth = R"(#version 410 core
|
|
||||||
layout(points, invocations = 16) in;
|
|
||||||
layout(triangle_strip, max_vertices = 4) out;
|
|
||||||
void main() {
|
|
||||||
gl_ViewportIndex = gl_InvocationID;
|
|
||||||
gl_Layer = gl_InvocationID;
|
|
||||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
EndPrimitive();
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* const kPlainGeometry = R"(#version 410 core
|
|
||||||
layout(points, invocations = 1) in;
|
|
||||||
layout(triangle_strip, max_vertices = 4) out;
|
|
||||||
void main() {
|
|
||||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
|
||||||
EndPrimitive();
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
class LowerViewportIndexTest : public ::testing::Test {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
MobileGL::Initialize();
|
|
||||||
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
|
|
||||||
<< "the lowered module did not survive spirv-val";
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint64 m_validationFailuresAtStart = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The probe is the gate that keeps every ordinary stage off an optimizer round trip, so it has to
|
|
||||||
// answer no for a shader that never routes a viewport - and yes for the one that does.
|
|
||||||
TEST_F(LowerViewportIndexTest, TheProbeAnswersOnlyForAViewportIndexWriter) {
|
|
||||||
const Vector<Uint32> plain = CompileToSpirv(GL_GEOMETRY_SHADER, kPlainGeometry);
|
|
||||||
ASSERT_FALSE(plain.empty());
|
|
||||||
EXPECT_FALSE(ShaderCompiler::DeclaresViewportIndexBuiltin(plain));
|
|
||||||
|
|
||||||
const Vector<Uint32> layerOnly = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesLayerOnly);
|
|
||||||
ASSERT_FALSE(layerOnly.empty());
|
|
||||||
EXPECT_FALSE(ShaderCompiler::DeclaresViewportIndexBuiltin(layerOnly));
|
|
||||||
|
|
||||||
const Vector<Uint32> writer = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesViewportIndex);
|
|
||||||
ASSERT_FALSE(writer.empty());
|
|
||||||
EXPECT_TRUE(ShaderCompiler::DeclaresViewportIndexBuiltin(writer));
|
|
||||||
|
|
||||||
// Runs on every stage of every program on a driver without the extension, so it must survive a
|
|
||||||
// stage that produced no SPIR-V rather than pushing a parse diagnostic for it.
|
|
||||||
EXPECT_FALSE(ShaderCompiler::DeclaresViewportIndexBuiltin({}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The whole point: the emitted ESSL must stop naming a builtin the language does not have.
|
|
||||||
TEST_F(LowerViewportIndexTest, DemotesTheBuiltinToAnOrdinaryGlobal) {
|
|
||||||
const Vector<Uint32> input = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesViewportIndex);
|
|
||||||
ASSERT_FALSE(input.empty());
|
|
||||||
|
|
||||||
// Negative control, and the bug itself: untouched, SPIRV-Cross prints gl_ViewportIndex into
|
|
||||||
// ESSL 320 and asks for no extension to go with it.
|
|
||||||
const String before = Transpile(input);
|
|
||||||
EXPECT_TRUE(Contains(before, "gl_ViewportIndex")) << before;
|
|
||||||
|
|
||||||
Vector<Uint32> output;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::LowerViewportIndexForEssl(input, output, true));
|
|
||||||
ASSERT_FALSE(output.empty());
|
|
||||||
|
|
||||||
const String dis = Disassemble(output);
|
|
||||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
|
||||||
ASSERT_TRUE(tools.Validate(output)) << dis;
|
|
||||||
EXPECT_FALSE(Contains(dis, "BuiltIn ViewportIndex")) << dis;
|
|
||||||
EXPECT_TRUE(Contains(dis, "mg_ViewportIndex")) << dis;
|
|
||||||
EXPECT_TRUE(Contains(dis, "Private")) << dis;
|
|
||||||
|
|
||||||
const String after = Transpile(output);
|
|
||||||
EXPECT_TRUE(Contains(after, "mg_ViewportIndex")) << after;
|
|
||||||
EXPECT_FALSE(Contains(after, "gl_ViewportIndex")) << after;
|
|
||||||
}
|
|
||||||
|
|
||||||
// gl_Layer is core in ESSL 3.20 geometry shaders and layered rendering works on this backend
|
|
||||||
// today. Lowering it too would trade one silent failure for another.
|
|
||||||
TEST_F(LowerViewportIndexTest, LeavesGlLayerAlone) {
|
|
||||||
const Vector<Uint32> input = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesBoth);
|
|
||||||
ASSERT_FALSE(input.empty());
|
|
||||||
|
|
||||||
Vector<Uint32> output;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::LowerViewportIndexForEssl(input, output, true));
|
|
||||||
ASSERT_FALSE(output.empty());
|
|
||||||
|
|
||||||
const String dis = Disassemble(output);
|
|
||||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
|
||||||
ASSERT_TRUE(tools.Validate(output)) << dis;
|
|
||||||
EXPECT_FALSE(Contains(dis, "BuiltIn ViewportIndex")) << dis;
|
|
||||||
EXPECT_TRUE(Contains(dis, "BuiltIn Layer")) << dis;
|
|
||||||
|
|
||||||
const String after = Transpile(output);
|
|
||||||
EXPECT_TRUE(Contains(after, "gl_Layer")) << after;
|
|
||||||
EXPECT_FALSE(Contains(after, "gl_ViewportIndex")) << after;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every other stage on a driver without the extension goes through this pass too (behind the
|
|
||||||
// probe), so a module it has nothing to do with must come out saying exactly what it said.
|
|
||||||
TEST_F(LowerViewportIndexTest, LeavesAModuleWithoutTheBuiltinUntouched) {
|
|
||||||
const Vector<Uint32> input = CompileToSpirv(GL_GEOMETRY_SHADER, kPlainGeometry);
|
|
||||||
ASSERT_FALSE(input.empty());
|
|
||||||
|
|
||||||
const String before = Transpile(input);
|
|
||||||
|
|
||||||
Vector<Uint32> output;
|
|
||||||
ASSERT_TRUE(ShaderCompiler::LowerViewportIndexForEssl(input, output, true));
|
|
||||||
ASSERT_FALSE(output.empty());
|
|
||||||
|
|
||||||
const String dis = Disassemble(output);
|
|
||||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
|
||||||
ASSERT_TRUE(tools.Validate(output)) << dis;
|
|
||||||
EXPECT_FALSE(Contains(dis, "mg_ViewportIndex")) << dis;
|
|
||||||
EXPECT_EQ(Transpile(output), before);
|
|
||||||
}
|
|
||||||
@@ -18,11 +18,6 @@
|
|||||||
// errors that guard a parameter-buffer draw.
|
// errors that guard a parameter-buffer draw.
|
||||||
// * KHR-GL43.compute_shader.api-indirect / .api-program.
|
// * KHR-GL43.compute_shader.api-indirect / .api-program.
|
||||||
// * KHR-GLxx.texture_storage.compressed_data - compressed formats on TEXTURE_3D.
|
// * KHR-GLxx.texture_storage.compressed_data - compressed formats on TEXTURE_3D.
|
||||||
// * KHR-GL32.api.coverage - glFenceSync's condition/flags and glWaitSync's flags/timeout.
|
|
||||||
// * KHR-GL31.api.coverage - a draw's mode INVALID_ENUM has to outrank MobileGL's own
|
|
||||||
// no-current-program guard.
|
|
||||||
// * KHR-GL30.api.coverage - glBlitFramebuffer's mask bits, filter enum and the LINEAR-with-
|
|
||||||
// depth/stencil rule.
|
|
||||||
// Plus the indexed-getter parity RC-7b is about: glGetBooleani_v / glGetInteger64i_v /
|
// Plus the indexed-getter parity RC-7b is about: glGetBooleani_v / glGetInteger64i_v /
|
||||||
// glGetFloati_v / glGetDoublei_v must answer every pname glGetIntegeri_v answers.
|
// glGetFloati_v / glGetDoublei_v must answer every pname glGetIntegeri_v answers.
|
||||||
//
|
//
|
||||||
@@ -38,12 +33,10 @@
|
|||||||
#include "Init.h"
|
#include "Init.h"
|
||||||
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||||
#include <MG_Impl/GLImpl/Drawing/GL_Drawing.h>
|
#include <MG_Impl/GLImpl/Drawing/GL_Drawing.h>
|
||||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
|
||||||
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
||||||
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
||||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||||
#include <MG_Impl/GLImpl/Sampler/GL_Sampler.h>
|
#include <MG_Impl/GLImpl/Sampler/GL_Sampler.h>
|
||||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
|
||||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||||
#include <MG_Impl/GLImpl/VertexArray/GL_VertexArray.h>
|
#include <MG_Impl/GLImpl/VertexArray/GL_VertexArray.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
@@ -477,114 +470,4 @@ void main() { g_color = vec4(1); }
|
|||||||
EXPECT_EQ(offset, 2048);
|
EXPECT_EQ(offset, 2048);
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// KHR-GL32.api.coverage: glFenceSync and glWaitSync took every argument they were handed and
|
|
||||||
// reported GL_NO_ERROR for the two calls GL 4.6 core 4.1.2 requires to fail. A rejected
|
|
||||||
// glFenceSync must also hand back 0 rather than a live handle.
|
|
||||||
TEST_F(NegativeApiErrorsTest, SyncEntryPointsRejectTheirIllegalArguments) {
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
RunRows({
|
|
||||||
{"glFenceSync with a condition other than GL_SYNC_GPU_COMMANDS_COMPLETE",
|
|
||||||
[] { EXPECT_EQ(FenceSync(GL_SYNC_FENCE, 0), nullptr); }, GL_INVALID_ENUM},
|
|
||||||
{"glFenceSync with nonzero flags", [] { EXPECT_EQ(FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 1), nullptr); },
|
|
||||||
GL_INVALID_VALUE},
|
|
||||||
});
|
|
||||||
|
|
||||||
// The legal fence still works, and with no backend function table it is the always-signaled
|
|
||||||
// fallback - which is all this GPU-free suite needs to reach glWaitSync's own checks.
|
|
||||||
const GLsync sync = FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
|
||||||
ASSERT_NE(sync, nullptr);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
EXPECT_EQ(IsSync(sync), GL_TRUE);
|
|
||||||
|
|
||||||
RunRows({
|
|
||||||
{"glWaitSync with nonzero flags", [&] { WaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, GL_TIMEOUT_IGNORED); },
|
|
||||||
GL_INVALID_VALUE},
|
|
||||||
{"glWaitSync with a finite timeout", [&] { WaitSync(sync, 0, 1000000000ull); }, GL_INVALID_VALUE},
|
|
||||||
{"glWaitSync with the only legal argument pair", [&] { WaitSync(sync, 0, GL_TIMEOUT_IGNORED); },
|
|
||||||
GL_NO_ERROR},
|
|
||||||
});
|
|
||||||
|
|
||||||
DeleteSync(sync);
|
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// KHR-GL31.api.coverage's first two calls are glDrawArraysInstanced / glDrawElementsInstanced
|
|
||||||
// with mode GL_POINTS-1 against a context that has no program and no VAO bound, and they must
|
|
||||||
// answer GL_INVALID_ENUM. MobileGL's own "there is no current program" guard - which the spec
|
|
||||||
// does not list as a draw error at all - used to run first and shadowed the enum check with
|
|
||||||
// GL_INVALID_OPERATION. Nothing here reaches a backend: the mode is rejected before the guard.
|
|
||||||
TEST_F(NegativeApiErrorsTest, BadPrimitiveModeOutranksTheNoProgramGuard) {
|
|
||||||
DrainErrors();
|
|
||||||
// Exactly what the coverage test passes: GL_POINTS is 0, so this is 0xFFFFFFFF.
|
|
||||||
constexpr GLenum kBadMode = static_cast<GLenum>(GL_POINTS - 1);
|
|
||||||
|
|
||||||
RunRows({
|
|
||||||
{"glDrawArraysInstanced with an unaccepted mode", [] { DrawArraysInstanced(kBadMode, 0, 3, 4); },
|
|
||||||
GL_INVALID_ENUM},
|
|
||||||
{"glDrawElementsInstanced with an unaccepted mode",
|
|
||||||
[] { DrawElementsInstanced(kBadMode, 3, GL_UNSIGNED_INT, nullptr, 4); }, GL_INVALID_ENUM},
|
|
||||||
{"glDrawArrays with an unaccepted mode", [] { DrawArrays(kBadMode, 0, 3); }, GL_INVALID_ENUM},
|
|
||||||
{"glDrawElements with an unaccepted mode",
|
|
||||||
[] { DrawElements(kBadMode, 3, GL_UNSIGNED_INT, nullptr); }, GL_INVALID_ENUM},
|
|
||||||
{"glMultiDrawArrays with an unaccepted mode",
|
|
||||||
[] { MultiDrawArrays(kBadMode, nullptr, nullptr, 0); }, GL_INVALID_ENUM},
|
|
||||||
{"glDrawRangeElements with an unaccepted mode",
|
|
||||||
[] { DrawRangeElements(kBadMode, 0, 2, 3, GL_UNSIGNED_INT, nullptr); }, GL_INVALID_ENUM},
|
|
||||||
{"glDrawElementsIndirect with an unaccepted mode",
|
|
||||||
[] { DrawElementsIndirect(kBadMode, GL_UNSIGNED_INT, nullptr); }, GL_INVALID_ENUM},
|
|
||||||
{"glDrawArraysIndirect with an unaccepted mode", [] { DrawArraysIndirect(kBadMode, nullptr); },
|
|
||||||
GL_INVALID_ENUM},
|
|
||||||
// A mode the enum check accepts falls through to the guard, so the INVALID_OPERATION
|
|
||||||
// that used to win is still raised for the calls it is actually about.
|
|
||||||
{"glDrawArrays with a legal mode and no program bound", [] { DrawArrays(GL_TRIANGLES, 0, 3); },
|
|
||||||
GL_INVALID_OPERATION},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// KHR-GL30.api.coverage's glBlitFramebuffer sub-check. The frontend passed mask and filter
|
|
||||||
// straight through, and DirectGLES drains the driver's error queue around the blit so the ES
|
|
||||||
// rejection never surfaced either - both illegal calls reported GL_NO_ERROR. Every row here
|
|
||||||
// is rejected before the backend function pointer is reached, which is what lets this
|
|
||||||
// GPU-free suite run them at all.
|
|
||||||
TEST_F(NegativeApiErrorsTest, BlitFramebufferRejectsBadMasksAndFilters) {
|
|
||||||
DrainErrors();
|
|
||||||
// The bit the coverage test smuggles in: a legal glMapBufferRange flag, not a blit one.
|
|
||||||
constexpr GLbitfield kForeignBit = GL_MAP_INVALIDATE_BUFFER_BIT;
|
|
||||||
|
|
||||||
RunRows({
|
|
||||||
{"glBlitFramebuffer with a mask bit outside COLOR|DEPTH|STENCIL",
|
|
||||||
[] {
|
|
||||||
BlitFramebuffer(0, 0, 16, 16, 0, 0, 16, 16, GL_COLOR_BUFFER_BIT | kForeignBit, GL_NEAREST);
|
|
||||||
},
|
|
||||||
GL_INVALID_VALUE},
|
|
||||||
{"glBlitFramebuffer with a filter that is neither GL_NEAREST nor GL_LINEAR",
|
|
||||||
[] { BlitFramebuffer(0, 0, 16, 16, 0, 0, 16, 16, GL_COLOR_BUFFER_BIT, GL_NONE); }, GL_INVALID_ENUM},
|
|
||||||
{"glBlitFramebuffer of colour+stencil with GL_LINEAR",
|
|
||||||
[] {
|
|
||||||
BlitFramebuffer(0, 0, 16, 16, 0, 0, 16, 16, GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, GL_LINEAR);
|
|
||||||
},
|
|
||||||
GL_INVALID_OPERATION},
|
|
||||||
{"glBlitFramebuffer of depth with GL_LINEAR",
|
|
||||||
[] { BlitFramebuffer(0, 0, 16, 16, 0, 0, 16, 16, GL_DEPTH_BUFFER_BIT, GL_LINEAR); },
|
|
||||||
GL_INVALID_OPERATION},
|
|
||||||
// The DSA form has to answer identically.
|
|
||||||
{"glBlitNamedFramebuffer with a mask bit outside COLOR|DEPTH|STENCIL",
|
|
||||||
[] {
|
|
||||||
BlitNamedFramebuffer(0, 0, 0, 0, 16, 16, 0, 0, 16, 16, GL_COLOR_BUFFER_BIT | kForeignBit,
|
|
||||||
GL_NEAREST);
|
|
||||||
},
|
|
||||||
GL_INVALID_VALUE},
|
|
||||||
{"glBlitNamedFramebuffer with a bad filter",
|
|
||||||
[] { BlitNamedFramebuffer(0, 0, 0, 0, 16, 16, 0, 0, 16, 16, GL_COLOR_BUFFER_BIT, GL_NONE); },
|
|
||||||
GL_INVALID_ENUM},
|
|
||||||
{"glBlitNamedFramebuffer of depth+stencil with GL_LINEAR",
|
|
||||||
[] {
|
|
||||||
BlitNamedFramebuffer(0, 0, 0, 0, 16, 16, 0, 0, 16, 16,
|
|
||||||
GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, GL_LINEAR);
|
|
||||||
},
|
|
||||||
GL_INVALID_OPERATION},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -377,40 +377,6 @@ TEST_F(TextureTest, ClearTexImageErrorContracts) {
|
|||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(GL_INVALID_ENUM));
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), static_cast<GLenum>(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
|
// 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.
|
// is authoritative and GetIntegerv would otherwise fall through to its INVALID_ENUM default.
|
||||||
TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) {
|
TEST_F(TextureTest, MaxTextureMaxAnisotropyIsAnsweredFromTheBackendLimit) {
|
||||||
@@ -1057,32 +1023,6 @@ TEST_F(TextureTest, TexImage2DAcceptsSpecCompliantFormatCombinations) {
|
|||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
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
|
// 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
|
// 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.
|
// feeds the named channel, the other color channels default to 0 and alpha to 1.
|
||||||
@@ -1377,56 +1317,6 @@ TEST_F(TextureTest, GetTextureImageReadsNamedObjectWithoutBinding) {
|
|||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
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) {
|
TEST_F(TextureTest, GetTextureSubImageReadsFullNamedLevelWithoutBinding) {
|
||||||
GLuint texture = 0;
|
GLuint texture = 0;
|
||||||
GLuint boundTexture = 0;
|
GLuint boundTexture = 0;
|
||||||
@@ -1761,56 +1651,6 @@ TEST_F(TextureTest, AnUncompressedRespecificationClearsTheCompressedTag) {
|
|||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
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<GLint>(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 {
|
namespace {
|
||||||
// A 16x16 RGBA8 texture with exactly `levelCount` levels, defined the way
|
// A 16x16 RGBA8 texture with exactly `levelCount` levels, defined the way
|
||||||
// KHR-GL43.copy_image.non_existent_mipmap defines its textures - glTexImage2D per
|
// KHR-GL43.copy_image.non_existent_mipmap defines its textures - glTexImage2D per
|
||||||
@@ -3135,58 +2975,6 @@ TEST_F(TextureTest, CtsStyleStateResetOnDefaultTexturesLeavesNoError) {
|
|||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_2D_MULTISAMPLE_ARRAY reset failed";
|
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_2D_MULTISAMPLE_ARRAY reset failed";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean is not enough: per GL 4.6 core 8.8 that zero-sized reset has to DEALLOCATE the image,
|
|
||||||
// not define an empty one. gluStateReset runs it on both default multisample textures on every
|
|
||||||
// texture unit of a 3.2+ context, and a default texture left 'defined' afterwards stops being
|
|
||||||
// skipped by IsUndefinedDefaultTexture - it then joins the per-draw sync and bind passes on
|
|
||||||
// every unit the reset touched and reaches an ES glTexStorage*Multisample(..., 0, 0), which ES
|
|
||||||
// 3.1 8.19 rejects on every driver.
|
|
||||||
TEST_F(TextureTest, ZeroSizedMultisampleTexImageDeallocatesTheImage) {
|
|
||||||
MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE0);
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
|
|
||||||
const auto& defaultMultisample = MG_State::pGLContext->GetTextureUnitObject(0)
|
|
||||||
.GetBindingSlot(TextureTarget::Texture2DMultisample)
|
|
||||||
.GetBoundObject();
|
|
||||||
MG_Impl::GLImpl::TexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 1, GL_RGBA8, 4, 4, GL_TRUE);
|
|
||||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
ASSERT_FALSE(MG_State::GLState::IsUndefinedDefaultTexture(defaultMultisample.get()));
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::TexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 1, GL_RGBA8, 0, 0, GL_TRUE);
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
EXPECT_TRUE(MG_State::GLState::IsUndefinedDefaultTexture(defaultMultisample.get()));
|
|
||||||
|
|
||||||
// The array target's reset also passes zero LAYERS, which deallocates just the same.
|
|
||||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 0);
|
|
||||||
const auto& defaultMultisampleArray = MG_State::pGLContext->GetTextureUnitObject(0)
|
|
||||||
.GetBindingSlot(TextureTarget::Texture2DMultisampleArray)
|
|
||||||
.GetBoundObject();
|
|
||||||
MG_Impl::GLImpl::TexImage3DMultisample(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 1, GL_RGBA8, 4, 4, 2, GL_TRUE);
|
|
||||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
ASSERT_FALSE(MG_State::GLState::IsUndefinedDefaultTexture(defaultMultisampleArray.get()));
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::TexImage3DMultisample(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 1, GL_RGBA8, 4, 4, 0, GL_TRUE);
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
EXPECT_TRUE(MG_State::GLState::IsUndefinedDefaultTexture(defaultMultisampleArray.get()));
|
|
||||||
|
|
||||||
// The immutable forms do NOT share that leniency: GL 4.6 core 8.19 makes a size below 1
|
|
||||||
// INVALID_VALUE, and freezing an imageless texture as immutable would be unrecoverable.
|
|
||||||
GLuint texture = 0;
|
|
||||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
|
||||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE, texture);
|
|
||||||
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
MG_Impl::GLImpl::TexStorage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 1, GL_RGBA8, 0, 0, GL_TRUE);
|
|
||||||
ExpectSingleGlError(GL_INVALID_VALUE);
|
|
||||||
EXPECT_FALSE(MG_State::pGLContext->GetTextureUnitObject(0)
|
|
||||||
.GetBindingSlot(TextureTarget::Texture2DMultisample)
|
|
||||||
.GetBoundObject()
|
|
||||||
->IsImmutable());
|
|
||||||
|
|
||||||
MG_Impl::GLImpl::DeleteTextures(1, &texture);
|
|
||||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
|
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- GL CTS packed_pixels / texture_swizzle readback root-cause regressions --------------------
|
// ---- GL CTS packed_pixels / texture_swizzle readback root-cause regressions --------------------
|
||||||
|
|
||||||
TEST_F(TextureTest, NormalizeLegacySizedFormatsMapToCanonicalShadowLayouts) {
|
TEST_F(TextureTest, NormalizeLegacySizedFormatsMapToCanonicalShadowLayouts) {
|
||||||
@@ -3480,29 +3268,6 @@ TEST(SharedExponentRGB9E5Test, RawPackedPixelTransferCoversOnlyIdenticalLayouts)
|
|||||||
TexturePixelDataType::UnsignedInt5999Rev));
|
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) {
|
TEST_F(TextureTest, TexImage2DRGB9E5KeepsNonCanonicalClientWords) {
|
||||||
// Upload direction: GL_RGB / GL_UNSIGNED_INT_5_9_9_9_REV into GL_RGB9_E5 stores the client
|
// 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.
|
// words untouched, including the redundant encodings the CTS generates.
|
||||||
@@ -4283,25 +4048,24 @@ namespace {
|
|||||||
GLint SrcZ = -1;
|
GLint SrcZ = -1;
|
||||||
GLint DstZ = -1;
|
GLint DstZ = -1;
|
||||||
GLsizei Depth = -1;
|
GLsizei Depth = -1;
|
||||||
Bool SrcIsRenderbuffer = false;
|
|
||||||
Bool DstIsRenderbuffer = false;
|
|
||||||
} g_copyImageSubDataCall;
|
} g_copyImageSubDataCall;
|
||||||
|
|
||||||
void RecordCopyImageSubData(const MG_Backend::CopyImageEndpoint& src, GLenum srcTarget,
|
void RecordCopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture, GLenum srcTarget,
|
||||||
GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
const MG_Backend::CopyImageEndpoint& dst, GLenum dstTarget,
|
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture, GLenum dstTarget,
|
||||||
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth,
|
GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth,
|
||||||
GLsizei srcHeight, GLsizei srcDepth) {
|
GLsizei srcHeight, GLsizei srcDepth) {
|
||||||
|
(void)srcTexture;
|
||||||
(void)srcLevel;
|
(void)srcLevel;
|
||||||
(void)srcX;
|
(void)srcX;
|
||||||
(void)srcY;
|
(void)srcY;
|
||||||
|
(void)dstTexture;
|
||||||
(void)dstLevel;
|
(void)dstLevel;
|
||||||
(void)dstX;
|
(void)dstX;
|
||||||
(void)dstY;
|
(void)dstY;
|
||||||
(void)srcWidth;
|
(void)srcWidth;
|
||||||
(void)srcHeight;
|
(void)srcHeight;
|
||||||
g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ,
|
g_copyImageSubDataCall = {true, srcTarget, dstTarget, srcZ, dstZ, srcDepth};
|
||||||
dstZ, srcDepth, src.IsRenderbuffer(), dst.IsRenderbuffer()};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two storage-backed 2D textures of the requested formats, so a copy between them is a legal
|
// Two storage-backed 2D textures of the requested formats, so a copy between them is a legal
|
||||||
@@ -4538,13 +4302,9 @@ TEST_F(TextureTest, CopyImageSubDataAcceptsAPlainMutableTexImage2DPair) {
|
|||||||
MG_Impl::GLImpl::GenTextures(1, &reusedSrc);
|
MG_Impl::GLImpl::GenTextures(1, &reusedSrc);
|
||||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 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::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::GenTextures(1, &reusedDst);
|
||||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 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::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);
|
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,
|
MG_Impl::GLImpl::CopyImageSubData(reusedSrc, GL_TEXTURE_2D, 0, 0, 0, 0, reusedDst, GL_TEXTURE_2D, 0, 0, 0, 0, 1,
|
||||||
@@ -4576,236 +4336,3 @@ TEST_F(TextureTest, CopyImageSubDataPassesTheRectangleTargetThroughUntranslated)
|
|||||||
EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
|
EXPECT_EQ(g_copyImageSubDataCall.DstTarget, static_cast<GLenum>(GL_TEXTURE_RECTANGLE));
|
||||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
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<GLenum>(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -692,12 +692,8 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
!f.glUnmapBuffer || !f.glMemoryBarrier || !f.glCreateShader || !f.glCreateProgram) {
|
!f.glUnmapBuffer || !f.glMemoryBarrier || !f.glCreateShader || !f.glCreateProgram) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Read from caps, not re-queried: the per-stage limits are resolved (and their query
|
GLint maxVertexSsboBlocks = 0;
|
||||||
// errors drained) before this probe runs, so asking the driver again would be a second
|
f.glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &maxVertexSsboBlocks);
|
||||||
// round trip that can disagree with the number MobileGL actually advertises - and, on
|
|
||||||
// the early return below, would leave its own GL_INVALID_ENUM in the queue for the
|
|
||||||
// application's first glGetError to find.
|
|
||||||
const GLint maxVertexSsboBlocks = caps.MaxVertexShaderStorageBlocks;
|
|
||||||
if (maxVertexSsboBlocks < 1) {
|
if (maxVertexSsboBlocks < 1) {
|
||||||
// The native indirect machinery cannot read the command buffer from the vertex
|
// The native indirect machinery cannot read the command buffer from the vertex
|
||||||
// stage on this driver anyway; assume conforming zero-based gl_InstanceID.
|
// stage on this driver anyway; assume conforming zero-based gl_InstanceID.
|
||||||
@@ -936,9 +932,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
if (std::strcmp(extension, "GL_EXT_clip_cull_distance") == 0) {
|
if (std::strcmp(extension, "GL_EXT_clip_cull_distance") == 0) {
|
||||||
caps.SupportsClipDistance = true;
|
caps.SupportsClipDistance = true;
|
||||||
}
|
}
|
||||||
if (std::strcmp(extension, "GL_OES_viewport_array") == 0) {
|
|
||||||
caps.SupportsViewportArray = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The pointer check on top of the extension check makes each flag sufficient on its own
|
// The pointer check on top of the extension check makes each flag sufficient on its own
|
||||||
@@ -996,8 +989,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
MGLOG_I(" base instance (EXT_base_instance; emulated by attribute offsets when absent): %s",
|
MGLOG_I(" base instance (EXT_base_instance; emulated by attribute offsets when absent): %s",
|
||||||
caps.SupportsBaseInstance ? "yes" : "no");
|
caps.SupportsBaseInstance ? "yes" : "no");
|
||||||
MGLOG_I(" clip distances (EXT_clip_cull_distance): %s", caps.SupportsClipDistance ? "yes" : "no");
|
MGLOG_I(" clip distances (EXT_clip_cull_distance): %s", caps.SupportsClipDistance ? "yes" : "no");
|
||||||
MGLOG_I(" viewport array (OES_viewport_array; gl_ViewportIndex collapses to viewport 0 when absent): %s",
|
|
||||||
caps.SupportsViewportArray ? "yes" : "no");
|
|
||||||
|
|
||||||
// LOAD-BEARING STRING, not just a banner. android-plugin/trace-replay-ci.sh's
|
// LOAD-BEARING STRING, not just a banner. android-plugin/trace-replay-ci.sh's
|
||||||
// is_angle_surface_lost() greps mobilegl.log for exactly "OpenGL ES capabilities:" to
|
// is_angle_surface_lost() greps mobilegl.log for exactly "OpenGL ES capabilities:" to
|
||||||
@@ -1040,15 +1031,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
GLint maxVertexAttribs = 16;
|
GLint maxVertexAttribs = 16;
|
||||||
GLint maxComputeShaderStorageBlocks = 8;
|
GLint maxComputeShaderStorageBlocks = 8;
|
||||||
GLint maxCombinedShaderStorageBlocks = 32;
|
GLint maxCombinedShaderStorageBlocks = 32;
|
||||||
// ES 3.2 table 21.44 minimums. Zero for the four graphics stages below fragment is not a
|
|
||||||
// placeholder - it is what the spec permits and what ARM's GLES driver actually reports,
|
|
||||||
// so a probe that never runs (pre-ES 3.2, unsupported pname) leaves behind the truthful
|
|
||||||
// answer rather than an optimistic one.
|
|
||||||
GLint maxVertexShaderStorageBlocks = 0;
|
|
||||||
GLint maxTessControlShaderStorageBlocks = 0;
|
|
||||||
GLint maxTessEvaluationShaderStorageBlocks = 0;
|
|
||||||
GLint maxGeometryShaderStorageBlocks = 0;
|
|
||||||
GLint maxFragmentShaderStorageBlocks = 4;
|
|
||||||
GLint maxComputeUniformBlocks = 12;
|
GLint maxComputeUniformBlocks = 12;
|
||||||
GLint maxComputeWorkGroupInvocations = 128;
|
GLint maxComputeWorkGroupInvocations = 128;
|
||||||
GLint maxShaderStorageBufferBindings = 8;
|
GLint maxShaderStorageBufferBindings = 8;
|
||||||
@@ -1140,59 +1122,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
|
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) {
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms);
|
glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms);
|
||||||
}
|
}
|
||||||
// Per-stage storage-block counts. Deliberately NOT batched with the unconditional probes
|
|
||||||
// above, for the reason GL_MAX_TEXTURE_BUFFER_SIZE is not: the vertex and fragment pnames
|
|
||||||
// are ES 3.1, but the tessellation and geometry ones only exist from ES 3.2 on (or under
|
|
||||||
// EXT_tessellation_shader / EXT_geometry_shader), so on an older context they raise
|
|
||||||
// GL_INVALID_ENUM, leave the local untouched, and - with nothing draining the queue until
|
|
||||||
// some later probe - let that error be misattributed to an unrelated query in between, or
|
|
||||||
// leak into the application's first glGetError.
|
|
||||||
//
|
|
||||||
// A stage whose probe does not run keeps the spec minimum, which for all four graphics
|
|
||||||
// stages is 0. That is the honest answer: DirectGLES emits ESSL 3.10 on an ES 3.1 context,
|
|
||||||
// where those stages do not exist at all.
|
|
||||||
{
|
|
||||||
const auto drainErrors = [&glesFuncs]() {
|
|
||||||
Bool hadError = false;
|
|
||||||
if (glesFuncs.glGetError) {
|
|
||||||
while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true;
|
|
||||||
}
|
|
||||||
return hadError;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Isolate from errors raised by the preceding probes so the drain below reports on
|
|
||||||
// these queries only.
|
|
||||||
drainErrors();
|
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &maxVertexShaderStorageBlocks);
|
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS, &maxFragmentShaderStorageBlocks);
|
|
||||||
if (drainErrors()) {
|
|
||||||
MGLOG_W("Per-stage shader storage block query failed for the vertex/fragment "
|
|
||||||
"stages; assuming the ES minimums (vertex 0, fragment 4)");
|
|
||||||
maxVertexShaderStorageBlocks = 0;
|
|
||||||
maxFragmentShaderStorageBlocks = 4;
|
|
||||||
}
|
|
||||||
if (esAtLeast32) {
|
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS,
|
|
||||||
&maxTessControlShaderStorageBlocks);
|
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS,
|
|
||||||
&maxTessEvaluationShaderStorageBlocks);
|
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS, &maxGeometryShaderStorageBlocks);
|
|
||||||
if (drainErrors()) {
|
|
||||||
MGLOG_W("Per-stage shader storage block query failed for the tessellation/"
|
|
||||||
"geometry stages; assuming the ES minimum of 0");
|
|
||||||
maxTessControlShaderStorageBlocks = 0;
|
|
||||||
maxTessEvaluationShaderStorageBlocks = 0;
|
|
||||||
maxGeometryShaderStorageBlocks = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// A driver is free to report a negative or nonsensical count into an untouched
|
|
||||||
// out-param; clamp before anything downstream treats it as a capacity.
|
|
||||||
maxVertexShaderStorageBlocks = std::max(maxVertexShaderStorageBlocks, 0);
|
|
||||||
maxTessControlShaderStorageBlocks = std::max(maxTessControlShaderStorageBlocks, 0);
|
|
||||||
maxTessEvaluationShaderStorageBlocks = std::max(maxTessEvaluationShaderStorageBlocks, 0);
|
|
||||||
maxGeometryShaderStorageBlocks = std::max(maxGeometryShaderStorageBlocks, 0);
|
|
||||||
maxFragmentShaderStorageBlocks = std::max(maxFragmentShaderStorageBlocks, 0);
|
|
||||||
}
|
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
|
glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments);
|
glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments);
|
||||||
glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
|
glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
|
||||||
@@ -1337,11 +1266,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
caps.MaxVertexAttribs = maxVertexAttribs;
|
caps.MaxVertexAttribs = maxVertexAttribs;
|
||||||
caps.MaxComputeShaderStorageBlocks = maxComputeShaderStorageBlocks;
|
caps.MaxComputeShaderStorageBlocks = maxComputeShaderStorageBlocks;
|
||||||
caps.MaxCombinedShaderStorageBlocks = maxCombinedShaderStorageBlocks;
|
caps.MaxCombinedShaderStorageBlocks = maxCombinedShaderStorageBlocks;
|
||||||
caps.MaxVertexShaderStorageBlocks = maxVertexShaderStorageBlocks;
|
|
||||||
caps.MaxTessControlShaderStorageBlocks = maxTessControlShaderStorageBlocks;
|
|
||||||
caps.MaxTessEvaluationShaderStorageBlocks = maxTessEvaluationShaderStorageBlocks;
|
|
||||||
caps.MaxGeometryShaderStorageBlocks = maxGeometryShaderStorageBlocks;
|
|
||||||
caps.MaxFragmentShaderStorageBlocks = maxFragmentShaderStorageBlocks;
|
|
||||||
caps.MaxComputeUniformBlocks = maxComputeUniformBlocks;
|
caps.MaxComputeUniformBlocks = maxComputeUniformBlocks;
|
||||||
caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations;
|
caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations;
|
||||||
caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings;
|
caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings;
|
||||||
@@ -1419,14 +1343,6 @@ namespace MobileGL::MG_Util::BackendLoader {
|
|||||||
MGLOG_I(" GL_MAX_VERTEX_ATTRIBS: %d", caps.MaxVertexAttribs);
|
MGLOG_I(" GL_MAX_VERTEX_ATTRIBS: %d", caps.MaxVertexAttribs);
|
||||||
MGLOG_I(" GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS: %d", caps.MaxComputeShaderStorageBlocks);
|
MGLOG_I(" GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS: %d", caps.MaxComputeShaderStorageBlocks);
|
||||||
MGLOG_I(" GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS: %d", caps.MaxCombinedShaderStorageBlocks);
|
MGLOG_I(" GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS: %d", caps.MaxCombinedShaderStorageBlocks);
|
||||||
// Worth a line each: a zero here is what stops an application's storage block from ever
|
|
||||||
// working in that stage, and reading it back from an artifact is the difference between
|
|
||||||
// "MobileGL dropped my draw" and "this driver has no SSBOs outside compute".
|
|
||||||
MGLOG_I(" GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: %d", caps.MaxVertexShaderStorageBlocks);
|
|
||||||
MGLOG_I(" GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS: %d", caps.MaxTessControlShaderStorageBlocks);
|
|
||||||
MGLOG_I(" GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS: %d", caps.MaxTessEvaluationShaderStorageBlocks);
|
|
||||||
MGLOG_I(" GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS: %d", caps.MaxGeometryShaderStorageBlocks);
|
|
||||||
MGLOG_I(" GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS: %d", caps.MaxFragmentShaderStorageBlocks);
|
|
||||||
MGLOG_I(" GL_MAX_COMPUTE_UNIFORM_BLOCKS: %d", caps.MaxComputeUniformBlocks);
|
MGLOG_I(" GL_MAX_COMPUTE_UNIFORM_BLOCKS: %d", caps.MaxComputeUniformBlocks);
|
||||||
MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS: %d", caps.MaxComputeWorkGroupInvocations);
|
MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS: %d", caps.MaxComputeWorkGroupInvocations);
|
||||||
MGLOG_I(" GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS: %d", caps.MaxShaderStorageBufferBindings);
|
MGLOG_I(" GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS: %d", caps.MaxShaderStorageBufferBindings);
|
||||||
|
|||||||
@@ -1182,21 +1182,6 @@ namespace MobileGL {
|
|||||||
// compile and the per-distance enables have nowhere to go - clipping silently never
|
// compile and the per-distance enables have nowhere to go - clipping silently never
|
||||||
// happens, which is exactly what KHR-GLxx.clip_distance.functional catches.
|
// happens, which is exactly what KHR-GLxx.clip_distance.functional catches.
|
||||||
Bool SupportsClipDistance = false;
|
Bool SupportsClipDistance = false;
|
||||||
// GL_OES_viewport_array is present: the driver knows gl_ViewportIndex in ESSL - and
|
|
||||||
// only then. ESSL has no core spelling for it at ANY version, while SPIRV-Cross prints
|
|
||||||
// the identifier bare and requests nothing for it (contrast gl_Layer, which it backs
|
|
||||||
// with GL_NV_viewport_array2 on ES), so the `#extension GL_OES_viewport_array :
|
|
||||||
// require` line has to be inserted into the emitted source - see
|
|
||||||
// RequestViewportArrayExtension. Without the extension the stage does not compile at
|
|
||||||
// all and the whole program becomes unusable, which on DirectGLES means every draw
|
|
||||||
// using it silently renders nothing; LowerViewportIndexPass is the fallback that
|
|
||||||
// demotes the builtin so the program still links and degrades to viewport 0.
|
|
||||||
//
|
|
||||||
// Extension string only, deliberately: DirectGLES does not call any of the indexed
|
|
||||||
// OES entry points yet, so there is no pointer to require. When that forwarding lands
|
|
||||||
// this must gain the pointer check as well - the rule everywhere else in this struct,
|
|
||||||
// because eglGetProcAddress can hand back a stub that silently drops every call.
|
|
||||||
Bool SupportsViewportArray = false;
|
|
||||||
// GL_RENDERER contains "ANGLE".
|
// GL_RENDERER contains "ANGLE".
|
||||||
Bool IsAngleRenderer = false;
|
Bool IsAngleRenderer = false;
|
||||||
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
|
||||||
@@ -1248,17 +1233,6 @@ namespace MobileGL {
|
|||||||
Int MaxVertexAttribs = 16;
|
Int MaxVertexAttribs = 16;
|
||||||
Int MaxComputeShaderStorageBlocks = 8;
|
Int MaxComputeShaderStorageBlocks = 8;
|
||||||
Int MaxCombinedShaderStorageBlocks = 32;
|
Int MaxCombinedShaderStorageBlocks = 32;
|
||||||
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS as the host GLES driver reports them.
|
|
||||||
// The defaults are the ES 3.2 minimums (table 21.44): 0 for every graphics stage
|
|
||||||
// except fragment, which is 4. ES only gained the tessellation and geometry pnames
|
|
||||||
// in 3.2 (or with EXT_tessellation_shader / EXT_geometry_shader), so those two are
|
|
||||||
// queried behind a support check and left at the default otherwise - see
|
|
||||||
// FillInGLESCapabilities.
|
|
||||||
Int MaxVertexShaderStorageBlocks = 0;
|
|
||||||
Int MaxTessControlShaderStorageBlocks = 0;
|
|
||||||
Int MaxTessEvaluationShaderStorageBlocks = 0;
|
|
||||||
Int MaxGeometryShaderStorageBlocks = 0;
|
|
||||||
Int MaxFragmentShaderStorageBlocks = 4;
|
|
||||||
Int MaxComputeUniformBlocks = 12;
|
Int MaxComputeUniformBlocks = 12;
|
||||||
Int MaxComputeWorkGroupInvocations = 128;
|
Int MaxComputeWorkGroupInvocations = 128;
|
||||||
Int MaxShaderStorageBufferBindings = 8;
|
Int MaxShaderStorageBufferBindings = 8;
|
||||||
|
|||||||
@@ -253,12 +253,6 @@ namespace MobileGL {
|
|||||||
return TextureInternalFormat::Depth32FStencil8;
|
return TextureInternalFormat::Depth32FStencil8;
|
||||||
case GL_STENCIL_INDEX8:
|
case GL_STENCIL_INDEX8:
|
||||||
return TextureInternalFormat::StencilIndex8;
|
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:
|
case GL_DEPTH_COMPONENT:
|
||||||
return TextureInternalFormat::DepthComponent;
|
return TextureInternalFormat::DepthComponent;
|
||||||
case GL_DEPTH_STENCIL:
|
case GL_DEPTH_STENCIL:
|
||||||
|
|||||||
@@ -124,9 +124,6 @@ namespace MobileGL {
|
|||||||
case TextureInternalFormat::DepthComponent32F:
|
case TextureInternalFormat::DepthComponent32F:
|
||||||
case TextureInternalFormat::Depth24Stencil8:
|
case TextureInternalFormat::Depth24Stencil8:
|
||||||
case TextureInternalFormat::Depth32FStencil8:
|
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;
|
return internalformat;
|
||||||
// probably we should assume unorm here?
|
// probably we should assume unorm here?
|
||||||
case TextureInternalFormat::RGBA: {
|
case TextureInternalFormat::RGBA: {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
#include "SpirvPasses/DecoratePositionInvariantPass.h"
|
#include "SpirvPasses/DecoratePositionInvariantPass.h"
|
||||||
#include "SpirvPasses/DemoteFloat64Pass.h"
|
#include "SpirvPasses/DemoteFloat64Pass.h"
|
||||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||||
#include "SpirvPasses/LowerViewportIndexPass.h"
|
|
||||||
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
||||||
#include "SpirvPasses/FlattenXfbInterfaceBlocksPass.h"
|
#include "SpirvPasses/FlattenXfbInterfaceBlocksPass.h"
|
||||||
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
|
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
|
||||||
@@ -28,12 +27,10 @@
|
|||||||
#include "SpirvPasses/ZeroBaseVertexPass.h"
|
#include "SpirvPasses/ZeroBaseVertexPass.h"
|
||||||
#include "SpirvPasses/DeriveNumSubgroupsPass.h"
|
#include "SpirvPasses/DeriveNumSubgroupsPass.h"
|
||||||
#include "SpirvPasses/EmulateSubgroupsPass.h"
|
#include "SpirvPasses/EmulateSubgroupsPass.h"
|
||||||
#include "SpirvPasses/FixIterationRPBarrierPass.h"
|
|
||||||
#include "SpirvPasses/FixIterationRPSubgroupScratchPass.h"
|
#include "SpirvPasses/FixIterationRPSubgroupScratchPass.h"
|
||||||
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
#include "SpirvPasses/NormalizeRectCoordinatesPass.h"
|
||||||
#include "SpirvPasses/Lower1DArrayImagesPass.h"
|
#include "SpirvPasses/Lower1DArrayImagesPass.h"
|
||||||
#include "SpirvPasses/BakeImageFormatsPass.h"
|
#include "SpirvPasses/BakeImageFormatsPass.h"
|
||||||
#include "SpirvPasses/ClampMultisampleFetchPass.h"
|
|
||||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||||
@@ -637,63 +634,6 @@ namespace MobileGL {
|
|||||||
outputBinary, true, enableSpirvValidation);
|
outputBinary, true, enableSpirvValidation);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::LowerViewportIndexForEssl(const Vector<Uint32>& inputBinary,
|
|
||||||
Vector<uint32_t>& outputBinary,
|
|
||||||
const bool enableSpirvValidation) {
|
|
||||||
using namespace spvtools;
|
|
||||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
|
||||||
optimizer.RegisterPass(LowerViewportIndexPass::CreateLowerViewportIndexPass());
|
|
||||||
|
|
||||||
return RunOptimizerChecked("LowerViewportIndexForEssl", optimizer, inputBinary,
|
|
||||||
outputBinary, true, enableSpirvValidation);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderCompiler::DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary) {
|
|
||||||
return LowerViewportIndexPass::DeclaresViewportIndexBuiltin(binary);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderCompiler::ClampMultisampleFetchesForEssl(const Vector<Uint32>& inputBinary,
|
|
||||||
Vector<uint32_t>& outputBinary,
|
|
||||||
const Int32 maxColorSamples,
|
|
||||||
const Int32 maxIntegerSamples,
|
|
||||||
const Int32 maxDepthSamples,
|
|
||||||
const Int32 advertisedMaxSamples,
|
|
||||||
const bool enableSpirvValidation) {
|
|
||||||
using namespace spvtools;
|
|
||||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
|
||||||
optimizer.RegisterPass(ClampMultisampleFetchPass::CreateClampMultisampleFetchPass(
|
|
||||||
maxColorSamples, maxIntegerSamples, maxDepthSamples, advertisedMaxSamples));
|
|
||||||
|
|
||||||
return RunOptimizerChecked("ClampMultisampleFetchesForEssl", optimizer, inputBinary,
|
|
||||||
outputBinary, true, enableSpirvValidation);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderCompiler::DeclaresMultisampledImage(const Vector<Uint32>& binary) {
|
|
||||||
return ClampMultisampleFetchPass::DeclaresMultisampledImage(binary);
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderCompiler::SpirvGateFeatures ShaderCompiler::ProbeSpirvGateFeatures(
|
|
||||||
const Vector<Uint32>& binary) {
|
|
||||||
SpirvGateFeatures features;
|
|
||||||
if (binary.empty()) {
|
|
||||||
return features;
|
|
||||||
}
|
|
||||||
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
|
|
||||||
SPV_ENV_VULKAN_1_1,
|
|
||||||
[](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
|
|
||||||
binary.data(), binary.size());
|
|
||||||
if (!context) {
|
|
||||||
// Unparseable here means unusable downstream too; let the ordinary transpile
|
|
||||||
// path produce the error rather than inventing a verdict from it.
|
|
||||||
return features;
|
|
||||||
}
|
|
||||||
features.WritesViewportIndexOutput =
|
|
||||||
LowerViewportIndexPass::DeclaresViewportIndexBuiltin(context.get());
|
|
||||||
features.DeclaresMultisampledImage =
|
|
||||||
ClampMultisampleFetchPass::DeclaresMultisampledImage(context.get());
|
|
||||||
return features;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderCompiler::SplitArrayVertexInputsForEssl(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::SplitArrayVertexInputsForEssl(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary,
|
Vector<uint32_t>& outputBinary,
|
||||||
const bool enableSpirvValidation) {
|
const bool enableSpirvValidation) {
|
||||||
@@ -972,29 +912,17 @@ namespace MobileGL {
|
|||||||
|
|
||||||
bool ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
|
bool ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
|
||||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
|
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
|
||||||
const Uint32 nativeSubgroupSize, const Uint32 maxWorkgroupScratchBytes,
|
const Uint32 nativeSubgroupSize, const bool enableSpirvValidation) {
|
||||||
const bool enableSpirvValidation) {
|
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
optimizer.RegisterPass(
|
optimizer.RegisterPass(
|
||||||
FixIterationRPSubgroupScratchPass::CreateFixIterationRPSubgroupScratchPass(
|
FixIterationRPSubgroupScratchPass::CreateFixIterationRPSubgroupScratchPass(
|
||||||
nativeSubgroupSize, maxWorkgroupScratchBytes));
|
nativeSubgroupSize));
|
||||||
|
|
||||||
return RunOptimizerChecked("FixIterationRPSubgroupScratchForVulkan", optimizer,
|
return RunOptimizerChecked("FixIterationRPSubgroupScratchForVulkan", optimizer,
|
||||||
inputBinary, outputBinary, true, enableSpirvValidation);
|
inputBinary, outputBinary, true, enableSpirvValidation);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShaderCompiler::FixIterationRPBarrierForVulkan(
|
|
||||||
const Vector<Uint32>& inputBinary, Vector<uint32_t>& outputBinary,
|
|
||||||
const bool enableSpirvValidation) {
|
|
||||||
using namespace spvtools;
|
|
||||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
|
||||||
optimizer.RegisterPass(FixIterationRPBarrierPass::CreateFixIterationRPBarrierPass());
|
|
||||||
|
|
||||||
return RunOptimizerChecked("FixIterationRPBarrierForVulkan", optimizer,
|
|
||||||
inputBinary, outputBinary, true, enableSpirvValidation);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
|
Vector<uint32_t>& outputBinary, const bool enableSpirvValidation) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
|
|||||||
@@ -32,49 +32,6 @@ namespace MobileGL {
|
|||||||
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary,
|
Vector<uint32_t>& outputBinary,
|
||||||
bool enableSpirvValidation = false);
|
bool enableSpirvValidation = false);
|
||||||
// Demotes the gl_ViewportIndex OUTPUT builtin to a plain Private global named
|
|
||||||
// mg_ViewportIndex, so SPIRV-Cross emits an ordinary declaration instead of a bare
|
|
||||||
// gl_ViewportIndex that ESSL has no core spelling for. Multi-viewport routing is
|
|
||||||
// lost (everything lands in viewport 0) but the stage compiles and the program
|
|
||||||
// runs, instead of every draw made with it becoming a silent no-op. Only for the
|
|
||||||
// DirectGLES transpile path on a driver WITHOUT GL_OES_viewport_array; gl_Layer is
|
|
||||||
// deliberately left alone, being core in ESSL 3.20 geometry shaders.
|
|
||||||
static bool LowerViewportIndexForEssl(const Vector<Uint32>& inputBinary,
|
|
||||||
Vector<uint32_t>& outputBinary,
|
|
||||||
bool enableSpirvValidation = false);
|
|
||||||
// Whether the module declares an output decorated BuiltIn ViewportIndex, i.e.
|
|
||||||
// whether the pass above has anything to do. The gate that keeps every other
|
|
||||||
// stage off an optimizer round trip it does not need.
|
|
||||||
static bool DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary);
|
|
||||||
// Clamps the Sample image-operand of every multisample fetch to the sample count
|
|
||||||
// the BACKEND can really deliver for that image's category, which on Adreno and
|
|
||||||
// Mali is 1 for integer formats while the frontend advertises the GL-mandated
|
|
||||||
// floor of 4. Without it a `texelFetch(usampler2DMS, coord, 3)` reads past the
|
|
||||||
// end of a one-sample allocation. Pass the backend-real per-category ceilings and
|
|
||||||
// the advertised maximum (GL_Getter's GetAdvertisedMaxSamples); a category that
|
|
||||||
// already reaches the advertised value is left alone. DirectGLES transpile path
|
|
||||||
// only. See ClampMultisampleFetchPass.
|
|
||||||
static bool ClampMultisampleFetchesForEssl(const Vector<Uint32>& inputBinary,
|
|
||||||
Vector<uint32_t>& outputBinary,
|
|
||||||
Int32 maxColorSamples,
|
|
||||||
Int32 maxIntegerSamples,
|
|
||||||
Int32 maxDepthSamples,
|
|
||||||
Int32 advertisedMaxSamples,
|
|
||||||
bool enableSpirvValidation = false);
|
|
||||||
// Whether the module declares any multisampled image type, i.e. whether the pass
|
|
||||||
// above has anything to do. The gate that keeps every other stage off an
|
|
||||||
// optimizer round trip it does not need.
|
|
||||||
static bool DeclaresMultisampledImage(const Vector<Uint32>& binary);
|
|
||||||
// Both gate questions above answered from ONE parse. Every armed gate costs a
|
|
||||||
// BuildModule per shader stage, and on a driver where both are armed (Mali: no
|
|
||||||
// GL_OES_viewport_array AND integer multisample squeezed to 1) the separate
|
|
||||||
// probes made compile-heavy workloads measurably slower - ReservedNames-class
|
|
||||||
// CTS cases paid ~10%. Callers with more than one armed gate use this instead.
|
|
||||||
struct SpirvGateFeatures {
|
|
||||||
Bool WritesViewportIndexOutput = false;
|
|
||||||
Bool DeclaresMultisampledImage = false;
|
|
||||||
};
|
|
||||||
static SpirvGateFeatures ProbeSpirvGateFeatures(const Vector<Uint32>& binary);
|
|
||||||
// Replaces an ARRAY vertex input with one input per element at consecutive
|
// Replaces an ARRAY vertex input with one input per element at consecutive
|
||||||
// locations, seeding a Private copy of the array so indexed reads still work.
|
// locations, seeding a Private copy of the array so indexed reads still work.
|
||||||
// GLSL ES has no array vertex inputs and SPIRV-Cross refuses the whole module
|
// GLSL ES has no array vertex inputs and SPIRV-Cross refuses the whole module
|
||||||
@@ -210,24 +167,13 @@ namespace MobileGL {
|
|||||||
Vector<uint32_t>& outputBinary,
|
Vector<uint32_t>& outputBinary,
|
||||||
Uint32 maxWorkgroupScratchBytes,
|
Uint32 maxWorkgroupScratchBytes,
|
||||||
bool enableSpirvValidation = false);
|
bool enableSpirvValidation = false);
|
||||||
// Grows iterationRP's under-declared gl_SubgroupID-indexed scratch to the
|
// Patches iterationRP's under-declared prefixSumCache[32] on sub-16-lane
|
||||||
// subgroup count the device actually partitions into, fingerprint-gated to
|
// devices, fingerprint-gated to that pack's reduction; every other module
|
||||||
// that pack's reduction idiom; every other module - and every device whose
|
// passes through byte-identical. See FixIterationRPSubgroupScratchPass.
|
||||||
// width the pack already assumed - passes through byte-identical.
|
|
||||||
// maxWorkgroupScratchBytes bounds the growth (pass the device's
|
|
||||||
// maxComputeSharedMemorySize; 0 falls back to the 16384-byte Vulkan
|
|
||||||
// minimum). See FixIterationRPSubgroupScratchPass.
|
|
||||||
static bool FixIterationRPSubgroupScratchForVulkan(const Vector<Uint32>& inputBinary,
|
static bool FixIterationRPSubgroupScratchForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary,
|
Vector<uint32_t>& outputBinary,
|
||||||
Uint32 nativeSubgroupSize,
|
Uint32 nativeSubgroupSize,
|
||||||
Uint32 maxWorkgroupScratchBytes,
|
|
||||||
bool enableSpirvValidation = false);
|
bool enableSpirvValidation = false);
|
||||||
// Inserts the missing workgroup rendezvous between Program 203's two
|
|
||||||
// prefixSumCache reductions. Fingerprint-gated to the iterationRP shape;
|
|
||||||
// unrelated and already-repaired modules pass through byte-identical.
|
|
||||||
static bool FixIterationRPBarrierForVulkan(const Vector<Uint32>& inputBinary,
|
|
||||||
Vector<uint32_t>& outputBinary,
|
|
||||||
bool enableSpirvValidation = false);
|
|
||||||
// Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair
|
// Re-declares 64-bit float vertex inputs as their 32-bit unsigned word pair
|
||||||
// (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no
|
// (double -> uvec2, dvec2 -> uvec4) and bitcasts them back to double at entry, so no
|
||||||
// VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex
|
// VK_FORMAT_R64*_SFLOAT is needed - lavapipe advertises none of them for vertex
|
||||||
|
|||||||
@@ -1,378 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.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
|
|
||||||
|
|
||||||
#include "ClampMultisampleFetchPass.h"
|
|
||||||
|
|
||||||
#include "spirv.hpp"
|
|
||||||
#include "source/opt/build_module.h"
|
|
||||||
#include "source/opt/constants.h"
|
|
||||||
#include "source/opt/def_use_manager.h"
|
|
||||||
#include "source/opt/instruction.h"
|
|
||||||
#include "source/opt/ir_builder.h"
|
|
||||||
#include "source/opt/ir_context.h"
|
|
||||||
#include "source/opt/module.h"
|
|
||||||
#include "source/opt/type_manager.h"
|
|
||||||
#include "source/opt/types.h"
|
|
||||||
#include "source/util/make_unique.h"
|
|
||||||
#include "source/util/string_utils.h"
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace MobileGL {
|
|
||||||
namespace MG_Util {
|
|
||||||
namespace ShaderTranspiler {
|
|
||||||
namespace {
|
|
||||||
using spvtools::opt::Instruction;
|
|
||||||
using spvtools::opt::InstructionBuilder;
|
|
||||||
using spvtools::opt::IRContext;
|
|
||||||
namespace analysis = spvtools::opt::analysis;
|
|
||||||
|
|
||||||
// GLSL.std.450 instruction numbers (see 3rdparty/glslang/SPIRV/GLSL.std.450.h).
|
|
||||||
// UMin is not interchangeable with SMin here: an unsigned operand large enough to
|
|
||||||
// read as negative would come back OUT of range from a signed minimum, which is
|
|
||||||
// the very thing this pass exists to prevent.
|
|
||||||
constexpr uint32_t kGlslUMin = 38u;
|
|
||||||
constexpr uint32_t kGlslSMin = 39u;
|
|
||||||
|
|
||||||
// OpTypeImage in-operands: 0 sampled type, 1 Dim, 2 Depth, 3 Arrayed, 4 MS,
|
|
||||||
// 5 Sampled, 6 Format.
|
|
||||||
constexpr uint32_t kSampledTypeOperand = 0;
|
|
||||||
constexpr uint32_t kDepthOperand = 2;
|
|
||||||
constexpr uint32_t kMultisampledOperand = 4;
|
|
||||||
|
|
||||||
// OpImageFetch / OpImageRead in-operands: 0 image, 1 coordinate, 2 the optional
|
|
||||||
// image-operands mask, 3.. the ids that mask asks for.
|
|
||||||
constexpr uint32_t kImageOperand = 0;
|
|
||||||
constexpr uint32_t kImageOperandsMaskOperand = 2;
|
|
||||||
|
|
||||||
// The categories GL keeps a separate GL_MAX_*_SAMPLES ceiling for.
|
|
||||||
enum class SampleCategory { Color, Depth, Integer };
|
|
||||||
|
|
||||||
// The two operations that can carry a Sample image-operand and take their
|
|
||||||
// coordinate in in-operand 1. OpImageWrite can carry one too, but its operand
|
|
||||||
// layout differs (image, coordinate, TEXEL, mask) and writing a multisample
|
|
||||||
// texel is not expressible in the ESSL this backend emits, so it is left out
|
|
||||||
// rather than given an untested second index arithmetic. The sparse forms are
|
|
||||||
// out of scope for the same reason: ESSL has no sparse texturing at all, so a
|
|
||||||
// module containing one cannot reach a driver through this path anyway.
|
|
||||||
bool CarriesSampleImageOperand(spv::Op opcode) {
|
|
||||||
return opcode == spv::Op::OpImageFetch || opcode == spv::Op::OpImageRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsMultisampledImageType(const Instruction* imageType) {
|
|
||||||
return imageType != nullptr && imageType->opcode() == spv::Op::OpTypeImage &&
|
|
||||||
imageType->NumInOperands() > kMultisampledOperand &&
|
|
||||||
imageType->GetSingleWordInOperand(kMultisampledOperand) == 1u;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The OpTypeImage behind whatever an image operation was handed - a sampled
|
|
||||||
// image, a bare image, or a pointer to (or array of) either. Same unwrapping as
|
|
||||||
// Lower1DArrayImagesPass.
|
|
||||||
Instruction* ResolveImageType(IRContext* context, uint32_t objectId) {
|
|
||||||
auto* defUseMgr = context->get_def_use_mgr();
|
|
||||||
Instruction* object = defUseMgr->GetDef(objectId);
|
|
||||||
if (object == nullptr) return nullptr;
|
|
||||||
Instruction* type = defUseMgr->GetDef(object->type_id());
|
|
||||||
while (type != nullptr) {
|
|
||||||
switch (type->opcode()) {
|
|
||||||
case spv::Op::OpTypeImage:
|
|
||||||
return type;
|
|
||||||
case spv::Op::OpTypeSampledImage:
|
|
||||||
case spv::Op::OpTypePointer:
|
|
||||||
case spv::Op::OpTypeArray:
|
|
||||||
case spv::Op::OpTypeRuntimeArray:
|
|
||||||
// Each names its element type in its last in-operand, except arrays,
|
|
||||||
// whose element type is the FIRST.
|
|
||||||
type = defUseMgr->GetDef(type->opcode() == spv::Op::OpTypeArray ||
|
|
||||||
type->opcode() == spv::Op::OpTypeRuntimeArray
|
|
||||||
? type->GetSingleWordInOperand(0)
|
|
||||||
: type->GetSingleWordInOperand(type->NumInOperands() - 1));
|
|
||||||
continue;
|
|
||||||
default:
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
SampleCategory CategoryOf(IRContext* context, const Instruction* imageType) {
|
|
||||||
const Instruction* sampledType =
|
|
||||||
context->get_def_use_mgr()->GetDef(imageType->GetSingleWordInOperand(kSampledTypeOperand));
|
|
||||||
if (sampledType != nullptr && sampledType->opcode() == spv::Op::OpTypeInt) {
|
|
||||||
return SampleCategory::Integer;
|
|
||||||
}
|
|
||||||
// Depth == 1 is the ONLY spelling that positively means a depth image.
|
|
||||||
// glslang writes 0 for a plain sampler and 2 ("no indication") wherever it
|
|
||||||
// cannot tell, and GLSL has no multisampled shadow sampler for it to write 1
|
|
||||||
// for, so everything but an explicit 1 falls to the colour ceiling - which is
|
|
||||||
// also the safer of the two to guess at, being the one GL_MAX_SAMPLES itself
|
|
||||||
// describes. Guarded because Depth is only readable on a well-formed type.
|
|
||||||
if (imageType->NumInOperands() > kDepthOperand &&
|
|
||||||
imageType->GetSingleWordInOperand(kDepthOperand) == 1u) {
|
|
||||||
return SampleCategory::Depth;
|
|
||||||
}
|
|
||||||
return SampleCategory::Color;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Where the Sample id sits among an image operation's in-operands, or false when
|
|
||||||
// the operation carries no Sample at all.
|
|
||||||
//
|
|
||||||
// The position is NOT fixed. The mask's ids follow it in ASCENDING BIT ORDER, so
|
|
||||||
// every lower bit that is set pushes Sample along by the number of ids that bit
|
|
||||||
// asks for: Bias/Lod/ConstOffset/Offset/ConstOffsets one each, Grad two (dx and
|
|
||||||
// dy). Bits at or above Sample cannot move it and are irrelevant here. glslang
|
|
||||||
// only ever emits Sample on its own for a GLSL texelFetch - there is no
|
|
||||||
// texelFetchOffset for a multisampled sampler - so in practice this always
|
|
||||||
// answers 3; the walk is what keeps that from being an assumption.
|
|
||||||
bool TryGetSampleOperandIndex(const Instruction& instruction, uint32_t* sampleOperandIndex) {
|
|
||||||
if (instruction.NumInOperands() <= kImageOperandsMaskOperand) {
|
|
||||||
// No image-operands mask at all, so no explicit sample: SPIR-V reads
|
|
||||||
// sample 0, which is in range of any allocation. Nothing to clamp.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const uint32_t mask = instruction.GetSingleWordInOperand(kImageOperandsMaskOperand);
|
|
||||||
const auto has = [mask](spv::ImageOperandsMask bit) {
|
|
||||||
return (mask & static_cast<uint32_t>(bit)) != 0u;
|
|
||||||
};
|
|
||||||
if (!has(spv::ImageOperandsMask::Sample)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t index = kImageOperandsMaskOperand + 1;
|
|
||||||
if (has(spv::ImageOperandsMask::Bias)) ++index;
|
|
||||||
if (has(spv::ImageOperandsMask::Lod)) ++index;
|
|
||||||
if (has(spv::ImageOperandsMask::Grad)) index += 2;
|
|
||||||
if (has(spv::ImageOperandsMask::ConstOffset)) ++index;
|
|
||||||
if (has(spv::ImageOperandsMask::Offset)) ++index;
|
|
||||||
if (has(spv::ImageOperandsMask::ConstOffsets)) ++index;
|
|
||||||
if (instruction.NumInOperands() <= index) {
|
|
||||||
// A mask promising more operands than the instruction carries is a
|
|
||||||
// malformed module; leave it to the validator rather than indexing past
|
|
||||||
// the end of it.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
*sampleOperandIndex = index;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The module's GLSL.std.450 import, creating it when the module has none.
|
|
||||||
// glslang emits one for all but the most trivial shaders, but a module that
|
|
||||||
// reached here without one must still be clampable. 0 means no id was available,
|
|
||||||
// and in that case NOTHING was added - the caller can still leave the module
|
|
||||||
// untouched. IRContext::AddExtInstImport rather than Module's: it is the one that
|
|
||||||
// keeps the def-use and feature managers in step with the new import.
|
|
||||||
uint32_t EnsureGlslStd450Import(IRContext* context) {
|
|
||||||
for (const Instruction& import : context->module()->ext_inst_imports()) {
|
|
||||||
if (spvtools::utils::MakeString(import.GetInOperand(0).words) == "GLSL.std.450") {
|
|
||||||
return import.result_id();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const uint32_t importId = context->TakeNextId();
|
|
||||||
if (importId == 0u) return 0u;
|
|
||||||
context->AddExtInstImport(spvtools::MakeUnique<Instruction>(
|
|
||||||
context, spv::Op::OpExtInstImport, 0, importId,
|
|
||||||
Instruction::OperandList{
|
|
||||||
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector("GLSL.std.450")}}));
|
|
||||||
return importId;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
bool ClampMultisampleFetchPass::DeclaresMultisampledImage(const Vector<Uint32>& binary) {
|
|
||||||
if (binary.empty()) {
|
|
||||||
// An empty module is a stage that produced no SPIR-V, which is not a verdict
|
|
||||||
// about multisample fetches; letting BuildModule reject it would push a
|
|
||||||
// spurious diagnostic through the message consumer first.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
std::unique_ptr<IRContext> context = spvtools::BuildModule(
|
|
||||||
SPV_ENV_VULKAN_1_1, [](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
|
|
||||||
binary.data(), binary.size());
|
|
||||||
if (!context) {
|
|
||||||
// Unparseable here means unusable downstream too; let the ordinary transpile
|
|
||||||
// path produce the error rather than inventing a verdict from it.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return DeclaresMultisampledImage(context.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ClampMultisampleFetchPass::DeclaresMultisampledImage(IRContext* context) {
|
|
||||||
for (const Instruction& type : context->module()->types_values()) {
|
|
||||||
if (IsMultisampledImageType(&type)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
spvtools::opt::Pass::Status ClampMultisampleFetchPass::Process() {
|
|
||||||
// No category is squeezed, so no fetch can be out of range. This is the whole
|
|
||||||
// answer on a driver whose per-format ceilings all reach what MobileGL
|
|
||||||
// advertises, and it costs nothing.
|
|
||||||
if (m_maxColorSamples >= m_advertisedMaxSamples &&
|
|
||||||
m_maxIntegerSamples >= m_advertisedMaxSamples &&
|
|
||||||
m_maxDepthSamples >= m_advertisedMaxSamples) {
|
|
||||||
return Status::SuccessWithoutChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* irContext = context();
|
|
||||||
|
|
||||||
// The type table settles it for almost every shader: no multisampled image
|
|
||||||
// declared, nothing any fetch in the body could be reading.
|
|
||||||
bool hasMultisampledImageType = false;
|
|
||||||
for (const Instruction& type : irContext->types_values()) {
|
|
||||||
if (IsMultisampledImageType(&type)) {
|
|
||||||
hasMultisampledImageType = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hasMultisampledImageType) {
|
|
||||||
return Status::SuccessWithoutChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
|
||||||
auto* typeMgr = irContext->get_type_mgr();
|
|
||||||
auto* constantMgr = irContext->get_constant_mgr();
|
|
||||||
|
|
||||||
bool clampedAnything = false;
|
|
||||||
for (auto& function : *irContext->module()) {
|
|
||||||
for (auto& block : function) {
|
|
||||||
for (auto& instruction : block) {
|
|
||||||
if (!CarriesSampleImageOperand(instruction.opcode()) ||
|
|
||||||
instruction.NumInOperands() <= kImageOperandsMaskOperand) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const Instruction* imageType =
|
|
||||||
ResolveImageType(irContext, instruction.GetSingleWordInOperand(kImageOperand));
|
|
||||||
if (!IsMultisampledImageType(imageType)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
uint32_t sampleOperandIndex = 0;
|
|
||||||
if (!TryGetSampleOperandIndex(instruction, &sampleOperandIndex)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Int32 categoryMaxSamples = m_maxColorSamples;
|
|
||||||
switch (CategoryOf(irContext, imageType)) {
|
|
||||||
case SampleCategory::Integer:
|
|
||||||
categoryMaxSamples = m_maxIntegerSamples;
|
|
||||||
break;
|
|
||||||
case SampleCategory::Depth:
|
|
||||||
categoryMaxSamples = m_maxDepthSamples;
|
|
||||||
break;
|
|
||||||
case SampleCategory::Color:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (categoryMaxSamples >= m_advertisedMaxSamples) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The replacement has to carry the ORIGINAL operand's type: SPIR-V
|
|
||||||
// permits either signedness for Sample, and handing OpImageFetch an
|
|
||||||
// int where it had a uint is an invalid module rather than a wrong
|
|
||||||
// answer - the kind of defect that reaches a driver as "compiles
|
|
||||||
// here, not there".
|
|
||||||
const uint32_t sampleOperandId = instruction.GetSingleWordInOperand(sampleOperandIndex);
|
|
||||||
const Instruction* sampleOperandDef = defUseMgr->GetDef(sampleOperandId);
|
|
||||||
if (sampleOperandDef == nullptr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const uint32_t sampleTypeId = sampleOperandDef->type_id();
|
|
||||||
const analysis::Type* sampleType =
|
|
||||||
sampleTypeId != 0u ? typeMgr->GetType(sampleTypeId) : nullptr;
|
|
||||||
const analysis::Integer* sampleInteger =
|
|
||||||
sampleType != nullptr ? sampleType->AsInteger() : nullptr;
|
|
||||||
if (sampleInteger == nullptr || sampleInteger->width() != 32u) {
|
|
||||||
// GLSL spells the sample index `int` and SPIR-V requires an
|
|
||||||
// integer scalar, so this is unreachable from any shader this
|
|
||||||
// backend compiles. Declining beats minting a constant of a
|
|
||||||
// width the operand never had.
|
|
||||||
MGLOG_D("ClampMultisampleFetchPass: sample operand %%%u of a "
|
|
||||||
"multisample fetch is not a 32-bit integer scalar; left "
|
|
||||||
"unclamped.",
|
|
||||||
sampleOperandId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (categoryMaxSamples <= 1) {
|
|
||||||
// One sample exists, and its index is 0.
|
|
||||||
const analysis::Constant* zero = constantMgr->GetConstant(sampleType, {0u});
|
|
||||||
const Instruction* zeroInst =
|
|
||||||
zero != nullptr ? constantMgr->GetDefiningInstruction(zero, sampleTypeId)
|
|
||||||
: nullptr;
|
|
||||||
if (zeroInst == nullptr) {
|
|
||||||
return Status::Failure;
|
|
||||||
}
|
|
||||||
instruction.SetInOperand(sampleOperandIndex, {zeroInst->result_id()});
|
|
||||||
irContext->UpdateDefUse(&instruction);
|
|
||||||
clampedAnything = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// min(operand, K-1). Only the upper bound: an index already inside
|
|
||||||
// the allocation comes through untouched, which is what makes this
|
|
||||||
// safe to apply to a shader that was already correct.
|
|
||||||
//
|
|
||||||
// Everything from here on either completes or fails the module.
|
|
||||||
// Anything that gives up half way - after the import or the bound
|
|
||||||
// constant has been added - would leave a MUTATED module reported as
|
|
||||||
// SuccessWithoutChange, which spvtools::Optimizer asserts against
|
|
||||||
// (it re-serialises and compares byte for byte in that case).
|
|
||||||
const uint32_t glslStd450Id = EnsureGlslStd450Import(irContext);
|
|
||||||
if (glslStd450Id == 0u) {
|
|
||||||
// Id space exhausted, and the import was NOT added. Nothing has
|
|
||||||
// changed yet, but nothing further can be built either.
|
|
||||||
return Status::Failure;
|
|
||||||
}
|
|
||||||
const uint32_t resultId = irContext->TakeNextId();
|
|
||||||
const analysis::Constant* bound = constantMgr->GetConstant(
|
|
||||||
sampleType, {static_cast<uint32_t>(categoryMaxSamples - 1)});
|
|
||||||
const Instruction* boundInst =
|
|
||||||
bound != nullptr ? constantMgr->GetDefiningInstruction(bound, sampleTypeId)
|
|
||||||
: nullptr;
|
|
||||||
if (resultId == 0u || boundInst == nullptr) {
|
|
||||||
return Status::Failure;
|
|
||||||
}
|
|
||||||
|
|
||||||
InstructionBuilder builder(
|
|
||||||
irContext, &instruction,
|
|
||||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
|
||||||
Instruction* clamped = builder.AddInstruction(spvtools::MakeUnique<Instruction>(
|
|
||||||
irContext, spv::Op::OpExtInst, sampleTypeId, resultId,
|
|
||||||
Instruction::OperandList{
|
|
||||||
{SPV_OPERAND_TYPE_ID, {glslStd450Id}},
|
|
||||||
{SPV_OPERAND_TYPE_EXTENSION_INSTRUCTION_NUMBER,
|
|
||||||
{sampleInteger->IsSigned() ? kGlslSMin : kGlslUMin}},
|
|
||||||
{SPV_OPERAND_TYPE_ID, {sampleOperandId}},
|
|
||||||
{SPV_OPERAND_TYPE_ID, {boundInst->result_id()}}}));
|
|
||||||
if (clamped == nullptr) {
|
|
||||||
return Status::Failure;
|
|
||||||
}
|
|
||||||
instruction.SetInOperand(sampleOperandIndex, {clamped->result_id()});
|
|
||||||
irContext->UpdateDefUse(&instruction);
|
|
||||||
clampedAnything = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!clampedAnything) {
|
|
||||||
return Status::SuccessWithoutChange;
|
|
||||||
}
|
|
||||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
|
||||||
return Status::SuccessWithChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
spvtools::Optimizer::PassToken ClampMultisampleFetchPass::CreateClampMultisampleFetchPass(
|
|
||||||
const Int32 maxColorSamples, const Int32 maxIntegerSamples, const Int32 maxDepthSamples,
|
|
||||||
const Int32 advertisedMaxSamples) {
|
|
||||||
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<ClampMultisampleFetchPass>(
|
|
||||||
maxColorSamples, maxIntegerSamples, maxDepthSamples, advertisedMaxSamples));
|
|
||||||
}
|
|
||||||
} // namespace ShaderTranspiler
|
|
||||||
} // namespace MG_Util
|
|
||||||
} // namespace MobileGL
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.h
|
|
||||||
// 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
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "source/opt/pass.h"
|
|
||||||
#include "spirv-tools/optimizer.hpp"
|
|
||||||
|
|
||||||
#include <Includes.h>
|
|
||||||
|
|
||||||
namespace MobileGL {
|
|
||||||
namespace MG_Util {
|
|
||||||
namespace ShaderTranspiler {
|
|
||||||
// GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so MobileGL floors every
|
|
||||||
// multisample ceiling it advertises to 4 (GL_Getter's kFrontendMaxSamples) no matter
|
|
||||||
// what the ES driver reports. The realised allocation cannot be floored the same way -
|
|
||||||
// the driver would simply reject it - so DirectGLES clamps the count it passes to
|
|
||||||
// glTexStorage*Multisample down to what the format really supports
|
|
||||||
// (ClampSamplesToBackendSupport). On Adreno and on Mali's Immortalis-G925 that is ONE
|
|
||||||
// sample for every integer format, while the frontend keeps telling the application
|
|
||||||
// GL_MAX_INTEGER_SAMPLES is 4.
|
|
||||||
//
|
|
||||||
// A shader written against the advertised ceiling therefore fetches a sample the
|
|
||||||
// backing storage does not have. KHR-GL33/40/41.texture_swizzle.functional_* and
|
|
||||||
// KHR-GLxx.texture_size_promotion.functional bake `texelFetch(usampler2DMS, coord, 3)`
|
|
||||||
// in as a literal, and the fetch comes back as 0/garbage ("Found pixel with wrong
|
|
||||||
// value", "read value = 0") on a texture the backend quietly allocated with one
|
|
||||||
// sample.
|
|
||||||
//
|
|
||||||
// This pass closes that gap from the shader side: for every fetch of a multisampled
|
|
||||||
// image it clamps the Sample image-operand to the backend's REAL maximum for that
|
|
||||||
// image's category, so the lookup lands inside the allocation the backend made.
|
|
||||||
// - K >= advertisedMaxSamples: the category is not squeezed, nothing is rewritten.
|
|
||||||
// - K <= 1: the Sample operand becomes a constant 0 of its own type - the only
|
|
||||||
// sample that exists.
|
|
||||||
// - 1 < K < advertisedMaxSamples: the operand is wrapped in min(operand, K-1),
|
|
||||||
// which leaves an in-range index exactly as it was.
|
|
||||||
// Only the UPPER bound is clamped. A negative index is out of range in GL before this
|
|
||||||
// pass and after it alike, and MobileGL is not the component that should be inventing
|
|
||||||
// a value for it.
|
|
||||||
//
|
|
||||||
// Category comes from the OpTypeImage: an OpTypeInt sampled type is the integer
|
|
||||||
// class (GL_MAX_INTEGER_SAMPLES), a float one is depth when the image's Depth operand
|
|
||||||
// is exactly 1 and colour otherwise. That last clause is deliberate: glslang writes
|
|
||||||
// Depth 0 for a plain sampler and 2 ("unknown") wherever it cannot tell, and GLSL has
|
|
||||||
// no multisampled shadow sampler at all, so only an explicit 1 is treated as a depth
|
|
||||||
// image and everything else falls to the colour limit - which is the one a
|
|
||||||
// mis-classified image would want anyway.
|
|
||||||
//
|
|
||||||
// DirectGLES transpile path only. DirectVulkan allocates the sample count it was
|
|
||||||
// asked for and must see the module unchanged.
|
|
||||||
class ClampMultisampleFetchPass : public spvtools::opt::Pass {
|
|
||||||
public:
|
|
||||||
// The three backend-REAL per-category ceilings, plus the count the GL frontend
|
|
||||||
// advertises (GL_Getter's GetAdvertisedMaxSamples). A category whose real ceiling
|
|
||||||
// already reaches the advertised one is left completely alone.
|
|
||||||
ClampMultisampleFetchPass(Int32 maxColorSamples, Int32 maxIntegerSamples,
|
|
||||||
Int32 maxDepthSamples, Int32 advertisedMaxSamples)
|
|
||||||
: m_maxColorSamples(maxColorSamples),
|
|
||||||
m_maxIntegerSamples(maxIntegerSamples),
|
|
||||||
m_maxDepthSamples(maxDepthSamples),
|
|
||||||
m_advertisedMaxSamples(advertisedMaxSamples) {}
|
|
||||||
|
|
||||||
const char* name() const override { return "clamp-multisample-fetch"; }
|
|
||||||
Status Process() override;
|
|
||||||
|
|
||||||
// Whether the module declares any multisampled image type, i.e. whether running
|
|
||||||
// this pass could change anything. Answered from a single parse so the caller can
|
|
||||||
// skip the optimizer round trip entirely - which is every shader but the handful
|
|
||||||
// that read a multisample texture directly.
|
|
||||||
static bool DeclaresMultisampledImage(const Vector<Uint32>& binary);
|
|
||||||
|
|
||||||
// Same question answered from an already-built module, so one parse can feed
|
|
||||||
// several gates (ShaderCompiler::ProbeSpirvGateFeatures).
|
|
||||||
static bool DeclaresMultisampledImage(spvtools::opt::IRContext* context);
|
|
||||||
|
|
||||||
static spvtools::Optimizer::PassToken CreateClampMultisampleFetchPass(
|
|
||||||
Int32 maxColorSamples, Int32 maxIntegerSamples, Int32 maxDepthSamples,
|
|
||||||
Int32 advertisedMaxSamples);
|
|
||||||
|
|
||||||
private:
|
|
||||||
Int32 m_maxColorSamples;
|
|
||||||
Int32 m_maxIntegerSamples;
|
|
||||||
Int32 m_maxDepthSamples;
|
|
||||||
Int32 m_advertisedMaxSamples;
|
|
||||||
};
|
|
||||||
} // namespace ShaderTranspiler
|
|
||||||
} // namespace MG_Util
|
|
||||||
} // namespace MobileGL
|
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
#include "source/opt/ir_builder.h"
|
#include "source/opt/ir_builder.h"
|
||||||
#include "source/opt/ir_context.h"
|
#include "source/opt/ir_context.h"
|
||||||
#include "source/opt/module.h"
|
#include "source/opt/module.h"
|
||||||
|
#include "source/opt/type_manager.h"
|
||||||
|
#include <cmath>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
@@ -27,6 +29,7 @@ namespace MobileGL {
|
|||||||
|
|
||||||
analysis::ConstantManager* const_mgr = context()->get_constant_mgr();
|
analysis::ConstantManager* const_mgr = context()->get_constant_mgr();
|
||||||
analysis::DefUseManager* def_use_mgr = context()->get_def_use_mgr();
|
analysis::DefUseManager* def_use_mgr = context()->get_def_use_mgr();
|
||||||
|
analysis::TypeManager* type_mgr = context()->get_type_mgr();
|
||||||
|
|
||||||
// 2. Import `GLSL.std.450` extension ID (for abs() func)
|
// 2. Import `GLSL.std.450` extension ID (for abs() func)
|
||||||
uint32_t glsl_std_450_id = context()->get_feature_mgr()->GetExtInstImportId_GLSLstd450();
|
uint32_t glsl_std_450_id = context()->get_feature_mgr()->GetExtInstImportId_GLSLstd450();
|
||||||
@@ -67,10 +70,6 @@ namespace MobileGL {
|
|||||||
uint32_t op2_id = inst.GetSingleWordInOperand(1);
|
uint32_t op2_id = inst.GetSingleWordInOperand(1);
|
||||||
|
|
||||||
uint32_t var_id = 0;
|
uint32_t var_id = 0;
|
||||||
// The zero the source spelled, reused verbatim as the right-hand side
|
|
||||||
// of the rewritten compare - so nothing has to be synthesized for a
|
|
||||||
// width this pass would have to encode by hand.
|
|
||||||
uint32_t zero_id = 0;
|
|
||||||
|
|
||||||
// The constant's WIDTH decides which accessor may read it, and asking
|
// The constant's WIDTH decides which accessor may read it, and asking
|
||||||
// the wrong one does not fail - it answers.
|
// the wrong one does not fail - it answers.
|
||||||
@@ -79,8 +78,8 @@ namespace MobileGL {
|
|||||||
// bits. On a 64-bit constant words()[0] is the LOW half of the
|
// bits. On a 64-bit constant words()[0] is the LOW half of the
|
||||||
// mantissa, and that half is zero for every round double a shader
|
// mantissa, and that half is zero for every round double a shader
|
||||||
// actually spells: 1.0lf, 2.0lf, 0.5lf, 100.0lf. Each of those
|
// actually spells: 1.0lf, 2.0lf, 0.5lf, 100.0lf. Each of those
|
||||||
// therefore looked like 0.0 here, and `d != 1.0lf` was rewritten into a
|
// therefore looked like 0.0 here, and `d != 1.0lf` was rewritten into
|
||||||
// test of `d` against ZERO - which is TRUE for d == 1.0. That is the whole
|
// `abs(d) >= epsilon` - which is TRUE for d == 1.0. That is the whole
|
||||||
// of KHR-GL43.compute_shader.fp64-case2: twelve uniforms compared
|
// of KHR-GL43.compute_shader.fp64-case2: twelve uniforms compared
|
||||||
// against vector and matrix constructors were untouched (a composite
|
// against vector and matrix constructors were untouched (a composite
|
||||||
// is not a FloatConstant) and the one scalar comparison in the shader
|
// is not a FloatConstant) and the one scalar comparison in the shader
|
||||||
@@ -98,24 +97,17 @@ namespace MobileGL {
|
|||||||
const analysis::Float* floatType =
|
const analysis::Float* floatType =
|
||||||
floatConstant->type() != nullptr ? floatConstant->type()->AsFloat() : nullptr;
|
floatConstant->type() != nullptr ? floatConstant->type()->AsFloat() : nullptr;
|
||||||
if (floatType == nullptr) return false;
|
if (floatType == nullptr) return false;
|
||||||
// Exactly zero - a near-zero constant is not a zero constant.
|
|
||||||
// `x == 1e-5` asks a different question than `x == 0.0` and must
|
|
||||||
// keep its own right-hand side. -0.0 compares equal to 0.0 here,
|
|
||||||
// which is correct: `x == -0.0` and `x == 0.0` are the same
|
|
||||||
// predicate in IEEE, and abs() maps both zeroes onto +0.
|
|
||||||
switch (floatType->width()) {
|
switch (floatType->width()) {
|
||||||
case 32: return floatConstant->GetFloatValue() == 0.0f;
|
case 32: return std::fabs(floatConstant->GetFloatValue()) <= K_EPSILON;
|
||||||
case 64: return floatConstant->GetDoubleValue() == 0.0;
|
case 64: return std::fabs(floatConstant->GetDoubleValue()) <= K_EPSILON;
|
||||||
default: return false;
|
default: return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (is_float_zero(op2_id)) {
|
if (is_float_zero(op2_id)) {
|
||||||
var_id = op1_id; // x == 0.0
|
var_id = op1_id; // x == 0.0
|
||||||
zero_id = op2_id;
|
|
||||||
} else if (is_float_zero(op1_id)) {
|
} else if (is_float_zero(op1_id)) {
|
||||||
var_id = op2_id; // 0.0 == x
|
var_id = op2_id; // 0.0 == x
|
||||||
zero_id = op1_id;
|
|
||||||
} else {
|
} else {
|
||||||
++itInst;
|
++itInst;
|
||||||
continue;
|
continue;
|
||||||
@@ -128,7 +120,12 @@ namespace MobileGL {
|
|||||||
uint32_t float_type_id = def_use_mgr->GetDef(var_id)->type_id();
|
uint32_t float_type_id = def_use_mgr->GetDef(var_id)->type_id();
|
||||||
uint32_t bool_type_id = inst.type_id();
|
uint32_t bool_type_id = inst.type_id();
|
||||||
|
|
||||||
// 2. Build Abs(x) inst
|
// 2. Create constant ID for `Epsilon`
|
||||||
|
const analysis::Constant* eps_const = const_mgr->GetConstant(
|
||||||
|
type_mgr->GetType(float_type_id), {*(reinterpret_cast<const uint32_t*>(&K_EPSILON))});
|
||||||
|
uint32_t eps_id = const_mgr->GetDefiningInstruction(eps_const)->result_id();
|
||||||
|
|
||||||
|
// 3. Build Abs(x) inst
|
||||||
// OpExtInst %float_type %glsl_import Abs %x
|
// OpExtInst %float_type %glsl_import Abs %x
|
||||||
InstructionBuilder builder(
|
InstructionBuilder builder(
|
||||||
context(), &inst, IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
context(), &inst, IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||||
@@ -143,42 +140,38 @@ namespace MobileGL {
|
|||||||
Instruction* abs_inst = builder.AddInstruction(MakeUnique<Instruction>(
|
Instruction* abs_inst = builder.AddInstruction(MakeUnique<Instruction>(
|
||||||
context(), spv::Op::OpExtInst, float_type_id, context()->TakeNextId(), abs_operands));
|
context(), spv::Op::OpExtInst, float_type_id, context()->TakeNextId(), abs_operands));
|
||||||
|
|
||||||
// 3. build Abs(x) <= 0.0, or Abs(x) > 0.0 for the NotEqual forms
|
// 4. build Abs(x) < Epsilon
|
||||||
// OpFOrdLessThanEqual %bool_type %abs_val %zero
|
// OpFOrdLessThan %bool_type %abs_val %eps
|
||||||
std::vector<Operand> cmp_operands;
|
std::vector<Operand> less_operands;
|
||||||
cmp_operands.push_back({SPV_OPERAND_TYPE_ID, {abs_inst->result_id()}});
|
less_operands.push_back({SPV_OPERAND_TYPE_ID, {abs_inst->result_id()}});
|
||||||
cmp_operands.push_back({SPV_OPERAND_TYPE_ID, {zero_id}});
|
less_operands.push_back({SPV_OPERAND_TYPE_ID, {eps_id}});
|
||||||
|
|
||||||
// Equality is INCLUDED in the replacement, which is what makes the
|
|
||||||
// rewrite exact: |x| <= 0 is true for +0 and -0 and false for every
|
|
||||||
// other finite value, |x| > 0 is its complement. The ordered/unordered
|
|
||||||
// half of the opcode is preserved, so NaN keeps answering as it did.
|
|
||||||
spv::Op replacementOp = spv::Op::OpNop;
|
spv::Op replacementOp = spv::Op::OpNop;
|
||||||
switch (inst.opcode()) {
|
switch (inst.opcode()) {
|
||||||
case spv::Op::OpFOrdEqual:
|
case spv::Op::OpFOrdEqual:
|
||||||
replacementOp = spv::Op::OpFOrdLessThanEqual;
|
replacementOp = spv::Op::OpFOrdLessThan;
|
||||||
break;
|
break;
|
||||||
case spv::Op::OpFUnordEqual:
|
case spv::Op::OpFUnordEqual:
|
||||||
replacementOp = spv::Op::OpFUnordLessThanEqual;
|
replacementOp = spv::Op::OpFUnordLessThan;
|
||||||
break;
|
break;
|
||||||
case spv::Op::OpFOrdNotEqual:
|
case spv::Op::OpFOrdNotEqual:
|
||||||
replacementOp = spv::Op::OpFOrdGreaterThan;
|
replacementOp = spv::Op::OpFOrdGreaterThanEqual;
|
||||||
break;
|
break;
|
||||||
case spv::Op::OpFUnordNotEqual:
|
case spv::Op::OpFUnordNotEqual:
|
||||||
replacementOp = spv::Op::OpFUnordGreaterThan;
|
replacementOp = spv::Op::OpFUnordGreaterThanEqual;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
MOBILEGL_ASSERT(false, "Unexpected float compare opcode: %d",
|
MOBILEGL_ASSERT(false, "Unexpected float compare opcode: %d",
|
||||||
static_cast<int>(inst.opcode()));
|
static_cast<int>(inst.opcode()));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Instruction* cmp_inst = builder.AddInstruction(MakeUnique<Instruction>(
|
Instruction* less_than_inst = builder.AddInstruction(MakeUnique<Instruction>(
|
||||||
context(), replacementOp, bool_type_id, context()->TakeNextId(), cmp_operands));
|
context(), replacementOp, bool_type_id, context()->TakeNextId(), less_operands));
|
||||||
|
|
||||||
// 4. Replaces all uses of old insn with new one
|
// 5. Replaces all uses of old insn with new one
|
||||||
context()->ReplaceAllUsesWith(inst.result_id(), cmp_inst->result_id());
|
context()->ReplaceAllUsesWith(inst.result_id(), less_than_inst->result_id());
|
||||||
|
|
||||||
// 5. Kill old instruction (will be cleaned up by DCE later)
|
// 6. Kill old instruction (will be cleaned up by DCE later)
|
||||||
auto nextInstIt = context()->KillInst(&inst);
|
auto nextInstIt = context()->KillInst(&inst);
|
||||||
if (nextInstIt) {
|
if (nextInstIt) {
|
||||||
itInst = nextInstIt;
|
itInst = nextInstIt;
|
||||||
|
|||||||
@@ -15,28 +15,15 @@
|
|||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
namespace MG_Util {
|
namespace MG_Util {
|
||||||
namespace ShaderTranspiler {
|
namespace ShaderTranspiler {
|
||||||
// Keeps the driver's float-EQUALITY instruction out of the module: every scalar
|
|
||||||
// comparison against a constant 0.0 is re-spelled through GLSL.std.450 FAbs, so no
|
|
||||||
// OpFOrdEqual / OpFUnordEqual / OpFOrdNotEqual / OpFUnordNotEqual against zero ever
|
|
||||||
// reaches a shader compiler that gets exact float compare wrong.
|
|
||||||
//
|
|
||||||
// The rewrite is EXACT, not a tolerance. `x == 0.0` becomes `abs(x) <= 0.0` and
|
|
||||||
// `x != 0.0` becomes `abs(x) > 0.0`, both against the module's own zero constant:
|
|
||||||
// |x| <= 0 holds for +0 and -0 and for nothing else, so the two forms agree on every
|
|
||||||
// input, at any float width, with or without denormal flushing. The ordered/unordered
|
|
||||||
// half of the opcode is carried across unchanged, which is what keeps NaN answering
|
|
||||||
// the way it did before.
|
|
||||||
//
|
|
||||||
// It used to be an epsilon ball (abs(x) < 1e-4). That silently classified any
|
|
||||||
// legitimately small value as zero - KHR-GL3x.buffer_objects.triangles renders a
|
|
||||||
// specular term of ~6e-5 at a large render target and came out black - so the fuzz is
|
|
||||||
// gone; the reason the pass exists never needed it.
|
|
||||||
class EliminateFloatEqualsZeroPass : public spvtools::opt::Pass {
|
class EliminateFloatEqualsZeroPass : public spvtools::opt::Pass {
|
||||||
public:
|
public:
|
||||||
const char* name() const override { return "float-equals-zero-elimination"; }
|
const char* name() const override { return "float-equals-zero-elimination"; }
|
||||||
Status Process() override;
|
Status Process() override;
|
||||||
|
|
||||||
static spvtools::Optimizer::PassToken CreateEliminateFloatEqualsZeroPass();
|
static spvtools::Optimizer::PassToken CreateEliminateFloatEqualsZeroPass();
|
||||||
|
|
||||||
|
private:
|
||||||
|
const float K_EPSILON = 0.0001f;
|
||||||
};
|
};
|
||||||
} // namespace ShaderTranspiler
|
} // namespace ShaderTranspiler
|
||||||
} // namespace MG_Util
|
} // namespace MG_Util
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
|
|
||||||
// Copyright (c) 2026 MobileGL-Dev
|
|
||||||
// Licensed under the GNU Lesser General Public License v3.0:
|
|
||||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
||||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// End of Source File Header
|
|
||||||
|
|
||||||
#include "FixIterationRPBarrierPass.h"
|
|
||||||
|
|
||||||
#include "spirv.hpp"
|
|
||||||
#include "source/opt/constants.h"
|
|
||||||
#include "source/opt/def_use_manager.h"
|
|
||||||
#include "source/opt/instruction.h"
|
|
||||||
#include "source/opt/ir_context.h"
|
|
||||||
#include "source/opt/module.h"
|
|
||||||
#include "source/util/make_unique.h"
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace MobileGL::MG_Util::ShaderTranspiler {
|
|
||||||
namespace {
|
|
||||||
using spvtools::opt::Instruction;
|
|
||||||
using spvtools::opt::IRContext;
|
|
||||||
using spvtools::opt::Operand;
|
|
||||||
|
|
||||||
const Instruction* RootVariable(IRContext* context, uint32_t pointerId) {
|
|
||||||
const Instruction* def = context->get_def_use_mgr()->GetDef(pointerId);
|
|
||||||
while (def != nullptr) {
|
|
||||||
switch (def->opcode()) {
|
|
||||||
case spv::Op::OpVariable:
|
|
||||||
return def;
|
|
||||||
case spv::Op::OpAccessChain:
|
|
||||||
case spv::Op::OpInBoundsAccessChain:
|
|
||||||
case spv::Op::OpCopyObject:
|
|
||||||
def = context->get_def_use_mgr()->GetDef(def->GetSingleWordInOperand(0));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsUintConstant(IRContext* context, uint32_t id, uint32_t wanted) {
|
|
||||||
const Instruction* def = context->get_def_use_mgr()->GetDef(id);
|
|
||||||
return def != nullptr && def->opcode() == spv::Op::OpConstant && def->NumInOperands() == 1u &&
|
|
||||||
def->GetSingleWordInOperand(0) == wanted;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsZeroElementPointer(IRContext* context, uint32_t pointerId, const Instruction** root) {
|
|
||||||
const Instruction* pointer = context->get_def_use_mgr()->GetDef(pointerId);
|
|
||||||
if (pointer == nullptr ||
|
|
||||||
(pointer->opcode() != spv::Op::OpAccessChain && pointer->opcode() != spv::Op::OpInBoundsAccessChain) ||
|
|
||||||
pointer->NumInOperands() < 2u) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (uint32_t i = 1u; i < pointer->NumInOperands(); ++i) {
|
|
||||||
if (!IsUintConstant(context, pointer->GetSingleWordInOperand(i), 0u)) return false;
|
|
||||||
}
|
|
||||||
*root = RootVariable(context, pointerId);
|
|
||||||
return *root != nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsWorkgroupVec2Array(IRContext* context, const Instruction* variable) {
|
|
||||||
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable || variable->NumInOperands() < 1u ||
|
|
||||||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) != spv::StorageClass::Workgroup) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
auto* defUseMgr = context->get_def_use_mgr();
|
|
||||||
const Instruction* pointerType = defUseMgr->GetDef(variable->type_id());
|
|
||||||
if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer ||
|
|
||||||
pointerType->NumInOperands() < 2u) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const Instruction* arrayType = defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1));
|
|
||||||
if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray ||
|
|
||||||
arrayType->NumInOperands() < 2u) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const Instruction* length = defUseMgr->GetDef(arrayType->GetSingleWordInOperand(1));
|
|
||||||
if (length == nullptr || length->opcode() != spv::Op::OpConstant || length->NumInOperands() != 1u) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const uint32_t arrayLength = length->GetSingleWordInOperand(0);
|
|
||||||
if (arrayLength < 32u || arrayLength > 512u) return false;
|
|
||||||
|
|
||||||
const Instruction* vectorType = defUseMgr->GetDef(arrayType->GetSingleWordInOperand(0));
|
|
||||||
if (vectorType == nullptr || vectorType->opcode() != spv::Op::OpTypeVector ||
|
|
||||||
vectorType->NumInOperands() < 2u || vectorType->GetSingleWordInOperand(1) != 2u) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const Instruction* scalarType = defUseMgr->GetDef(vectorType->GetSingleWordInOperand(0));
|
|
||||||
return scalarType != nullptr && scalarType->opcode() == spv::Op::OpTypeFloat &&
|
|
||||||
scalarType->NumInOperands() == 1u && scalarType->GetSingleWordInOperand(0) == 32u;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsVec2FloatInclusiveAdd(IRContext* context, const Instruction* inst) {
|
|
||||||
if (inst->opcode() != spv::Op::OpGroupNonUniformFAdd || inst->NumInOperands() < 3u ||
|
|
||||||
static_cast<spv::GroupOperation>(inst->GetSingleWordInOperand(1)) !=
|
|
||||||
spv::GroupOperation::InclusiveScan) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const Instruction* vectorType = context->get_def_use_mgr()->GetDef(inst->type_id());
|
|
||||||
if (vectorType == nullptr || vectorType->opcode() != spv::Op::OpTypeVector ||
|
|
||||||
vectorType->NumInOperands() < 2u || vectorType->GetSingleWordInOperand(1) != 2u) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const Instruction* scalarType = context->get_def_use_mgr()->GetDef(vectorType->GetSingleWordInOperand(0));
|
|
||||||
return scalarType != nullptr && scalarType->opcode() == spv::Op::OpTypeFloat &&
|
|
||||||
scalarType->NumInOperands() == 1u && scalarType->GetSingleWordInOperand(0) == 32u;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HasProgram203LocalSize(IRContext* context) {
|
|
||||||
for (const Instruction& entryPoint : context->module()->entry_points()) {
|
|
||||||
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
|
|
||||||
spv::ExecutionModel::GLCompute) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const Instruction& mode : context->module()->execution_modes()) {
|
|
||||||
if (mode.opcode() == spv::Op::OpExecutionMode && mode.NumInOperands() >= 5u &&
|
|
||||||
static_cast<spv::ExecutionMode>(mode.GetSingleWordInOperand(1)) == spv::ExecutionMode::LocalSize) {
|
|
||||||
return mode.GetSingleWordInOperand(2) == 32u && mode.GetSingleWordInOperand(3) == 16u &&
|
|
||||||
mode.GetSingleWordInOperand(4) == 1u;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsStoreToRoot(IRContext* context, const Instruction* inst, const Instruction* root) {
|
|
||||||
return inst->opcode() == spv::Op::OpStore && inst->NumInOperands() >= 2u &&
|
|
||||||
RootVariable(context, inst->GetSingleWordInOperand(0)) == root;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
spvtools::opt::Pass::Status FixIterationRPBarrierPass::Process() {
|
|
||||||
auto* irContext = context();
|
|
||||||
if (!HasProgram203LocalSize(irContext)) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
for (auto& function : *irContext->module()) {
|
|
||||||
std::vector<Instruction*> instructions;
|
|
||||||
std::vector<size_t> scans;
|
|
||||||
for (auto& block : function) {
|
|
||||||
for (auto& inst : block) {
|
|
||||||
if (IsVec2FloatInclusiveAdd(irContext, &inst)) scans.push_back(instructions.size());
|
|
||||||
instructions.push_back(&inst);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Program 203 has exactly two vec2 inclusive adds: the luminance reduction
|
|
||||||
// and the weighted-exposure reduction. More or fewer is not our fingerprint.
|
|
||||||
if (scans.size() != 2u) continue;
|
|
||||||
|
|
||||||
const size_t firstScan = scans[0];
|
|
||||||
const size_t secondScan = scans[1];
|
|
||||||
const Instruction* scratch = nullptr;
|
|
||||||
size_t averageLoad = instructions.size();
|
|
||||||
|
|
||||||
for (size_t i = firstScan + 1u; i < secondScan; ++i) {
|
|
||||||
Instruction* inst = instructions[i];
|
|
||||||
if (inst->opcode() != spv::Op::OpLoad || inst->NumInOperands() < 1u) continue;
|
|
||||||
const Instruction* root = nullptr;
|
|
||||||
if (!IsZeroElementPointer(irContext, inst->GetSingleWordInOperand(0), &root) ||
|
|
||||||
!IsWorkgroupVec2Array(irContext, root)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// The broadcast is read as prefixSumCache[0].x, hence a scalar load.
|
|
||||||
const Instruction* type = irContext->get_def_use_mgr()->GetDef(inst->type_id());
|
|
||||||
if (type == nullptr || type->opcode() != spv::Op::OpTypeFloat || type->NumInOperands() != 1u ||
|
|
||||||
type->GetSingleWordInOperand(0) != 32u) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
scratch = root;
|
|
||||||
averageLoad = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (scratch == nullptr) continue;
|
|
||||||
|
|
||||||
bool sawZeroBroadcastStore = false;
|
|
||||||
bool sawPublishBarrier = false;
|
|
||||||
for (size_t i = firstScan + 1u; i < averageLoad; ++i) {
|
|
||||||
const Instruction* root = nullptr;
|
|
||||||
if (instructions[i]->opcode() == spv::Op::OpStore &&
|
|
||||||
IsZeroElementPointer(irContext, instructions[i]->GetSingleWordInOperand(0), &root) &&
|
|
||||||
root == scratch) {
|
|
||||||
sawZeroBroadcastStore = true;
|
|
||||||
} else if (sawZeroBroadcastStore && instructions[i]->opcode() == spv::Op::OpControlBarrier) {
|
|
||||||
sawPublishBarrier = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!sawZeroBroadcastStore || !sawPublishBarrier) continue;
|
|
||||||
|
|
||||||
bool alreadySynchronized = false;
|
|
||||||
for (size_t i = averageLoad + 1u; i < secondScan; ++i) {
|
|
||||||
if (instructions[i]->opcode() == spv::Op::OpControlBarrier) {
|
|
||||||
alreadySynchronized = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (alreadySynchronized) return Status::SuccessWithoutChange;
|
|
||||||
|
|
||||||
bool secondPhaseReusesScratch = false;
|
|
||||||
for (size_t i = secondScan + 1u; i < instructions.size(); ++i) {
|
|
||||||
if (IsStoreToRoot(irContext, instructions[i], scratch)) {
|
|
||||||
secondPhaseReusesScratch = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!secondPhaseReusesScratch) continue;
|
|
||||||
|
|
||||||
auto* constantMgr = irContext->get_constant_mgr();
|
|
||||||
const uint32_t scopeId = constantMgr->GetUIntConstId(static_cast<uint32_t>(spv::Scope::Workgroup));
|
|
||||||
const uint32_t semanticsId =
|
|
||||||
constantMgr->GetUIntConstId(static_cast<uint32_t>(spv::MemorySemanticsMask::AcquireRelease) |
|
|
||||||
static_cast<uint32_t>(spv::MemorySemanticsMask::WorkgroupMemory));
|
|
||||||
if (scopeId == 0u || semanticsId == 0u) return Status::Failure;
|
|
||||||
|
|
||||||
instructions[secondScan]->InsertBefore(spvtools::MakeUnique<Instruction>(
|
|
||||||
irContext, spv::Op::OpControlBarrier, 0u, 0u,
|
|
||||||
Instruction::OperandList{Operand{SPV_OPERAND_TYPE_ID, {scopeId}},
|
|
||||||
Operand{SPV_OPERAND_TYPE_ID, {scopeId}},
|
|
||||||
Operand{SPV_OPERAND_TYPE_ID, {semanticsId}}}));
|
|
||||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
|
||||||
return Status::SuccessWithChange;
|
|
||||||
}
|
|
||||||
return Status::SuccessWithoutChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
spvtools::Optimizer::PassToken FixIterationRPBarrierPass::CreateFixIterationRPBarrierPass() {
|
|
||||||
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<FixIterationRPBarrierPass>());
|
|
||||||
}
|
|
||||||
} // namespace MobileGL::MG_Util::ShaderTranspiler
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.h
|
|
||||||
// Copyright (c) 2026 MobileGL-Dev
|
|
||||||
// Licensed under the GNU Lesser General Public License v3.0:
|
|
||||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
||||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// End of Source File Header
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "source/opt/pass.h"
|
|
||||||
#include "spirv-tools/optimizer.hpp"
|
|
||||||
|
|
||||||
namespace MobileGL::MG_Util::ShaderTranspiler {
|
|
||||||
// Repairs iterationRP Program 203's missing workgroup rendezvous between two
|
|
||||||
// reductions that reuse prefixSumCache. The first phase broadcasts its result
|
|
||||||
// through prefixSumCache[0], but the second phase may overwrite that element before
|
|
||||||
// every invocation has read it. The pass fingerprints that exact two-scan,
|
|
||||||
// 512-invocation shape and inserts one Workgroup control barrier immediately before
|
|
||||||
// the second scan. Unrelated modules and already-repaired modules are byte-identical.
|
|
||||||
class FixIterationRPBarrierPass : public spvtools::opt::Pass {
|
|
||||||
public:
|
|
||||||
const char* name() const override { return "fix-iterationrp-barrier"; }
|
|
||||||
Status Process() override;
|
|
||||||
|
|
||||||
static spvtools::Optimizer::PassToken CreateFixIterationRPBarrierPass();
|
|
||||||
};
|
|
||||||
} // namespace MobileGL::MG_Util::ShaderTranspiler
|
|
||||||
+88
-294
@@ -26,13 +26,13 @@ namespace MobileGL {
|
|||||||
using spvtools::opt::Instruction;
|
using spvtools::opt::Instruction;
|
||||||
using spvtools::opt::IRContext;
|
using spvtools::opt::IRContext;
|
||||||
|
|
||||||
// The Vulkan minimum for maxComputeSharedMemorySize, used when the caller
|
// iterationRP's reduction fingerprint, spelled out.
|
||||||
// could not tell us the device's real limit.
|
constexpr uint32_t kIterationRPLocalSizeX = 32u;
|
||||||
constexpr uint32_t kMinimumSharedMemoryBytes = 16384u;
|
constexpr uint32_t kIterationRPLocalSizeY = 16u;
|
||||||
|
constexpr uint32_t kIterationRPLocalSizeZ = 1u;
|
||||||
// The narrowest subgroup width iterationRP's declarations are sized for.
|
constexpr uint32_t kIterationRPInvocations =
|
||||||
// At or above it both shipped shapes fit and nothing may be rewritten.
|
kIterationRPLocalSizeX * kIterationRPLocalSizeY * kIterationRPLocalSizeZ;
|
||||||
constexpr uint32_t kPackAssumedSubgroupWidth = 16u;
|
constexpr uint32_t kIterationRPScratchLength = 32u;
|
||||||
|
|
||||||
Instruction* FindBuiltinDefinition(IRContext* context, spv::BuiltIn builtin) {
|
Instruction* FindBuiltinDefinition(IRContext* context, spv::BuiltIn builtin) {
|
||||||
auto* defUseMgr = context->get_def_use_mgr();
|
auto* defUseMgr = context->get_def_use_mgr();
|
||||||
@@ -73,121 +73,18 @@ namespace MobileGL {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A 32-bit float scalar or vector - the shape of every accumulator the
|
// vec2 of 32-bit float - the type of iterationRP's luminance/exposure
|
||||||
// pack runs through its scans (float, vec2 and vec4 all appear). Returns
|
// accumulator and of its prefixSumCache entries.
|
||||||
// the component count, or 0 for anything else.
|
bool IsVec2Float32(IRContext* context, uint32_t typeId) {
|
||||||
uint32_t Float32ComponentCount(IRContext* context, uint32_t typeId) {
|
const Instruction* type = context->get_def_use_mgr()->GetDef(typeId);
|
||||||
auto* defUseMgr = context->get_def_use_mgr();
|
if (type == nullptr || type->opcode() != spv::Op::OpTypeVector ||
|
||||||
const Instruction* type = defUseMgr->GetDef(typeId);
|
type->GetSingleWordInOperand(1) != 2u) {
|
||||||
if (type == nullptr) return 0u;
|
|
||||||
uint32_t components = 1u;
|
|
||||||
if (type->opcode() == spv::Op::OpTypeVector) {
|
|
||||||
components = type->GetSingleWordInOperand(1);
|
|
||||||
if (components < 2u || components > 4u) return 0u;
|
|
||||||
type = defUseMgr->GetDef(type->GetSingleWordInOperand(0));
|
|
||||||
if (type == nullptr) return 0u;
|
|
||||||
}
|
|
||||||
if (type->opcode() != spv::Op::OpTypeFloat ||
|
|
||||||
type->GetSingleWordInOperand(0) != 32u) {
|
|
||||||
return 0u;
|
|
||||||
}
|
|
||||||
return components;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t RoundUp(uint32_t value, uint32_t alignment) {
|
|
||||||
return alignment == 0u ? value : ((value + alignment - 1u) / alignment) * alignment;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size AND alignment of a workgroup-storage type. Drivers lay shared
|
|
||||||
// memory out at natural alignment and the limit
|
|
||||||
// (VUID-RuntimeSpirv-Workgroup-06530) counts the padding that produces,
|
|
||||||
// so a model that sums unpadded sizes would under-count exactly where the
|
|
||||||
// budget check matters. Returns false for anything not modelled here,
|
|
||||||
// which the caller answers by declining to grow at all rather than by
|
|
||||||
// certifying growth against a total it knows is an underestimate.
|
|
||||||
bool WorkgroupTypeLayout(IRContext* context, uint32_t typeId, uint32_t* size,
|
|
||||||
uint32_t* alignment, uint32_t depth = 0u) {
|
|
||||||
if (depth > 8u) return false;
|
|
||||||
auto* defUseMgr = context->get_def_use_mgr();
|
|
||||||
const Instruction* type = defUseMgr->GetDef(typeId);
|
|
||||||
if (type == nullptr) return false;
|
|
||||||
switch (type->opcode()) {
|
|
||||||
case spv::Op::OpTypeBool:
|
|
||||||
*size = 4u;
|
|
||||||
*alignment = 4u;
|
|
||||||
return true;
|
|
||||||
case spv::Op::OpTypeInt:
|
|
||||||
case spv::Op::OpTypeFloat: {
|
|
||||||
const uint32_t width = type->GetSingleWordInOperand(0) / 8u;
|
|
||||||
if (width == 0u) return false;
|
|
||||||
*size = width;
|
|
||||||
*alignment = width;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case spv::Op::OpTypeVector: {
|
|
||||||
uint32_t componentSize = 0u;
|
|
||||||
uint32_t componentAlignment = 0u;
|
|
||||||
if (!WorkgroupTypeLayout(context, type->GetSingleWordInOperand(0),
|
|
||||||
&componentSize, &componentAlignment, depth + 1u)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const uint32_t components = type->GetSingleWordInOperand(1);
|
|
||||||
if (components < 2u || components > 4u) return false;
|
|
||||||
*size = componentSize * components;
|
|
||||||
// A three-component vector aligns like a four-component one.
|
|
||||||
*alignment = componentSize * (components == 3u ? 4u : components);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case spv::Op::OpTypeMatrix:
|
|
||||||
case spv::Op::OpTypeArray: {
|
|
||||||
uint32_t elementSize = 0u;
|
|
||||||
uint32_t elementAlignment = 0u;
|
|
||||||
if (!WorkgroupTypeLayout(context, type->GetSingleWordInOperand(0), &elementSize,
|
|
||||||
&elementAlignment, depth + 1u)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
uint32_t count = 0u;
|
|
||||||
if (type->opcode() == spv::Op::OpTypeMatrix) {
|
|
||||||
count = type->GetSingleWordInOperand(1);
|
|
||||||
} else {
|
|
||||||
const Instruction* length =
|
|
||||||
defUseMgr->GetDef(type->GetSingleWordInOperand(1));
|
|
||||||
if (length == nullptr || length->opcode() != spv::Op::OpConstant) {
|
|
||||||
return false; // spec-constant length: not sizeable here
|
|
||||||
}
|
|
||||||
count = length->GetSingleWordInOperand(0);
|
|
||||||
}
|
|
||||||
*size = RoundUp(elementSize, elementAlignment) * count;
|
|
||||||
*alignment = elementAlignment;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case spv::Op::OpTypeStruct: {
|
|
||||||
uint32_t offset = 0u;
|
|
||||||
uint32_t structAlignment = 1u;
|
|
||||||
for (uint32_t i = 0; i < type->NumInOperands(); ++i) {
|
|
||||||
uint32_t memberSize = 0u;
|
|
||||||
uint32_t memberAlignment = 0u;
|
|
||||||
if (!WorkgroupTypeLayout(context, type->GetSingleWordInOperand(i),
|
|
||||||
&memberSize, &memberAlignment, depth + 1u)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
offset = RoundUp(offset, memberAlignment) + memberSize;
|
|
||||||
if (memberAlignment > structAlignment) structAlignment = memberAlignment;
|
|
||||||
}
|
|
||||||
*size = RoundUp(offset, structAlignment);
|
|
||||||
*alignment = structAlignment;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
const Instruction* component =
|
||||||
|
context->get_def_use_mgr()->GetDef(type->GetSingleWordInOperand(0));
|
||||||
// The group operations the pack's prefix scans use.
|
return component != nullptr && component->opcode() == spv::Op::OpTypeFloat &&
|
||||||
bool IsScanOrReduce(spv::GroupOperation operation) {
|
component->GetSingleWordInOperand(0) == 32u;
|
||||||
return operation == spv::GroupOperation::Reduce ||
|
|
||||||
operation == spv::GroupOperation::InclusiveScan ||
|
|
||||||
operation == spv::GroupOperation::ExclusiveScan;
|
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -195,15 +92,13 @@ namespace MobileGL {
|
|||||||
auto* irContext = context();
|
auto* irContext = context();
|
||||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||||
|
|
||||||
// Without a known device width there is no topology to compare against;
|
// A device whose native width already satisfies the pack's assumption
|
||||||
// and a width the pack already assumed needs no patch at all. Both of
|
// (>= 16 lanes -> at most 32 subgroups) needs no patch at all.
|
||||||
// iterationRP's shapes are sized for >= 16 lanes (512/16 = 32 entries,
|
if (m_nativeSubgroupSize == 0u || m_nativeSubgroupSize >= 16u) {
|
||||||
// 1024/16 = 64), so every module on such a device - the pack's or anyone
|
|
||||||
// else's - must pass through byte-identical. The per-array length test
|
|
||||||
// further down is the second gate, not a replacement for this one.
|
|
||||||
if (m_nativeSubgroupSize == 0u || m_nativeSubgroupSize >= kPackAssumedSubgroupWidth) {
|
|
||||||
return Status::SuccessWithoutChange;
|
return Status::SuccessWithoutChange;
|
||||||
}
|
}
|
||||||
|
const uint32_t requiredLength =
|
||||||
|
(kIterationRPInvocations + m_nativeSubgroupSize - 1u) / m_nativeSubgroupSize;
|
||||||
|
|
||||||
for (const Instruction& entryPoint : irContext->module()->entry_points()) {
|
for (const Instruction& entryPoint : irContext->module()->entry_points()) {
|
||||||
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
|
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) !=
|
||||||
@@ -212,8 +107,7 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fingerprint 1: a literal workgroup size, so the subgroup count the
|
// Fingerprint 1: the pack's exposure-pass workgroup shape, 32x16x1.
|
||||||
// dispatch actually partitions into is known here.
|
|
||||||
const auto resolveUintConstant = [&](uint32_t id, uint32_t* value) {
|
const auto resolveUintConstant = [&](uint32_t id, uint32_t* value) {
|
||||||
const Instruction* def = defUseMgr->GetDef(id);
|
const Instruction* def = defUseMgr->GetDef(id);
|
||||||
if (def == nullptr || def->opcode() != spv::Op::OpConstant) return false;
|
if (def == nullptr || def->opcode() != spv::Op::OpConstant) return false;
|
||||||
@@ -245,40 +139,26 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!haveLocalSize || localSize[0] == 0u || localSize[1] == 0u || localSize[2] == 0u) {
|
if (!haveLocalSize || localSize[0] != kIterationRPLocalSizeX ||
|
||||||
|
localSize[1] != kIterationRPLocalSizeY || localSize[2] != kIterationRPLocalSizeZ) {
|
||||||
return Status::SuccessWithoutChange;
|
return Status::SuccessWithoutChange;
|
||||||
}
|
}
|
||||||
const uint64_t totalInvocations =
|
|
||||||
static_cast<uint64_t>(localSize[0]) * localSize[1] * localSize[2];
|
|
||||||
if (totalInvocations == 0u || totalInvocations > (1u << 20)) {
|
|
||||||
return Status::SuccessWithoutChange;
|
|
||||||
}
|
|
||||||
const uint32_t requiredLength = static_cast<uint32_t>(
|
|
||||||
(totalInvocations + m_nativeSubgroupSize - 1u) / m_nativeSubgroupSize);
|
|
||||||
|
|
||||||
// Fingerprint 2: a subgroup scan over a 32-bit float value - the pack's
|
// Fingerprint 2: the reduction's subgroupInclusiveAdd on a vec2.
|
||||||
// prefix-sum reduction, and the reason its scratch is indexed per subgroup.
|
bool sawVec2InclusiveAdd = false;
|
||||||
bool sawFloatSubgroupScan = false;
|
|
||||||
for (auto& function : *irContext->module()) {
|
for (auto& function : *irContext->module()) {
|
||||||
for (auto& block : function) {
|
for (auto& block : function) {
|
||||||
for (auto& inst : block) {
|
for (auto& inst : block) {
|
||||||
if (inst.opcode() != spv::Op::OpGroupNonUniformFAdd &&
|
if (inst.opcode() == spv::Op::OpGroupNonUniformFAdd &&
|
||||||
inst.opcode() != spv::Op::OpGroupNonUniformFMin &&
|
static_cast<spv::GroupOperation>(inst.GetSingleWordInOperand(1)) ==
|
||||||
inst.opcode() != spv::Op::OpGroupNonUniformFMax) {
|
spv::GroupOperation::InclusiveScan &&
|
||||||
continue;
|
IsVec2Float32(irContext, inst.type_id())) {
|
||||||
}
|
sawVec2InclusiveAdd = true;
|
||||||
if (inst.NumInOperands() < 2) continue;
|
|
||||||
if (!IsScanOrReduce(static_cast<spv::GroupOperation>(
|
|
||||||
inst.GetSingleWordInOperand(1)))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (Float32ComponentCount(irContext, inst.type_id()) != 0u) {
|
|
||||||
sawFloatSubgroupScan = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!sawFloatSubgroupScan) {
|
if (!sawVec2InclusiveAdd) {
|
||||||
return Status::SuccessWithoutChange;
|
return Status::SuccessWithoutChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,89 +171,60 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
const uint32_t subgroupIdVariableId = subgroupIdVariable->result_id();
|
const uint32_t subgroupIdVariableId = subgroupIdVariable->result_id();
|
||||||
|
|
||||||
// The pack indexes its scratch with gl_SubgroupID ITSELF, so only values
|
// Conservative taint walk over values, and through Function/Private
|
||||||
// that ARE that id qualify - not everything computed from it. An index
|
// temporaries by variable (glslang routinely spills builtin loads into
|
||||||
// that is masked or clamped (cache[gl_SubgroupID & 3u]) is bounded by
|
// locals before they reach an index expression). Over-tainting is safe:
|
||||||
// construction and is none of this pass's business; accepting it would
|
// the candidate filter below still demands the exact vec2[32] shape.
|
||||||
// turn a targeted repair into a general array resizer. Identity survives
|
std::unordered_map<uint32_t, bool> valueTainted; // result id -> tainted
|
||||||
// OpCopyObject, a signedness OpBitcast, and the Function/Private spill
|
std::unordered_map<uint32_t, bool> variableTainted; // variable id -> tainted
|
||||||
// glslang emits for a builtin load - and nothing else. A spill variable
|
bool changedTaint = true;
|
||||||
// counts only when EVERY store into it is the id.
|
while (changedTaint) {
|
||||||
std::unordered_map<uint32_t, bool> subgroupIdValues; // result id IS the id
|
changedTaint = false;
|
||||||
std::unordered_map<uint32_t, bool> subgroupIdVariables; // spill holding only it
|
|
||||||
bool changedIdentity = true;
|
|
||||||
while (changedIdentity) {
|
|
||||||
changedIdentity = false;
|
|
||||||
|
|
||||||
std::unordered_map<uint32_t, uint32_t> totalStores;
|
|
||||||
std::unordered_map<uint32_t, uint32_t> idStores;
|
|
||||||
for (auto& function : *irContext->module()) {
|
for (auto& function : *irContext->module()) {
|
||||||
for (auto& block : function) {
|
for (auto& block : function) {
|
||||||
for (auto& inst : block) {
|
for (auto& inst : block) {
|
||||||
if (inst.opcode() != spv::Op::OpStore) continue;
|
const spv::Op opcode = inst.opcode();
|
||||||
const uint32_t pointerId = inst.GetSingleWordInOperand(0);
|
if (opcode == spv::Op::OpStore) {
|
||||||
const Instruction* target = defUseMgr->GetDef(pointerId);
|
if (!valueTainted.count(inst.GetSingleWordInOperand(1))) continue;
|
||||||
if (target == nullptr || target->opcode() != spv::Op::OpVariable) {
|
const Instruction* root =
|
||||||
|
RootVariable(irContext, inst.GetSingleWordInOperand(0));
|
||||||
|
if (root == nullptr) continue;
|
||||||
|
if (!variableTainted.count(root->result_id())) {
|
||||||
|
variableTainted[root->result_id()] = true;
|
||||||
|
changedTaint = true;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const auto storageClass = static_cast<spv::StorageClass>(
|
if (inst.result_id() == 0 || valueTainted.count(inst.result_id())) {
|
||||||
target->GetSingleWordInOperand(0));
|
|
||||||
if (storageClass != spv::StorageClass::Function &&
|
|
||||||
storageClass != spv::StorageClass::Private) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
totalStores[pointerId] += 1u;
|
bool tainted = false;
|
||||||
if (subgroupIdValues.count(inst.GetSingleWordInOperand(1))) {
|
if (opcode == spv::Op::OpLoad) {
|
||||||
idStores[pointerId] += 1u;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const auto& entry : totalStores) {
|
|
||||||
if (entry.second != 0u && idStores[entry.first] == entry.second &&
|
|
||||||
!subgroupIdVariables.count(entry.first)) {
|
|
||||||
subgroupIdVariables[entry.first] = true;
|
|
||||||
changedIdentity = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto& function : *irContext->module()) {
|
|
||||||
for (auto& block : function) {
|
|
||||||
for (auto& inst : block) {
|
|
||||||
if (inst.result_id() == 0 ||
|
|
||||||
subgroupIdValues.count(inst.result_id())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
bool isSubgroupId = false;
|
|
||||||
switch (inst.opcode()) {
|
|
||||||
case spv::Op::OpLoad: {
|
|
||||||
const uint32_t pointerId = inst.GetSingleWordInOperand(0);
|
const uint32_t pointerId = inst.GetSingleWordInOperand(0);
|
||||||
isSubgroupId = pointerId == subgroupIdVariableId ||
|
if (pointerId == subgroupIdVariableId) tainted = true;
|
||||||
subgroupIdVariables.count(pointerId) != 0u;
|
const Instruction* root = RootVariable(irContext, pointerId);
|
||||||
break;
|
if (root != nullptr && variableTainted.count(root->result_id())) {
|
||||||
|
tainted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
inst.ForEachInId([&](const uint32_t* operandId) {
|
||||||
|
if (valueTainted.count(*operandId)) tainted = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
case spv::Op::OpCopyObject:
|
if (tainted) {
|
||||||
case spv::Op::OpBitcast:
|
valueTainted[inst.result_id()] = true;
|
||||||
isSubgroupId =
|
changedTaint = true;
|
||||||
subgroupIdValues.count(inst.GetSingleWordInOperand(0)) != 0u;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (isSubgroupId) {
|
|
||||||
subgroupIdValues[inst.result_id()] = true;
|
|
||||||
changedIdentity = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (subgroupIdValues.empty()) {
|
if (valueTainted.empty()) {
|
||||||
return Status::SuccessWithoutChange;
|
return Status::SuccessWithoutChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fingerprint 3: workgroup-shared float arrays indexed by gl_SubgroupID
|
// Fingerprint 3: workgroup-shared vec2[32] arrays whose access-chain
|
||||||
// itself - the under-declared prefixSumCache.
|
// index depends on gl_SubgroupID - the under-declared prefixSumCache.
|
||||||
std::map<uint32_t, Instruction*> candidates;
|
std::map<uint32_t, Instruction*> candidates;
|
||||||
for (auto& function : *irContext->module()) {
|
for (auto& function : *irContext->module()) {
|
||||||
for (auto& block : function) {
|
for (auto& block : function) {
|
||||||
@@ -383,7 +234,7 @@ namespace MobileGL {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (inst.NumInOperands() < 2) continue;
|
if (inst.NumInOperands() < 2) continue;
|
||||||
if (!subgroupIdValues.count(inst.GetSingleWordInOperand(1))) continue;
|
if (!valueTainted.count(inst.GetSingleWordInOperand(1))) continue;
|
||||||
Instruction* baseVariable =
|
Instruction* baseVariable =
|
||||||
defUseMgr->GetDef(inst.GetSingleWordInOperand(0));
|
defUseMgr->GetDef(inst.GetSingleWordInOperand(0));
|
||||||
if (baseVariable == nullptr ||
|
if (baseVariable == nullptr ||
|
||||||
@@ -401,16 +252,7 @@ namespace MobileGL {
|
|||||||
return Status::SuccessWithoutChange;
|
return Status::SuccessWithoutChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything that survives the filter, with the bytes each grown array
|
bool changedModule = false;
|
||||||
// will need. Nothing is mutated until the whole set fits the device's
|
|
||||||
// shared-memory budget, so a module is never left half-grown.
|
|
||||||
struct Growth {
|
|
||||||
Instruction* variable = nullptr;
|
|
||||||
uint32_t elementTypeId = 0;
|
|
||||||
uint32_t lengthTypeId = 0;
|
|
||||||
uint32_t addedBytes = 0;
|
|
||||||
};
|
|
||||||
std::vector<Growth> growths;
|
|
||||||
for (auto& entry : candidates) {
|
for (auto& entry : candidates) {
|
||||||
Instruction* variable = entry.second;
|
Instruction* variable = entry.second;
|
||||||
|
|
||||||
@@ -448,70 +290,18 @@ namespace MobileGL {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const uint32_t elementTypeId = arrayType->GetSingleWordInOperand(0);
|
const uint32_t elementTypeId = arrayType->GetSingleWordInOperand(0);
|
||||||
const uint32_t components = Float32ComponentCount(irContext, elementTypeId);
|
if (!IsVec2Float32(irContext, elementTypeId)) continue;
|
||||||
if (components == 0u) continue;
|
|
||||||
const Instruction* lengthConstant =
|
const Instruction* lengthConstant =
|
||||||
defUseMgr->GetDef(arrayType->GetSingleWordInOperand(1));
|
defUseMgr->GetDef(arrayType->GetSingleWordInOperand(1));
|
||||||
if (lengthConstant == nullptr || lengthConstant->opcode() != spv::Op::OpConstant) {
|
uint32_t currentLength = 0;
|
||||||
|
if (lengthConstant == nullptr ||
|
||||||
|
lengthConstant->opcode() != spv::Op::OpConstant ||
|
||||||
|
!((currentLength = lengthConstant->GetSingleWordInOperand(0),
|
||||||
|
currentLength == kIterationRPScratchLength))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const uint32_t currentLength = lengthConstant->GetSingleWordInOperand(0);
|
|
||||||
|
|
||||||
// The pack's own assumption holds on this device: the declared array
|
|
||||||
// already covers every subgroup the workgroup partitions into. That is
|
|
||||||
// every >= 16-lane device for the shapes iterationRP ships, and those
|
|
||||||
// modules must pass through byte-identical.
|
|
||||||
if (currentLength >= requiredLength) continue;
|
if (currentLength >= requiredLength) continue;
|
||||||
|
|
||||||
// vec3 strides at its 16-byte alignment, so charge the padded stride.
|
|
||||||
const uint32_t elementStride = (components == 3u ? 4u : components) * 4u;
|
|
||||||
growths.push_back(Growth{variable, elementTypeId, lengthConstant->type_id(),
|
|
||||||
(requiredLength - currentLength) * elementStride});
|
|
||||||
}
|
|
||||||
if (growths.empty()) {
|
|
||||||
return Status::SuccessWithoutChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Growing must not push the module past what the device can launch: a
|
|
||||||
// pipeline that fails to create is worse than the pack's own overrun.
|
|
||||||
{
|
|
||||||
uint64_t declaredBytes = 0;
|
|
||||||
bool sawUnsizeable = false;
|
|
||||||
for (auto& global : irContext->module()->types_values()) {
|
|
||||||
if (global.opcode() != spv::Op::OpVariable ||
|
|
||||||
static_cast<spv::StorageClass>(global.GetSingleWordInOperand(0)) !=
|
|
||||||
spv::StorageClass::Workgroup) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const Instruction* pointerType = defUseMgr->GetDef(global.type_id());
|
|
||||||
uint32_t bytes = 0u;
|
|
||||||
uint32_t alignment = 0u;
|
|
||||||
if (pointerType == nullptr ||
|
|
||||||
pointerType->opcode() != spv::Op::OpTypePointer ||
|
|
||||||
!WorkgroupTypeLayout(irContext, pointerType->GetSingleWordInOperand(1),
|
|
||||||
&bytes, &alignment)) {
|
|
||||||
sawUnsizeable = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
declaredBytes = RoundUp(static_cast<uint32_t>(declaredBytes), alignment) + bytes;
|
|
||||||
}
|
|
||||||
// A declaration this pass cannot size leaves the total an
|
|
||||||
// underestimate, so the growth cannot be certified against the device
|
|
||||||
// limit at all - decline rather than guess.
|
|
||||||
if (sawUnsizeable) {
|
|
||||||
return Status::SuccessWithoutChange;
|
|
||||||
}
|
|
||||||
for (const Growth& growth : growths) declaredBytes += growth.addedBytes;
|
|
||||||
|
|
||||||
const uint32_t deviceBudget = m_maxWorkgroupScratchBytes != 0u
|
|
||||||
? m_maxWorkgroupScratchBytes
|
|
||||||
: kMinimumSharedMemoryBytes;
|
|
||||||
if (declaredBytes > deviceBudget) {
|
|
||||||
return Status::SuccessWithoutChange;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const Growth& growth : growths) {
|
|
||||||
// Build the grown array type. All three new instructions are inserted
|
// Build the grown array type. All three new instructions are inserted
|
||||||
// immediately BEFORE the variable so definition-before-use holds in the
|
// immediately BEFORE the variable so definition-before-use holds in the
|
||||||
// module's global section (manager-created instructions append to its
|
// module's global section (manager-created instructions append to its
|
||||||
@@ -520,17 +310,17 @@ namespace MobileGL {
|
|||||||
// scalar constant is legal SPIR-V); the fresh array type makes the
|
// scalar constant is legal SPIR-V); the fresh array type makes the
|
||||||
// pointer type unique by construction, so neither collides with an
|
// pointer type unique by construction, so neither collides with an
|
||||||
// existing declaration.
|
// existing declaration.
|
||||||
Instruction* variable = growth.variable;
|
const uint32_t lengthTypeId = lengthConstant->type_id();
|
||||||
const uint32_t newLengthId = irContext->TakeNextId();
|
const uint32_t newLengthId = irContext->TakeNextId();
|
||||||
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
|
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
irContext, spv::Op::OpConstant, growth.lengthTypeId, newLengthId,
|
irContext, spv::Op::OpConstant, lengthTypeId, newLengthId,
|
||||||
Instruction::OperandList{{SPV_OPERAND_TYPE_TYPED_LITERAL_NUMBER,
|
Instruction::OperandList{{SPV_OPERAND_TYPE_TYPED_LITERAL_NUMBER,
|
||||||
{requiredLength}}}));
|
{requiredLength}}}));
|
||||||
const uint32_t newArrayTypeId = irContext->TakeNextId();
|
const uint32_t newArrayTypeId = irContext->TakeNextId();
|
||||||
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
|
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
irContext, spv::Op::OpTypeArray, 0, newArrayTypeId,
|
irContext, spv::Op::OpTypeArray, 0, newArrayTypeId,
|
||||||
Instruction::OperandList{
|
Instruction::OperandList{
|
||||||
{SPV_OPERAND_TYPE_ID, {growth.elementTypeId}},
|
{SPV_OPERAND_TYPE_ID, {elementTypeId}},
|
||||||
{SPV_OPERAND_TYPE_ID, {newLengthId}}}));
|
{SPV_OPERAND_TYPE_ID, {newLengthId}}}));
|
||||||
const uint32_t newPointerTypeId = irContext->TakeNextId();
|
const uint32_t newPointerTypeId = irContext->TakeNextId();
|
||||||
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
|
variable->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||||
@@ -541,17 +331,21 @@ namespace MobileGL {
|
|||||||
{SPV_OPERAND_TYPE_ID, {newArrayTypeId}}}));
|
{SPV_OPERAND_TYPE_ID, {newArrayTypeId}}}));
|
||||||
|
|
||||||
variable->SetResultType(newPointerTypeId);
|
variable->SetResultType(newPointerTypeId);
|
||||||
|
changedModule = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!changedModule) {
|
||||||
|
return Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||||
return Status::SuccessWithChange;
|
return Status::SuccessWithChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
spvtools::Optimizer::PassToken
|
spvtools::Optimizer::PassToken
|
||||||
FixIterationRPSubgroupScratchPass::CreateFixIterationRPSubgroupScratchPass(
|
FixIterationRPSubgroupScratchPass::CreateFixIterationRPSubgroupScratchPass(
|
||||||
const Uint32 nativeSubgroupSize, const Uint32 maxWorkgroupScratchBytes) {
|
const Uint32 nativeSubgroupSize) {
|
||||||
return spvtools::Optimizer::PassToken(MakeUnique<FixIterationRPSubgroupScratchPass>(
|
return spvtools::Optimizer::PassToken(
|
||||||
nativeSubgroupSize, maxWorkgroupScratchBytes));
|
MakeUnique<FixIterationRPSubgroupScratchPass>(nativeSubgroupSize));
|
||||||
}
|
}
|
||||||
} // namespace ShaderTranspiler
|
} // namespace ShaderTranspiler
|
||||||
} // namespace MG_Util
|
} // namespace MG_Util
|
||||||
|
|||||||
@@ -16,57 +16,48 @@
|
|||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
namespace MG_Util {
|
namespace MG_Util {
|
||||||
namespace ShaderTranspiler {
|
namespace ShaderTranspiler {
|
||||||
// Patches ONE known shader-pack defect: iterationRP hard-sizes the scratch
|
// Patches ONE known shader-pack defect: iterationRP's auto-exposure reduction
|
||||||
// its subgroup prefix scans write through prefixSumCache[gl_SubgroupID].
|
// declares `shared vec2 prefixSumCache[32]` for its 512-invocation workgroup
|
||||||
// The pack ships that idiom twice, sized for the >= 16-lane subgroups
|
// and stores per-subgroup subtotals through prefixSumCache[gl_SubgroupID].
|
||||||
// desktop GL drivers give it:
|
// The pack hard-sized that scratch for the >=16-lane subgroups desktop GL
|
||||||
// - the auto-exposure reduction: 32x16 (512 invocations), vec2[32];
|
// drivers ship; on a narrower Vulkan device (lavapipe's 8 lanes -> 64
|
||||||
// - the RTW importance warp: 1024 invocations, float[64].
|
// subgroups) every subgroup past entry 31 indexes shared memory out of
|
||||||
// On a narrower Vulkan device (lavapipe's 8 lanes -> 64 and 128 subgroups)
|
// bounds - on a CPU rasterizer that is literal heap corruption. The
|
||||||
// every subgroup past the last declared entry indexes shared memory out of
|
// reduction ALGORITHM is width-agnostic (its combine loop is sized by
|
||||||
// bounds - on a CPU rasterizer that is literal heap corruption. Both
|
// gl_NumSubgroups), so the faithful repair is to grow the one under-declared
|
||||||
// reduction ALGORITHMS are width-agnostic (their combine loops are sized by
|
// array to ceil(512 / native width) and change nothing else. This is the
|
||||||
// gl_NumSubgroups), so the faithful repair is to grow the under-declared
|
// pack author's bug, not MobileGL's; the patch is therefore deliberately
|
||||||
// arrays to ceil(invocations / native width) and change nothing else.
|
// NOT a general mechanism - it only rewrites modules that positively match
|
||||||
|
// iterationRP's reduction fingerprint:
|
||||||
|
// - GLCompute entry point with local size exactly 32x16x1;
|
||||||
|
// - a subgroupInclusiveAdd on a vec2 (OpGroupNonUniformFAdd InclusiveScan,
|
||||||
|
// the pack's luminance/exposure accumulator signature);
|
||||||
|
// - a workgroup-shared array of exactly vec2[32] whose access-chain index
|
||||||
|
// is data-dependent on gl_SubgroupID.
|
||||||
|
// Matching at the SPIR-V level keeps the recognition robust against
|
||||||
|
// whitespace/identifier-level drift that made the old source-text template
|
||||||
|
// rewrite (removed in 7769156) so brittle, while still refusing to touch
|
||||||
|
// anything that is not this pack's reduction. On devices whose native width
|
||||||
|
// already satisfies the pack's assumption (>= 16 lanes: desktop GL, Adreno),
|
||||||
|
// the grown length equals or undershoots the declared 32 and every module
|
||||||
|
// passes through byte-identical.
|
||||||
//
|
//
|
||||||
// This is the pack author's bug, not MobileGL's, so the patch is
|
// The pass never fails a module: anything it cannot prove is this exact
|
||||||
// deliberately NOT a general "resize shared arrays" mechanism. It rewrites
|
// pattern - or cannot grow safely (a whole-array use, a spec-constant
|
||||||
// an array only when the module positively matches the pack's reduction
|
// length, an initializer) - is left exactly as it was.
|
||||||
// idiom AND the device's own topology proves the declaration too small:
|
|
||||||
// - GLCompute entry point with a literal workgroup size;
|
|
||||||
// - a subgroup scan/reduce over a 32-bit float scalar or vector
|
|
||||||
// (OpGroupNonUniformF{Add,Min,Max}), the pack's accumulator signature;
|
|
||||||
// - a workgroup-shared array of 32-bit float scalars/vectors whose
|
|
||||||
// access-chain index is data-dependent on gl_SubgroupID;
|
|
||||||
// - a declared length strictly below ceil(invocations / native width).
|
|
||||||
// That last clause is what keeps the patch inert wherever the pack is
|
|
||||||
// correct: on any device whose width satisfies the pack's assumption
|
|
||||||
// (>= 16 lanes: desktop GL, Adreno) both shapes already fit and every
|
|
||||||
// module passes through byte-identical. Matching at the SPIR-V level keeps
|
|
||||||
// recognition robust against the whitespace/identifier drift that made the
|
|
||||||
// old source-text template rewrite (removed in 7769156) so brittle.
|
|
||||||
//
|
|
||||||
// The pass never fails a module: anything it cannot prove is this pattern -
|
|
||||||
// or cannot grow safely (a whole-array use, a spec-constant length, an
|
|
||||||
// initializer, or growth that would not fit maxWorkgroupScratchBytes) - is
|
|
||||||
// left exactly as it was. Pass the device's maxComputeSharedMemorySize as
|
|
||||||
// maxWorkgroupScratchBytes; 0 falls back to the 16384-byte Vulkan minimum.
|
|
||||||
class FixIterationRPSubgroupScratchPass : public spvtools::opt::Pass {
|
class FixIterationRPSubgroupScratchPass : public spvtools::opt::Pass {
|
||||||
public:
|
public:
|
||||||
FixIterationRPSubgroupScratchPass(Uint32 nativeSubgroupSize,
|
explicit FixIterationRPSubgroupScratchPass(Uint32 nativeSubgroupSize)
|
||||||
Uint32 maxWorkgroupScratchBytes)
|
: m_nativeSubgroupSize(nativeSubgroupSize) {}
|
||||||
: m_nativeSubgroupSize(nativeSubgroupSize),
|
|
||||||
m_maxWorkgroupScratchBytes(maxWorkgroupScratchBytes) {}
|
|
||||||
|
|
||||||
const char* name() const override { return "fix-iterationrp-subgroup-scratch"; }
|
const char* name() const override { return "fix-iterationrp-subgroup-scratch"; }
|
||||||
Status Process() override;
|
Status Process() override;
|
||||||
|
|
||||||
static spvtools::Optimizer::PassToken CreateFixIterationRPSubgroupScratchPass(
|
static spvtools::Optimizer::PassToken CreateFixIterationRPSubgroupScratchPass(
|
||||||
Uint32 nativeSubgroupSize, Uint32 maxWorkgroupScratchBytes);
|
Uint32 nativeSubgroupSize);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Uint32 m_nativeSubgroupSize;
|
Uint32 m_nativeSubgroupSize;
|
||||||
Uint32 m_maxWorkgroupScratchBytes;
|
|
||||||
};
|
};
|
||||||
} // namespace ShaderTranspiler
|
} // namespace ShaderTranspiler
|
||||||
} // namespace MG_Util
|
} // namespace MG_Util
|
||||||
|
|||||||
@@ -1,250 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.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
|
|
||||||
|
|
||||||
#include "LowerViewportIndexPass.h"
|
|
||||||
|
|
||||||
#include "spirv.hpp"
|
|
||||||
#include "source/opt/build_module.h"
|
|
||||||
#include "source/opt/def_use_manager.h"
|
|
||||||
#include "source/opt/instruction.h"
|
|
||||||
#include "source/opt/ir_context.h"
|
|
||||||
#include "source/opt/module.h"
|
|
||||||
#include "source/util/make_unique.h"
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace MobileGL {
|
|
||||||
namespace MG_Util {
|
|
||||||
namespace ShaderTranspiler {
|
|
||||||
namespace {
|
|
||||||
using spvtools::opt::Instruction;
|
|
||||||
using spvtools::opt::IRContext;
|
|
||||||
using spvtools::opt::Operand;
|
|
||||||
|
|
||||||
// The name the decompiled ESSL ends up declaring. Same mg_ prefix as the
|
|
||||||
// draw-parameter lowering, so a global that came from a demoted builtin is
|
|
||||||
// recognisable in a driver log.
|
|
||||||
constexpr const char* kLoweredName = "mg_ViewportIndex";
|
|
||||||
|
|
||||||
// The one decoration this pass lowers. OpDecorate only, never OpMemberDecorate:
|
|
||||||
// glslang emits gl_ViewportIndex as a standalone variable, and a member of a
|
|
||||||
// gl_PerVertex-shaped block could not be demoted on its own anyway. BuiltIn Layer
|
|
||||||
// is deliberately not matched - see the header.
|
|
||||||
Bool IsViewportIndexBuiltinDecoration(const Instruction& annotation) {
|
|
||||||
if (annotation.opcode() != spv::Op::OpDecorate ||
|
|
||||||
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
|
|
||||||
spv::Decoration::BuiltIn) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) ==
|
|
||||||
spv::BuiltIn::ViewportIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The OUTPUT variable that decoration names, or nullptr. Only an output is
|
|
||||||
// demotable: a fragment stage READS gl_ViewportIndex as an Input, and a Private
|
|
||||||
// global has no defined value to read, so lowering that one would answer the
|
|
||||||
// shader with garbage instead of the viewport it asked for. That case is left for
|
|
||||||
// the driver to reject.
|
|
||||||
Instruction* GetDecoratedViewportIndexOutput(IRContext* context,
|
|
||||||
const Instruction& annotation) {
|
|
||||||
Instruction* variable =
|
|
||||||
context->get_def_use_mgr()->GetDef(annotation.GetSingleWordInOperand(0));
|
|
||||||
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable ||
|
|
||||||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) !=
|
|
||||||
spv::StorageClass::Output) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return variable;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decorations the validator accepts only on an Input/Output variable, so they have
|
|
||||||
// to go with the storage class or the demoted module stops validating. glslang
|
|
||||||
// puts none of these on gl_ViewportIndex today - the BuiltIn is all it writes -
|
|
||||||
// but a geometry `layout(stream = N)` qualifier decorates every output of the
|
|
||||||
// stage, and the pass must not be the thing that produces an invalid module.
|
|
||||||
Bool IsInterfaceOnlyDecoration(spv::Decoration decoration) {
|
|
||||||
switch (decoration) {
|
|
||||||
case spv::Decoration::Flat:
|
|
||||||
case spv::Decoration::NoPerspective:
|
|
||||||
case spv::Decoration::Centroid:
|
|
||||||
case spv::Decoration::Sample:
|
|
||||||
case spv::Decoration::Patch:
|
|
||||||
case spv::Decoration::Invariant:
|
|
||||||
case spv::Decoration::Location:
|
|
||||||
case spv::Decoration::Component:
|
|
||||||
case spv::Decoration::Stream:
|
|
||||||
case spv::Decoration::XfbBuffer:
|
|
||||||
case spv::Decoration::XfbStride:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReplaceName(IRContext* context, uint32_t id, const char* name) {
|
|
||||||
for (auto& debugInst : context->debugs2()) {
|
|
||||||
if (debugInst.opcode() == spv::Op::OpName && debugInst.GetSingleWordInOperand(0) == id) {
|
|
||||||
debugInst.SetInOperand(
|
|
||||||
1, spvtools::utils::MakeVector<spvtools::opt::Operand::OperandData>(name));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context->AddDebug2Inst(spvtools::MakeUnique<Instruction>(
|
|
||||||
context, spv::Op::OpName, 0, 0,
|
|
||||||
std::initializer_list<Operand>{
|
|
||||||
{SPV_OPERAND_TYPE_ID, {id}},
|
|
||||||
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)}}));
|
|
||||||
}
|
|
||||||
|
|
||||||
void RemoveFromEntryPointInterfaces(IRContext* context, uint32_t id) {
|
|
||||||
for (Instruction& entryPoint : context->module()->entry_points()) {
|
|
||||||
std::vector<Operand> newOperands;
|
|
||||||
Bool changed = false;
|
|
||||||
for (uint32_t i = 0; i < entryPoint.NumInOperands(); ++i) {
|
|
||||||
const Operand& operand = entryPoint.GetInOperand(i);
|
|
||||||
// Interface ids start after execution model, entry-point id and name.
|
|
||||||
if (i >= 3 && operand.type == SPV_OPERAND_TYPE_ID &&
|
|
||||||
entryPoint.GetSingleWordInOperand(i) == id) {
|
|
||||||
changed = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
newOperands.push_back(operand);
|
|
||||||
}
|
|
||||||
if (changed) {
|
|
||||||
entryPoint.SetInOperands(std::move(newOperands));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
bool LowerViewportIndexPass::DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary) {
|
|
||||||
if (binary.empty()) {
|
|
||||||
// An empty module is a stage that produced no SPIR-V, which is not a verdict
|
|
||||||
// about viewport routing; letting BuildModule reject it would push a spurious
|
|
||||||
// diagnostic through the message consumer first.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
std::unique_ptr<IRContext> context = spvtools::BuildModule(
|
|
||||||
SPV_ENV_VULKAN_1_1, [](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
|
|
||||||
binary.data(), binary.size());
|
|
||||||
if (!context) {
|
|
||||||
// Unparseable here means unusable downstream too; let the ordinary transpile
|
|
||||||
// path produce the error rather than inventing a verdict from it.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return DeclaresViewportIndexBuiltin(context.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool LowerViewportIndexPass::DeclaresViewportIndexBuiltin(IRContext* context) {
|
|
||||||
for (const Instruction& annotation : context->annotations()) {
|
|
||||||
if (IsViewportIndexBuiltinDecoration(annotation) &&
|
|
||||||
GetDecoratedViewportIndexOutput(context, annotation) != nullptr) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
spvtools::opt::Pass::Status LowerViewportIndexPass::Process() {
|
|
||||||
auto* irContext = context();
|
|
||||||
|
|
||||||
// Collect the decorations to lower first; mutating while iterating annotations
|
|
||||||
// invalidates the range.
|
|
||||||
struct LoweredVariable {
|
|
||||||
Instruction* variable = nullptr;
|
|
||||||
Instruction* decoration = nullptr;
|
|
||||||
};
|
|
||||||
std::vector<LoweredVariable> targets;
|
|
||||||
|
|
||||||
for (auto& annotation : irContext->annotations()) {
|
|
||||||
if (!IsViewportIndexBuiltinDecoration(annotation)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Instruction* variable = GetDecoratedViewportIndexOutput(irContext, annotation);
|
|
||||||
if (variable == nullptr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
targets.push_back({variable, &annotation});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targets.empty()) {
|
|
||||||
return Status::SuccessWithoutChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second collection pass, for the same reason as the first: the decorations that
|
|
||||||
// stop being legal once the variable leaves the Output storage class.
|
|
||||||
std::vector<Instruction*> deadDecorations;
|
|
||||||
for (auto& annotation : irContext->annotations()) {
|
|
||||||
if (annotation.opcode() != spv::Op::OpDecorate ||
|
|
||||||
!IsInterfaceOnlyDecoration(
|
|
||||||
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const uint32_t decoratedId = annotation.GetSingleWordInOperand(0);
|
|
||||||
for (const auto& target : targets) {
|
|
||||||
if (target.variable->result_id() == decoratedId) {
|
|
||||||
deadDecorations.push_back(&annotation);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
|
||||||
auto* typeMgr = irContext->get_type_mgr();
|
|
||||||
|
|
||||||
for (auto& target : targets) {
|
|
||||||
Instruction* variable = target.variable;
|
|
||||||
const uint32_t variableId = variable->result_id();
|
|
||||||
|
|
||||||
// Demote the Output builtin to a plain Private global. Every store the shader
|
|
||||||
// already makes stays exactly where it is - it simply no longer reaches the
|
|
||||||
// rasterizer, which is the whole of the degradation.
|
|
||||||
Instruction* pointerType = defUseMgr->GetDef(variable->type_id());
|
|
||||||
const uint32_t pointeeTypeId = pointerType->GetSingleWordInOperand(1);
|
|
||||||
const uint32_t privatePointerTypeId =
|
|
||||||
typeMgr->FindPointerToType(pointeeTypeId, spv::StorageClass::Private);
|
|
||||||
variable->SetResultType(privatePointerTypeId);
|
|
||||||
variable->SetInOperand(0, {static_cast<uint32_t>(spv::StorageClass::Private)});
|
|
||||||
|
|
||||||
// FindPointerToType APPENDS a newly minted pointer type to the end of the
|
|
||||||
// globals section - after this variable - and SPIR-V requires def before use.
|
|
||||||
// Re-anchor the variable directly after its new type, which is equally correct
|
|
||||||
// when the type already existed further up.
|
|
||||||
Instruction* privatePointerType = defUseMgr->GetDef(privatePointerTypeId);
|
|
||||||
variable->RemoveFromList();
|
|
||||||
variable->InsertAfter(privatePointerType);
|
|
||||||
|
|
||||||
irContext->KillInst(target.decoration);
|
|
||||||
RemoveFromEntryPointInterfaces(irContext, variableId);
|
|
||||||
ReplaceName(irContext, variableId, kLoweredName);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto* decoration : deadDecorations) {
|
|
||||||
irContext->KillInst(decoration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The MultiViewport / ShaderViewportIndexLayerEXT capabilities are deliberately
|
|
||||||
// left declared, unlike DrawParameters in the sibling pass. They are not exclusive
|
|
||||||
// to this builtin: ShaderViewportIndexLayerEXT also enables gl_Layer in the
|
|
||||||
// pre-geometry stages, and it DEPENDS on MultiViewport, so dropping either can
|
|
||||||
// invalidate a module that still writes Layer. A declared-but-unused capability is
|
|
||||||
// legal SPIR-V and SPIRV-Cross's GLSL backend reads neither of them, so leaving
|
|
||||||
// both costs nothing.
|
|
||||||
|
|
||||||
return Status::SuccessWithChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
spvtools::Optimizer::PassToken LowerViewportIndexPass::CreateLowerViewportIndexPass() {
|
|
||||||
return spvtools::Optimizer::PassToken(MakeUnique<LowerViewportIndexPass>());
|
|
||||||
}
|
|
||||||
} // namespace ShaderTranspiler
|
|
||||||
} // namespace MG_Util
|
|
||||||
} // namespace MobileGL
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.h
|
|
||||||
// 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
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include "source/opt/pass.h"
|
|
||||||
#include "spirv-tools/optimizer.hpp"
|
|
||||||
|
|
||||||
#include <Includes.h>
|
|
||||||
|
|
||||||
namespace MobileGL {
|
|
||||||
namespace MG_Util {
|
|
||||||
namespace ShaderTranspiler {
|
|
||||||
// ESSL has no core gl_ViewportIndex at any version - only GL_OES_viewport_array
|
|
||||||
// introduces it - and SPIRV-Cross prints the identifier bare, requesting no extension
|
|
||||||
// for it (contrast BuiltInLayer, which it backs with GL_NV_viewport_array2 on ES). On
|
|
||||||
// a driver WITHOUT that extension the stage therefore fails to compile, DirectGLES
|
|
||||||
// marks the program unusable and binds program 0 for it, and every draw silently
|
|
||||||
// renders nothing while GL_LINK_STATUS still answers TRUE - the failure signature
|
|
||||||
// KHR-GL4x.viewport_array reports as "expected N, got -1", i.e. the untouched upload.
|
|
||||||
//
|
|
||||||
// This pass demotes the ViewportIndex OUTPUT to a plain Private global named
|
|
||||||
// mg_ViewportIndex, so the decompiled ESSL declares an ordinary global the shader
|
|
||||||
// still writes and nothing reads. The program compiles and rendering degrades to
|
|
||||||
// viewport 0 - which is the single-viewport behaviour MG_IntegrationTest's
|
|
||||||
// ViewportArrayScenario already documents for this backend - instead of the whole
|
|
||||||
// program becoming a no-op. Only meant for the DirectGLES transpile path; the Vulkan
|
|
||||||
// backend keeps the native builtin and routes it for real.
|
|
||||||
//
|
|
||||||
// gl_Layer is deliberately NOT touched: BuiltIn Layer IS core in ESSL 3.20 geometry
|
|
||||||
// shaders, and demoting it would break layered rendering that works today.
|
|
||||||
class LowerViewportIndexPass : public spvtools::opt::Pass {
|
|
||||||
public:
|
|
||||||
const char* name() const override { return "lower-viewport-index"; }
|
|
||||||
Status Process() override;
|
|
||||||
|
|
||||||
// Whether the module declares an output decorated BuiltIn ViewportIndex, i.e.
|
|
||||||
// whether running this pass could change anything. Answered from a single parse so
|
|
||||||
// the caller can skip the optimizer round trip entirely - which is every shader
|
|
||||||
// but the handful that route viewports from the shader.
|
|
||||||
static bool DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary);
|
|
||||||
|
|
||||||
// Same question answered from an already-built module, so one parse can feed
|
|
||||||
// several gates (ShaderCompiler::ProbeSpirvGateFeatures).
|
|
||||||
static bool DeclaresViewportIndexBuiltin(spvtools::opt::IRContext* context);
|
|
||||||
|
|
||||||
static spvtools::Optimizer::PassToken CreateLowerViewportIndexPass();
|
|
||||||
};
|
|
||||||
} // namespace ShaderTranspiler
|
|
||||||
} // namespace MG_Util
|
|
||||||
} // namespace MobileGL
|
|
||||||
@@ -138,11 +138,6 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
|||||||
case TextureInternalFormat::DepthComponent32F:
|
case TextureInternalFormat::DepthComponent32F:
|
||||||
out = {1, ShadowComponent::Float32, false};
|
out = {1, ShadowComponent::Float32, false};
|
||||||
return true;
|
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::R8:
|
||||||
case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true;
|
case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true;
|
||||||
@@ -341,13 +336,8 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
|||||||
case TextureInputFormat::BGRAInteger: out = {{2, 1, 0, 3}, 4, true}; return true;
|
case TextureInputFormat::BGRAInteger: out = {{2, 1, 0, 3}, 4, true}; return true;
|
||||||
// A depth value converts like a single normalized/float channel.
|
// A depth value converts like a single normalized/float channel.
|
||||||
case TextureInputFormat::DepthComponent: out = {{0, -1, -1, -1}, 1, false}; return true;
|
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:
|
default:
|
||||||
return false; // packed depth-stencil / unknown
|
return false; // stencil / packed depth-stencil / unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,14 +806,6 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
|||||||
return IsRawPackedPixelPair(packedInternal.kind, clientFormat, clientType);
|
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
|
// assume 8 bit per channel
|
||||||
// swizzle.size() == channel count
|
// swizzle.size() == channel count
|
||||||
void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector<TextureSwizzleParam>& swizzle) {
|
void ProcessColorSwizzle(void* data, SizeT pixelCount, const Vector<TextureSwizzleParam>& swizzle) {
|
||||||
@@ -1008,11 +990,6 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
|||||||
const void* inputPixel,
|
const void* inputPixel,
|
||||||
Vector<Uint8>& outputPixel) {
|
Vector<Uint8>& outputPixel) {
|
||||||
outputPixel.clear();
|
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;
|
if (inputPixel == nullptr || !IsValidUnpackPixelPair(textureInputFormat, inputDataType)) return false;
|
||||||
|
|
||||||
PixelStoreParameters params{};
|
PixelStoreParameters params{};
|
||||||
|
|||||||
@@ -44,17 +44,6 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
|
|||||||
Bool IsRawPackedPixelTransfer(TextureInternalFormat internalFormat, TextureInputFormat clientFormat,
|
Bool IsRawPackedPixelTransfer(TextureInternalFormat internalFormat, TextureInputFormat clientFormat,
|
||||||
TexturePixelDataType clientType);
|
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
|
// 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
|
// 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
|
// 4 Floats per texel; integer formats fill it with 4 Uint32/Int32 per texel and set
|
||||||
|
|||||||
@@ -164,21 +164,6 @@ bool LoadMobileGL(const Request& request, std::string& error) {
|
|||||||
} else {
|
} else {
|
||||||
unsetenv("MOBILEGL_COHERENT_AS_FLUSH");
|
unsetenv("MOBILEGL_COHERENT_AS_FLUSH");
|
||||||
}
|
}
|
||||||
if (request.fixIterationRPSubgroupScratch) {
|
|
||||||
setenv("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH", "1", 1);
|
|
||||||
} else {
|
|
||||||
unsetenv("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
|
||||||
}
|
|
||||||
if (request.deriveNumSubgroups) {
|
|
||||||
setenv("MOBILEGL_DERIVE_NUM_SUBGROUPS", "1", 1);
|
|
||||||
} else {
|
|
||||||
unsetenv("MOBILEGL_DERIVE_NUM_SUBGROUPS");
|
|
||||||
}
|
|
||||||
if (request.iterationRPFixBarrier) {
|
|
||||||
setenv("MOBILEGL_ITERATIONRP_FIX_BARRIER", "1", 1);
|
|
||||||
} else {
|
|
||||||
unsetenv("MOBILEGL_ITERATIONRP_FIX_BARRIER");
|
|
||||||
}
|
|
||||||
if (request.fboAttachmentDumps.empty()) {
|
if (request.fboAttachmentDumps.empty()) {
|
||||||
unsetenv("MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS");
|
unsetenv("MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS");
|
||||||
} else {
|
} else {
|
||||||
@@ -833,10 +818,6 @@ bool WriteResultJson(const Request& request, const Result& result) {
|
|||||||
<< (request.avoidAngleLlvmpipeSamplerMipmapMinFilter ? "true" : "false") << ",\n";
|
<< (request.avoidAngleLlvmpipeSamplerMipmapMinFilter ? "true" : "false") << ",\n";
|
||||||
file << " \"avoidAngleLlvmpipeExplicitLodBias\": "
|
file << " \"avoidAngleLlvmpipeExplicitLodBias\": "
|
||||||
<< (request.avoidAngleLlvmpipeExplicitLodBias ? "true" : "false") << ",\n";
|
<< (request.avoidAngleLlvmpipeExplicitLodBias ? "true" : "false") << ",\n";
|
||||||
file << " \"fixIterationRPSubgroupScratch\": " << (request.fixIterationRPSubgroupScratch ? "true" : "false")
|
|
||||||
<< ",\n";
|
|
||||||
file << " \"deriveNumSubgroups\": " << (request.deriveNumSubgroups ? "true" : "false") << ",\n";
|
|
||||||
file << " \"iterationRPFixBarrier\": " << (request.iterationRPFixBarrier ? "true" : "false") << ",\n";
|
|
||||||
file << " \"holdMs\": " << request.holdMs << ",\n";
|
file << " \"holdMs\": " << request.holdMs << ",\n";
|
||||||
file << " \"mismatchPixels\": " << result.mismatchPixels << "\n";
|
file << " \"mismatchPixels\": " << result.mismatchPixels << "\n";
|
||||||
file << "}\n";
|
file << "}\n";
|
||||||
|
|||||||
@@ -44,9 +44,6 @@ struct Request {
|
|||||||
bool avoidAngleLlvmpipeSamplerMipmapMinFilter = false;
|
bool avoidAngleLlvmpipeSamplerMipmapMinFilter = false;
|
||||||
bool avoidAngleLlvmpipeExplicitLodBias = false;
|
bool avoidAngleLlvmpipeExplicitLodBias = false;
|
||||||
bool coherentAsFlush = false;
|
bool coherentAsFlush = false;
|
||||||
bool fixIterationRPSubgroupScratch = false;
|
|
||||||
bool deriveNumSubgroups = false;
|
|
||||||
bool iterationRPFixBarrier = false;
|
|
||||||
int holdMs = 0;
|
int holdMs = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -122,9 +122,6 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
|
|||||||
jboolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
|
jboolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
|
||||||
jboolean avoidAngleLlvmpipeExplicitLodBias,
|
jboolean avoidAngleLlvmpipeExplicitLodBias,
|
||||||
jboolean coherentAsFlush,
|
jboolean coherentAsFlush,
|
||||||
jboolean fixIterationRPSubgroupScratch,
|
|
||||||
jboolean deriveNumSubgroups,
|
|
||||||
jboolean iterationRPFixBarrier,
|
|
||||||
jstring texture2dDumps) {
|
jstring texture2dDumps) {
|
||||||
mobilegl_trace::Request request;
|
mobilegl_trace::Request request;
|
||||||
request.tracePath = ToString(env, tracePath);
|
request.tracePath = ToString(env, tracePath);
|
||||||
@@ -153,9 +150,6 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
|
|||||||
avoidAngleLlvmpipeSamplerMipmapMinFilter == JNI_TRUE;
|
avoidAngleLlvmpipeSamplerMipmapMinFilter == JNI_TRUE;
|
||||||
request.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias == JNI_TRUE;
|
request.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias == JNI_TRUE;
|
||||||
request.coherentAsFlush = coherentAsFlush == JNI_TRUE;
|
request.coherentAsFlush = coherentAsFlush == JNI_TRUE;
|
||||||
request.fixIterationRPSubgroupScratch = fixIterationRPSubgroupScratch == JNI_TRUE;
|
|
||||||
request.deriveNumSubgroups = deriveNumSubgroups == JNI_TRUE;
|
|
||||||
request.iterationRPFixBarrier = iterationRPFixBarrier == JNI_TRUE;
|
|
||||||
|
|
||||||
ScopedTraceReplayState replayState;
|
ScopedTraceReplayState replayState;
|
||||||
mobilegl_trace_set_requested_size(request.width, request.height);
|
mobilegl_trace_set_requested_size(request.width, request.height);
|
||||||
|
|||||||
@@ -116,9 +116,6 @@ public final class TraceReplayActivity extends Activity {
|
|||||||
request.avoidAngleLlvmpipeSamplerMipmapMinFilter,
|
request.avoidAngleLlvmpipeSamplerMipmapMinFilter,
|
||||||
request.avoidAngleLlvmpipeExplicitLodBias,
|
request.avoidAngleLlvmpipeExplicitLodBias,
|
||||||
request.coherentAsFlush,
|
request.coherentAsFlush,
|
||||||
request.fixIterationRPSubgroupScratch,
|
|
||||||
request.deriveNumSubgroups,
|
|
||||||
request.iterationRPFixBarrier,
|
|
||||||
request.texture2dDumps
|
request.texture2dDumps
|
||||||
);
|
);
|
||||||
Log.i(TAG, result.toString());
|
Log.i(TAG, result.toString());
|
||||||
@@ -152,9 +149,6 @@ public final class TraceReplayActivity extends Activity {
|
|||||||
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
|
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
|
||||||
boolean avoidAngleLlvmpipeExplicitLodBias,
|
boolean avoidAngleLlvmpipeExplicitLodBias,
|
||||||
boolean coherentAsFlush,
|
boolean coherentAsFlush,
|
||||||
boolean fixIterationRPSubgroupScratch,
|
|
||||||
boolean deriveNumSubgroups,
|
|
||||||
boolean iterationRPFixBarrier,
|
|
||||||
String texture2dDumps
|
String texture2dDumps
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -180,9 +174,6 @@ public final class TraceReplayActivity extends Activity {
|
|||||||
final boolean avoidAngleLlvmpipeSamplerMipmapMinFilter;
|
final boolean avoidAngleLlvmpipeSamplerMipmapMinFilter;
|
||||||
final boolean avoidAngleLlvmpipeExplicitLodBias;
|
final boolean avoidAngleLlvmpipeExplicitLodBias;
|
||||||
final boolean coherentAsFlush;
|
final boolean coherentAsFlush;
|
||||||
final boolean fixIterationRPSubgroupScratch;
|
|
||||||
final boolean deriveNumSubgroups;
|
|
||||||
final boolean iterationRPFixBarrier;
|
|
||||||
final String texture2dDumps;
|
final String texture2dDumps;
|
||||||
|
|
||||||
private TraceReplayRequest(
|
private TraceReplayRequest(
|
||||||
@@ -207,9 +198,6 @@ public final class TraceReplayActivity extends Activity {
|
|||||||
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
|
boolean avoidAngleLlvmpipeSamplerMipmapMinFilter,
|
||||||
boolean avoidAngleLlvmpipeExplicitLodBias,
|
boolean avoidAngleLlvmpipeExplicitLodBias,
|
||||||
boolean coherentAsFlush,
|
boolean coherentAsFlush,
|
||||||
boolean fixIterationRPSubgroupScratch,
|
|
||||||
boolean deriveNumSubgroups,
|
|
||||||
boolean iterationRPFixBarrier,
|
|
||||||
String texture2dDumps
|
String texture2dDumps
|
||||||
) {
|
) {
|
||||||
this.tracePath = tracePath;
|
this.tracePath = tracePath;
|
||||||
@@ -233,9 +221,6 @@ public final class TraceReplayActivity extends Activity {
|
|||||||
this.avoidAngleLlvmpipeSamplerMipmapMinFilter = avoidAngleLlvmpipeSamplerMipmapMinFilter;
|
this.avoidAngleLlvmpipeSamplerMipmapMinFilter = avoidAngleLlvmpipeSamplerMipmapMinFilter;
|
||||||
this.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias;
|
this.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias;
|
||||||
this.coherentAsFlush = coherentAsFlush;
|
this.coherentAsFlush = coherentAsFlush;
|
||||||
this.fixIterationRPSubgroupScratch = fixIterationRPSubgroupScratch;
|
|
||||||
this.deriveNumSubgroups = deriveNumSubgroups;
|
|
||||||
this.iterationRPFixBarrier = iterationRPFixBarrier;
|
|
||||||
this.texture2dDumps = texture2dDumps;
|
this.texture2dDumps = texture2dDumps;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,9 +249,6 @@ public final class TraceReplayActivity extends Activity {
|
|||||||
intent.getBooleanExtra("avoid_angle_llvmpipe_sampler_mipmap_min_filter", false),
|
intent.getBooleanExtra("avoid_angle_llvmpipe_sampler_mipmap_min_filter", false),
|
||||||
intent.getBooleanExtra("avoid_angle_llvmpipe_explicit_lod_bias", false),
|
intent.getBooleanExtra("avoid_angle_llvmpipe_explicit_lod_bias", false),
|
||||||
intent.getBooleanExtra("coherent_as_flush", false),
|
intent.getBooleanExtra("coherent_as_flush", false),
|
||||||
intent.getBooleanExtra("fix_iterationrp_subgroup_scratch", false),
|
|
||||||
intent.getBooleanExtra("derive_num_subgroups", false),
|
|
||||||
intent.getBooleanExtra("iterationrp_fix_barrier", false),
|
|
||||||
readString(intent, "texture_2d_dumps", "")
|
readString(intent, "texture_2d_dumps", "")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,9 +40,6 @@ Set MOBILEGL_TRACE_ANGLE_VARIANT to the packaged ANGLE short hash used by
|
|||||||
DirectGLES replay.
|
DirectGLES replay.
|
||||||
Set MOBILEGL_RETRACE_USE_PBUFFER=1 or pass --use-pbuffer to run DirectGLES
|
Set MOBILEGL_RETRACE_USE_PBUFFER=1 or pass --use-pbuffer to run DirectGLES
|
||||||
against an offscreen EGL pbuffer instead of the Activity surface.
|
against an offscreen EGL pbuffer instead of the Activity surface.
|
||||||
Set MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1,
|
|
||||||
MOBILEGL_DERIVE_NUM_SUBGROUPS=1, and MOBILEGL_ITERATIONRP_FIX_BARRIER=1 to
|
|
||||||
forward the corresponding iterationRP SPIR-V repairs into the APK process.
|
|
||||||
Pass --avoid-angle-llvmpipe-sampler-mipmap-min-filter for DirectGLES traces that
|
Pass --avoid-angle-llvmpipe-sampler-mipmap-min-filter for DirectGLES traces that
|
||||||
need ANGLE llvmpipe sampler mipmap filters downgraded to avoid driver stalls.
|
need ANGLE llvmpipe sampler mipmap filters downgraded to avoid driver stalls.
|
||||||
Pass --avoid-angle-llvmpipe-explicit-lod-bias for DirectGLES traces whose shaders
|
Pass --avoid-angle-llvmpipe-explicit-lod-bias for DirectGLES traces whose shaders
|
||||||
@@ -366,15 +363,6 @@ run_retrace() {
|
|||||||
if [ "${coherent_as_flush}" -eq 1 ]; then
|
if [ "${coherent_as_flush}" -eq 1 ]; then
|
||||||
set -- "$@" --ez coherent_as_flush true
|
set -- "$@" --ez coherent_as_flush true
|
||||||
fi
|
fi
|
||||||
if [ "${MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH:-}" = "1" ]; then
|
|
||||||
set -- "$@" --ez fix_iterationrp_subgroup_scratch true
|
|
||||||
fi
|
|
||||||
if [ "${MOBILEGL_DERIVE_NUM_SUBGROUPS:-}" = "1" ]; then
|
|
||||||
set -- "$@" --ez derive_num_subgroups true
|
|
||||||
fi
|
|
||||||
if [ "${MOBILEGL_ITERATIONRP_FIX_BARRIER:-}" = "1" ]; then
|
|
||||||
set -- "$@" --ez iterationrp_fix_barrier true
|
|
||||||
fi
|
|
||||||
if [ -n "${texture_2d_dumps}" ]; then
|
if [ -n "${texture_2d_dumps}" ]; then
|
||||||
set -- "$@" --es texture_2d_dumps "${texture_2d_dumps}"
|
set -- "$@" --es texture_2d_dumps "${texture_2d_dumps}"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -86,19 +86,6 @@ def main():
|
|||||||
ap.add_argument("--outdir", required=True)
|
ap.add_argument("--outdir", required=True)
|
||||||
ap.add_argument("--device-dir", default="/data/local/tmp/mgcts")
|
ap.add_argument("--device-dir", default="/data/local/tmp/mgcts")
|
||||||
ap.add_argument("--surface", default="fbo", help="--deqp-surface-type value")
|
ap.add_argument("--surface", default="fbo", help="--deqp-surface-type value")
|
||||||
# Without an explicit size, dEQP's FboRenderContext sizes the wrapper FBO to
|
|
||||||
# GL_MAX_RENDERBUFFER_SIZE (16384^2 here) and size-derived test allocations
|
|
||||||
# explode (a 4-sample 16K depth texture alone is 4 GiB).
|
|
||||||
ap.add_argument("--surface-size", type=int, default=256,
|
|
||||||
help="--deqp-surface-width/height value")
|
|
||||||
# With DONT_CARE depth/stencil bits dEQP's FboRenderContext picks the first entry of
|
|
||||||
# its own format list, GL_DEPTH32F_STENCIL8. framebuffer_blit meanwhile hardcodes
|
|
||||||
# GL_DEPTH24_STENCIL8 for its own buffers whenever it detects an FBO surface, then
|
|
||||||
# blits depth between the two - which the spec forbids for mismatched formats, so a
|
|
||||||
# conformant driver has to fail it. Asking for a config the test agrees with avoids
|
|
||||||
# the contradiction instead of papering over it.
|
|
||||||
ap.add_argument("--gl-config-name", default="rgba8888d24s8",
|
|
||||||
help="--deqp-gl-config-name value (empty string to leave it unset)")
|
|
||||||
ap.add_argument("--max-rounds", type=int, default=4000)
|
ap.add_argument("--max-rounds", type=int, default=4000)
|
||||||
ap.add_argument("--max-empty-streak", type=int, default=64,
|
ap.add_argument("--max-empty-streak", type=int, default=64,
|
||||||
help="abort after this many consecutive chunks that produce no log at all")
|
help="abort after this many consecutive chunks that produce no log at all")
|
||||||
@@ -164,22 +151,14 @@ def main():
|
|||||||
adb(args.serial, "shell", f"rm -f {dev_qpa}", timeout=60)
|
adb(args.serial, "shell", f"rm -f {dev_qpa}", timeout=60)
|
||||||
|
|
||||||
extra_env = "".join(f"{kv} " for kv in args.env)
|
extra_env = "".join(f"{kv} " for kv in args.env)
|
||||||
config_flag = (
|
|
||||||
f"--deqp-gl-config-name={args.gl_config_name} " if args.gl_config_name else ""
|
|
||||||
)
|
|
||||||
# The trailing sync makes the qpa durable: a hard GPU hang reboots the
|
|
||||||
# device, and f2fs rolls back unsynced writes, silently eating the log.
|
|
||||||
cmd = (
|
cmd = (
|
||||||
f"cd {args.device_dir} && "
|
f"cd {args.device_dir} && "
|
||||||
f"MOBILEGL_BACKEND_TYPE={args.backend} LD_LIBRARY_PATH=. {extra_env}"
|
f"MOBILEGL_BACKEND_TYPE={args.backend} LD_LIBRARY_PATH=. {extra_env}"
|
||||||
f"./glcts --deqp-caselist-file={dev_list} "
|
f"./glcts --deqp-caselist-file={dev_list} "
|
||||||
f"--deqp-surface-type={args.surface} "
|
f"--deqp-surface-type={args.surface} "
|
||||||
f"--deqp-surface-width={args.surface_size} "
|
|
||||||
f"--deqp-surface-height={args.surface_size} "
|
|
||||||
f"{config_flag}"
|
|
||||||
f"--deqp-terminate-on-device-lost=disable "
|
f"--deqp-terminate-on-device-lost=disable "
|
||||||
f"--deqp-log-images=disable --deqp-log-shader-sources=disable "
|
f"--deqp-log-images=disable --deqp-log-shader-sources=disable "
|
||||||
f"--deqp-log-filename={dev_qpa} > /dev/null 2>&1; rc=$?; sync; echo RC=$rc"
|
f"--deqp-log-filename={dev_qpa} > /dev/null 2>&1; echo RC=$?"
|
||||||
)
|
)
|
||||||
run = adb(args.serial, "shell", cmd, timeout=args.chunk_timeout)
|
run = adb(args.serial, "shell", cmd, timeout=args.chunk_timeout)
|
||||||
if run.returncode == 124:
|
if run.returncode == 124:
|
||||||
@@ -245,16 +224,9 @@ def main():
|
|||||||
f"to label the rest of the suite as crashes.", file=sys.stderr)
|
f"to label the rest of the suite as crashes.", file=sys.stderr)
|
||||||
break
|
break
|
||||||
|
|
||||||
# No log at all. If the device rebooted, the first unrun case took the
|
|
||||||
# whole device down (a reboot can also roll back the freshly created
|
|
||||||
# qpa on f2fs) - that is a hang to quarantine, not a process crash.
|
|
||||||
victim = remaining[0]
|
victim = remaining[0]
|
||||||
if rebooted:
|
print(f"[run_cts] no output at all; recording {victim} as Crash")
|
||||||
print(f"[run_cts] DEVICE HANG in {victim} (no log at all) - quarantining it")
|
crashed.append(victim)
|
||||||
hung.append(victim)
|
|
||||||
else:
|
|
||||||
print(f"[run_cts] no output at all; recording {victim} as Crash")
|
|
||||||
crashed.append(victim)
|
|
||||||
done.add(victim)
|
done.add(victim)
|
||||||
progressed = 1
|
progressed = 1
|
||||||
|
|
||||||
|
|||||||
@@ -46,10 +46,8 @@ DEFAULT_DEQP_ARGS = (
|
|||||||
"--deqp-gl-context-type=wgl",
|
"--deqp-gl-context-type=wgl",
|
||||||
"--deqp-surface-type=fbo",
|
"--deqp-surface-type=fbo",
|
||||||
"--deqp-gl-config-name=rgba8888d24s8",
|
"--deqp-gl-config-name=rgba8888d24s8",
|
||||||
# Height <= 0 is DONT_CARE, which FboRenderContext resolves to
|
|
||||||
# GL_MAX_RENDERBUFFER_SIZE - a 64x16384 surface. Pin both.
|
|
||||||
"--deqp-surface-width=64",
|
"--deqp-surface-width=64",
|
||||||
"--deqp-surface-height=64",
|
"--deqp-surface-height=-1",
|
||||||
"--deqp-base-seed=3",
|
"--deqp-base-seed=3",
|
||||||
"--deqp-visibility=hidden",
|
"--deqp-visibility=hidden",
|
||||||
"--deqp-watchdog=enable",
|
"--deqp-watchdog=enable",
|
||||||
|
|||||||
@@ -72,16 +72,8 @@ bool ReadDouble(int argc, char **argv, int &index, double &out) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ReadEnvFlag(const char *name) {
|
|
||||||
const char *value = std::getenv(name);
|
|
||||||
return value != nullptr && std::string(value) == "1";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) {
|
bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) {
|
||||||
request.backend = "DirectGLES";
|
request.backend = "DirectGLES";
|
||||||
request.fixIterationRPSubgroupScratch = ReadEnvFlag("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
|
||||||
request.deriveNumSubgroups = ReadEnvFlag("MOBILEGL_DERIVE_NUM_SUBGROUPS");
|
|
||||||
request.iterationRPFixBarrier = ReadEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
|
|
||||||
|
|
||||||
for (int i = 1; i < argc; ++i) {
|
for (int i = 1; i < argc; ++i) {
|
||||||
const std::string arg = argv[i];
|
const std::string arg = argv[i];
|
||||||
|
|||||||
Reference in New Issue
Block a user