mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24dfbb41f9 | ||
|
|
ca7878bf3a | ||
|
|
e418063b08 | ||
|
|
b43ec25bd7 | ||
|
|
525607bad6 | ||
|
|
b045024b6c | ||
|
|
14dfbeeed9 | ||
|
|
d7e79409b3 | ||
|
|
ca3b524396 | ||
|
|
071c8eb673 | ||
|
|
51a43518ac | ||
|
|
34ff95f6f5 | ||
|
|
b9d1504cc5 | ||
|
|
a223499143 | ||
|
|
5dca617f01 | ||
|
|
eb8ef893be | ||
|
|
20567fba6d | ||
|
|
e3f44e8da1 | ||
|
|
9f79a88af5 | ||
|
|
6bf32acdef | ||
|
|
23b53eacce | ||
|
|
e829e70d8b | ||
|
|
7e765e1535 | ||
|
|
bf6061811f | ||
|
|
87750c3b21 | ||
|
|
45b309db37 | ||
|
|
403c82ac4a | ||
|
|
827d46cad3 | ||
|
|
1748da0443 | ||
|
|
7efee8e3e6 | ||
|
|
08ca897a07 | ||
|
|
98f2a55214 | ||
|
|
cedc257566 | ||
|
|
821c0e0d4e | ||
|
|
bd9680ad67 | ||
|
|
6375e07030 | ||
|
|
53cac39d4e | ||
|
|
f6b1ea635b | ||
|
|
8b2711e32a | ||
|
|
9776cc8047 | ||
|
|
3b0591e0ba | ||
|
|
e62f158c22 |
@@ -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
|
||||||
|
|||||||
+4
-2
@@ -1,4 +1,4 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。
|
# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
@@ -16,6 +16,9 @@ MobileGLCodeManager
|
|||||||
MobileGL/MG_Test/build
|
MobileGL/MG_Test/build
|
||||||
/build_*
|
/build_*
|
||||||
/cmake-build*
|
/cmake-build*
|
||||||
|
/build-*/
|
||||||
|
/local.properties
|
||||||
|
/.jspace/
|
||||||
.idea
|
.idea
|
||||||
MobileGL/MG*/build*
|
MobileGL/MG*/build*
|
||||||
MobileGL/MG*/cmake-build*
|
MobileGL/MG*/cmake-build*
|
||||||
@@ -27,4 +30,3 @@ MobileGL/MG*/cmake-build*
|
|||||||
tools/trace_replay/work/
|
tools/trace_replay/work/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
/.gradle
|
|
||||||
|
|||||||
Vendored
+1
-1
Submodule 3rdparty/apitrace updated: c8036190fc...10935bb5e4
Vendored
+1
-1
Submodule 3rdparty/glslang updated: fa562bb911...6f12598784
+43
-25
@@ -199,7 +199,6 @@ set(SPIRV_REFLECT_ENABLE_ASSERTS OFF CACHE BOOL "Enable asserts for debugging"
|
|||||||
set(SPIRV_REFLECT_ENABLE_ASAN OFF CACHE BOOL "Use address sanitization" FORCE)
|
set(SPIRV_REFLECT_ENABLE_ASAN OFF CACHE BOOL "Use address sanitization" FORCE)
|
||||||
set(SPIRV_REFLECT_INSTALL OFF CACHE BOOL "Whether to install" FORCE)
|
set(SPIRV_REFLECT_INSTALL OFF CACHE BOOL "Whether to install" FORCE)
|
||||||
|
|
||||||
# add_subdirectory(3rdparty/DiligentCore)
|
|
||||||
add_subdirectory(3rdparty/glslang)
|
add_subdirectory(3rdparty/glslang)
|
||||||
add_subdirectory(3rdparty/SPIRV-Cross)
|
add_subdirectory(3rdparty/SPIRV-Cross)
|
||||||
add_subdirectory(3rdparty/VulkanMemoryAllocator)
|
add_subdirectory(3rdparty/VulkanMemoryAllocator)
|
||||||
@@ -211,6 +210,23 @@ set(XXHASH_BUILD_XXHSUM OFF)
|
|||||||
option(BUILD_SHARED_LIBS OFF)
|
option(BUILD_SHARED_LIBS OFF)
|
||||||
add_subdirectory(3rdparty/xxHash/build/cmake xxhash_build EXCLUDE_FROM_ALL)
|
add_subdirectory(3rdparty/xxHash/build/cmake xxhash_build EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
|
# Diligent-based backend. Enabled by default on local builds; only the Vulkan
|
||||||
|
# engine from DiligentCore is built. Added after the other 3rdparty projects so
|
||||||
|
# DiligentCore reuses the glslang / SPIRV-Cross / SPIRV-Tools / xxHash targets
|
||||||
|
# already defined by MobileGL instead of building its bundled copies.
|
||||||
|
option(MOBILEGL_ENABLE_DILIGENT "Enable the Diligent/Vulkan backend" ON)
|
||||||
|
if(MOBILEGL_ENABLE_DILIGENT)
|
||||||
|
set(DILIGENT_NO_DIRECT3D11 ON CACHE BOOL "Disable Direct3D11 backend" FORCE)
|
||||||
|
set(DILIGENT_NO_DIRECT3D12 ON CACHE BOOL "Disable Direct3D12 backend" FORCE)
|
||||||
|
set(DILIGENT_NO_OPENGL ON CACHE BOOL "Disable OpenGL backend" FORCE)
|
||||||
|
set(DILIGENT_NO_METAL ON CACHE BOOL "Disable Metal backend" FORCE)
|
||||||
|
set(DILIGENT_NO_WEBGPU ON CACHE BOOL "Disable WebGPU backend" FORCE)
|
||||||
|
set(DILIGENT_NO_ARCHIVER ON CACHE BOOL "Disable Archiver" FORCE)
|
||||||
|
set(DILIGENT_BUILD_TESTS OFF CACHE BOOL "Build Diligent tests" FORCE)
|
||||||
|
set(DILIGENT_INSTALL_CORE OFF CACHE BOOL "Install DiligentCore" FORCE)
|
||||||
|
add_subdirectory(3rdparty/DiligentCore)
|
||||||
|
endif()
|
||||||
|
|
||||||
set(TRACY_ENABLE ${MOBILEGL_ENABLE_TRACY} CACHE BOOL "Enable Tracy, this is an internal variable" FORCE)
|
set(TRACY_ENABLE ${MOBILEGL_ENABLE_TRACY} CACHE BOOL "Enable Tracy, this is an internal variable" FORCE)
|
||||||
|
|
||||||
if (TRACY_ENABLE)
|
if (TRACY_ENABLE)
|
||||||
@@ -270,7 +286,6 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
|
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
|
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp
|
|
||||||
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
|
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
||||||
@@ -279,40 +294,26 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
||||||
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/FlattenFloat64StorageBlockPass.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/UniquifyIoBlockNamesPass.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/FixIterationRPBarrierPass.cpp
|
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.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/Lower1DSampledImagesPass.cpp
|
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.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
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
|
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
|
|
||||||
|
|
||||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||||
|
|
||||||
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
|
||||||
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
||||||
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
|
||||||
|
|
||||||
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
||||||
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
||||||
@@ -396,7 +397,6 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.cpp
|
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
||||||
@@ -412,6 +412,14 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
|
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(MOBILEGL_ENABLE_DILIGENT)
|
||||||
|
list(APPEND SOURCE_FILES
|
||||||
|
MobileGL/MG_Backend/Diligent/BackendObject_Diligent.cpp
|
||||||
|
MobileGL/MG_Backend/Diligent/DiligentVulkan.cpp
|
||||||
|
MobileGL/MG_Backend/Diligent/Renderer/DiligentRenderer.cpp
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if (APPLE AND NOT MOBILEGL_IOS)
|
if (APPLE AND NOT MOBILEGL_IOS)
|
||||||
list(APPEND SOURCE_FILES
|
list(APPEND SOURCE_FILES
|
||||||
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
|
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
|
||||||
@@ -452,6 +460,21 @@ set(MOBILEGL_LINK_LIBRARIES
|
|||||||
Threads::Threads
|
Threads::Threads
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(MOBILEGL_ENABLE_DILIGENT)
|
||||||
|
list(APPEND MOBILEGL_LINK_LIBRARIES
|
||||||
|
Diligent-GraphicsEngineVk-static
|
||||||
|
Diligent-GraphicsEngine
|
||||||
|
Diligent-GraphicsEngineNextGenBase
|
||||||
|
Diligent-GraphicsAccessories
|
||||||
|
Diligent-ShaderTools
|
||||||
|
Diligent-GraphicsTools
|
||||||
|
Diligent-Common
|
||||||
|
Diligent-Primitives
|
||||||
|
Diligent-TargetPlatform
|
||||||
|
Vulkan::Headers
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
set(MOBILEGL_COMPILE_DEF
|
set(MOBILEGL_COMPILE_DEF
|
||||||
-DVMA_STATIC_VULKAN_FUNCTIONS=0
|
-DVMA_STATIC_VULKAN_FUNCTIONS=0
|
||||||
-DVMA_DYNAMIC_VULKAN_FUNCTIONS=1
|
-DVMA_DYNAMIC_VULKAN_FUNCTIONS=1
|
||||||
@@ -474,7 +497,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
|
||||||
@@ -517,6 +540,7 @@ target_compile_definitions(${CMAKE_PROJECT_NAME}
|
|||||||
${MOBILEGL_COMPILE_DEF}
|
${MOBILEGL_COMPILE_DEF}
|
||||||
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
|
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
|
||||||
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>:MOBILEGL_TRACE_ANGLE_VARIANTS=1>
|
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>:MOBILEGL_TRACE_ANGLE_VARIANTS=1>
|
||||||
|
$<$<BOOL:${MOBILEGL_ENABLE_DILIGENT}>:MOBILEGL_ENABLE_DILIGENT=1>
|
||||||
)
|
)
|
||||||
|
|
||||||
if(UNIX AND NOT APPLE AND NOT ANDROID)
|
if(UNIX AND NOT APPLE AND NOT ANDROID)
|
||||||
@@ -575,6 +599,7 @@ if(NOT ANDROID)
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
${MOBILEGL_COMPILE_DEF}
|
${MOBILEGL_COMPILE_DEF}
|
||||||
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
|
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
|
||||||
|
$<$<BOOL:${MOBILEGL_ENABLE_DILIGENT}>:MOBILEGL_ENABLE_DILIGENT=1>
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -686,10 +711,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()
|
|
||||||
|
|||||||
@@ -0,0 +1,278 @@
|
|||||||
|
# Handoff: Diligent/Vulkan GL3.2 Backend for MobileGL
|
||||||
|
|
||||||
|
Date: 2026-08-18
|
||||||
|
Branch: `feat/diligent-vulkan-backend`
|
||||||
|
Repo: `~/MobileGL-dev`
|
||||||
|
Status: **Active work-in-progress. Do not mark complete yet.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Goal
|
||||||
|
|
||||||
|
Implement a complete OpenGL 3.2 front-end emulation on a new Diligent/Vulkan backend inside MobileGL, instead of the DirectVulkan / DirectGLES backends.
|
||||||
|
|
||||||
|
Target state:
|
||||||
|
- Fully wire MobileGL front-end `MG_State` (buffers, VAO, program, texture, sampler, framebuffer, render-state) into Diligent.
|
||||||
|
- Implement all GL 3.2 core entry points through the Diligent backend.
|
||||||
|
- Pass local non-Android GL3.2 tests on the Turnip Adreno 750 GPU.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Current Branch / Commits
|
||||||
|
|
||||||
|
Latest 12 commits on `feat/diligent-vulkan-backend`:
|
||||||
|
|
||||||
|
```
|
||||||
|
2f5abf83 test(diligent): verify indexed DrawElements path from real frontend state
|
||||||
|
c31b7381 feat(diligent): add basic texture binding and textured state-draw test
|
||||||
|
8945c507 feat(diligent): clear depth in GL Clear when GL_DEPTH_BUFFER_BIT set
|
||||||
|
558d3aea feat(diligent): add offscreen depth target and depth clear
|
||||||
|
7e2f0bc8 feat(diligent): wire stencil and color-mask state into state PSO
|
||||||
|
f99786f6 feat(diligent): wire viewport/scissor state into state draws
|
||||||
|
be4cc3ce feat(diligent): wire blend/depth/cull render state into state PSO
|
||||||
|
c855e6cf feat(diligent): verify state-driven draw with real MobileGL frontend state
|
||||||
|
02e60bfa feat(diligent): add state-driven draw path (VAO/buffer/program to Diligent)
|
||||||
|
a9515c92 feat(diligent): add dynamic vertex buffer upload path
|
||||||
|
2f57582a feat(diligent): wire Clear/Draw/Present into GLFunctionsTable
|
||||||
|
beb21123 feat(diligent): add real offscreen renderer with clear and triangle draw
|
||||||
|
```
|
||||||
|
|
||||||
|
Working tree is clean.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Key Files
|
||||||
|
|
||||||
|
### Backend core
|
||||||
|
|
||||||
|
- `MobileGL/MG_Backend/Diligent/BackendObject_Diligent.h/.cpp`
|
||||||
|
- `BackendObject_Diligent`
|
||||||
|
- Creates Diligent Vulkan device/context
|
||||||
|
- Owns `DiligentRenderer`
|
||||||
|
- Wires `GLFunctionsTable`:
|
||||||
|
- `Clear` (color + depth)
|
||||||
|
- `DrawArrays`
|
||||||
|
- `DrawElements`
|
||||||
|
- `Present`
|
||||||
|
- `MobileGL/MG_Backend/Diligent/DiligentVulkan.h/.cpp`
|
||||||
|
- Backend identity helper / translation unit
|
||||||
|
- `MobileGL/MG_Backend/Diligent/Renderer/DiligentRenderer.h/.cpp`
|
||||||
|
- Offscreen RGBA8 + D32F targets
|
||||||
|
- Clear / ClearDepth / DrawTriangle / DrawVertices
|
||||||
|
- `CreateTestTexture` (RGBA8 texture + SRV + sampler)
|
||||||
|
- `DrawFromState` (main front-end emulation draw path)
|
||||||
|
- `CreatePipelineFromState`:
|
||||||
|
- SPIR-V → Diligent shaders via SPIRV-Reflect
|
||||||
|
- VAO attributes → input layout
|
||||||
|
- primitive topology from GL mode
|
||||||
|
- blend / depth / cull / stencil / color-mask state
|
||||||
|
- `UploadVertexDataFromState`:
|
||||||
|
- packs enabled VAO attributes from `BufferObject` into interleaved vertex buffer
|
||||||
|
- supports `DrawArrays`, `DrawElements`, triangle-fan and line-loop expansion
|
||||||
|
- Static texture binding to `g_Texture` through PSO static variables + SRB
|
||||||
|
|
||||||
|
### Integration changes
|
||||||
|
|
||||||
|
- `CMakeLists.txt`
|
||||||
|
- New option `MOBILEGL_ENABLE_DILIGENT` (default ON for local)
|
||||||
|
- DiligentCore added **after** glslang/SPIRV-Cross/xxHash/Vulkan-Headers so it reuses existing CMake targets
|
||||||
|
- Diligent static libraries linked into `MobileGL` / `MobileGL_s`
|
||||||
|
- New Diligent backend sources added
|
||||||
|
- `MobileGL/MG_Backend/BackendObject.h`
|
||||||
|
- New `BackendType::DiligentVulkan`
|
||||||
|
- `MobileGL/MG_Backend/Init.cpp`
|
||||||
|
- New backend switch case
|
||||||
|
- `MobileGL/ConfigLoader.cpp`
|
||||||
|
- `MOBILEGL_BACKEND_TYPE=DiligentVulkan` accepted
|
||||||
|
- `MobileGL/MG_Test/CMakeLists.txt`
|
||||||
|
- New `MobileGL/MG_Test/Backend/Diligent` subdirectory
|
||||||
|
- `MobileGL/MG_Test/Backend/Diligent/`
|
||||||
|
- `CMakeLists.txt`
|
||||||
|
- `SanityTest.cpp`
|
||||||
|
|
||||||
|
### Local test files
|
||||||
|
|
||||||
|
- `MobileGL/MG_Test/Backend/Diligent/SanityTest.cpp`
|
||||||
|
- `CreatesDiligentDeviceAndAdvertisesGL32`
|
||||||
|
- `ClearsAndDrawsTriangleOffscreen`
|
||||||
|
- `DrawsFromMobileGLState`
|
||||||
|
- `DrawsTexturedFromMobileGLState`
|
||||||
|
- `DrawsIndexedFromMobileGLState`
|
||||||
|
- `DrawsRealTexturedFromMobileGLState`
|
||||||
|
- `DrawsUniformFromMobileGLState`
|
||||||
|
- `DrawsToOffscreenFramebufferFromMobileGLState`
|
||||||
|
- `DrawsWithScissorFromMobileGLState`
|
||||||
|
- `DrawsWithBlendFromMobileGLState`
|
||||||
|
- `DrawsWithDepthTestFromMobileGLState`
|
||||||
|
- `DrawsNamedUniformBlockFromMobileGLState`
|
||||||
|
- `DrawsWithStencilTestFromMobileGLState`
|
||||||
|
- `DrawsToRenderbufferFramebufferFromMobileGLState`
|
||||||
|
- `DrawsToMultipleColorAttachmentsFromMobileGLState`
|
||||||
|
- `DrawsIndexedBaseVertexFromMobileGLState`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. What Works Today
|
||||||
|
|
||||||
|
Verified locally on Turnip Adreno 750:
|
||||||
|
|
||||||
|
- Diligent device/context creation
|
||||||
|
- EGL window-surface swapchain creation path through Diligent `ISwapChain` (offscreen tests still use the offscreen target)
|
||||||
|
- GL 3.2 / GLSL 1.50 capability advertisement
|
||||||
|
- Offscreen color + depth rendering
|
||||||
|
- Clear color and depth
|
||||||
|
- Real mobilegl front-end state-driven drawing:
|
||||||
|
- Program SPIR-V → Diligent shaders
|
||||||
|
- VAO attributes + bound GL buffer → interleaved vertex buffer
|
||||||
|
- `DrawArrays` path
|
||||||
|
- `DrawElements` path (index buffer)
|
||||||
|
- Texture basics:
|
||||||
|
- Offscreen texture creation
|
||||||
|
- CPU → Diligent texture (`CreateTestTexture`)
|
||||||
|
- Static sampler2D binding to `g_Texture`
|
||||||
|
- Textured draw test passes
|
||||||
|
- Render state:
|
||||||
|
- Blend enable/factors/equations
|
||||||
|
- Stencil clear + test enabled on a D24S8 default depth/stencil target
|
||||||
|
- Depth test enable/func/write mask
|
||||||
|
- Cull face enable/mode/front-face winding
|
||||||
|
- Stencil test enable/masks/ops/func/ref
|
||||||
|
- Color write mask
|
||||||
|
- Viewport
|
||||||
|
- Scissor rect
|
||||||
|
- Texture/sampler full integration:
|
||||||
|
- `ITextureObject` → Diligent `ITexture` + SRV with automatic dirty upload
|
||||||
|
- `SamplerObject` / texture-object sampler → Diligent `ISampler`
|
||||||
|
- Real front-end `glTexImage2D` path (not only `CreateTestTexture`) verified
|
||||||
|
- Global UBO upload:
|
||||||
|
- Front-end `glUniform*` shadow → Diligent uniform buffer bound as `MGL_GLOBAL_UBO`
|
||||||
|
- User framebuffer mapping:
|
||||||
|
- Current draw/read FBO resolves texture attachments to Diligent RTV/DSV
|
||||||
|
- `ReadPixels` can read back from a user FBO color attachment
|
||||||
|
- More GL entry points wired:
|
||||||
|
- `DrawRangeElements` / `DrawRangeElementsBaseVertex`
|
||||||
|
- `DrawElementsBaseVertex` with real baseVertex selection
|
||||||
|
- `MultiDrawArrays` / `MultiDrawElements` / `MultiDrawElementsBaseVertex`
|
||||||
|
- `DrawArraysInstanced` / `DrawElementsInstanced` family
|
||||||
|
- Indirect draw CPU fallback: `DrawArraysIndirect`, `DrawElementsIndirect`, `MultiDraw*Indirect`, `*IndirectCount`
|
||||||
|
- `ClearBufferfv` / `ClearBufferfi` / `ClearBufferiv` / `ClearBufferuiv` (incl. stencil clear)
|
||||||
|
- `BlitFramebuffer` / `BlitNamedFramebuffer` (same-size color copy between read/draw FBOs)
|
||||||
|
- `CopyTexImage2D` / `CopyTexSubImage2D` (whole-color copy fallback)
|
||||||
|
- `CopyImageSubData` (whole-texture copy between two texture objects)
|
||||||
|
- `GenerateMipmap` (Diligent GPU mip generation on state textures)
|
||||||
|
- `GetTexImage` / `GetTextureImage` (RGBA8 readback)
|
||||||
|
- Fence sync entries (`FenceSync` / `ClientWaitSync` / `WaitSync` / `DeleteSync` / `GetSyncStatus`) as CPU always-signaled fallback
|
||||||
|
- Timer query entries (`BeginTimeElapsedQuery` / `EndTimeElapsedQuery` / `QueryCounterTimestamp` / `GetQueryResult64` etc.) as CPU `steady_clock` fallback
|
||||||
|
- `ReadPixels` from default and user color attachments
|
||||||
|
- Primitive expansion:
|
||||||
|
- `GL_TRIANGLE_FAN` expanded to triangle list
|
||||||
|
- `GL_LINE_LOOP` expanded to line strip
|
||||||
|
- Local test result:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ PASSED ] 16 tests
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. How to Build and Run Locally
|
||||||
|
|
||||||
|
From repo root `~/MobileGL-dev`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -S . -B build-diligent -G Ninja \
|
||||||
|
-DCMAKE_BUILD_TYPE=Debug \
|
||||||
|
-DMOBILEGL_ENABLE_DILIGENT=ON \
|
||||||
|
-DMOBILEGL_BUILD_TEST=ON \
|
||||||
|
-DMOBILEGL_BUILD_BENCHMARK=OFF \
|
||||||
|
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST="$PWD/3rdparty/DiligentCore/ThirdParty/googletest"
|
||||||
|
|
||||||
|
cmake --build build-diligent --target DiligentVulkanSanityTest -j 4
|
||||||
|
|
||||||
|
./build-diligent/MobileGL/MG_Test/Backend/Diligent/DiligentVulkanSanityTest --gtest_color=no
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `MOBILEGL_BUILD_BENCHMARK=OFF` avoids network fetch of google/benchmark in this environment.
|
||||||
|
- `FETCHCONTENT_SOURCE_DIR_GOOGLETEST` pins googletest to DiligentCore's bundled copy, avoiding flaky network clone.
|
||||||
|
- Max 4 cores is intentional: use `-j 4`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Environment Notes
|
||||||
|
|
||||||
|
- Host: Linux `aarch64`, glibc 2.43 (Fedora container on Android/Droidspaces)
|
||||||
|
- GPU: Turnip Adreno 750, Vulkan API 1.4.354
|
||||||
|
- GPU nodes available:
|
||||||
|
- `/dev/dri/renderD128`
|
||||||
|
- `/dev/kgsl-3d0`
|
||||||
|
- Android SDK/NDK: `~/android-sdk` (aarch64 glibc)
|
||||||
|
- NDK `27.3.13750724`
|
||||||
|
- CMake `3.22.1`
|
||||||
|
- JDK/Gradle for APK builds:
|
||||||
|
- `~/android-build-tools/jdk17`
|
||||||
|
- `~/android-build-tools/gradle/gradle-8.10.2`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Known Limitations / Not Yet Implemented
|
||||||
|
|
||||||
|
- User framebuffers now support texture color attachments, renderbuffer color readback, multiple simultaneous color targets, and depth/stencil texture or renderbuffer attachments.
|
||||||
|
- Textures auto-sync `ITextureObject` → Diligent resources, including mip levels and sampler state; compressed textures and integer/3-channel formats that Diligent lacks are still skipped.
|
||||||
|
- Global UBO (default-block `glUniform*`) and named application UBO blocks (through `glBindBufferBase`/`glUniformBlockBinding`) now upload and bind; SSBOs are still not fed from frontend buffer bindings.
|
||||||
|
- Swapchain creation and resize are wired for native EGL window surfaces via `Diligent::ISwapChain`; `Present()` presents the active swap chain when present and otherwise flushes the offscreen target. Actual on-screen EGL presentation is still untested in this headless environment, and the X11 display/connection fields are not yet plumbed through `WindowHandle`. `SetSwapInterval` now forwards the requested sync interval to `ISwapChain::Present()`.
|
||||||
|
- No transform feedback / GPU-accelerated queries / non-color readback; fence sync and timer queries use CPU fallbacks.
|
||||||
|
- Draw range, multi-draw, instanced-draw wrappers, clear-buffer, blit, read-pixels, CopyTexImage*, CopyImageSubData, GenerateMipmap, GetTexImage/GetTextureImage and indirect draws are now wired; buffer subdata paths still remain.
|
||||||
|
- A last-PSO cache now avoids recreating the pipeline when program/render-state/topology/VAO layout is unchanged; texture/UBO resources are still rebound dynamically per draw.
|
||||||
|
- The `GLFunctionsTable` is only partially populated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Recommended Next Steps
|
||||||
|
|
||||||
|
1. **Framebuffer / Renderbuffer mapping**
|
||||||
|
- [x] Map `MG_State::GLState::FramebufferObject` attachments to Diligent `ITextureView` / `ITexture`.
|
||||||
|
- [x] Support default framebuffer as current offscreen target.
|
||||||
|
- [x] Support `glBindFramebuffer`, `glFramebufferTexture2D`, renderbuffer color/depth attachments and renderbuffer color readback.
|
||||||
|
- [x] Multiple simultaneous color attachments.
|
||||||
|
|
||||||
|
2. **Texture / Sampler full integration**
|
||||||
|
- [x] Translate MobileGL `ITextureObject` to Diligent `ITexture` and cache by `GetLifetimeId()`.
|
||||||
|
- [x] Propagate texture unit bindings into the PSO SRB.
|
||||||
|
- [x] Translate `SamplerObject` state into Diligent `SamplerDesc`.
|
||||||
|
|
||||||
|
3. **Uniform / UBO support**
|
||||||
|
- [x] Create Diligent buffer for `ProgramObject::GetUBOData()` / `GetUBOSize()`.
|
||||||
|
- [x] Bind the global UBO as a dynamic shader resource.
|
||||||
|
- [x] Handle per-program uniform block bindings / named UBO blocks.
|
||||||
|
|
||||||
|
4. **PSO / resource caching**
|
||||||
|
- [~] Cache PSOs by program + VAO config + render state + topology (single last-PSO fast path).
|
||||||
|
- [~] Cache textures and samplers; buffers/SRBs can still be re-bound per draw.
|
||||||
|
|
||||||
|
5. **More GL 3.2 entry points**
|
||||||
|
- [x] `DrawRangeElements`
|
||||||
|
- [x] `MultiDraw*`
|
||||||
|
- [x] `BlitFramebuffer` (same-size color copy)
|
||||||
|
- [x] `ReadPixels` from non-default framebuffer
|
||||||
|
- [x] `CopyTexImage*` / `CopyImageSubData` wired as whole-resource copies
|
||||||
|
- [x] `GetTexImage` / `GetTextureImage` (RGBA8)
|
||||||
|
- [x] Indirect draws (CPU fallback)
|
||||||
|
|
||||||
|
6. **Expand local test suite**
|
||||||
|
- [x] Scissor test
|
||||||
|
- [x] Blend test
|
||||||
|
- [x] Texture filtering / sampler state test
|
||||||
|
- [x] framebuffer offscreen render-to-texture test
|
||||||
|
- [x] Depth test visual test
|
||||||
|
- [x] Stencil test
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Handoff Notes for Next Agent
|
||||||
|
|
||||||
|
- Do **not** reference `origin/Deprecated/Feat/Diligent`; that old implementation is intentionally ignored.
|
||||||
|
- Work from this branch, keep tests green.
|
||||||
|
- The command `./build-diligent/.../DiligentVulkanSanityTest` runs all 5 Diligent tests.
|
||||||
|
- If a new test crashes during shader resource binding, remember Diligent texture SRVs need a sampler attached via `ITextureView::SetSampler()` before `InitializeStaticSRBResources()`.
|
||||||
|
- When re-creating a PSO or buffer, call `Release()` (or assign `nullptr`) before the create call to avoid Diligent debug “Overwriting reference” assertions.
|
||||||
+1
-56
@@ -78,41 +78,8 @@ namespace MobileGL::MG_Config {
|
|||||||
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
|
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
|
||||||
String TraceAngleVariant;
|
String TraceAngleVariant;
|
||||||
#endif
|
#endif
|
||||||
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
|
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
|
||||||
// including the opt-in emulated compute path below.
|
|
||||||
Bool DisableSubgroup = false;
|
Bool DisableSubgroup = false;
|
||||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
|
|
||||||
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
|
|
||||||
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
|
|
||||||
// engages when this flag is set AND the device has no native subgroup support at
|
|
||||||
// all - a device with real subgroup operations always uses them natively,
|
|
||||||
// whatever their width (the known iterationRP defect is patched by
|
|
||||||
// FixIterationRPSubgroupScratch below instead). Off by default.
|
|
||||||
Bool MagmaEmulateSubgroup = false;
|
|
||||||
// MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
|
|
||||||
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
|
|
||||||
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
|
|
||||||
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
|
|
||||||
// bounds. The pass grows that one array to what the device's topology needs and
|
|
||||||
// touches nothing else; it only rewrites modules positively matching the pack's
|
|
||||||
// reduction fingerprint (ShaderTranspiler::FixIterationRPSubgroupScratchPass),
|
|
||||||
// so every other shader passes through byte-identical - as does iterationRP
|
|
||||||
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
|
|
||||||
// verbatim.
|
|
||||||
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
|
|
||||||
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
|
|
||||||
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
|
|
||||||
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
|
|
||||||
// dispatch emits IDs 0..7, and the derived value is the one Vulkan guarantees
|
|
||||||
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
|
|
||||||
// does whenever local_size_x is a multiple of the native width). ForceOff returns
|
|
||||||
// to the raw driver builtin.
|
|
||||||
QuirkOverride DeriveNumSubgroups = QuirkOverride::Auto;
|
|
||||||
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
||||||
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||||
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||||
@@ -203,28 +170,6 @@ namespace MobileGL::MG_Config {
|
|||||||
// immediately stay serial by their own construction). Off by default; never
|
// immediately stay serial by their own construction). Off by default; never
|
||||||
// advertise it.
|
// advertise it.
|
||||||
QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto;
|
QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto;
|
||||||
// MOBILEGL_SHADER_CACHE: the three-level, in-memory shader translation memo
|
|
||||||
// (MG_Util/ShaderTranspiler/TranslationCache.h). The levels follow the GL
|
|
||||||
// entry points - L1c memoizes one glCompileShader's PARSE VERDICT, L1 a
|
|
||||||
// linked program's whole front end, L2 DirectGLES's emitted ESSL. Auto is
|
|
||||||
// ON; ForceOff turns ALL THREE off and makes every translation run from
|
|
||||||
// scratch. The escape hatch exists because a wrong cache hit is a silently
|
|
||||||
// miscompiled shader: if a device ever renders differently with the cache
|
|
||||||
// on, one run with this falsy says so.
|
|
||||||
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
|
|
||||||
// MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
|
|
||||||
// emulation - the builtin becomes a flat varying, the fragment stage gets a
|
|
||||||
// per-pass gate, and a routed draw is REPLAYED once per distinct viewport state
|
|
||||||
// with the real glViewport/glScissor/glDepthRangef set for it. Auto is ON, and
|
|
||||||
// it is ON even where the driver advertises GL_OES_viewport_array, because that
|
|
||||||
// extension only ever gave the SHADER a compilable name: MobileGL has never
|
|
||||||
// programmed a driver's INDEXED viewport state (SyncRenderState pushes index 0
|
|
||||||
// and nothing else), so on an extension-capable driver every index rasterized as
|
|
||||||
// index 0 exactly as it did without one. ForceOff returns to that behaviour -
|
|
||||||
// the pre-emulation path, extension passthrough where it exists and
|
|
||||||
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
|
|
||||||
// the negative control the emulation is measured against.
|
|
||||||
QuirkOverride ViewportArrayEmulation = QuirkOverride::Auto;
|
|
||||||
};
|
};
|
||||||
extern FeaturesTable Features;
|
extern FeaturesTable Features;
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|||||||
@@ -168,11 +168,6 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||||
#endif
|
#endif
|
||||||
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
||||||
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
|
|
||||||
features.FixIterationRPSubgroupScratch =
|
|
||||||
QueryEnvQuirkOverride("MOBILEGL_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
|
||||||
features.IterationRPFixBarrier = QueryEnvFlag("MOBILEGL_ITERATIONRP_FIX_BARRIER");
|
|
||||||
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");
|
||||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||||
@@ -194,9 +189,6 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
||||||
features.AsyncOptimisticShaderStatus =
|
features.AsyncOptimisticShaderStatus =
|
||||||
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
||||||
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
|
|
||||||
features.ViewportArrayEmulation =
|
|
||||||
QueryEnvQuirkOverride("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void InitBackendType() {
|
inline void InitBackendType() {
|
||||||
@@ -210,6 +202,7 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
}
|
}
|
||||||
ENTRY(DirectGLES)
|
ENTRY(DirectGLES)
|
||||||
ENTRY(DirectVulkan)
|
ENTRY(DirectVulkan)
|
||||||
|
ENTRY(DiligentVulkan)
|
||||||
ENTRY(Unknown)
|
ENTRY(Unknown)
|
||||||
MG_Config::ActiveBackendType = BackendType::Unknown;
|
MG_Config::ActiveBackendType = BackendType::Unknown;
|
||||||
#undef ENTRY
|
#undef ENTRY
|
||||||
|
|||||||
@@ -15,11 +15,8 @@
|
|||||||
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
||||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||||
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
|
||||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
|
|
||||||
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -54,11 +51,6 @@ namespace MobileGL {
|
|||||||
// before a re-initialized library could pair them with the wrong
|
// before a re-initialized library could pair them with the wrong
|
||||||
// backend's DeleteSync).
|
// backend's DeleteSync).
|
||||||
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
||||||
// Queries die with their contexts for the same reason, and their registry
|
|
||||||
// is the same shape of process-global map: drain it here too, while the
|
|
||||||
// function table can still pair each backend handle with the backend that
|
|
||||||
// minted it.
|
|
||||||
MG_Impl::GLImpl::DestroyAllQueryObjects();
|
|
||||||
MG_Backend::pActiveBackendObject.reset();
|
MG_Backend::pActiveBackendObject.reset();
|
||||||
MG_State::pGLContext.reset();
|
MG_State::pGLContext.reset();
|
||||||
MG_State::pEGLContext.reset();
|
MG_State::pEGLContext.reset();
|
||||||
@@ -74,14 +66,6 @@ namespace MobileGL {
|
|||||||
// built-in symbol tables the prewarm latch stands for, so leaving it set would
|
// built-in symbol tables the prewarm latch stands for, so leaving it set would
|
||||||
// make the next Initialize() skip a prewarm it genuinely needs.
|
// make the next Initialize() skip a prewarm it genuinely needs.
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch();
|
MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch();
|
||||||
// The two-level translation memo. Nothing in it references a glslang object -
|
|
||||||
// both levels hold plain bytes - so this is RSS hygiene rather than a lifetime
|
|
||||||
// requirement, and it is safe either side of FinalizeProcess. Stats first: an
|
|
||||||
// fordebug build gets one line per level saying how the run went.
|
|
||||||
MG_Util::ShaderTranspiler::LogShaderTranslationCacheStats();
|
|
||||||
MG_Util::ShaderTranspiler::ClearShaderTranslationCaches();
|
|
||||||
MG_State::GLState::LogProgramTranslationCacheStats();
|
|
||||||
MG_State::GLState::ClearProgramTranslationCache();
|
|
||||||
MG_Backend::gBackendFunctionsTable = {};
|
MG_Backend::gBackendFunctionsTable = {};
|
||||||
g_isInitialized = false;
|
g_isInitialized = false;
|
||||||
if (logLifecycle) {
|
if (logLifecycle) {
|
||||||
|
|||||||
@@ -14,30 +14,17 @@ 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 {
|
||||||
DirectGLES,
|
DirectGLES,
|
||||||
DirectVulkan,
|
DirectVulkan,
|
||||||
|
DiligentVulkan,
|
||||||
BackendTypeCount,
|
BackendTypeCount,
|
||||||
Unknown = -1
|
Unknown = -1
|
||||||
};
|
};
|
||||||
|
|
||||||
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 +161,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 +237,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 +319,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;
|
||||||
@@ -372,32 +335,8 @@ namespace MobileGL {
|
|||||||
Int MaxComputeImageUniforms = 8;
|
Int MaxComputeImageUniforms = 8;
|
||||||
Int MaxDrawBuffers = 8;
|
Int MaxDrawBuffers = 8;
|
||||||
Int MaxColorAttachments = 8;
|
Int MaxColorAttachments = 8;
|
||||||
// GL_MAX_CLIP_DISTANCES. Zero is a legal answer here, not a placeholder, and a
|
|
||||||
// backend that cannot host a clip distance MUST report it: advertising eight the
|
|
||||||
// backend will refuse does not make gl_ClipDistance work, it only moves the failure
|
|
||||||
// from an honest "unsupported" at query time to a backend shader-compile error the
|
|
||||||
// frontend never surfaces, after which every draw with that program silently renders
|
|
||||||
// nothing. DirectGLES fills it from GL_EXT_clip_cull_distance, DirectVulkan from the
|
|
||||||
// shaderClipDistance device feature. The DEFAULT stays at the GL 4.3 core minimum
|
|
||||||
// because it describes the no-backend case (standalone shader compiles, unit tests),
|
|
||||||
// where there is no device to be honest about and BuildTBuiltInResource still has to
|
|
||||||
// hand glslang a workable gl_MaxClipDistances.
|
|
||||||
Int MaxClipDistances = 8;
|
Int MaxClipDistances = 8;
|
||||||
Int MaxViewports = 16;
|
Int MaxViewports = 16;
|
||||||
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a
|
|
||||||
// primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes
|
|
||||||
// GL_UNDEFINED_VERTEX a legal answer for both, and it is the honest default - naming
|
|
||||||
// a convention is a statement about behaviour, so a backend that does not pin one
|
|
||||||
// must not claim it does. DirectGLES fills the layer one from the ES 3.2 query and
|
|
||||||
// the viewport one from GL_OES_viewport_array, and leaves UNDEFINED where the
|
|
||||||
// capability is absent: without the viewport array extension only viewport 0 is ever
|
|
||||||
// rasterized, so no convention selects anything. DirectVulkan keeps UNDEFINED for
|
|
||||||
// both - which vertex provokes is decided per pipeline by
|
|
||||||
// VulkanRenderer::SelectProvokingVertexMode out of VK_EXT_provoking_vertex,
|
|
||||||
// provokingVertexModePerPipeline and the topology, so no single convention is true
|
|
||||||
// of the backend.
|
|
||||||
GLenum LayerProvokingVertex = GL_UNDEFINED_VERTEX;
|
|
||||||
GLenum ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
|
||||||
Int MaxViewportWidth = 16384;
|
Int MaxViewportWidth = 16384;
|
||||||
Int MaxViewportHeight = 16384;
|
Int MaxViewportHeight = 16384;
|
||||||
Float ViewportBoundsRangeMin = 0.0f;
|
Float ViewportBoundsRangeMin = 0.0f;
|
||||||
@@ -445,36 +384,12 @@ namespace MobileGL {
|
|||||||
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
|
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
|
||||||
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
|
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
|
||||||
}
|
}
|
||||||
// Whether this backend can CONSUME a shader module that still declares 64-bit floats,
|
|
||||||
// i.e. whether `double` survives the transpile instead of being narrowed to `float`
|
|
||||||
// (ShaderTranspiler::DemoteFloat64Pass). Detected, never assumed:
|
|
||||||
// * DirectVulkan sets it from VkPhysicalDeviceFeatures::shaderFloat64, the feature
|
|
||||||
// VUID-VkShaderModuleCreateInfo-pCode-08740 requires before a module declaring
|
|
||||||
// OpCapability Float64 may be created at all. lavapipe has it; Adreno and Mali
|
|
||||||
// both report VK_FALSE, so no real mobile device does.
|
|
||||||
// * DirectGLES can NEVER have it. GLSL ES has no 64-bit float type in any version
|
|
||||||
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES
|
|
||||||
// profile") and the demotion there is mathematically mandatory, always.
|
|
||||||
// Defaults to false so a backend that never sets it - and the no-backend case, which
|
|
||||||
// is what standalone shader compiles and the unit tests run under - keeps the
|
|
||||||
// demotion, which is the behaviour that works everywhere.
|
|
||||||
Bool SupportsShaderFloat64 = false;
|
|
||||||
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
|
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
|
||||||
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
|
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
|
||||||
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
|
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
|
||||||
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
|
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
|
||||||
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
|
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
|
||||||
// all. Defaults to false so a backend that never sets it gets the conservative answer.
|
// all. Defaults to false so a backend that never sets it gets the conservative answer.
|
||||||
//
|
|
||||||
// INDEPENDENT of SupportsShaderFloat64, and it has to be: this flag decides a VkFormat
|
|
||||||
// from the VAO ATTRIBUTE alone, which does not know what type the shader declared, and
|
|
||||||
// glVertexAttribFormat(GL_DOUBLE) feeding a plain `in vec4` is both legal and common
|
|
||||||
// (KHR-GL43.vertex_attrib_binding.basic-input-case4/5, advanced-bindingUpdate). A
|
|
||||||
// backend with native fp64 that still cannot FETCH 64 bits keeps this false and relies
|
|
||||||
// on the per-MODULE rule in ShaderCompiler::SanitizeAndOptimizeBinary instead: a vertex
|
|
||||||
// module that declares a 64-bit float INPUT is demoted whole, so the two shader-side
|
|
||||||
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
|
|
||||||
// still see one consistent world.
|
|
||||||
Bool SupportsFloat64VertexAttributes = false;
|
Bool SupportsFloat64VertexAttributes = false;
|
||||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||||
Uint32 SubgroupSize = 0;
|
Uint32 SubgroupSize = 0;
|
||||||
|
|||||||
@@ -0,0 +1,906 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/Diligent/BackendObject_Diligent.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
|
||||||
|
|
||||||
|
#include "BackendObject_Diligent.h"
|
||||||
|
#include "DiligentVulkan.h"
|
||||||
|
#include "Renderer/DiligentRenderer.h"
|
||||||
|
#include <MG_Backend/BackendObject.h>
|
||||||
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
|
||||||
|
#include <EngineFactoryVk.h>
|
||||||
|
#include <RenderDevice.h>
|
||||||
|
#include <DeviceContext.h>
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Backend::DiligentBackend {
|
||||||
|
namespace {
|
||||||
|
const RendererInfo BuildInitialRendererInfo() {
|
||||||
|
RendererInfo info;
|
||||||
|
info.RendererName = "MobileGL (Diligent/Vulkan)";
|
||||||
|
info.BackendName = "Diligent Vulkan";
|
||||||
|
info.RendererGLInfo.TargetGLVersion = {3, 2, 0};
|
||||||
|
info.RendererGLInfo.TargetGLSLVersion = {1, 50, 0};
|
||||||
|
info.RendererGLInfo.IsCompatibilityProfile = false;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
DiligentRenderer* GetActiveRenderer() {
|
||||||
|
auto* backend = dynamic_cast<BackendObject_Diligent*>(pActiveBackendObject.get());
|
||||||
|
return backend != nullptr ? backend->GetRenderer() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DrawArraysIndirectCommand {
|
||||||
|
Uint32 Count = 0;
|
||||||
|
Uint32 InstanceCount = 0;
|
||||||
|
Uint32 First = 0;
|
||||||
|
Uint32 BaseInstance = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DrawElementsIndirectCommand {
|
||||||
|
Uint32 Count = 0;
|
||||||
|
Uint32 InstanceCount = 0;
|
||||||
|
Uint32 FirstIndex = 0;
|
||||||
|
Int32 BaseVertex = 0;
|
||||||
|
Uint32 BaseInstance = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CpuTimerQuery {
|
||||||
|
std::chrono::steady_clock::time_point Start;
|
||||||
|
Uint64 TimestampNs = 0;
|
||||||
|
Bool Available = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
|
||||||
|
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||||
|
if (drawBuffer) {
|
||||||
|
drawBuffer->SyncPersistentMappedRange();
|
||||||
|
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
|
||||||
|
if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) {
|
||||||
|
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return drawBuffer->MappedData() + commandOffset;
|
||||||
|
}
|
||||||
|
if (indirect == nullptr) {
|
||||||
|
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return reinterpret_cast<const Uint8*>(indirect);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Clear(GLbitfield mask) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || MG_State::pGLContext == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||||
|
const auto& color = MG_State::pGLContext->GetClearColor();
|
||||||
|
renderer->Clear(color.x(), color.y(), color.z(), color.w());
|
||||||
|
}
|
||||||
|
if ((mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||||
|
renderer->ClearDepth(MG_State::pGLContext->GetClearDepth());
|
||||||
|
}
|
||||||
|
if ((mask & GL_STENCIL_BUFFER_BIT) != 0) {
|
||||||
|
renderer->ClearStencil(MG_State::pGLContext->GetClearStencil());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer != nullptr) {
|
||||||
|
renderer->DrawFromState(mode, first, count, 0, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer != nullptr) {
|
||||||
|
renderer->DrawFromState(mode, 0, count, type, indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||||
|
const void* indices) {
|
||||||
|
// The CPU-side UploadVertexDataFromState path already honors the selected index
|
||||||
|
// range. start/end only restrict which indices may be referenced; they do not
|
||||||
|
// change the vertex buffer layout for this backend.
|
||||||
|
(void)start;
|
||||||
|
(void)end;
|
||||||
|
DrawElements(mode, count, type, indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||||
|
const void* indices, GLint basevertex) {
|
||||||
|
(void)start;
|
||||||
|
(void)end;
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer != nullptr) {
|
||||||
|
renderer->DrawFromState(mode, 0, count, type, indices, basevertex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (count[i] > 0) {
|
||||||
|
renderer->DrawFromState(mode, first[i], count[i], 0, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (count[i] > 0) {
|
||||||
|
renderer->DrawFromState(mode, 0, count[i], type, indices[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||||
|
GLint basevertex) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer != nullptr) {
|
||||||
|
renderer->DrawFromState(mode, 0, count, type, indices, basevertex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type,
|
||||||
|
const GLvoid* const* indices, GLsizei drawcount,
|
||||||
|
const GLint* basevertex) {
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (count[i] > 0) {
|
||||||
|
DrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||||
|
basevertex != nullptr ? basevertex[i] : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || MG_State::pGLContext == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto* bytes = ResolveIndirectCommandBytes(indirect, sizeof(DrawArraysIndirectCommand),
|
||||||
|
"DrawArraysIndirect");
|
||||||
|
if (bytes == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DrawArraysIndirectCommand cmd{};
|
||||||
|
std::memcpy(&cmd, bytes, sizeof(cmd));
|
||||||
|
if (cmd.Count == 0 || cmd.InstanceCount == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Uint32 i = 0; i < cmd.InstanceCount; ++i) {
|
||||||
|
renderer->DrawFromState(mode, static_cast<GLint>(cmd.First), static_cast<GLsizei>(cmd.Count),
|
||||||
|
0, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || MG_State::pGLContext == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||||
|
if (indexSize == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto* bytes = ResolveIndirectCommandBytes(indirect, sizeof(DrawElementsIndirectCommand),
|
||||||
|
"DrawElementsIndirect");
|
||||||
|
if (bytes == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DrawElementsIndirectCommand cmd{};
|
||||||
|
std::memcpy(&cmd, bytes, sizeof(cmd));
|
||||||
|
if (cmd.Count == 0 || cmd.InstanceCount == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const void* indices = reinterpret_cast<const void*>(static_cast<SizeT>(cmd.FirstIndex) * indexSize);
|
||||||
|
for (Uint32 i = 0; i < cmd.InstanceCount; ++i) {
|
||||||
|
renderer->DrawFromState(mode, 0, static_cast<GLsizei>(cmd.Count), type, indices,
|
||||||
|
static_cast<GLint>(cmd.BaseVertex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || MG_State::pGLContext == nullptr || drawcount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const GLsizei realStride = stride == 0 ? static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand)) : stride;
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
const auto* bytes = ResolveIndirectCommandBytes(
|
||||||
|
static_cast<const Uint8*>(indirect) + static_cast<SizeT>(i) * static_cast<SizeT>(realStride),
|
||||||
|
sizeof(DrawArraysIndirectCommand), "MultiDrawArraysIndirect");
|
||||||
|
if (bytes == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DrawArraysIndirectCommand cmd{};
|
||||||
|
std::memcpy(&cmd, bytes, sizeof(cmd));
|
||||||
|
if (cmd.Count == 0 || cmd.InstanceCount == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (Uint32 instance = 0; instance < cmd.InstanceCount; ++instance) {
|
||||||
|
renderer->DrawFromState(mode, static_cast<GLint>(cmd.First),
|
||||||
|
static_cast<GLsizei>(cmd.Count), 0, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount,
|
||||||
|
GLsizei stride) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || MG_State::pGLContext == nullptr || drawcount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||||
|
if (indexSize == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const GLsizei realStride = stride == 0 ? static_cast<GLsizei>(sizeof(DrawElementsIndirectCommand)) : stride;
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
const auto* bytes = ResolveIndirectCommandBytes(
|
||||||
|
static_cast<const Uint8*>(indirect) + static_cast<SizeT>(i) * static_cast<SizeT>(realStride),
|
||||||
|
sizeof(DrawElementsIndirectCommand), "MultiDrawElementsIndirect");
|
||||||
|
if (bytes == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DrawElementsIndirectCommand cmd{};
|
||||||
|
std::memcpy(&cmd, bytes, sizeof(cmd));
|
||||||
|
if (cmd.Count == 0 || cmd.InstanceCount == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const void* indices = reinterpret_cast<const void*>(static_cast<SizeT>(cmd.FirstIndex) * indexSize);
|
||||||
|
for (Uint32 instance = 0; instance < cmd.InstanceCount; ++instance) {
|
||||||
|
renderer->DrawFromState(mode, 0, static_cast<GLsizei>(cmd.Count), type, indices,
|
||||||
|
static_cast<GLint>(cmd.BaseVertex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
|
||||||
|
GLsizei maxdrawcount, GLsizei stride) {
|
||||||
|
if (MG_State::pGLContext == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto paramBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||||
|
if (!paramBuffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
paramBuffer->SyncPersistentMappedRange();
|
||||||
|
const Uint8* paramData = paramBuffer->MappedData();
|
||||||
|
if (paramData == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Uint32 actualDrawCount = 0;
|
||||||
|
std::memcpy(&actualDrawCount, paramData + static_cast<SizeT>(drawcount), sizeof(actualDrawCount));
|
||||||
|
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
|
||||||
|
MultiDrawArraysIndirect(mode, indirect, static_cast<GLsizei>(actualDrawCount), stride);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect,
|
||||||
|
GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) {
|
||||||
|
if (MG_State::pGLContext == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto paramBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
|
||||||
|
if (!paramBuffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
paramBuffer->SyncPersistentMappedRange();
|
||||||
|
const Uint8* paramData = paramBuffer->MappedData();
|
||||||
|
if (paramData == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Uint32 actualDrawCount = 0;
|
||||||
|
std::memcpy(&actualDrawCount, paramData + static_cast<SizeT>(drawcount), sizeof(actualDrawCount));
|
||||||
|
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
|
||||||
|
MultiDrawElementsIndirect(mode, type, indirect, static_cast<GLsizei>(actualDrawCount), stride);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || instancecount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (GLsizei i = 0; i < instancecount; ++i) {
|
||||||
|
renderer->DrawFromState(mode, first, count, 0, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||||
|
GLuint baseinstance) {
|
||||||
|
(void)baseinstance;
|
||||||
|
DrawArraysInstanced(mode, first, count, instancecount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||||
|
GLsizei instancecount) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || instancecount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (GLsizei i = 0; i < instancecount; ++i) {
|
||||||
|
renderer->DrawFromState(mode, 0, count, type, indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||||
|
GLsizei instancecount, GLint basevertex) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || instancecount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (GLsizei i = 0; i < instancecount; ++i) {
|
||||||
|
renderer->DrawFromState(mode, 0, count, type, indices, basevertex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
|
||||||
|
GLsizei instancecount, GLuint baseinstance) {
|
||||||
|
(void)baseinstance;
|
||||||
|
DrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type,
|
||||||
|
const void* indices, GLsizei instancecount,
|
||||||
|
GLint basevertex, GLuint baseinstance) {
|
||||||
|
(void)baseinstance;
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || instancecount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (GLsizei i = 0; i < instancecount; ++i) {
|
||||||
|
renderer->DrawFromState(mode, 0, count, type, indices, basevertex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || value == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (buffer == GL_COLOR && drawbuffer == 0) {
|
||||||
|
renderer->Clear(value[0], value[1], value[2], value[3]);
|
||||||
|
} else if (buffer == GL_DEPTH && drawbuffer == 0) {
|
||||||
|
renderer->ClearDepth(value[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||||
|
if (value == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (buffer == GL_STENCIL) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer != nullptr) {
|
||||||
|
renderer->ClearStencil(static_cast<Uint32>(value[0]));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Float color[4] = {
|
||||||
|
static_cast<Float>(value[0]) / 255.0f,
|
||||||
|
static_cast<Float>(value[1]) / 255.0f,
|
||||||
|
static_cast<Float>(value[2]) / 255.0f,
|
||||||
|
static_cast<Float>(value[3]) / 255.0f,
|
||||||
|
};
|
||||||
|
ClearBufferfv(buffer, drawbuffer, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||||
|
if (value == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (buffer == GL_STENCIL) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer != nullptr) {
|
||||||
|
renderer->ClearStencil(value[0]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Float color[4] = {
|
||||||
|
static_cast<Float>(value[0]) / 255.0f,
|
||||||
|
static_cast<Float>(value[1]) / 255.0f,
|
||||||
|
static_cast<Float>(value[2]) / 255.0f,
|
||||||
|
static_cast<Float>(value[3]) / 255.0f,
|
||||||
|
};
|
||||||
|
ClearBufferfv(buffer, drawbuffer, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || buffer != GL_DEPTH_STENCIL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(void)drawbuffer;
|
||||||
|
renderer->ClearDepth(depth);
|
||||||
|
renderer->ClearStencil(static_cast<Uint32>(stencil));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || pixels == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The Diligent backend's offscreen targets are RGBA8; the frontend currently
|
||||||
|
// uses this entry for the common GL_RGBA/GL_UNSIGNED_BYTE readback path.
|
||||||
|
if (format != GL_RGBA || type != GL_UNSIGNED_BYTE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderer->ReadPixels(static_cast<Uint32>(x), static_cast<Uint32>(y),
|
||||||
|
static_cast<Uint32>(width), static_cast<Uint32>(height), pixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||||
|
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||||
|
GLbitfield mask, GLenum filter) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer != nullptr) {
|
||||||
|
renderer->BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1,
|
||||||
|
mask, filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
||||||
|
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
|
||||||
|
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||||
|
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||||
|
GLbitfield mask, GLenum filter) {
|
||||||
|
(void)srcX0;
|
||||||
|
(void)srcY0;
|
||||||
|
(void)srcX1;
|
||||||
|
(void)srcY1;
|
||||||
|
(void)dstX0;
|
||||||
|
(void)dstY0;
|
||||||
|
(void)dstX1;
|
||||||
|
(void)dstY1;
|
||||||
|
(void)filter;
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer != nullptr) {
|
||||||
|
renderer->BlitNamedFramebuffer(readFramebuffer, drawFramebuffer, mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y,
|
||||||
|
GLsizei width, GLsizei height, GLint border) {
|
||||||
|
(void)level;
|
||||||
|
(void)internalformat;
|
||||||
|
(void)x;
|
||||||
|
(void)y;
|
||||||
|
(void)width;
|
||||||
|
(void)height;
|
||||||
|
(void)border;
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || MG_State::pGLContext == nullptr || target != GL_TEXTURE_2D) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||||
|
auto texture = unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
|
||||||
|
if (texture) {
|
||||||
|
renderer->CopyReadFramebufferToTexture(*texture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||||
|
GLsizei width, GLsizei height) {
|
||||||
|
(void)level;
|
||||||
|
(void)xoffset;
|
||||||
|
(void)yoffset;
|
||||||
|
(void)x;
|
||||||
|
(void)y;
|
||||||
|
(void)width;
|
||||||
|
(void)height;
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || MG_State::pGLContext == nullptr || target != GL_TEXTURE_2D) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||||
|
auto texture = unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
|
||||||
|
if (texture) {
|
||||||
|
renderer->CopyReadFramebufferToTexture(*texture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || MG_State::pGLContext == nullptr || target != GL_TEXTURE_2D ||
|
||||||
|
format != GL_RGBA || type != GL_UNSIGNED_BYTE || pixels == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||||
|
auto texture = unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
|
||||||
|
if (texture) {
|
||||||
|
renderer->ReadTextureImage(*texture, static_cast<Uint32>(level), pixels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
|
||||||
|
TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
|
||||||
|
GLsizei bufSize, GLvoid* pixels) {
|
||||||
|
(void)uploadTarget;
|
||||||
|
(void)bufSize;
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || !texture || format != GL_RGBA || type != GL_UNSIGNED_BYTE ||
|
||||||
|
pixels == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderer->ReadTextureImage(*texture, static_cast<Uint32>(level), pixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||||
|
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||||
|
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||||
|
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||||
|
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
|
||||||
|
(void)srcTarget;
|
||||||
|
(void)srcLevel;
|
||||||
|
(void)srcX;
|
||||||
|
(void)srcY;
|
||||||
|
(void)srcZ;
|
||||||
|
(void)dstTarget;
|
||||||
|
(void)dstLevel;
|
||||||
|
(void)dstX;
|
||||||
|
(void)dstY;
|
||||||
|
(void)dstZ;
|
||||||
|
(void)srcWidth;
|
||||||
|
(void)srcHeight;
|
||||||
|
(void)srcDepth;
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer != nullptr && srcTexture && dstTexture) {
|
||||||
|
renderer->CopyTextureSubData(*srcTexture, *dstTexture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GenerateMipmap(GLenum target) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer == nullptr || MG_State::pGLContext == nullptr || target != GL_TEXTURE_2D) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
|
||||||
|
auto texture = unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
|
||||||
|
if (texture) {
|
||||||
|
renderer->GenerateMipmap(*texture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool IsTimerQuerySupported() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BackendQueryHandle BeginTimeElapsedQuery() {
|
||||||
|
auto* query = new CpuTimerQuery;
|
||||||
|
query->Start = std::chrono::steady_clock::now();
|
||||||
|
query->Available = false;
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndTimeElapsedQuery(BackendQueryHandle query) {
|
||||||
|
if (query == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto* cpuQuery = static_cast<CpuTimerQuery*>(query);
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
cpuQuery->TimestampNs = static_cast<Uint64>(
|
||||||
|
std::chrono::duration_cast<std::chrono::nanoseconds>(now - cpuQuery->Start).count());
|
||||||
|
cpuQuery->Available = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BackendQueryHandle QueryCounterTimestamp() {
|
||||||
|
auto* query = new CpuTimerQuery;
|
||||||
|
query->TimestampNs = static_cast<Uint64>(
|
||||||
|
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||||
|
query->Available = true;
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool IsQueryResultAvailable(BackendQueryHandle query) {
|
||||||
|
return query != nullptr && static_cast<CpuTimerQuery*>(query)->Available;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool GetQueryResult64(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds) {
|
||||||
|
if (query == nullptr || outNanoseconds == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto* cpuQuery = static_cast<CpuTimerQuery*>(query);
|
||||||
|
if (!cpuQuery->Available && !wait) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*outNanoseconds = cpuQuery->TimestampNs;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeleteBackendQuery(BackendQueryHandle query) {
|
||||||
|
delete static_cast<CpuTimerQuery*>(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
BackendSyncHandle FenceSync() {
|
||||||
|
// CPU fallback fence: always signaled is a valid implementation for a
|
||||||
|
// backend without native sync primitives. The handle still round-trips
|
||||||
|
// through ClientWaitSync/DeleteSync so frontend state stays balanced.
|
||||||
|
return new int(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
GLenum ClientWaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout) {
|
||||||
|
(void)sync;
|
||||||
|
(void)flags;
|
||||||
|
(void)timeout;
|
||||||
|
return GL_ALREADY_SIGNALED;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout) {
|
||||||
|
(void)sync;
|
||||||
|
(void)flags;
|
||||||
|
(void)timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeleteSync(BackendSyncHandle sync) {
|
||||||
|
delete static_cast<int*>(sync);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool GetSyncStatus(BackendSyncHandle sync) {
|
||||||
|
(void)sync;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetSwapInterval(Int interval) {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer != nullptr) {
|
||||||
|
renderer->SetSwapInterval(interval > 0 ? static_cast<Uint32>(interval) : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Present() {
|
||||||
|
auto* renderer = GetActiveRenderer();
|
||||||
|
if (renderer != nullptr) {
|
||||||
|
renderer->Present();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
BackendObject_Diligent::BackendObject_Diligent()
|
||||||
|
: m_rendererInfo(BuildInitialRendererInfo()) {}
|
||||||
|
|
||||||
|
BackendObject_Diligent::~BackendObject_Diligent() {
|
||||||
|
m_pRenderer.reset();
|
||||||
|
m_pContext.Release();
|
||||||
|
m_pDevice.Release();
|
||||||
|
m_pFactoryVk = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Diligent::CreateDiligentDevice() {
|
||||||
|
if (m_pDevice && m_pContext) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (m_pFactoryVk == nullptr) {
|
||||||
|
m_pFactoryVk = ::Diligent::GetEngineFactoryVk();
|
||||||
|
if (m_pFactoryVk == nullptr) {
|
||||||
|
MGLOG_E("Diligent: failed to load Vulkan engine factory");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_pFactoryVk->SetBreakOnError(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
::Diligent::Uint32 numAdapters = 0;
|
||||||
|
m_pFactoryVk->EnumerateAdapters(::Diligent::Version{}, numAdapters, nullptr);
|
||||||
|
if (numAdapters == 0) {
|
||||||
|
MGLOG_W("Diligent: no Vulkan adapters available; skipping device creation");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
::Diligent::EngineVkCreateInfo engineCI;
|
||||||
|
::Diligent::ImmediateContextCreateInfo ctxCI;
|
||||||
|
ctxCI.Name = "MobileGL Diligent Main Context";
|
||||||
|
ctxCI.QueueId = 0;
|
||||||
|
ctxCI.Priority = ::Diligent::QUEUE_PRIORITY_MEDIUM;
|
||||||
|
engineCI.NumImmediateContexts = 1;
|
||||||
|
engineCI.pImmediateContextInfo = &ctxCI;
|
||||||
|
|
||||||
|
::Diligent::IRenderDevice* pDevice = nullptr;
|
||||||
|
::Diligent::IDeviceContext* pContext = nullptr;
|
||||||
|
m_pFactoryVk->CreateDeviceAndContextsVk(engineCI, &pDevice, &pContext);
|
||||||
|
if (pDevice == nullptr || pContext == nullptr) {
|
||||||
|
MGLOG_E("Diligent: failed to create Vulkan device/context");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_pDevice.Attach(pDevice);
|
||||||
|
m_pContext.Attach(pContext);
|
||||||
|
MGLOG_I("Diligent: Vulkan device created");
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
MGLOG_W("Diligent: Vulkan device creation failed: %s", e.what());
|
||||||
|
return false;
|
||||||
|
} catch (...) {
|
||||||
|
MGLOG_W("Diligent: Vulkan device creation failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BackendObject_Diligent::Initialize() {
|
||||||
|
if (m_initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!CreateDiligentDevice()) {
|
||||||
|
MGLOG_W("Diligent: backend initialization failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_pRenderer = std::make_unique<DiligentRenderer>(m_pDevice, m_pContext);
|
||||||
|
if (!m_pRenderer->Initialize(256, 256)) {
|
||||||
|
MGLOG_W("Diligent: renderer initialization failed");
|
||||||
|
m_pRenderer.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_functions.GL.Clear = Clear;
|
||||||
|
m_functions.GL.DrawArrays = DrawArrays;
|
||||||
|
m_functions.GL.DrawElements = DrawElements;
|
||||||
|
m_functions.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
|
||||||
|
m_functions.GL.DrawRangeElements = DrawRangeElements;
|
||||||
|
m_functions.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex;
|
||||||
|
m_functions.GL.MultiDrawArrays = MultiDrawArrays;
|
||||||
|
m_functions.GL.MultiDrawElements = MultiDrawElements;
|
||||||
|
m_functions.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
|
||||||
|
m_functions.GL.DrawArraysInstanced = DrawArraysInstanced;
|
||||||
|
m_functions.GL.DrawArraysInstancedBaseInstance = DrawArraysInstancedBaseInstance;
|
||||||
|
m_functions.GL.DrawElementsInstanced = DrawElementsInstanced;
|
||||||
|
m_functions.GL.DrawElementsInstancedBaseVertex = DrawElementsInstancedBaseVertex;
|
||||||
|
m_functions.GL.DrawElementsInstancedBaseInstance = DrawElementsInstancedBaseInstance;
|
||||||
|
m_functions.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance;
|
||||||
|
m_functions.GL.DrawArraysIndirect = DrawArraysIndirect;
|
||||||
|
m_functions.GL.DrawElementsIndirect = DrawElementsIndirect;
|
||||||
|
m_functions.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
|
||||||
|
m_functions.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
|
||||||
|
m_functions.GL.MultiDrawArraysIndirectCount = MultiDrawArraysIndirectCount;
|
||||||
|
m_functions.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
|
||||||
|
m_functions.GL.ClearBufferfv = ClearBufferfv;
|
||||||
|
m_functions.GL.ClearBufferfi = ClearBufferfi;
|
||||||
|
m_functions.GL.ClearBufferiv = ClearBufferiv;
|
||||||
|
m_functions.GL.ClearBufferuiv = ClearBufferuiv;
|
||||||
|
m_functions.GL.BlitFramebuffer = BlitFramebuffer;
|
||||||
|
m_functions.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
|
||||||
|
m_functions.GL.CopyTexImage2D = CopyTexImage2D;
|
||||||
|
m_functions.GL.CopyTexSubImage2D = CopyTexSubImage2D;
|
||||||
|
m_functions.GL.CopyImageSubData = CopyImageSubData;
|
||||||
|
m_functions.GL.GenerateMipmap = GenerateMipmap;
|
||||||
|
m_functions.GL.GetTexImage = GetTexImage;
|
||||||
|
m_functions.GL.GetTextureImage = GetTextureImage;
|
||||||
|
m_functions.GL.ReadPixels = ReadPixels;
|
||||||
|
m_functions.GL.FenceSync = FenceSync;
|
||||||
|
m_functions.GL.ClientWaitSync = ClientWaitSync;
|
||||||
|
m_functions.GL.WaitSync = WaitSync;
|
||||||
|
m_functions.GL.DeleteSync = DeleteSync;
|
||||||
|
m_functions.GL.GetSyncStatus = GetSyncStatus;
|
||||||
|
m_functions.GL.IsTimerQuerySupported = IsTimerQuerySupported;
|
||||||
|
m_functions.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
|
||||||
|
m_functions.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
|
||||||
|
m_functions.GL.QueryCounterTimestamp = QueryCounterTimestamp;
|
||||||
|
m_functions.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||||
|
m_functions.GL.GetQueryResult64 = GetQueryResult64;
|
||||||
|
m_functions.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||||
|
m_functions.Present = Present;
|
||||||
|
m_functions.SetSwapInterval = SetSwapInterval;
|
||||||
|
|
||||||
|
m_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
DiligentRenderer* BackendObject_Diligent::GetRenderer() {
|
||||||
|
return m_pRenderer.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Diligent::InitCapabilities() {
|
||||||
|
// Skeleton: no format probing yet. The backend advertises GL 3.2 core
|
||||||
|
// capability, and the capability tables will be filled as resource
|
||||||
|
// creation paths are ported.
|
||||||
|
m_backendCapabilitiesInitialized = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Diligent::InitWindowSurface() {
|
||||||
|
if (!m_windowHandle.Handle) {
|
||||||
|
MGLOG_E("BackendObject_Diligent::InitWindowSurface failed: native window handle is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (m_pRenderer == nullptr || m_pFactoryVk == nullptr) {
|
||||||
|
MGLOG_E("BackendObject_Diligent::InitWindowSurface failed: renderer/factory is not ready");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return m_pRenderer->CreateSwapChain(m_pFactoryVk, m_windowHandle,
|
||||||
|
m_windowHandle.Width, m_windowHandle.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Diligent::InitPbufferSurface(EGLint width, EGLint height) {
|
||||||
|
// The Diligent backend keeps its offscreen target for pbuffer EGL surfaces.
|
||||||
|
// A future enhancement can resize/recreate the offscreen target to match the
|
||||||
|
// pbuffer dimensions.
|
||||||
|
(void)width;
|
||||||
|
(void)height;
|
||||||
|
return m_pRenderer != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BackendObject_Diligent::ReleaseEGLResources() {
|
||||||
|
if (m_pRenderer != nullptr) {
|
||||||
|
m_pRenderer->ReleaseSwapChain();
|
||||||
|
}
|
||||||
|
BackendObject::ReleaseEGLResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BackendObject_Diligent::OnEGLSurfaceReleased(EGLSurface surface) {
|
||||||
|
(void)surface;
|
||||||
|
if (m_pRenderer != nullptr) {
|
||||||
|
m_pRenderer->ReleaseSwapChain();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Diligent::CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) {
|
||||||
|
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||||
|
if (!m_initialized) {
|
||||||
|
MGLOG_E("BackendObject_Diligent::CreateEGLWindowSurface failed: backend not initialized");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!handle.Handle || (handle.Backend != WindowBackend::Android && handle.Backend != WindowBackend::X11 &&
|
||||||
|
handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::Win32)) {
|
||||||
|
MGLOG_E("BackendObject_Diligent::CreateEGLWindowSurface failed: unsupported native window backend");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return RegisterEGLWindowSurface(surface, handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Diligent::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
|
||||||
|
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||||
|
if (!m_initialized) {
|
||||||
|
MGLOG_E("BackendObject_Diligent::CreateEGLPbufferSurface failed: backend not initialized");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return RegisterEGLPbufferSurface(surface, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendObject_Diligent::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||||
|
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||||
|
if (!BackendObject::ResizeEGLWindowSurface(surface, width, height)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (m_eglSurface == surface && m_pRenderer != nullptr) {
|
||||||
|
return m_pRenderer->ResizeSwapChain(width, height);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RendererInfo& BackendObject_Diligent::GetRendererInfo() const {
|
||||||
|
return m_rendererInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
String BackendObject_Diligent::GetBackendAPIVersionString() const {
|
||||||
|
return "Diligent Vulkan 0.1 (GL 3.2 skeleton)";
|
||||||
|
}
|
||||||
|
|
||||||
|
const GlobalBackendFunctionsTable& BackendObject_Diligent::GetBackendFunctions() const {
|
||||||
|
return m_functions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DynamicBackendParameters& BackendObject_Diligent::GetDynamicParameters() const {
|
||||||
|
return m_dynamicParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
BackendType BackendObject_Diligent::GetBackendType() const {
|
||||||
|
return BackendType::DiligentVulkan;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Backend::DiligentBackend
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/Diligent/BackendObject_Diligent.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
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Includes.h>
|
||||||
|
#include "../BackendObject.h"
|
||||||
|
|
||||||
|
// X11 (pulled in by Includes.h through Vulkan-Headers) defines True/False as
|
||||||
|
// macros, which collide with Diligent's Bool constants in BasicTypes.h.
|
||||||
|
#if defined(True)
|
||||||
|
#undef True
|
||||||
|
#endif
|
||||||
|
#if defined(False)
|
||||||
|
#undef False
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <RefCntAutoPtr.hpp>
|
||||||
|
|
||||||
|
namespace Diligent {
|
||||||
|
struct IEngineFactoryVk;
|
||||||
|
struct IRenderDevice;
|
||||||
|
struct IDeviceContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Backend::DiligentBackend {
|
||||||
|
class DiligentRenderer;
|
||||||
|
|
||||||
|
// New Diligent/Vulkan backend, implemented from scratch on top of
|
||||||
|
// DiligentCore. The backend object owns the Diligent device/context and
|
||||||
|
// currently advertises OpenGL 3.2 core capability; the GL function table
|
||||||
|
// is intentionally empty until drawing/resource paths are ported.
|
||||||
|
class BackendObject_Diligent : public BackendObject {
|
||||||
|
public:
|
||||||
|
BackendObject_Diligent();
|
||||||
|
~BackendObject_Diligent() override;
|
||||||
|
|
||||||
|
void Initialize() override;
|
||||||
|
Bool InitCapabilities() override;
|
||||||
|
Bool InitWindowSurface() override;
|
||||||
|
Bool InitPbufferSurface(EGLint width, EGLint height) override;
|
||||||
|
Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) override;
|
||||||
|
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
|
||||||
|
Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) override;
|
||||||
|
void OnEGLSurfaceReleased(EGLSurface surface) override;
|
||||||
|
|
||||||
|
const RendererInfo& GetRendererInfo() const override;
|
||||||
|
String GetBackendAPIVersionString() const override;
|
||||||
|
const GlobalBackendFunctionsTable& GetBackendFunctions() const override;
|
||||||
|
const DynamicBackendParameters& GetDynamicParameters() const override;
|
||||||
|
BackendType GetBackendType() const override;
|
||||||
|
void ReleaseEGLResources() override;
|
||||||
|
|
||||||
|
DiligentRenderer* GetRenderer();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Bool CreateDiligentDevice();
|
||||||
|
|
||||||
|
RendererInfo m_rendererInfo;
|
||||||
|
DynamicBackendParameters m_dynamicParameters;
|
||||||
|
GlobalBackendFunctionsTable m_functions{};
|
||||||
|
::Diligent::IEngineFactoryVk* m_pFactoryVk = nullptr;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::IRenderDevice> m_pDevice;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::IDeviceContext> m_pContext;
|
||||||
|
std::unique_ptr<DiligentRenderer> m_pRenderer;
|
||||||
|
Bool m_initialized = false;
|
||||||
|
};
|
||||||
|
} // namespace MobileGL::MG_Backend::DiligentBackend
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/Diligent/DiligentVulkan.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
|
||||||
|
|
||||||
|
#include "DiligentVulkan.h"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/Diligent/DiligentVulkan.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
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Backend::DiligentBackend {
|
||||||
|
// Backend identity string used by the backend object and local smoke tests.
|
||||||
|
inline String GetDiligentVulkanBackendName() {
|
||||||
|
return "DiligentVulkan";
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Backend::DiligentBackend
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,153 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/Diligent/Renderer/DiligentRenderer.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
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
// X11 (pulled in by Includes.h through Vulkan-Headers) defines True/False as
|
||||||
|
// macros, which collide with Diligent's Bool constants in BasicTypes.h.
|
||||||
|
#if defined(True)
|
||||||
|
#undef True
|
||||||
|
#endif
|
||||||
|
#if defined(False)
|
||||||
|
#undef False
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <RefCntAutoPtr.hpp>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Backend {
|
||||||
|
struct WindowHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Diligent {
|
||||||
|
struct IRenderDevice;
|
||||||
|
struct IDeviceContext;
|
||||||
|
struct ITexture;
|
||||||
|
struct ITextureView;
|
||||||
|
struct IPipelineState;
|
||||||
|
struct IBuffer;
|
||||||
|
struct ISampler;
|
||||||
|
struct IShaderResourceBinding;
|
||||||
|
struct ISwapChain;
|
||||||
|
struct IEngineFactoryVk;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
class ITextureObject;
|
||||||
|
class SamplerObject;
|
||||||
|
class ProgramObject;
|
||||||
|
class RenderbufferObject;
|
||||||
|
class FramebufferObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Backend::DiligentBackend {
|
||||||
|
// Minimal real Diligent renderer used to prove the GL 3.2 basic path:
|
||||||
|
// clear an offscreen color target, draw a hardcoded triangle, and read
|
||||||
|
// pixels back. This is the first concrete rendering layer on top of the
|
||||||
|
// Diligent device; it will be expanded into the full MobileGL backend.
|
||||||
|
class DiligentRenderer {
|
||||||
|
public:
|
||||||
|
DiligentRenderer(::Diligent::IRenderDevice* device, ::Diligent::IDeviceContext* context);
|
||||||
|
~DiligentRenderer();
|
||||||
|
|
||||||
|
Bool Initialize(Uint32 width, Uint32 height);
|
||||||
|
void Clear(Float r, Float g, Float b, Float a);
|
||||||
|
void ClearDepth(Float depth);
|
||||||
|
void ClearStencil(Uint32 stencil);
|
||||||
|
void DrawTriangle();
|
||||||
|
void DrawVertices(const Float* vertices, Uint32 vertexCount);
|
||||||
|
// Creates a real Diligent swap chain for a native EGL window surface.
|
||||||
|
Bool CreateSwapChain(::Diligent::IEngineFactoryVk* factory, const WindowHandle& handle,
|
||||||
|
Uint32 width, Uint32 height);
|
||||||
|
Bool ResizeSwapChain(Uint32 width, Uint32 height);
|
||||||
|
void SetSwapInterval(Uint32 interval);
|
||||||
|
// Creates a simple 2D RGBA8 texture from CPU data and makes it available
|
||||||
|
// to state PSOs under the shader variable name "g_Texture".
|
||||||
|
Bool CreateTestTexture(const void* data, Uint32 width, Uint32 height);
|
||||||
|
// Draws using the live MG_State GL context: current program, VAO and
|
||||||
|
// bound buffers. This is the front-end emulation entry point.
|
||||||
|
void DrawFromState(GLenum mode, GLint first, GLsizei count, GLenum type, const void* indices,
|
||||||
|
GLint baseVertex = 0);
|
||||||
|
void ReadPixels(Uint32 x, Uint32 y, Uint32 width, Uint32 height, void* pixels);
|
||||||
|
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||||
|
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||||
|
GLbitfield mask, GLenum filter);
|
||||||
|
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFbo,
|
||||||
|
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFbo,
|
||||||
|
GLbitfield mask);
|
||||||
|
void CopyReadFramebufferToTexture(MG_State::GLState::ITextureObject& dst);
|
||||||
|
void CopyTextureSubData(MG_State::GLState::ITextureObject& src, MG_State::GLState::ITextureObject& dst);
|
||||||
|
void GenerateMipmap(MG_State::GLState::ITextureObject& texture);
|
||||||
|
Bool ReadTextureImage(MG_State::GLState::ITextureObject& texture, Uint32 level, void* pixels);
|
||||||
|
void ReleaseSwapChain();
|
||||||
|
void Present();
|
||||||
|
|
||||||
|
::Diligent::IRenderDevice* GetDevice() const { return m_pDevice; }
|
||||||
|
::Diligent::IDeviceContext* GetContext() const { return m_pContext; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct TextureResource {
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::ITexture> Texture;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> SRV;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> RTV;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> DSV;
|
||||||
|
Uint64 ContentVersion = 0;
|
||||||
|
Uint16 ParamsVersion = 0;
|
||||||
|
Bool IsDepth = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SamplerResource {
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::ISampler> Sampler;
|
||||||
|
Uint16 Version = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
Bool CreateOffscreenTargets();
|
||||||
|
Bool CreatePipeline();
|
||||||
|
Bool CreateVertexBuffer();
|
||||||
|
Bool CreatePipelineFromState(GLenum mode);
|
||||||
|
Bool UploadVertexDataFromState(GLenum mode, GLint first, GLsizei count, GLenum type, const void* indices,
|
||||||
|
GLint baseVertex = 0);
|
||||||
|
::Diligent::ITextureView* SyncTexture(MG_State::GLState::ITextureObject& texture);
|
||||||
|
::Diligent::ITextureView* SyncTextureForAttachment(MG_State::GLState::ITextureObject& texture, Bool depth);
|
||||||
|
::Diligent::ITextureView* SyncRenderbuffer(MG_State::GLState::RenderbufferObject& renderbuffer);
|
||||||
|
::Diligent::ISampler* SyncSampler(const MG_State::GLState::SamplerObject& sampler);
|
||||||
|
Bool BindShaderResourcesFromState(const MG_State::GLState::ProgramObject& program);
|
||||||
|
Bool UploadUBOFromState(const MG_State::GLState::ProgramObject& program);
|
||||||
|
Bool ResolveCurrentRenderTargets(Vector<::Diligent::ITextureView*>& rtvs,
|
||||||
|
::Diligent::ITextureView*& dsv);
|
||||||
|
|
||||||
|
::Diligent::IRenderDevice* m_pDevice = nullptr;
|
||||||
|
::Diligent::IDeviceContext* m_pContext = nullptr;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::ITexture> m_pColorTarget;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> m_pColorRTV;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::ITexture> m_pDepthTarget;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> m_pDepthDSV;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::ISwapChain> m_pSwapChain;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::ITexture> m_pTestTexture;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> m_pTestSRV;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::ISampler> m_pTestSampler;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::IShaderResourceBinding> m_pStateSRB;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::IPipelineState> m_pPSO;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::IBuffer> m_pVertexBuffer;
|
||||||
|
::Diligent::RefCntAutoPtr<::Diligent::IBuffer> m_pUBO;
|
||||||
|
Uint32 m_uboSize = 0;
|
||||||
|
Uint32 m_uboContentVersion = 0;
|
||||||
|
Uint64 m_uboProgramLifetimeId = 0;
|
||||||
|
UnorderedMap<Uint64, TextureResource> m_textureCache;
|
||||||
|
UnorderedMap<Uint64, SamplerResource> m_samplerCache;
|
||||||
|
UnorderedMap<Uint32, TextureResource> m_renderbufferCache;
|
||||||
|
UnorderedMap<Uint64, ::Diligent::RefCntAutoPtr<::Diligent::IBuffer>> m_namedUboCache;
|
||||||
|
Uint32 m_width = 256;
|
||||||
|
Uint32 m_height = 256;
|
||||||
|
Uint32 m_swapInterval = 0;
|
||||||
|
Uint32 m_lastDrawVertexCount = 0;
|
||||||
|
Uint64 m_lastPSOKey = 0;
|
||||||
|
Bool m_hasCachedPSO = false;
|
||||||
|
Bool m_initialized = false;
|
||||||
|
};
|
||||||
|
} // namespace MobileGL::MG_Backend::DiligentBackend
|
||||||
@@ -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>
|
||||||
@@ -213,10 +212,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||||
reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
|
reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
|
||||||
}
|
}
|
||||||
// A format is either 8- or 16-bit signed normalized, so at most one of the two ever
|
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||||
// survives GetApplicablePixelFormatNormalizeOptions and the reason is not duplicated.
|
|
||||||
if ((options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) ||
|
|
||||||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
|
|
||||||
reasons.push_back("EXT_render_snorm not supported");
|
reasons.push_back("EXT_render_snorm not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,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;
|
||||||
}
|
}
|
||||||
@@ -435,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);
|
||||||
@@ -535,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) {
|
||||||
@@ -658,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;
|
||||||
@@ -680,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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -786,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();
|
||||||
}
|
}
|
||||||
@@ -1169,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;
|
||||||
@@ -1254,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
|
||||||
@@ -1331,26 +1241,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Not a driver question and never will be: GLSL ES has no 64-bit float type in ANY version
|
// Not a driver question and never will be: OpenGL ES has no double-precision vertex format
|
||||||
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES profile") and a
|
// and ESSL has no fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to
|
||||||
// module that still declared Float64 would never reach the driver at all. The demotion is
|
// land on this backend regardless of what the driver underneath happens to support.
|
||||||
// mathematically mandatory here, on every device, forever - which is why this stays false
|
|
||||||
// regardless of what the driver underneath happens to support.
|
|
||||||
m_dynamicParameters.SupportsShaderFloat64 = false;
|
|
||||||
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
|
|
||||||
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
|
|
||||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||||
m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
|
m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
|
||||||
// Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX
|
|
||||||
// for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test
|
|
||||||
// devices. That is not a shortfall being hidden: without the extension only viewport 0 is
|
|
||||||
// ever rasterized, so no vertex "selects" a viewport index and naming a convention would
|
|
||||||
// describe behaviour this backend does not implement.
|
|
||||||
m_dynamicParameters.LayerProvokingVertex = m_GLESCapabilities.LayerProvokingVertex;
|
|
||||||
m_dynamicParameters.ViewportIndexProvokingVertex = m_GLESCapabilities.ViewportIndexProvokingVertex;
|
|
||||||
m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth;
|
m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth;
|
||||||
m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight;
|
m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight;
|
||||||
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
|
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -21,29 +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);
|
||||||
|
|
||||||
// The ESSL half of the gl_ViewportIndex routing emulation, in the order a program's stages
|
|
||||||
// meet it. Both are pure String -> String rewrites over what SPIRV-Cross emitted once
|
|
||||||
// LowerViewportIndexPass has demoted the builtin to the plain global `mg_ViewportIndex`.
|
|
||||||
//
|
|
||||||
// The producing stage's global becomes an ordinary flat varying; true when there was one to
|
|
||||||
// promote, which is also the answer to "does this program route viewports at all".
|
|
||||||
Bool PromoteViewportIndexGlobalToVarying(String& source);
|
|
||||||
// The fragment stage grows a matching flat input, the mg_ViewportPassMask uniform the draw
|
|
||||||
// path writes, and a wrapper entry point that discards every fragment whose primitive routed
|
|
||||||
// to an index the current replay pass is not drawing. False when the stage has no entry point
|
|
||||||
// to wrap, which leaves the program renderable but unrouted.
|
|
||||||
Bool InjectViewportIndexPassGate(String& source);
|
|
||||||
|
|
||||||
// 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
|
||||||
@@ -126,58 +103,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// link.
|
// link.
|
||||||
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices);
|
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices);
|
||||||
|
|
||||||
// ---- gl_ViewportIndex routing emulation, draw half ---------------------------------------
|
|
||||||
//
|
|
||||||
// GLES has ONE viewport, ONE scissor rectangle and ONE depth range; GL 4.1 has sixteen of
|
|
||||||
// each, selected per primitive by gl_ViewportIndex. There is no ES entry point to program the
|
|
||||||
// other fifteen with (GL_OES_viewport_array exists but Adreno 830 does not have it, verified
|
|
||||||
// three ways), so the only way to rasterize a primitive against index i's rectangle is to
|
|
||||||
// make index i's rectangle THE viewport for the duration of a draw - which means issuing the
|
|
||||||
// draw once per distinct viewport state and letting the fragment stage throw away the
|
|
||||||
// primitives that belong to the other indices (the gate Managers.cpp injects).
|
|
||||||
//
|
|
||||||
// Indices whose whole state tuple (viewport rectangle, scissor rectangle, scissor-test enable,
|
|
||||||
// depth range) is identical share ONE pass, so the overwhelmingly common case - every index
|
|
||||||
// still holding what glViewport/glScissor/glDepthRange broadcast to all sixteen - collapses
|
|
||||||
// to a single pass with an all-ones gate mask, i.e. one draw and no behaviour change at all.
|
|
||||||
//
|
|
||||||
// Whether emulation runs. Off only under MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION falsy, which
|
|
||||||
// restores the pre-emulation path as a negative control.
|
|
||||||
Bool ViewportArrayEmulationEnabled();
|
|
||||||
// Whether ANY program built in this process has come out with a viewport gate. Sticky once
|
|
||||||
// true; it exists so that BeginViewportRoutingPasses - which runs on every draw of every
|
|
||||||
// workload - can answer with one static load in the case that matters, which is every
|
|
||||||
// application that has never heard of gl_ViewportIndex.
|
|
||||||
extern Bool g_anyProgramRoutesViewportIndex;
|
|
||||||
// Number of times the current draw has to be issued. Always >= 1, and exactly 1 - with no
|
|
||||||
// state touched - whenever the current program does not route viewports, whenever every
|
|
||||||
// configured index shares one state, and whenever replaying would multiply a side effect the
|
|
||||||
// fragment gate cannot undo (transform feedback, rasterizer discard). Also seeds the pass
|
|
||||||
// mask uniform for that single-pass case, so a gated fragment shader never runs against the
|
|
||||||
// zero every GLSL uniform starts at - which would discard the whole draw.
|
|
||||||
Uint BeginViewportRoutingPasses();
|
|
||||||
// Push pass `pass`'s viewport / scissor / scissor-test / depth range onto the ES context and
|
|
||||||
// set the gate mask to the indices it serves. Only called when the count above exceeds 1.
|
|
||||||
void ApplyViewportRoutingPass(Uint pass);
|
|
||||||
// Restore the gate mask and mark the render-state shadow dirty, so the next ordinary draw
|
|
||||||
// re-pushes index 0's state. Takes the count so it can do nothing at all in the common case.
|
|
||||||
void EndViewportRoutingPasses(Uint passCount);
|
|
||||||
|
|
||||||
// Issue one draw, replayed once per viewport-routing pass. Every application-visible draw
|
|
||||||
// entry point wraps its native glDraw* call in this; the internal blit and clear helpers
|
|
||||||
// deliberately do not, because they bind their own programs, which never route.
|
|
||||||
template <typename IssueDraw>
|
|
||||||
inline void ForEachViewportRoutingPass(IssueDraw&& issue) {
|
|
||||||
const Uint passCount = BeginViewportRoutingPasses();
|
|
||||||
for (Uint pass = 0; pass < passCount; ++pass) {
|
|
||||||
if (passCount > 1) {
|
|
||||||
ApplyViewportRoutingPass(pass);
|
|
||||||
}
|
|
||||||
issue();
|
|
||||||
}
|
|
||||||
EndViewportRoutingPasses(passCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename StateObject, typename BackendObject>
|
template <typename StateObject, typename BackendObject>
|
||||||
class StateBackendObjectRegistry {
|
class StateBackendObjectRegistry {
|
||||||
public:
|
public:
|
||||||
@@ -204,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.
|
||||||
@@ -299,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -446,14 +346,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// client-attribute staging buffers): scrub every buffer-binding shadow that
|
// client-attribute staging buffers): scrub every buffer-binding shadow that
|
||||||
// could false-skip when the name is recycled.
|
// could false-skip when the name is recycled.
|
||||||
void NoteBufferIdDeleted(Uint id);
|
void NoteBufferIdDeleted(Uint id);
|
||||||
// Bumped whenever a live GLESBufferResource's driver id is retired and re-minted
|
|
||||||
// while its frontend buffer stays alive (persistent-map adoption, immutable-store
|
|
||||||
// retire). The VAO twins' baked glVertexAttribPointer / element-array bindings
|
|
||||||
// key on FRONTEND versions, which a backend-side re-mint does not move - without
|
|
||||||
// this generation the driver VAO would keep fetching through the deleted id (or
|
|
||||||
// its retained store) forever. Compared and stamped by
|
|
||||||
// BackendVertexArrayObject::SyncToBackend.
|
|
||||||
extern Uint64 g_bufferBackendIdGeneration;
|
|
||||||
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
|
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
|
||||||
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
|
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
|
||||||
// (id, range) already at that index matches, like the array-buffer/texture/
|
// (id, range) already at that index matches, like the array-buffer/texture/
|
||||||
@@ -461,13 +353,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
|
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id);
|
||||||
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
|
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size);
|
||||||
void InvalidateIndexedBufferBindingCache();
|
void InvalidateIndexedBufferBindingCache();
|
||||||
// Re-issues the GL_ATOMIC_COUNTER_BUFFER binding points a program's shaders declare as
|
|
||||||
// GL_SHADER_STORAGE_BUFFER bindings at the reserved slots the transpiled ESSL was built
|
|
||||||
// against (BackendProgramObjectImpl::GetAtomicCounterBindings /
|
|
||||||
// GetAtomicCounterEsslBindingTop). ES has no counter-buffer target at all, so without
|
|
||||||
// this the shader reads a storage block nobody ever bound a buffer to and the buffer the
|
|
||||||
// application bound never reaches the driver.
|
|
||||||
void SyncAtomicCounterBuffers(const Vector<Int>& glBindings, Int esslBindingTop);
|
|
||||||
// Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries
|
// Buffer-storage pool maintenance. TrimBufferPool evicts over-budget entries
|
||||||
// (called once per frame from Present); ClearBufferPool drops all pooled ids
|
// (called once per frame from Present); ClearBufferPool drops all pooled ids
|
||||||
// without glDeleteBuffers (called when the ES context is going away).
|
// without glDeleteBuffers (called when the ES context is going away).
|
||||||
@@ -574,51 +459,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
PendingAttribValueMask& GetPendingAttribValueMaskMemo() { return m_pendingAttribValueMask; }
|
PendingAttribValueMask& GetPendingAttribValueMaskMemo() { return m_pendingAttribValueMask; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Narrows one enabled GL_DOUBLE array into a tightly packed float32 stream held in
|
|
||||||
// this VAO's own scratch buffer and declares the attribute against it. ES has no
|
|
||||||
// 64-bit vertex format, but the source bytes are ordinary IEEE-754 doubles and every
|
|
||||||
// fp64 value in every shader is already narrowed to 32 bits (DemoteFloat64Pass), so
|
|
||||||
// narrowing the ARRAY is the coherent completion of that decision rather than
|
|
||||||
// dropping it. Returns false when the stream cannot be built, in which case the
|
|
||||||
// caller must DISABLE the array - leaving a 64-bit array enabled with no pointer is
|
|
||||||
// what the Adreno driver turns into a SIGSEGV at the next draw.
|
|
||||||
Bool SyncFloat64AttributeAsFloat32(Uint attribIndex, const MG_State::GLState::VertexAttribute& attrib,
|
|
||||||
Uint32 fetchBaseInstance);
|
|
||||||
|
|
||||||
// What the converted float32 stream in m_convertedAttributeBufferIds[i] was built
|
|
||||||
// from. A hit skips the CPU conversion and the re-upload; the buffer's change serial
|
|
||||||
// is part of the key, so a glBufferSubData into the source invalidates it.
|
|
||||||
struct ConvertedFloat64Stream {
|
|
||||||
Bool valid = false;
|
|
||||||
Uint64 sourceLifetimeId = 0;
|
|
||||||
Uint64 sourceChangeSerial = 0;
|
|
||||||
SizeT sourceOffset = 0;
|
|
||||||
SizeT sourceStride = 0;
|
|
||||||
SizeT componentCount = 0;
|
|
||||||
SizeT elementCount = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
ResolvedDrawBuffers m_resolvedDrawBuffers;
|
ResolvedDrawBuffers m_resolvedDrawBuffers;
|
||||||
PendingAttribValueMask m_pendingAttribValueMask;
|
PendingAttribValueMask m_pendingAttribValueMask;
|
||||||
Uint m_backendVAOId = 0;
|
Uint m_backendVAOId = 0;
|
||||||
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_clientAttributeBufferIds;
|
||||||
// Scratch stores for the buffer-backed GL_DOUBLE narrowing. Deliberately separate
|
|
||||||
// from m_clientAttributeBufferIds: that one holds the per-draw upload of a
|
|
||||||
// CLIENT-MEMORY array, and an attribute index can carry both shapes over its life.
|
|
||||||
Array<Uint, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> m_convertedAttributeBufferIds;
|
|
||||||
Array<ConvertedFloat64Stream, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
|
|
||||||
m_convertedAttributeStreams;
|
|
||||||
// True while at least one attribute of this VAO is fed by a converted stream. Such a
|
|
||||||
// stream is derived from buffer CONTENT, which no VAO version covers, so the config
|
|
||||||
// version early-out in SyncToBackend must not be trusted while it is set.
|
|
||||||
Bool m_hasConvertedFloat64Attribute = false;
|
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
Uint16 m_syncedIndexBufferVersion = 0;
|
Uint16 m_syncedIndexBufferVersion = 0;
|
||||||
// Identity of the buffer the version above was stamped against. Raw and never
|
|
||||||
// dereferenced: the slot version is a wrapping Uint16 (see the ResolvedDrawBuffers
|
|
||||||
// IBO memo and the packed_pixels postmortem at BindCurrentFBO), so the version
|
|
||||||
// alone would read a wrapped-back count with a different buffer bound as clean.
|
|
||||||
const MG_State::GLState::BufferObject* m_syncedIndexBufferObject = nullptr;
|
|
||||||
// Aggregate gate over the per-attribute walk below: the frontend bumps its config
|
// Aggregate gate over the per-attribute walk below: the frontend bumps its config
|
||||||
// version on every per-attribute version bump (the three Bump*Version functions are
|
// version on every per-attribute version bump (the three Bump*Version functions are
|
||||||
// its only writers), so an unchanged config version proves every per-attribute
|
// its only writers), so an unchanged config version proves every per-attribute
|
||||||
@@ -634,11 +480,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Kept here because it describes what was last EMITTED, which is what the next sync
|
// Kept here because it describes what was last EMITTED, which is what the next sync
|
||||||
// has to correct.
|
// has to correct.
|
||||||
Uint32 m_syncedFetchBaseInstance = 0;
|
Uint32 m_syncedFetchBaseInstance = 0;
|
||||||
// BufferImpl::g_bufferBackendIdGeneration as of this twin's last emit. A
|
|
||||||
// mismatch means some live buffer's driver id was re-minted since; the ids
|
|
||||||
// baked into the driver VAO's attribute/element bindings may be dead even
|
|
||||||
// though every frontend version matches, so the next sync re-emits them all.
|
|
||||||
Uint64 m_syncedBufferIdGeneration = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
|
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
|
||||||
@@ -745,21 +586,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Returns `data` untouched when no widening applies. Pure CPU and context-free so a unit
|
// Returns `data` untouched when no widening applies. Pure CPU and context-free so a unit
|
||||||
// test can exercise the exact packing the driver is handed; `widenedData` is the caller's
|
// test can exercise the exact packing the driver is handed; `widenedData` is the caller's
|
||||||
// scratch buffer and has to outlive the returned pointer.
|
// scratch buffer and has to outlive the returned pointer.
|
||||||
// `alphaOneCodeOverride`, when non-zero, replaces the value written into the synthetic
|
|
||||||
// alpha channel: an image carrier that holds a NORMALIZED format's channel CODES has to
|
|
||||||
// pad alpha with that channel's saturated CODE (65535, 32767, 3), which neither of the
|
|
||||||
// transfer type's own "ones" is.
|
|
||||||
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data,
|
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data,
|
||||||
SizeT byteSize, GLenum uploadType, Vector<Uint8>& widenedData,
|
SizeT byteSize, GLenum uploadType, Vector<Uint8>& widenedData,
|
||||||
Bool integerData = false, Uint32 alphaOneCodeOverride = 0u);
|
Bool integerData = false);
|
||||||
|
|
||||||
// Splits a GL_UNSIGNED_INT_2_10_10_10_REV shadow (rgb10_a2, rgb10_a2ui) into the four
|
|
||||||
// GL_UNSIGNED_SHORT channel CODES its GL_RGBA16UI image carrier is uploaded as: red in
|
|
||||||
// bits 0-9, green 10-19, blue 20-29, alpha 30-31. Pure CPU and context-free so a unit test
|
|
||||||
// can pin the exact fields; `widenedData` is the caller's scratch and has to outlive the
|
|
||||||
// returned pointer.
|
|
||||||
const void* PreparePackedIntWidenedUpload(const IntVec3& texelSize, const void* data, SizeT byteSize,
|
|
||||||
Vector<Uint8>& widenedData);
|
|
||||||
|
|
||||||
struct StateTextureBasicInfo { // Used for tracking texture state changes
|
struct StateTextureBasicInfo { // Used for tracking texture state changes
|
||||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||||
@@ -794,24 +623,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||||
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||||
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
||||||
// Marks the texture as one whose ES storage has to be image-bindable, which for a
|
void RequireImageBindableStorage();
|
||||||
// non-core image format means re-minting it in the widening's carrier. Takes the state
|
|
||||||
// object because the levels already uploaded have to be marked dirty again: the
|
|
||||||
// re-mint allocates fresh storage and only replays what the shadow still calls dirty.
|
|
||||||
void RequireImageBindableStorage(
|
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
|
|
||||||
// Whether this texture's ES storage was minted in an image carrier rather than in the
|
|
||||||
// frontend format's own layout - the readback has to ask, because for a NORMALIZED
|
|
||||||
// carrier the storage is an integer texture holding codes and glGetTexImage still owes
|
|
||||||
// the application floats.
|
|
||||||
Bool RequiresImageBindableStorage() const { return m_imageBindableStorageRequired; }
|
|
||||||
void Bind(GLenum target, Uint unit = TempTextureUnit);
|
void Bind(GLenum target, Uint unit = TempTextureUnit);
|
||||||
Uint GetBackendTextureId() const;
|
Uint GetBackendTextureId() const;
|
||||||
|
|
||||||
// The id to hand glBindImageTexture for a SPLIT buffer image, or 0 when this texture
|
|
||||||
// takes no split. See m_bufferImageSplitViewId.
|
|
||||||
Uint GetBufferImageSplitViewId() const { return m_bufferImageSplitViewId; }
|
|
||||||
|
|
||||||
// Aggregate first-level clean gate for the per-draw trio
|
// Aggregate first-level clean gate for the per-draw trio
|
||||||
// SyncTextureParamsToBackend + SyncBuiltinSamplerToBackend +
|
// SyncTextureParamsToBackend + SyncBuiltinSamplerToBackend +
|
||||||
// SyncMipmapsToBackend: EXACTLY the conjunction of their own early-outs
|
// SyncMipmapsToBackend: EXACTLY the conjunction of their own early-outs
|
||||||
@@ -848,25 +663,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void RecreateBackendTexture();
|
void RecreateBackendTexture();
|
||||||
|
|
||||||
Uint m_backendTextureId = 0;
|
Uint m_backendTextureId = 0;
|
||||||
// A SECOND buffer-texture name over the SAME buffer object, viewed in the split's
|
|
||||||
// single-channel base format, used only as the glBindImageTexture target.
|
|
||||||
//
|
|
||||||
// The split needs the view to say r32f where the application said rg32f, but a buffer
|
|
||||||
// texture that is image-bound may ALSO be read through a samplerBuffer - and the
|
|
||||||
// sampler side is not subscript-rewritten, so re-describing the application's own
|
|
||||||
// texture broke it: texelFetch(s, i) returned component 2i of the base view instead of
|
|
||||||
// texel i's pair. That is exactly and only
|
|
||||||
// KHR-GL42/43.shader_image_load_store.advanced-sync-imageAccess, which image-stores
|
|
||||||
// into a GL_RG32F buffer texture and then reads the same texture through both an
|
|
||||||
// imageBuffer and a samplerBuffer in one shader, comparing the two.
|
|
||||||
//
|
|
||||||
// Two names over one buffer cost nothing and alias exactly: a buffer texture owns no
|
|
||||||
// storage, so both views are the application's bytes, and the split's whole premise is
|
|
||||||
// that the two describe the same memory. The application's own name therefore keeps
|
|
||||||
// the format it asked for - rg32f IS a legal SAMPLED buffer-texture format in ES 3.2,
|
|
||||||
// it is only the IMAGE binding ES cannot spell - and the private name below carries
|
|
||||||
// the split the shader was rewritten against. 0 when this texture takes no split.
|
|
||||||
Uint m_bufferImageSplitViewId = 0;
|
|
||||||
// ES context generation the id was created under; a dtor running after
|
// ES context generation the id was created under; a dtor running after
|
||||||
// that context died must not delete a foreign (recycled) name.
|
// that context died must not delete a foreign (recycled) name.
|
||||||
Uint m_contextGeneration = 0;
|
Uint m_contextGeneration = 0;
|
||||||
@@ -1003,11 +799,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
using FramebufferObject = MG_State::GLState::FramebufferObject;
|
using FramebufferObject = MG_State::GLState::FramebufferObject;
|
||||||
FramebufferObject::FramebufferAttachmentVersionArray m_syncedFrontendAttachmentVersions = {0};
|
FramebufferObject::FramebufferAttachmentVersionArray m_syncedFrontendAttachmentVersions = {0};
|
||||||
// g_attachmentBackendIdGeneration as of this twin's last attachment walk. A
|
|
||||||
// mismatch means some backend texture id was re-minted since, and any of this
|
|
||||||
// twin's attachment points may still hold the dead id even though the frontend
|
|
||||||
// attachment versions match - so the walk re-attaches everything first.
|
|
||||||
Uint64 m_syncedBackendIdGeneration = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
||||||
@@ -1097,19 +888,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
||||||
g_fboSyncedObjects;
|
g_fboSyncedObjects;
|
||||||
|
|
||||||
// Bumped whenever a live backend texture's driver id is re-minted while its
|
|
||||||
// frontend texture may still be attached to application FBOs
|
|
||||||
// (BackendTextureObject::RecreateBackendTexture - e.g. a respecify of a texture
|
|
||||||
// whose backend storage went immutable). The FBO twins' attachment memos key on
|
|
||||||
// FRONTEND attachment versions, which a backend-side re-mint does not move, so
|
|
||||||
// the driver FBO would keep the deleted texture name attached forever. The
|
|
||||||
// SyncCurrentFBO gate compares this generation (below) to re-enter the sync,
|
|
||||||
// and each twin re-arms its per-attachment memo on a mismatch (SyncToBackend).
|
|
||||||
extern Uint64 g_attachmentBackendIdGeneration;
|
|
||||||
// What g_attachmentBackendIdGeneration was when SyncCurrentFBO last stamped each
|
|
||||||
// target; part of the synced tuple above.
|
|
||||||
extern Array<Uint64, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedBackendIdGenerations;
|
|
||||||
|
|
||||||
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
|
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
|
||||||
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
|
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
|
||||||
// save/restore the current binding without a glGetIntegerv round-trip (that
|
// save/restore the current binding without a glGetIntegerv round-trip (that
|
||||||
@@ -1213,51 +991,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
|
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
|
||||||
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
|
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
|
||||||
// assigned through glUniform1i.
|
// assigned through glUniform1i.
|
||||||
//
|
|
||||||
// ALL THIRTY-THREE of them, in the one contiguous block ARB_shader_image_load_store allocated
|
|
||||||
// (GL_IMAGE_1D 0x904C through GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY 0x906C). The list
|
|
||||||
// used to hold only the fifteen whose TARGET exists in ES, which read as a reasonable
|
|
||||||
// shortcut and was two bugs: an image uniform this says "no" to is one
|
|
||||||
// CollectImageFormatBakeInputs never walks, so its non-core format is neither baked nor
|
|
||||||
// widened and SPIRV-Cross throws for the whole stage ("Attempting to use image format not
|
|
||||||
// supported in ES profile"), and it is also one SyncToBackend then treats as a SAMPLER and
|
|
||||||
// assigns with glUniform1i, which ES makes an INVALID_OPERATION. A GL_TEXTURE_CUBE_MAP_ARRAY
|
|
||||||
// image - which ES 3.2 has in core, so it is not even an emulated target - hit both.
|
|
||||||
inline Bool IsImageUniformType(GLenum type) {
|
inline Bool IsImageUniformType(GLenum type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 0x904C: /*GL_IMAGE_1D*/
|
|
||||||
case 0x904D: /*GL_IMAGE_2D*/
|
case 0x904D: /*GL_IMAGE_2D*/
|
||||||
case 0x904E: /*GL_IMAGE_3D*/
|
case 0x904E: /*GL_IMAGE_3D*/
|
||||||
case 0x904F: /*GL_IMAGE_2D_RECT*/
|
|
||||||
case 0x9050: /*GL_IMAGE_CUBE*/
|
case 0x9050: /*GL_IMAGE_CUBE*/
|
||||||
case 0x9051: /*GL_IMAGE_BUFFER*/
|
case 0x9051: /*GL_IMAGE_BUFFER*/
|
||||||
case 0x9052: /*GL_IMAGE_1D_ARRAY*/
|
|
||||||
case 0x9053: /*GL_IMAGE_2D_ARRAY*/
|
case 0x9053: /*GL_IMAGE_2D_ARRAY*/
|
||||||
case 0x9054: /*GL_IMAGE_CUBE_MAP_ARRAY*/
|
|
||||||
case 0x9055: /*GL_IMAGE_2D_MULTISAMPLE*/
|
|
||||||
case 0x9056: /*GL_IMAGE_2D_MULTISAMPLE_ARRAY*/
|
|
||||||
case 0x9057: /*GL_INT_IMAGE_1D*/
|
|
||||||
case 0x9058: /*GL_INT_IMAGE_2D*/
|
case 0x9058: /*GL_INT_IMAGE_2D*/
|
||||||
case 0x9059: /*GL_INT_IMAGE_3D*/
|
case 0x9059: /*GL_INT_IMAGE_3D*/
|
||||||
case 0x905A: /*GL_INT_IMAGE_2D_RECT*/
|
|
||||||
case 0x905B: /*GL_INT_IMAGE_CUBE*/
|
case 0x905B: /*GL_INT_IMAGE_CUBE*/
|
||||||
case 0x905C: /*GL_INT_IMAGE_BUFFER*/
|
case 0x905C: /*GL_INT_IMAGE_BUFFER*/
|
||||||
case 0x905D: /*GL_INT_IMAGE_1D_ARRAY*/
|
|
||||||
case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/
|
case 0x905E: /*GL_INT_IMAGE_2D_ARRAY*/
|
||||||
case 0x905F: /*GL_INT_IMAGE_CUBE_MAP_ARRAY*/
|
|
||||||
case 0x9060: /*GL_INT_IMAGE_2D_MULTISAMPLE*/
|
|
||||||
case 0x9061: /*GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY*/
|
|
||||||
case 0x9062: /*GL_UNSIGNED_INT_IMAGE_1D*/
|
|
||||||
case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/
|
case 0x9063: /*GL_UNSIGNED_INT_IMAGE_2D*/
|
||||||
case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/
|
case 0x9064: /*GL_UNSIGNED_INT_IMAGE_3D*/
|
||||||
case 0x9065: /*GL_UNSIGNED_INT_IMAGE_2D_RECT*/
|
|
||||||
case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/
|
case 0x9066: /*GL_UNSIGNED_INT_IMAGE_CUBE*/
|
||||||
case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/
|
case 0x9067: /*GL_UNSIGNED_INT_IMAGE_BUFFER*/
|
||||||
case 0x9068: /*GL_UNSIGNED_INT_IMAGE_1D_ARRAY*/
|
|
||||||
case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/
|
case 0x9069: /*GL_UNSIGNED_INT_IMAGE_2D_ARRAY*/
|
||||||
case 0x906A: /*GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY*/
|
|
||||||
case 0x906B: /*GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE*/
|
|
||||||
case 0x906C: /*GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY*/
|
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -1265,9 +1015,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
// Defined further down, next to CollectImageFormatBakeInputs; only referenced here.
|
|
||||||
struct ImageFormatBakeInputs;
|
|
||||||
|
|
||||||
class BackendProgramObjectImpl {
|
class BackendProgramObjectImpl {
|
||||||
public:
|
public:
|
||||||
// Per-link cache of a sampler-style uniform's backend location: built once in
|
// Per-link cache of a sampler-style uniform's backend location: built once in
|
||||||
@@ -1327,7 +1074,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
BackendProgramObjectImpl();
|
BackendProgramObjectImpl();
|
||||||
~BackendProgramObjectImpl();
|
~BackendProgramObjectImpl();
|
||||||
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||||
void Use();
|
void Use() const;
|
||||||
void SetBaseInstance(Uint32 baseInstance) const;
|
void SetBaseInstance(Uint32 baseInstance) const;
|
||||||
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
|
||||||
void SetDrawID(Uint32 drawId) const;
|
void SetDrawID(Uint32 drawId) const;
|
||||||
@@ -1338,14 +1085,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
|
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
|
||||||
// uniform write, and only such a program needs the reset after one.
|
// uniform write, and only such a program needs the reset after one.
|
||||||
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
|
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
|
||||||
// Which viewport indices the next draw's fragments may keep, one bit each. Written
|
|
||||||
// once per replay pass; see ForEachViewportRoutingPass.
|
|
||||||
void SetViewportPassMask(Uint32 indexMask) const;
|
|
||||||
// True when this build injected the fragment-stage viewport gate, i.e. when a
|
|
||||||
// pre-rasterization stage routes by gl_ViewportIndex AND the fragment stage can act
|
|
||||||
// on it. The uniform is the honest test for both halves: it exists only where the
|
|
||||||
// gate was injected, and the gate is injected only where a stage routes.
|
|
||||||
Bool RoutesViewportIndex() const { return m_viewportPassMaskUniformLocation >= 0; }
|
|
||||||
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
|
||||||
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
Uint GetBackendProgramId() const { return m_backendProgramId; }
|
||||||
// False when the last SyncToBackend could not produce a usable program (a
|
// False when the last SyncToBackend could not produce a usable program (a
|
||||||
@@ -1361,22 +1100,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// qualifier, so the overrides are baked into the source). A mismatch means the
|
// qualifier, so the overrides are baked into the source). A mismatch means the
|
||||||
// program is stale exactly like the clamp masks above.
|
// program is stale exactly like the clamp masks above.
|
||||||
Uint64 GetShaderStorageBlockBindingSignature() const { return m_shaderStorageBlockBindingSignature; }
|
Uint64 GetShaderStorageBlockBindingSignature() const { return m_shaderStorageBlockBindingSignature; }
|
||||||
// GL atomic-counter binding points the transpiled stages declare (sorted, unique),
|
|
||||||
// and the top of the reserved shader-storage range their counter blocks were
|
|
||||||
// transpiled against - the slot for GL binding N is `top - N`. Empty for every
|
|
||||||
// program that uses no atomic counter, which is what keeps the per-draw cost of the
|
|
||||||
// counter sync at one empty-vector test.
|
|
||||||
const Vector<Int>& GetAtomicCounterBindings() const { return m_atomicCounterGlBindings; }
|
|
||||||
Int GetAtomicCounterEsslBindingTop() const { return m_atomicCounterEsslBindingTop; }
|
|
||||||
// GL_PATCH_VERTICES the synthesized pass-through tessellation control stage was built
|
|
||||||
// for, or -1 when this program needed no such stage. Another of the same shape as the
|
|
||||||
// signatures above: the value is compiled INTO the synthesized stage as
|
|
||||||
// `layout(vertices = N) out`, so a program built for one patch size is stale for
|
|
||||||
// another and the draw path has to say so. -1 compares equal to itself for every
|
|
||||||
// program that has a control stage of its own, i.e. for all but a handful.
|
|
||||||
Int GetPassthroughTessControlPatchVertices() const {
|
|
||||||
return m_passthroughTessControlPatchVertices;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||||
@@ -1425,33 +1148,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
private:
|
private:
|
||||||
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||||
|
|
||||||
// Builds, compiles and attaches the pass-through tessellation control stage GL 4.6
|
|
||||||
// core 11.2.2 describes, for a program that has an evaluation stage and none of its
|
|
||||||
// own - which ES 3.2 rejects outright. Called from SyncToBackend after every real
|
|
||||||
// stage has been attached and before the link; see the definition for why it cannot
|
|
||||||
// regress a program that works today.
|
|
||||||
void AttachPassthroughTessControlStage(
|
|
||||||
const MG_State::GLState::ProgramObject& stateProgramObject, Int tessEvalShaderIndex,
|
|
||||||
const Vector<Vector<unsigned int>>& shaderSpirvs, const String& vertexStageEssl,
|
|
||||||
const String& tessEvalStageEssl);
|
|
||||||
|
|
||||||
// One stage's SPIR-V through the DirectGLES pass chain and SPIRV-Cross, producing
|
|
||||||
// the raw emitted ESSL and the interface blocks this stage's XFB flattening
|
|
||||||
// rewrote. This is the segment the L2 shader-translation memo keys on, so every
|
|
||||||
// input it reads must appear in EsslTranslationKeyInputs - see the definition's
|
|
||||||
// header comment in Managers.cpp and MG_Util/ShaderTranspiler/TranslationCache.h.
|
|
||||||
// False means SPIRV-Cross refused the module; `outError` then carries its message.
|
|
||||||
Bool TranspileSpirvToEssl(const Vector<unsigned int>& spirvCode, GLenum glShaderType,
|
|
||||||
const std::set<String>& xfbCaptureBlockNames,
|
|
||||||
const ImageFormatBakeInputs& imageFormatBake,
|
|
||||||
const UnorderedMap<String, Int>& storageBlockBindingOverrides,
|
|
||||||
const std::map<String, String>& inputBlockRenames,
|
|
||||||
const std::map<String, String>& outputBlockRenames,
|
|
||||||
Int atomicCounterEsslBindingTop, Bool enableSpirvValidation,
|
|
||||||
String& outSource,
|
|
||||||
std::set<String>& outFlattenedXfbBlockNames,
|
|
||||||
Vector<Int>& outAtomicCounterGlBindings, String& outError) const;
|
|
||||||
|
|
||||||
Uint m_backendProgramId = 0;
|
Uint m_backendProgramId = 0;
|
||||||
// GL name of the frontend program this was last synced from; diagnostics only, so
|
// GL name of the frontend program this was last synced from; diagnostics only, so
|
||||||
// an unusable backend program can be traced back to the glCreateProgram id the app
|
// an unusable backend program can be traced back to the glCreateProgram id the app
|
||||||
@@ -1462,7 +1158,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Int m_drawIdUniformLocation = -1;
|
Int m_drawIdUniformLocation = -1;
|
||||||
Int m_baseVertexUniformLocation = -1;
|
Int m_baseVertexUniformLocation = -1;
|
||||||
Int m_baseInstanceWordIndexUniformLocation = -1;
|
Int m_baseInstanceWordIndexUniformLocation = -1;
|
||||||
Int m_viewportPassMaskUniformLocation = -1;
|
|
||||||
Int m_indirectParamsBinding = -1;
|
Int m_indirectParamsBinding = -1;
|
||||||
Uint32 m_snormFallbackClampOutputMask = 0;
|
Uint32 m_snormFallbackClampOutputMask = 0;
|
||||||
Uint32 m_unormFallbackClampOutputMask = 0;
|
Uint32 m_unormFallbackClampOutputMask = 0;
|
||||||
@@ -1471,19 +1166,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint m_fragColorBroadcastCount = 1;
|
Uint m_fragColorBroadcastCount = 1;
|
||||||
// 0 is the signature of an empty override set, i.e. what almost every program has.
|
// 0 is the signature of an empty override set, i.e. what almost every program has.
|
||||||
Uint64 m_shaderStorageBlockBindingSignature = 0;
|
Uint64 m_shaderStorageBlockBindingSignature = 0;
|
||||||
Vector<Int> m_atomicCounterGlBindings;
|
|
||||||
Int m_atomicCounterEsslBindingTop = -1;
|
|
||||||
// -1 for every program that has a tessellation control stage of its own (or none at
|
|
||||||
// all); otherwise the GL_PATCH_VERTICES the synthesized pass-through stage was built
|
|
||||||
// with. See GetPassthroughTessControlPatchVertices.
|
|
||||||
Int m_passthroughTessControlPatchVertices = -1;
|
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
Bool m_backendProgramUsable = false;
|
Bool m_backendProgramUsable = false;
|
||||||
// Set by SyncToBackend every time it relinks the driver program, cleared by the
|
|
||||||
// next Use(). Use() dedupes on a GL program NAME, and a relink replaces the
|
|
||||||
// executable behind that name without changing it - see the note at the
|
|
||||||
// glLinkProgram in SyncToBackend for what the driver runs otherwise.
|
|
||||||
Bool m_rebindAfterRelink = false;
|
|
||||||
|
|
||||||
Int m_globalUboBackendBlockIndex = -1;
|
Int m_globalUboBackendBlockIndex = -1;
|
||||||
Int m_globalUboBackendBlockSize = 0;
|
Int m_globalUboBackendBlockSize = 0;
|
||||||
@@ -1573,14 +1257,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Some format in play - declared or baked - is outside the GLSL ES core image
|
// Some format in play - declared or baked - is outside the GLSL ES core image
|
||||||
// format set, so the emitted ESSL needs the GL_NV_image_formats directive.
|
// format set, so the emitted ESSL needs the GL_NV_image_formats directive.
|
||||||
Bool needsExtendedImageFormats = false;
|
Bool needsExtendedImageFormats = false;
|
||||||
// Some DECLARED format in play is one WidenImageFormatsForEssl will re-declare in a
|
|
||||||
// core carrier. Answered from the uniform reflection rather than from a module parse
|
|
||||||
// on purpose: the widening is armed on every driver, so a per-stage BuildModule to
|
|
||||||
// find out would land on every stage of every program - which is the cost
|
|
||||||
// SpirvGateFeatures exists to avoid. Program-wide, so it can over-arm a stage that
|
|
||||||
// declares no image; the pass then finds nothing, reports no change, and the caller
|
|
||||||
// keeps the module it already had.
|
|
||||||
Bool declaresWidenableImageFormat = false;
|
|
||||||
};
|
};
|
||||||
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
ImageFormatBakeInputs CollectImageFormatBakeInputs(
|
||||||
const MG_State::GLState::ProgramObject& stateProgramObject);
|
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||||
|
|||||||
@@ -414,18 +414,14 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
|||||||
const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
|
const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
|
||||||
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
|
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
|
||||||
if (batched) {
|
if (batched) {
|
||||||
ForEachViewportRoutingPass([&] {
|
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
|
||||||
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
|
drawcount, 0);
|
||||||
drawcount, 0);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||||
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
|
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
|
||||||
ForEachViewportRoutingPass([&] {
|
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
||||||
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (feedDrawID) SetCurrentDrawID(0);
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||||
@@ -446,10 +442,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
|||||||
if (count[i] <= 0) continue;
|
if (count[i] <= 0) continue;
|
||||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||||
ForEachViewportRoutingPass([&] {
|
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
basevertex ? basevertex[i] : 0);
|
||||||
basevertex ? basevertex[i] : 0);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (feedDrawID) SetCurrentDrawID(0);
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||||
@@ -521,10 +515,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
|||||||
// driver sees none - but gl_BaseVertex still has to report the value the
|
// driver sees none - but gl_BaseVertex still has to report the value the
|
||||||
// application passed for this sub-draw.
|
// application passed for this sub-draw.
|
||||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||||
ForEachViewportRoutingPass([&] {
|
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
|
||||||
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
|
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
||||||
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
|
||||||
});
|
|
||||||
cursor += static_cast<SizeT>(count[i]);
|
cursor += static_cast<SizeT>(count[i]);
|
||||||
}
|
}
|
||||||
if (feedDrawID) SetCurrentDrawID(0);
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
@@ -878,9 +870,7 @@ void main() {
|
|||||||
if (flattened.indexCount != 0) {
|
if (flattened.indexCount != 0) {
|
||||||
const Uint previousIndexBinding = BoundIndexBufferId();
|
const Uint previousIndexBinding = BoundIndexBufferId();
|
||||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId);
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId);
|
||||||
ForEachViewportRoutingPass([&] {
|
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
|
||||||
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
|
|
||||||
});
|
|
||||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
#include "MG_Backend/BackendObjects.h"
|
#include "MG_Backend/BackendObjects.h"
|
||||||
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
|
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
|
||||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
|
||||||
|
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||||
@@ -172,12 +171,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
|
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
|
||||||
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||||
}
|
}
|
||||||
// 8-bit signed-normalized storage is core ES, so only the rendering half is in
|
|
||||||
// question here; the 16-bit bit above additionally needs EXT_texture_norm16 for the
|
|
||||||
// encoding to exist at all.
|
|
||||||
if (!capabilities.SupportsRenderSnorm) {
|
|
||||||
options |= PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget;
|
|
||||||
}
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,105 +227,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) {
|
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) {
|
||||||
return BackendFormatAddsAlpha(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
return BackendFormatAddsAlpha(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat) {
|
|
||||||
const GLenum requested = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
|
||||||
const auto carrier = static_cast<GLenum>(
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::WidenedCoreEsslImageFormat(requested));
|
|
||||||
if (carrier == 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
// EXACTLY the arming WidenImageFormatsForEssl uses, and it has to be: the shader, the
|
|
||||||
// storage and the bind must all widen or none of them may, or the shader addresses a
|
|
||||||
// texel size the storage does not have (which every driver tested accepts silently,
|
|
||||||
// reading and writing out of bounds).
|
|
||||||
//
|
|
||||||
// A driver WITH GL_NV_image_formats can spell the narrow format - but only for the
|
|
||||||
// formats SPIRV-Cross will actually print. It throws for its is_desktop_only_format
|
|
||||||
// set instead of emitting a token, and the throw loses the stage whatever the driver
|
|
||||||
// would have accepted: on Mesa, which advertises the extension, `layout(r8ui)
|
|
||||||
// uimage2D` still lost its whole program until the widening ran for it too.
|
|
||||||
if (g_GLESCapabilities.SupportsExtendedImageFormats &&
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(requested)) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
ImageBindableStorageWidening widening;
|
|
||||||
widening.InternalFormat = carrier;
|
|
||||||
widening.SourceChannels =
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::ImageFormatChannelCount(requested);
|
|
||||||
switch (carrier) {
|
|
||||||
case GL_RGBA32UI:
|
|
||||||
case GL_RGBA16UI:
|
|
||||||
case GL_RGBA8UI:
|
|
||||||
case GL_RGBA32I:
|
|
||||||
case GL_RGBA16I:
|
|
||||||
case GL_RGBA8I:
|
|
||||||
widening.IntegerData = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
widening.IntegerData = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// The carrier is a core ES format in every case, so it needs no fallback options of
|
|
||||||
// its own; this call is only here to spell the transfer pair that describes it.
|
|
||||||
MG_Util::TextureFormatProcessor::NormalizePixelFormat(carrier, Flags<PixelFormatNormalizeOptionBit>{},
|
|
||||||
nullptr, &widening.Format, &widening.Type);
|
|
||||||
// The two carriers that are not channel widenings, whose transfer pair has to say so.
|
|
||||||
// Every other entry keeps the frontend format's own component type - a GL_RG16F shadow
|
|
||||||
// is halves and so is its GL_RGBA16F carrier, so padding the channels is the whole
|
|
||||||
// conversion. These two shadows are a PACKED 32-bit word per texel
|
|
||||||
// (TextureFormatProcessor::NormalizePixelFormat), and no ES driver accepts either
|
|
||||||
// packed type for the carrier's level, so the transfer names the carrier's own layout
|
|
||||||
// and PrepareImageWidenedUpload splits the word into it.
|
|
||||||
switch (internalFormat) {
|
|
||||||
case TextureInternalFormat::R11FG11FB10F:
|
|
||||||
// GL_UNSIGNED_INT_10F_11F_11F_REV -> GL_RGBA / GL_FLOAT, legal for GL_RGBA16F.
|
|
||||||
widening.Format = GL_RGBA;
|
|
||||||
widening.Type = GL_FLOAT;
|
|
||||||
widening.SourceEncoding = ImageWidenSourceEncoding::PackedFloat11f11f10f;
|
|
||||||
break;
|
|
||||||
case TextureInternalFormat::RGB10A2UI:
|
|
||||||
case TextureInternalFormat::RGB10A2:
|
|
||||||
// GL_UNSIGNED_INT_2_10_10_10_REV -> the GL_RGBA_INTEGER / GL_UNSIGNED_SHORT the
|
|
||||||
// GL_RGBA16UI carrier already asked for above; only the split is new. The two
|
|
||||||
// formats share it: rgb10_a2's channel codes are the same fields rgb10_a2ui's are,
|
|
||||||
// and what the shader divides them by is not the transfer's business.
|
|
||||||
widening.SourceEncoding = ImageWidenSourceEncoding::PackedInt2101010Rev;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// The seven normalized formats whose carrier holds CODES rather than values. Both
|
|
||||||
// halves of the transfer need to know: a missing alpha is padded with the saturated
|
|
||||||
// code rather than the integer 1, and glGetTexImage has to divide the codes back out.
|
|
||||||
bool signedNormalized = false;
|
|
||||||
Uint32 channelMax[4] = {0u, 0u, 0u, 0u};
|
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::NormalizedImageCarrierCodes(requested, channelMax,
|
|
||||||
signedNormalized)) {
|
|
||||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
|
||||||
widening.ChannelMax[channel] = channelMax[channel];
|
|
||||||
}
|
|
||||||
widening.SignedNormalized = signedNormalized;
|
|
||||||
}
|
|
||||||
return widening;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat) {
|
|
||||||
const GLenum requested = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
|
||||||
const auto base = static_cast<GLenum>(
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::SplitCoreEsslBufferImageFormat(requested));
|
|
||||||
if (base == 0) {
|
|
||||||
return GL_UNKNOWN_MGL;
|
|
||||||
}
|
|
||||||
// EXACTLY the arming WidenImageFormatsForEssl uses, for the reason the widening's is:
|
|
||||||
// the shader, the glTexBuffer view and the glBindImageTexture argument must all split
|
|
||||||
// or none of them may, or the shader subscripts a view the buffer is not described as.
|
|
||||||
if (g_GLESCapabilities.SupportsExtendedImageFormats &&
|
|
||||||
MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(requested)) {
|
|
||||||
return GL_UNKNOWN_MGL;
|
|
||||||
}
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
String ProcessOutColorLocations(const String& glslCode) {
|
String ProcessOutColorLocations(const String& glslCode) {
|
||||||
@@ -675,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
|
||||||
@@ -800,82 +657,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, const Bool input) {
|
|
||||||
#ifdef TRACY_ENABLE
|
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
||||||
#endif
|
|
||||||
// Deliberately a scan for the DECLARATION rather than a regex over the whole text:
|
|
||||||
// "gl_PerVertex" also appears inside the block's own body in some emissions, and the
|
|
||||||
// direction keyword has to be the one immediately preceding the name for the match to
|
|
||||||
// mean what this needs it to mean.
|
|
||||||
const auto isIdentifierChar = [](char c) {
|
|
||||||
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '_';
|
|
||||||
};
|
|
||||||
const String keyword = input ? String("in") : String("out");
|
|
||||||
SizeT pos = 0;
|
|
||||||
while ((pos = essl.find("gl_PerVertex", pos)) != String::npos) {
|
|
||||||
// Walk back over whitespace to the direction keyword.
|
|
||||||
SizeT before = pos;
|
|
||||||
while (before > 0 && std::isspace(static_cast<unsigned char>(essl[before - 1]))) --before;
|
|
||||||
const Bool matches = before >= keyword.size() &&
|
|
||||||
essl.compare(before - keyword.size(), keyword.size(), keyword) == 0 &&
|
|
||||||
(before == keyword.size() ||
|
|
||||||
!isIdentifierChar(essl[before - keyword.size() - 1]));
|
|
||||||
if (!matches) {
|
|
||||||
pos += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const SizeT open = essl.find('{', pos);
|
|
||||||
if (open == String::npos) return std::nullopt;
|
|
||||||
const SizeT close = essl.find('}', open);
|
|
||||||
if (close == String::npos) return std::nullopt;
|
|
||||||
return essl.substr(open + 1, close - open - 1);
|
|
||||||
}
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
String BuildPassthroughTessControlEssl(const Uint esslVersion, const Uint patchVertices,
|
|
||||||
const String& inPerVertexMembers,
|
|
||||||
const String& outPerVertexMembers) {
|
|
||||||
#ifdef TRACY_ENABLE
|
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
||||||
#endif
|
|
||||||
// Tessellation is core in ES 3.2 and reachable in 3.1 only through
|
|
||||||
// GL_EXT_tessellation_shader. The caller has already established that the driver runs
|
|
||||||
// the evaluation stage at all, so the only question here is which spelling to use.
|
|
||||||
const Bool core = esslVersion >= 320;
|
|
||||||
String source = "#version " + std::to_string(core ? 320u : 310u) + " es\n";
|
|
||||||
if (!core) {
|
|
||||||
source += "#extension GL_EXT_tessellation_shader : require\n";
|
|
||||||
}
|
|
||||||
source += "precision highp float;\n";
|
|
||||||
source += "precision highp int;\n";
|
|
||||||
source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n";
|
|
||||||
// Mirrored, never invented. An empty member list means the neighbouring stage did not
|
|
||||||
// redeclare the block either, and the driver's own built-in declaration is then what
|
|
||||||
// both sides agree on - redeclaring here would be the thing that broke the match.
|
|
||||||
if (!inPerVertexMembers.empty()) {
|
|
||||||
source += "in gl_PerVertex {" + inPerVertexMembers + "} gl_in[gl_MaxPatchVertices];\n";
|
|
||||||
}
|
|
||||||
if (!outPerVertexMembers.empty()) {
|
|
||||||
source += "out gl_PerVertex {" + outPerVertexMembers + "} gl_out[];\n";
|
|
||||||
}
|
|
||||||
source += "void main() {\n";
|
|
||||||
// Only gl_Position is forwarded. That is the whole of what the pass-through owes the
|
|
||||||
// evaluation stage: a program whose evaluation stage reads anything else per-vertex
|
|
||||||
// was declined before this was ever called (ModuleReadsLocatedInput), and gl_PointSize
|
|
||||||
// from a tessellation stage is a separate capability on both targets.
|
|
||||||
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
|
|
||||||
source += " gl_TessLevelOuter[0] = 1.0;\n";
|
|
||||||
source += " gl_TessLevelOuter[1] = 1.0;\n";
|
|
||||||
source += " gl_TessLevelOuter[2] = 1.0;\n";
|
|
||||||
source += " gl_TessLevelOuter[3] = 1.0;\n";
|
|
||||||
source += " gl_TessLevelInner[0] = 1.0;\n";
|
|
||||||
source += " gl_TessLevelInner[1] = 1.0;\n";
|
|
||||||
source += "}\n";
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
Bool IsImagePassIdentifierChar(char c) {
|
Bool IsImagePassIdentifierChar(char c) {
|
||||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||||
@@ -987,8 +768,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
struct ImageUniformDecl {
|
struct ImageUniformDecl {
|
||||||
String name;
|
String name;
|
||||||
String aliasName; // the repair-tagged name the rewritten declaration takes; empty
|
|
||||||
// for a declaration this pass leaves alone
|
|
||||||
String writeName; // the writeonly half's name, when split
|
String writeName; // the writeonly half's name, when split
|
||||||
String layout; // raw contents of layout(...)
|
String layout; // raw contents of layout(...)
|
||||||
String qualifiers; // memory/precision qualifiers, normalized, no trailing space
|
String qualifiers; // memory/precision qualifiers, normalized, no trailing space
|
||||||
@@ -996,35 +775,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
String arraySuffix; // "" or "[7]"
|
String arraySuffix; // "" or "[7]"
|
||||||
SizeT declStart = 0;
|
SizeT declStart = 0;
|
||||||
SizeT declLength = 0;
|
SizeT declLength = 0;
|
||||||
SizeT nameStart = 0; // the name token alone, for a rename that edits nothing else
|
|
||||||
SizeT nameLength = 0;
|
|
||||||
SizeT referenceCount = 0; // uses this pass recognized and accounted for
|
SizeT referenceCount = 0; // uses this pass recognized and accounted for
|
||||||
Bool loaded = false;
|
Bool loaded = false;
|
||||||
Bool stored = false;
|
Bool stored = false;
|
||||||
Bool unknownUse = false;
|
Bool unknownUse = false;
|
||||||
Bool split = false;
|
Bool split = false;
|
||||||
// SPIRV-Cross already tagged this one readonly or writeonly, so it needs no
|
|
||||||
// qualifier repair - only the rename that keeps two stages from merging it.
|
|
||||||
Bool preTaggedReadonly = false;
|
|
||||||
Bool preTaggedWriteonly = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// A rebuilt declaration. Keeps SPIRV-Cross's own word order (`uniform readonly
|
// A rebuilt declaration. Keeps SPIRV-Cross's own word order (`uniform readonly
|
||||||
// highp image2D`) so the image-rebinding regex in Managers.cpp still matches what
|
// highp image2D`) so the image-rebinding regex in Managers.cpp still matches what
|
||||||
// comes out of here, whichever order the two passes end up running in.
|
// comes out of here, whichever order the two passes end up running in.
|
||||||
//
|
|
||||||
// `forceCoherent` is for the SPLIT pair only. GLSL guarantees that a write through
|
|
||||||
// one image variable is visible to a read through a DIFFERENT one only when both are
|
|
||||||
// declared coherent, and the split turns a same-variable read-after-write - which
|
|
||||||
// desktop GLSL orders by construction, so the source almost never says `coherent` -
|
|
||||||
// into exactly that cross-variable shape. Without it the driver may serve the load
|
|
||||||
// from a cache that never saw the store through the writeonly half.
|
|
||||||
String BuildImageDeclaration(const ImageUniformDecl& decl, const char* memoryQualifier,
|
String BuildImageDeclaration(const ImageUniformDecl& decl, const char* memoryQualifier,
|
||||||
const String& variableName, Bool forceCoherent = false) {
|
const String& variableName) {
|
||||||
String out = "layout(" + decl.layout + ") uniform ";
|
String out = "layout(" + decl.layout + ") uniform ";
|
||||||
if (forceCoherent && !ContainsIdentifier(decl.qualifiers, "coherent")) {
|
|
||||||
out += "coherent ";
|
|
||||||
}
|
|
||||||
out += memoryQualifier;
|
out += memoryQualifier;
|
||||||
out += ' ';
|
out += ' ';
|
||||||
if (!decl.qualifiers.empty()) {
|
if (!decl.qualifiers.empty()) {
|
||||||
@@ -1039,11 +802,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A name for a rewritten declaration that no identifier in the shader (and no other
|
// A name for the writeonly half that no identifier in the shader (and no other
|
||||||
// alias already minted for this stage) can collide with.
|
// half already minted) can collide with.
|
||||||
String MakeImageAliasName(const String& prefix, const String& name, const String& source,
|
String MakeImageWriteAliasName(const String& name, const String& source,
|
||||||
const Vector<String>& taken) {
|
const Vector<String>& taken) {
|
||||||
String candidate = prefix + name;
|
String candidate = String(IMAGE_WRITE_ALIAS_PREFIX) + name;
|
||||||
// "__" anywhere in an identifier is reserved (GLSL ES 3.20 3.7), which a name
|
// "__" anywhere in an identifier is reserved (GLSL ES 3.20 3.7), which a name
|
||||||
// that already starts with '_' would otherwise produce.
|
// that already starts with '_' would otherwise produce.
|
||||||
for (SizeT doubled = candidate.find("__"); doubled != String::npos;
|
for (SizeT doubled = candidate.find("__"); doubled != String::npos;
|
||||||
@@ -1066,265 +829,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
SizeT length;
|
SizeT length;
|
||||||
String text;
|
String text;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The offset just past the `;` that terminates the call whose argument list opens at
|
|
||||||
// `openParen`, or npos when what follows is not a plain statement. Parentheses alone
|
|
||||||
// are counted: every other bracket a GLSL argument list can contain is balanced
|
|
||||||
// inside them, and imageStore returns void, so a well-formed call site is always
|
|
||||||
// `imageStore(...);` and anything else is a shape this pass declines to edit.
|
|
||||||
SizeT FindEndOfCallStatement(const String& code, SizeT openParen) {
|
|
||||||
Int depth = 0;
|
|
||||||
SizeT scan = openParen;
|
|
||||||
for (; scan < code.size(); ++scan) {
|
|
||||||
if (code[scan] == '(') {
|
|
||||||
++depth;
|
|
||||||
} else if (code[scan] == ')' && --depth == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (scan >= code.size()) return String::npos;
|
|
||||||
const SizeT after = code.find_first_not_of(" \t\r\n", scan + 1);
|
|
||||||
if (after == String::npos || code[after] != ';') return String::npos;
|
|
||||||
return after + 1;
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace {
|
String SplitReadWriteImageUniforms(const String& glslCode) {
|
||||||
// The digits of an array extent or of an element subscript, or -1 for "not a plain
|
|
||||||
// decimal literal".
|
|
||||||
//
|
|
||||||
// One trailing `u`/`U` is PART of the literal rather than grounds for rejection.
|
|
||||||
// SPIRV-Cross prints an index in the type SPIR-V gave it, and
|
|
||||||
// LegalizeResourceArrayIndexPass mints its per-element constants in the type of the
|
|
||||||
// index it replaced (ConstantLikeIndex reads that index's own type_id), so an image
|
|
||||||
// array reached through anything unsigned - `for (uint i = 0u; i < 4u; ++i)`, or any
|
|
||||||
// expression on gl_LocalInvocationIndex, which is uint by definition - arrives here
|
|
||||||
// spelled `g_image[0u]`. Reading that as "not a literal" declined the array and left
|
|
||||||
// it on one layout(binding = N), which hands its elements the consecutive units
|
|
||||||
// N, N+1, ... - exactly the silently-wrong-units defect the split exists to remove.
|
|
||||||
Int ParseNonNegativeIntLiteral(const String& text) {
|
|
||||||
if (text.empty()) return -1;
|
|
||||||
SizeT digitCount = text.size();
|
|
||||||
if (text[digitCount - 1] == 'u' || text[digitCount - 1] == 'U') --digitCount;
|
|
||||||
if (digitCount == 0) return -1;
|
|
||||||
Int value = 0;
|
|
||||||
for (SizeT i = 0; i < digitCount; ++i) {
|
|
||||||
const char c = text[i];
|
|
||||||
if (c < '0' || c > '9') return -1;
|
|
||||||
value = value * 10 + (c - '0');
|
|
||||||
if (value > 4096) return -1; // no image array is anywhere near this
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
|
|
||||||
Vector<String>* outDeclined) {
|
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (outDeclined != nullptr) outDeclined->clear();
|
|
||||||
if (plans.empty() || glslCode.find("image") == String::npos) return glslCode;
|
|
||||||
|
|
||||||
// Same declaration shape as the split pass reads, with the array extent captured.
|
|
||||||
static const std::regex imageDeclRegex(
|
|
||||||
R"(layout\s*\(([^)]*)\)\s*uniform\s+)"
|
|
||||||
R"(((?:(?:readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*))"
|
|
||||||
R"(([iu]?image[A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:\[\s*([0-9]*)\s*\])?\s*;)");
|
|
||||||
static const std::regex bindingValueRegex(R"(binding\s*=\s*\d+)");
|
|
||||||
|
|
||||||
struct StageImageDecl {
|
|
||||||
String name;
|
|
||||||
String layout;
|
|
||||||
String qualifiers;
|
|
||||||
String type;
|
|
||||||
Int elementCount = 1;
|
|
||||||
SizeT declStart = 0;
|
|
||||||
SizeT declLength = 0;
|
|
||||||
};
|
|
||||||
// Every image declaration in the stage; the plans are program-wide and name arrays
|
|
||||||
// this stage may not declare at all.
|
|
||||||
Vector<StageImageDecl> decls;
|
|
||||||
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), imageDeclRegex), last; it != last; ++it) {
|
|
||||||
const std::smatch& match = *it;
|
|
||||||
StageImageDecl decl;
|
|
||||||
decl.layout = match[1].str();
|
|
||||||
decl.qualifiers = NormalizeDeclarationSpacing(match[2].str());
|
|
||||||
decl.type = match[3].str();
|
|
||||||
decl.name = match[4].str();
|
|
||||||
decl.elementCount = match[5].matched ? ParseNonNegativeIntLiteral(match[5].str()) : 1;
|
|
||||||
decl.declStart = static_cast<SizeT>(match.position(0));
|
|
||||||
decl.declLength = match[0].str().size();
|
|
||||||
decls.push_back(Move(decl));
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector<ImageSourceEdit> edits;
|
|
||||||
Vector<String> takenNames;
|
|
||||||
for (const ImageArrayUnitPlan& plan : plans) {
|
|
||||||
const auto decline = [&](const char* why) {
|
|
||||||
if (outDeclined != nullptr) outDeclined->push_back(plan.name + ": " + why);
|
|
||||||
};
|
|
||||||
if (plan.units.size() < 2) continue;
|
|
||||||
|
|
||||||
const StageImageDecl* decl = nullptr;
|
|
||||||
for (const auto& candidate : decls) {
|
|
||||||
if (candidate.name == plan.name) {
|
|
||||||
decl = &candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (decl == nullptr) {
|
|
||||||
// Absent from this stage entirely is the normal outcome - the reflection is
|
|
||||||
// program-wide and this pass runs per stage. Named but not RECOGNIZED is not:
|
|
||||||
// it means the declaration is spelled in some shape the regex above does not
|
|
||||||
// read, and staying quiet about that is how the wrong units got shipped.
|
|
||||||
if (ContainsIdentifier(glslCode, plan.name)) {
|
|
||||||
decline("the stage names it but declares it in a shape this pass cannot read");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (decl->elementCount < 0 || static_cast<SizeT>(decl->elementCount) != plan.units.size()) {
|
|
||||||
decline("the emitted array extent disagrees with the reflected element count");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool consecutive = true;
|
|
||||||
Bool everyElementHasAUnit = true;
|
|
||||||
for (SizeT element = 0; element < plan.units.size(); ++element) {
|
|
||||||
const Int unit = plan.units[element];
|
|
||||||
if (unit < 0) {
|
|
||||||
everyElementHasAUnit = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (unit != plan.units[0] + static_cast<Int>(element)) consecutive = false;
|
|
||||||
}
|
|
||||||
if (!everyElementHasAUnit) {
|
|
||||||
decline("an element has no image unit");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Already exactly what ESSL would do on its own. The caller filters these out;
|
|
||||||
// repeating the test here keeps the pass correct on its own terms.
|
|
||||||
if (consecutive) continue;
|
|
||||||
|
|
||||||
// Every use has to be `name[<literal>]`. The literal is what the split turns
|
|
||||||
// into a name, and by the time this runs there is always one:
|
|
||||||
// LegalizeResourceArrayIndexingForEssl has already folded or lowered every
|
|
||||||
// dynamic image-array subscript in the module, because ESSL forbids one
|
|
||||||
// outright ("image arrays indexed with non-constant expressions are forbidden
|
|
||||||
// in GLSL ES"). A subscript that is still an expression here is therefore a
|
|
||||||
// stage that was never going to compile, and guessing which element it meant
|
|
||||||
// would only change which unit it addressed wrongly.
|
|
||||||
struct ElementUse {
|
|
||||||
SizeT start; // the first character of the name
|
|
||||||
SizeT length; // through the closing ']'
|
|
||||||
SizeT element;
|
|
||||||
};
|
|
||||||
Vector<ElementUse> uses;
|
|
||||||
const char* refusal = nullptr;
|
|
||||||
for (SizeT pos = glslCode.find(plan.name); pos != String::npos;
|
|
||||||
pos = glslCode.find(plan.name, pos + 1)) {
|
|
||||||
if (pos > 0 && IsImagePassIdentifierChar(glslCode[pos - 1])) continue;
|
|
||||||
const SizeT after = pos + plan.name.size();
|
|
||||||
if (after < glslCode.size() && IsImagePassIdentifierChar(glslCode[after])) continue;
|
|
||||||
if (pos >= decl->declStart && pos < decl->declStart + decl->declLength) {
|
|
||||||
continue; // the declaration's own name
|
|
||||||
}
|
|
||||||
const SizeT open = glslCode.find_first_not_of(" \t\r\n", after);
|
|
||||||
if (open == String::npos || glslCode[open] != '[') {
|
|
||||||
refusal = "it is reached by something other than a subscript, so there is no "
|
|
||||||
"element index to rewrite";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Int depth = 0;
|
|
||||||
SizeT scan = open;
|
|
||||||
for (; scan < glslCode.size(); ++scan) {
|
|
||||||
if (glslCode[scan] == '[') {
|
|
||||||
++depth;
|
|
||||||
} else if (glslCode[scan] == ']' && --depth == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (scan >= glslCode.size() || open + 1 >= scan) {
|
|
||||||
refusal = "it is reached by something other than a subscript, so there is no "
|
|
||||||
"element index to rewrite";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const Int element = ParseNonNegativeIntLiteral(
|
|
||||||
NormalizeDeclarationSpacing(glslCode.substr(open + 1, scan - open - 1)));
|
|
||||||
if (element < 0 || element >= decl->elementCount) {
|
|
||||||
refusal = "its subscript is not a literal element index, so which unit the "
|
|
||||||
"access reaches cannot be decided here";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
uses.push_back({pos, scan + 1 - pos, static_cast<SizeT>(element)});
|
|
||||||
}
|
|
||||||
if (refusal != nullptr) {
|
|
||||||
decline(refusal);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One SCALAR declaration per element, each carrying its own binding. ESSL nails
|
|
||||||
// an ARRAY's elements to consecutive units and offers no way to move them, so
|
|
||||||
// the only spelling that reaches an arbitrary set of units is one declaration
|
|
||||||
// per unit - and with every subscript a literal, every use has exactly one of
|
|
||||||
// them to be rewritten to.
|
|
||||||
//
|
|
||||||
// It costs precisely the image uniforms the application declared, which is why
|
|
||||||
// there is no budget test here: an array of four elements becomes four scalars
|
|
||||||
// however far apart their units are.
|
|
||||||
const SizeT elementCount = plan.units.size();
|
|
||||||
Vector<String> elementNames;
|
|
||||||
String replacement;
|
|
||||||
for (SizeT element = 0; element < elementCount; ++element) {
|
|
||||||
const String elementName =
|
|
||||||
MakeImageAliasName(IMAGE_ARRAY_ELEMENT_PREFIX,
|
|
||||||
plan.name + "_" + std::to_string(element), glslCode, takenNames);
|
|
||||||
takenNames.push_back(elementName);
|
|
||||||
elementNames.push_back(elementName);
|
|
||||||
|
|
||||||
String layout = decl->layout;
|
|
||||||
const String bindingText = "binding = " + std::to_string(plan.units[element]);
|
|
||||||
if (std::regex_search(layout, bindingValueRegex)) {
|
|
||||||
layout = std::regex_replace(layout, bindingValueRegex, bindingText);
|
|
||||||
} else {
|
|
||||||
layout = bindingText + (layout.empty() ? String() : ", " + layout);
|
|
||||||
}
|
|
||||||
if (element != 0) replacement += '\n';
|
|
||||||
replacement += "layout(" + layout + ") uniform ";
|
|
||||||
if (!decl->qualifiers.empty()) {
|
|
||||||
replacement += decl->qualifiers;
|
|
||||||
replacement += ' ';
|
|
||||||
}
|
|
||||||
replacement += decl->type + " " + elementName + ";";
|
|
||||||
}
|
|
||||||
edits.push_back({decl->declStart, decl->declLength, Move(replacement)});
|
|
||||||
|
|
||||||
// `name[k]` -> the scalar declared for element k, subscript and all.
|
|
||||||
for (const ElementUse& use : uses) {
|
|
||||||
edits.push_back({use.start, use.length, elementNames[use.element]});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (edits.empty()) return glslCode;
|
|
||||||
|
|
||||||
// Back to front, so an earlier edit's offsets stay valid. No two edits overlap: each
|
|
||||||
// one covers either a whole declaration or a whole `name[k]`, the declaration's own
|
|
||||||
// name is skipped when the uses are collected, and one occurrence of a name yields at
|
|
||||||
// most one edit.
|
|
||||||
std::sort(edits.begin(), edits.end(),
|
|
||||||
[](const ImageSourceEdit& a, const ImageSourceEdit& b) { return a.start > b.start; });
|
|
||||||
String result = glslCode;
|
|
||||||
for (const ImageSourceEdit& edit : edits) {
|
|
||||||
result.replace(edit.start, edit.length, edit.text);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
String SplitReadWriteImageUniforms(const String& glslCode, Uint* outSplitCount) {
|
|
||||||
#ifdef TRACY_ENABLE
|
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
||||||
#endif
|
|
||||||
// Written before any early return, so the caller never reads a stale count.
|
|
||||||
if (outSplitCount != nullptr) *outSplitCount = 0;
|
|
||||||
if (glslCode.find("image") == String::npos) {
|
if (glslCode.find("image") == String::npos) {
|
||||||
return glslCode;
|
return glslCode;
|
||||||
}
|
}
|
||||||
@@ -1343,12 +853,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), imageDeclRegex), last; it != last; ++it) {
|
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), imageDeclRegex), last; it != last; ++it) {
|
||||||
const std::smatch& match = *it;
|
const std::smatch& match = *it;
|
||||||
const String qualifiers = match[2].str();
|
const String qualifiers = match[2].str();
|
||||||
const Bool hasReadonly = ContainsIdentifier(qualifiers, "readonly");
|
// Already legal: SPIRV-Cross decided one way, leave it alone.
|
||||||
const Bool hasWriteonly = ContainsIdentifier(qualifiers, "writeonly");
|
if (ContainsIdentifier(qualifiers, "readonly") || ContainsIdentifier(qualifiers, "writeonly")) {
|
||||||
// Carrying BOTH is a spelling no per-stage access analysis produces (SPIRV-Cross
|
continue;
|
||||||
// clears one decoration or the other as soon as it sees a load or a store), so it
|
}
|
||||||
// came from the application and is identical in every stage. Nothing to do.
|
|
||||||
if (hasReadonly && hasWriteonly) continue;
|
|
||||||
|
|
||||||
Bool hasFormat = false;
|
Bool hasFormat = false;
|
||||||
Bool exemptFormat = false;
|
Bool exemptFormat = false;
|
||||||
@@ -1357,12 +865,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
hasFormat = true;
|
hasFormat = true;
|
||||||
exemptFormat = IsMemoryQualifierExemptImageFormat(token);
|
exemptFormat = IsMemoryQualifierExemptImageFormat(token);
|
||||||
}
|
}
|
||||||
// A declaration carrying neither qualifier is illegal ES unless its format is
|
// No format qualifier at all is a different (and, in ES, unconditionally
|
||||||
// r32f/r32i/r32ui, and no format qualifier at all is a shape SPIRV-Cross refuses
|
// illegal) shape that GL_EXT_shader_image_load_formatted would be needed for;
|
||||||
// to emit for an ES target. Either way there is no repair to make - and no rename
|
// SPIRV-Cross refuses to emit it for an ES target, so nothing to do here.
|
||||||
// to make either, because a declaration with no access qualifier is spelled the
|
if (!hasFormat || exemptFormat) continue;
|
||||||
// same in every stage.
|
|
||||||
if (!hasReadonly && !hasWriteonly && (!hasFormat || exemptFormat)) continue;
|
|
||||||
|
|
||||||
ImageUniformDecl decl;
|
ImageUniformDecl decl;
|
||||||
decl.layout = match[1].str();
|
decl.layout = match[1].str();
|
||||||
@@ -1372,10 +878,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
decl.arraySuffix = NormalizeDeclarationSpacing(match[5].str());
|
decl.arraySuffix = NormalizeDeclarationSpacing(match[5].str());
|
||||||
decl.declStart = static_cast<SizeT>(match.position(0));
|
decl.declStart = static_cast<SizeT>(match.position(0));
|
||||||
decl.declLength = match[0].str().size();
|
decl.declLength = match[0].str().size();
|
||||||
decl.nameStart = static_cast<SizeT>(match.position(4));
|
|
||||||
decl.nameLength = match[4].str().size();
|
|
||||||
decl.preTaggedReadonly = hasReadonly;
|
|
||||||
decl.preTaggedWriteonly = hasWriteonly;
|
|
||||||
decls.push_back(Move(decl));
|
decls.push_back(Move(decl));
|
||||||
}
|
}
|
||||||
if (decls.empty()) {
|
if (decls.empty()) {
|
||||||
@@ -1390,17 +892,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Walk every `image*(` call and attribute its first argument to a declaration.
|
// Walk every `image*(` call and attribute its first argument to a declaration.
|
||||||
// EVERY recognized use is recorded, not only the stores: a declaration this pass
|
struct StoreSite {
|
||||||
// renames has to take all of its uses with it, and the "every occurrence was one I
|
|
||||||
// saw" check below is what makes the recorded set provably the complete set.
|
|
||||||
struct ImageUseSite {
|
|
||||||
SizeT declIndex;
|
SizeT declIndex;
|
||||||
SizeT start;
|
SizeT start;
|
||||||
SizeT length;
|
SizeT length;
|
||||||
SizeT callOpen; // the '(' of the call this argument belongs to
|
|
||||||
Bool stores; // an imageStore, i.e. the use a split redirects to the write half
|
|
||||||
};
|
};
|
||||||
Vector<ImageUseSite> useSites;
|
Vector<StoreSite> storeSites;
|
||||||
for (SizeT pos = glslCode.find("image"); pos != String::npos; pos = glslCode.find("image", pos + 1)) {
|
for (SizeT pos = glslCode.find("image"); pos != String::npos; pos = glslCode.find("image", pos + 1)) {
|
||||||
if (pos > 0 && IsImagePassIdentifierChar(glslCode[pos - 1])) continue; // uimage2D, myimageFoo
|
if (pos > 0 && IsImagePassIdentifierChar(glslCode[pos - 1])) continue; // uimage2D, myimageFoo
|
||||||
SizeT tokenEnd = pos;
|
SizeT tokenEnd = pos;
|
||||||
@@ -1445,16 +942,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
switch (ClassifyImageBuiltin(builtin)) {
|
switch (ClassifyImageBuiltin(builtin)) {
|
||||||
case ImageBuiltinAccess::Load:
|
case ImageBuiltinAccess::Load:
|
||||||
decl.loaded = true;
|
decl.loaded = true;
|
||||||
useSites.push_back({declIndex, argStart, argEnd - argStart, openParen, false});
|
|
||||||
break;
|
break;
|
||||||
case ImageBuiltinAccess::Store:
|
case ImageBuiltinAccess::Store:
|
||||||
decl.stored = true;
|
decl.stored = true;
|
||||||
useSites.push_back({declIndex, argStart, argEnd - argStart, openParen, true});
|
storeSites.push_back({declIndex, argStart, argEnd - argStart});
|
||||||
break;
|
break;
|
||||||
case ImageBuiltinAccess::None:
|
case ImageBuiltinAccess::None:
|
||||||
// imageSize/imageSamples touch nothing, but they still NAME the variable, so
|
|
||||||
// a rename has to reach them.
|
|
||||||
useSites.push_back({declIndex, argStart, argEnd - argStart, openParen, false});
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
decl.unknownUse = true;
|
decl.unknownUse = true;
|
||||||
@@ -1471,122 +964,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Vector<ImageSourceEdit> edits;
|
Vector<ImageSourceEdit> edits;
|
||||||
Vector<String> takenNames;
|
Vector<String> takenAliases;
|
||||||
for (auto& decl : decls) {
|
for (auto& decl : decls) {
|
||||||
if (decl.unknownUse) continue; // leave it exactly as it was; no guessing
|
if (decl.unknownUse) continue; // leave it exactly as it was; no guessing
|
||||||
// EVERY declaration this pass rewrites is also RENAMED, under the prefix of the
|
|
||||||
// repair it is about to receive - the qualifier below is a decision about ONE
|
|
||||||
// STAGE's accesses, and GLSL requires a uniform declared in two stages to be
|
|
||||||
// declared IDENTICALLY (GLSL 4.3 4.3.9 / GLSL ES 3.20 4.3.9). A shader that
|
|
||||||
// stores to an image in the vertex stage and loads it in the fragment stage gets
|
|
||||||
// `writeonly` on one and `readonly` on the other, and on Adreno the linker merges
|
|
||||||
// the two same-named declarations and SILENTLY DISCARDS the vertex-stage stores:
|
|
||||||
// no GL error, no link log, LINK_STATUS = 1, and the image still holding its
|
|
||||||
// initial contents afterwards
|
|
||||||
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation, and any
|
|
||||||
// shader pack that writes an image in one stage to read it in another).
|
|
||||||
//
|
|
||||||
// Keyed on the REPAIR and not on the stage, which is what makes the rename
|
|
||||||
// exactly as wide as the problem. Two stages that use the image the same way
|
|
||||||
// reach the same prefix and emit byte-identical declarations, so they keep ONE
|
|
||||||
// shared uniform and there is nothing mismatched to merge; two that use it
|
|
||||||
// differently reach different prefixes and cannot be merged at all. Tagging by
|
|
||||||
// stage instead also broke the merge - but it broke it for the agreeing stages
|
|
||||||
// too, turning one image uniform into one PER STAGE that names it, and Adreno
|
|
||||||
// allocates image locations per distinct uniform: the five stages of
|
|
||||||
// KHR-GL43.shading_language_420pack.binding_images_texture_type_* went from 6
|
|
||||||
// image uniforms to 30 and the link failed outright with "Error: Image Image
|
|
||||||
// location or component exceeds max allowed." on an Adreno 830, where Mali and
|
|
||||||
// Mesa both accept the same text.
|
|
||||||
//
|
|
||||||
// Nothing downstream reads these names: the two passes that key on the GL uniform
|
|
||||||
// name (RebindImageUniformsToFrontendUnits, BakeImageFormatQualifiers) both run
|
|
||||||
// BEFORE this one, RemoveLayoutBinding recognises an image declaration by its TYPE
|
|
||||||
// token, and CacheResourceLocations skips image uniforms outright because ES image
|
|
||||||
// units come only from layout(binding=N). The declarations this pass LEAVES ALONE -
|
|
||||||
// already readonly/writeonly in the source, or r32f/r32i/r32ui, which need no
|
|
||||||
// qualifier - keep their names, and they are exactly the ones that already match
|
|
||||||
// across stages.
|
|
||||||
if (decl.preTaggedReadonly || decl.preTaggedWriteonly) {
|
|
||||||
// No repair: SPIRV-Cross already emitted a legal qualifier. But it derived
|
|
||||||
// that qualifier from THIS STAGE's accesses, so a uniform stored in one stage
|
|
||||||
// and loaded in another arrives here `writeonly` in one and `readonly` in the
|
|
||||||
// other under ONE name - precisely the same-name/mismatched-qualifier pair
|
|
||||||
// Adreno merges while silently discarding the writing stage's stores
|
|
||||||
// (advanced-memory-dependentInvocation; a raw-ES probe reproduces it with no
|
|
||||||
// MobileGL in the process, and renaming either half fixes it). Keyed on the
|
|
||||||
// qualifier for the same reason the repair below is: two stages that agree
|
|
||||||
// spell the same alias and stay merged, so no shader gains an image uniform.
|
|
||||||
const char* preTagPrefix =
|
|
||||||
decl.preTaggedReadonly ? IMAGE_READONLY_ALIAS_PREFIX : IMAGE_WRITEONLY_ALIAS_PREFIX;
|
|
||||||
decl.aliasName = MakeImageAliasName(preTagPrefix, decl.name, glslCode, takenNames);
|
|
||||||
takenNames.push_back(decl.aliasName);
|
|
||||||
// The name token alone: the qualifiers are already right, and re-emitting the
|
|
||||||
// whole declaration would only risk changing them.
|
|
||||||
edits.push_back({decl.nameStart, decl.nameLength, decl.aliasName});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* aliasPrefix = decl.loaded && decl.stored ? IMAGE_SPLIT_READ_ALIAS_PREFIX
|
|
||||||
: decl.stored ? IMAGE_WRITEONLY_ALIAS_PREFIX
|
|
||||||
: IMAGE_READONLY_ALIAS_PREFIX;
|
|
||||||
decl.aliasName = MakeImageAliasName(aliasPrefix, decl.name, glslCode, takenNames);
|
|
||||||
takenNames.push_back(decl.aliasName);
|
|
||||||
if (decl.loaded && decl.stored) {
|
if (decl.loaded && decl.stored) {
|
||||||
// Minted from the ALREADY access-tagged name, so the write half of a split
|
decl.writeName = MakeImageWriteAliasName(decl.name, glslCode, takenAliases);
|
||||||
// can never collide with the single declaration another stage's repair mints
|
takenAliases.push_back(decl.writeName);
|
||||||
// for the same image.
|
|
||||||
decl.writeName =
|
|
||||||
MakeImageAliasName(IMAGE_WRITE_ALIAS_PREFIX, decl.aliasName, glslCode, takenNames);
|
|
||||||
takenNames.push_back(decl.writeName);
|
|
||||||
decl.split = true;
|
decl.split = true;
|
||||||
if (outSplitCount != nullptr) ++*outSplitCount;
|
|
||||||
// Both halves carry `coherent`; see BuildImageDeclaration. The
|
|
||||||
// single-declaration cases below stay as they were - nothing aliases them, so
|
|
||||||
// there is no visibility to restore and no reason to pay for the cache
|
|
||||||
// behaviour.
|
|
||||||
edits.push_back({decl.declStart, decl.declLength,
|
edits.push_back({decl.declStart, decl.declLength,
|
||||||
BuildImageDeclaration(decl, "readonly", decl.aliasName,
|
BuildImageDeclaration(decl, "readonly", decl.name) + "\n" +
|
||||||
/*forceCoherent=*/true) +
|
BuildImageDeclaration(decl, "writeonly", decl.writeName)});
|
||||||
"\n" +
|
|
||||||
BuildImageDeclaration(decl, "writeonly", decl.writeName,
|
|
||||||
/*forceCoherent=*/true)});
|
|
||||||
} else if (decl.stored) {
|
} else if (decl.stored) {
|
||||||
edits.push_back({decl.declStart, decl.declLength,
|
edits.push_back({decl.declStart, decl.declLength,
|
||||||
BuildImageDeclaration(decl, "writeonly", decl.aliasName)});
|
BuildImageDeclaration(decl, "writeonly", decl.name)});
|
||||||
} else {
|
} else {
|
||||||
// Loaded only, or only ever handed to imageSize (or unused): readonly is
|
// Loaded only, or only ever handed to imageSize (or unused): readonly is
|
||||||
// the qualifier that keeps every one of those legal.
|
// the qualifier that keeps every one of those legal.
|
||||||
edits.push_back({decl.declStart, decl.declLength,
|
edits.push_back({decl.declStart, decl.declLength,
|
||||||
BuildImageDeclaration(decl, "readonly", decl.aliasName)});
|
BuildImageDeclaration(decl, "readonly", decl.name)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const ImageUseSite& site : useSites) {
|
for (const StoreSite& site : storeSites) {
|
||||||
const ImageUniformDecl& decl = decls[site.declIndex];
|
const ImageUniformDecl& decl = decls[site.declIndex];
|
||||||
// Empty exactly when the declaration was poisoned above and left untouched; its
|
if (!decl.split) continue;
|
||||||
// uses must keep naming the variable that is still called that.
|
edits.push_back({site.start, site.length, decl.writeName});
|
||||||
if (decl.aliasName.empty()) continue;
|
|
||||||
edits.push_back(
|
|
||||||
{site.start, site.length, decl.split && site.stores ? decl.writeName : decl.aliasName});
|
|
||||||
if (!decl.split || !site.stores) continue;
|
|
||||||
// ...and an explicit barrier behind it. `coherent` on both halves is what makes
|
|
||||||
// the store VISIBLE to a load through the other variable, but it says nothing
|
|
||||||
// about ORDER within one invocation - and the whole reason a declaration is split
|
|
||||||
// is that the shader both stores and loads through it, which on the ES side is now
|
|
||||||
// a write to one variable followed by a read of another the compiler has no reason
|
|
||||||
// to believe alias. Adreno duly serves the load from before the store
|
|
||||||
// (KHR-GL4x.shader_image_load_store.advanced-memory-order's store/load/compare
|
|
||||||
// loop reads back the previous iteration's value). memoryBarrierImage() is the
|
|
||||||
// GLSL primitive for exactly that ordering, is core GLSL ES 3.10 in every stage,
|
|
||||||
// and is not an execution barrier, so it is legal in non-uniform control flow too.
|
|
||||||
//
|
|
||||||
// Confined to the split pair: a single-declaration repair has nothing aliasing it
|
|
||||||
// and must not pay for this, and a shader that never got split never sees it at
|
|
||||||
// all.
|
|
||||||
const SizeT statementEnd = FindEndOfCallStatement(glslCode, site.callOpen);
|
|
||||||
if (statementEnd != String::npos) {
|
|
||||||
edits.push_back({statementEnd, 0, " memoryBarrierImage();"});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (edits.empty()) {
|
if (edits.empty()) {
|
||||||
return glslCode;
|
return glslCode;
|
||||||
|
|||||||
@@ -60,115 +60,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
|
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
|
||||||
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
|
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
|
||||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
||||||
|
|
||||||
// The CHANNEL WIDENING an image-bindable texture's ES storage takes, so that a format
|
|
||||||
// GLSL ES cannot spell as an image is carried by one it can.
|
|
||||||
//
|
|
||||||
// GL has forty image formats, GLSL ES core has thirteen, and no test device advertises
|
|
||||||
// GL_NV_image_formats - so a shader declaring one of the other twenty-six has no legal
|
|
||||||
// ESSL at all and glBindImageTexture rejects the narrow format outright for most of them
|
|
||||||
// (GL_INVALID_VALUE for nineteen of twenty-six on Adreno, twenty-five on both Malis).
|
|
||||||
// Seventeen have a core format of the SAME per-channel width and component type,
|
|
||||||
// differing only in channel count, and in one of those the emulation is EXACT: GL already
|
|
||||||
// defines an imageLoad from a narrower format as (r, 0, 0, 1) and an imageStore as
|
|
||||||
// dropping the components the format does not have, so the carrier's surplus channels
|
|
||||||
// hold values GL has already named. WidenImageFormatsPass pins them in the shader; this
|
|
||||||
// is the storage half, and DirectGLES::TextureImpl::SyncImageTextureBinding the bind
|
|
||||||
// half. All three ask WidenedCoreEsslImageFormat, so they cannot pick different carriers.
|
|
||||||
//
|
|
||||||
// Reports nothing (InternalFormat == GL_UNKNOWN_MGL) for a format that is core already,
|
|
||||||
// for the nine with no exact carrier (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16,
|
|
||||||
// r16, rgba16_snorm, rg16_snorm, r16_snorm - those keep the honest "no GLSL ES spelling"
|
|
||||||
// diagnostic rather than a silent approximation), and on a driver that HAS
|
|
||||||
// GL_NV_image_formats, where the shader keeps the declared format and no widening may
|
|
||||||
// happen behind it.
|
|
||||||
//
|
|
||||||
// The widened triple REPLACES what GenerateTextureFormatInfo chose, including any
|
|
||||||
// renderability substitution: an image that cannot be image-bound is useless whatever its
|
|
||||||
// attachment behaviour, so the image constraint wins. In practice that only bites
|
|
||||||
// RG8_SNORM/R8_SNORM on a driver without EXT_render_snorm, where the storage stays
|
|
||||||
// signed-normalized instead of becoming the half float that fallback would have picked -
|
|
||||||
// so an image-bound texture in one of those two formats is no longer attachable, and
|
|
||||||
// glGetTexImage on it falls through to the CPU shadow, which a shader-side imageStore
|
|
||||||
// does not update. Accepted deliberately: before the widening, an image binding in either
|
|
||||||
// format was refused outright by every driver tested and the stage that declared it never
|
|
||||||
// compiled at all, so nothing that works today is being given up.
|
|
||||||
//
|
|
||||||
// KNOWN GAP, for the same "all three layers move together" reason: a widened texture that
|
|
||||||
// is ALSO an FBO colour attachment gains one to three writable channels, and a draw into
|
|
||||||
// it can leave values in channels GL says are 0 and 1. Sampling and imageLoad are covered
|
|
||||||
// (the swizzle composition in SyncTextureParamsToBackend and the shader-side mask), but a
|
|
||||||
// glReadPixels/glGetTexImage that asks for more channels than the frontend format has
|
|
||||||
// would see them. Closing it needs the per-draw-buffer colour mask the three-channel
|
|
||||||
// widening already carries (FramebufferImpl::g_alphaWidenedDrawBufferMask) generalized
|
|
||||||
// from "alpha" to a channel count, which is its own change.
|
|
||||||
// How the FRONTEND's CPU shadow for a widened format is laid out relative to the carrier's
|
|
||||||
// transfer, i.e. what the upload has to do to it. Almost every entry is `Components`: the
|
|
||||||
// shadow already holds SourceChannels components of exactly the carrier's own type, so
|
|
||||||
// padding it out to four is the whole conversion. The packed entries do not - their shadow
|
|
||||||
// is ONE 32-bit word per texel - and reading such a word as components of the carrier's
|
|
||||||
// type takes twelve or sixteen bytes out of four and shears the level.
|
|
||||||
enum class ImageWidenSourceEncoding : Uint8 {
|
|
||||||
Components = 0,
|
|
||||||
// r11f_g11f_b10f: GL_UNSIGNED_INT_10F_11F_11F_REV -> four GL_FLOATs of an rgba16f.
|
|
||||||
PackedFloat11f11f10f,
|
|
||||||
// rgb10_a2 and rgb10_a2ui: GL_UNSIGNED_INT_2_10_10_10_REV -> four GL_UNSIGNED_SHORT
|
|
||||||
// channel CODES of an rgba16ui. The same split serves both: the two formats differ
|
|
||||||
// only in what the codes MEAN, which is the shader's business and not the transfer's.
|
|
||||||
PackedInt2101010Rev,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ImageBindableStorageWidening {
|
|
||||||
GLenum InternalFormat = GL_UNKNOWN_MGL;
|
|
||||||
GLenum Format = GL_UNKNOWN_MGL;
|
|
||||||
GLenum Type = GL_UNKNOWN_MGL;
|
|
||||||
// Channels the FRONTEND format has, i.e. how many of the carrier's four the client
|
|
||||||
// data fills. The rest are uploaded as 0, and the fourth as the format's implied 1.
|
|
||||||
Uint SourceChannels = 0;
|
|
||||||
// Whether that implied 1 is the integer one or a saturated normalized field - the
|
|
||||||
// transfer type cannot tell the two apart (GL_UNSIGNED_BYTE serves both RG8 and
|
|
||||||
// RG8UI), so the carrier decides.
|
|
||||||
Bool IntegerData = false;
|
|
||||||
// What the upload has to do to the frontend shadow before it describes the level to
|
|
||||||
// the driver (PrepareImageWidenedUpload).
|
|
||||||
ImageWidenSourceEncoding SourceEncoding = ImageWidenSourceEncoding::Components;
|
|
||||||
// Non-zero when the carrier holds this format's channels as the INTEGER CODES of a
|
|
||||||
// NORMALIZED value - the seven 16-bit and 10-bit normalized formats, which core ESSL
|
|
||||||
// has no image format of any width for and which a float carrier would requantise.
|
|
||||||
// Each entry is the largest code that channel can hold, i.e. the denominator of GL 4.6
|
|
||||||
// 2.3.5; SignedNormalized picks which of the two conversions it is the denominator of.
|
|
||||||
//
|
|
||||||
// Two things depend on it, both because the ES storage no longer shares the frontend
|
|
||||||
// format's component class: the upload pads a missing alpha with ChannelMax[3] instead
|
|
||||||
// of the transfer type's own "one" (through a uint carrier the saturated field IS the
|
|
||||||
// one), and glGetTexImage divides the codes back out into the floats the application
|
|
||||||
// is still owed.
|
|
||||||
Uint ChannelMax[4] = {0u, 0u, 0u, 0u};
|
|
||||||
Bool SignedNormalized = false;
|
|
||||||
|
|
||||||
Bool CarriesNormalizedCodes() const { return ChannelMax[0] != 0u; }
|
|
||||||
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
|
|
||||||
};
|
|
||||||
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat);
|
|
||||||
|
|
||||||
// The single-channel core format an image-bindable BUFFER texture's view is SPLIT into, or
|
|
||||||
// GL_UNKNOWN_MGL for a format that needs no split (or has no core base).
|
|
||||||
//
|
|
||||||
// A buffer texture cannot be widened: its texels are the application's buffer object, at
|
|
||||||
// the size and layout the application gave it, and it is usually also a vertex, index or
|
|
||||||
// storage buffer whose bytes are not ours to restride. But an rg32f view of N texels and
|
|
||||||
// an r32f view of 2N texels describe exactly the SAME bytes, so the split changes only
|
|
||||||
// how the shader subscripts them - component j of texel i is texel 2i + j of the base
|
|
||||||
// view - which WidenImageFormatsPass rewrites every access to do. The same rule as the
|
|
||||||
// widening decides WHETHER: a driver that can spell rg32f for an imageBuffer needs
|
|
||||||
// nothing.
|
|
||||||
//
|
|
||||||
// KNOWN GAP, and the reason this is not applied to a texture that is merely sampled: a
|
|
||||||
// buffer texture that is BOTH image-bound and read through a samplerBuffer would have its
|
|
||||||
// sampled view split too, and the sampler side is not rewritten. Accepted for the same
|
|
||||||
// reason the storage widening's gaps are - on a driver where the split applies at all
|
|
||||||
// there is no legal ESSL for the image declaration, so such a program did not compile.
|
|
||||||
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat);
|
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
|
|
||||||
namespace FramebufferImpl {} // namespace FramebufferImpl
|
namespace FramebufferImpl {} // namespace FramebufferImpl
|
||||||
@@ -263,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
|
||||||
@@ -287,113 +168,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// stops being safe to edit by hand.
|
// stops being safe to edit by hand.
|
||||||
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
|
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
|
||||||
String RemoveLayoutBinding(const String& glslCode);
|
String RemoveLayoutBinding(const String& glslCode);
|
||||||
// Prefix of the per-element scalar declarations RemapImageArrayElementUnits splits an
|
|
||||||
// image array into; the suffix is the array's own name and the element's index.
|
|
||||||
constexpr const char* IMAGE_ARRAY_ELEMENT_PREFIX = "mg_imageElem_";
|
|
||||||
// One image ARRAY whose elements the application pointed at units that are not
|
|
||||||
// consecutive-from-element-zero.
|
|
||||||
struct ImageArrayUnitPlan {
|
|
||||||
String name; // the array's name, exactly as the emitted ESSL declares it
|
|
||||||
Vector<Int> units; // the frontend image unit element k has to reach
|
|
||||||
};
|
|
||||||
// Desktop GL lets an application give each element of an image array an ARBITRARY unit
|
|
||||||
// (glUniform1i per element). ES has no such call at all - "ES image units come
|
|
||||||
// exclusively from the layout(binding=N) qualifier" - and one declaration carries one
|
|
||||||
// binding, so ESSL nails an array's elements to the CONSECUTIVE units N, N+1, N+2, ...
|
|
||||||
// MobileGL used to stamp element [0]'s unit as the binding and let the rest fall where
|
|
||||||
// they fell: KHR-GL4x.shader_image_load_store.advanced-sso-simple assigns 0,2,4,6 and
|
|
||||||
// 1,3,5,7, so its two programs actually addressed 0,1,2,3 and 1,2,3,4 - one layer got the
|
|
||||||
// wrong value and three were never written, with no GL error and no link log. The same
|
|
||||||
// defect for SAMPLER arrays was fixed API-side (SubscriptUniformNameForElement); an image
|
|
||||||
// array has no API side to fix, because ES makes glUniform1i on an image uniform an
|
|
||||||
// INVALID_OPERATION.
|
|
||||||
//
|
|
||||||
// Repaired by SPLITTING the array into one SCALAR image uniform per element, each with
|
|
||||||
// its own layout(binding = N), and rewriting `name[k]` to the scalar declared for
|
|
||||||
// element k. One declaration carries one binding, so one declaration per unit is the
|
|
||||||
// only spelling that reaches an arbitrary set of them.
|
|
||||||
//
|
|
||||||
// That rewrite needs every k in the emitted text to be a LITERAL, and it is:
|
|
||||||
// LegalizeResourceArrayIndexingForEssl has already folded or lowered every dynamic
|
|
||||||
// image-array subscript in the module, because ESSL forbids one outright ("image arrays
|
|
||||||
// indexed with non-constant expressions are forbidden in GLSL ES", Mesa 26.1.4 at
|
|
||||||
// ES 3.2, on a raw GLES probe with no MobileGL in the loop). The earlier shape here -
|
|
||||||
// widening the array to cover the whole span of units and routing each subscript through
|
|
||||||
// a `const highp int` offset table - was written before that pass covered images, and
|
|
||||||
// the table lookup was itself one of the non-constant expressions the same probe refuses.
|
|
||||||
// The split also costs exactly the image uniforms the application declared, where the
|
|
||||||
// widening cost the whole SPAN (seven for the four elements of
|
|
||||||
// KHR-GL42.shader_image_load_store.advanced-sso-simple), so there is no budget for it to
|
|
||||||
// fail to fit in.
|
|
||||||
//
|
|
||||||
// Declines - leaving the array exactly as it was, and naming it in `outDeclined` for the
|
|
||||||
// caller to report - when the emitted extent disagrees with the reflection, when the
|
|
||||||
// array is reached by anything other than a subscript, or when a subscript is not a
|
|
||||||
// literal element index. Silence was the whole defect here, so a decline must be audible.
|
|
||||||
//
|
|
||||||
// Must run AFTER RebindImageUniformsToFrontendUnits and BakeImageFormatQualifiers (both
|
|
||||||
// key on the GL uniform name and on a binding already being stamped) and BEFORE
|
|
||||||
// SplitReadWriteImageUniforms (so each element that is both read and written is split
|
|
||||||
// with its own binding already on it) and RemoveLayoutBinding (which is what preserves
|
|
||||||
// image bindings). Like them, it is downstream of the L2 shader-translation memo, so the
|
|
||||||
// per-program units it reads need no entry in BuildEsslTranslationKey.
|
|
||||||
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
|
|
||||||
Vector<String>* outDeclined = nullptr);
|
|
||||||
// The member list of a `gl_PerVertex { ... }` redeclaration in already-emitted ESSL -
|
|
||||||
// the text between the braces, verbatim - or nullopt when the shader does not redeclare
|
|
||||||
// the block in that direction. `input` selects the `in gl_PerVertex` form over the
|
|
||||||
// `out` one.
|
|
||||||
//
|
|
||||||
// Exists so BuildPassthroughTessControlEssl can MIRROR the stages it has to sit between
|
|
||||||
// rather than guess at them. Whether SPIRV-Cross redeclares the built-in block, and with
|
|
||||||
// which members, depends on what the application's shader touched; a synthesized stage
|
|
||||||
// that redeclares a different shape than its neighbours is an ES link error against a
|
|
||||||
// program that has no other problem.
|
|
||||||
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, Bool input);
|
|
||||||
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes: "the input
|
|
||||||
// patch is passed through unmodified", the output patch has PATCH_VERTICES vertices, and
|
|
||||||
// the levels come from the PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
|
|
||||||
//
|
|
||||||
// Desktop GL makes the control stage OPTIONAL. OpenGL ES 3.2 does not: it has no
|
|
||||||
// PATCH_DEFAULT_*_LEVEL state at all (only glPatchParameteri, for PATCH_VERTICES) and
|
|
||||||
// rejects a program that has an evaluation stage without a control stage - with an EMPTY
|
|
||||||
// info log, verified on an Adreno 830 with no MobileGL in the process. MobileGL's own
|
|
||||||
// frontend link succeeds, so the program reports GL_LINK_STATUS = TRUE, program 0 is
|
|
||||||
// bound in its place, and every draw silently renders nothing.
|
|
||||||
//
|
|
||||||
// `inPerVertexMembers` / `outPerVertexMembers` are the member lists to redeclare gl_in
|
|
||||||
// and gl_out with - normally taken from the neighbouring stages' own emitted ESSL via
|
|
||||||
// ExtractPerVertexBlockMembers, and empty to leave the driver's built-in declaration
|
|
||||||
// alone, which is what matching a neighbour that did not redeclare requires.
|
|
||||||
//
|
|
||||||
// All four outer levels and both inner levels are written unconditionally: writing a
|
|
||||||
// level the evaluation stage's domain does not use is legal and ignored, and it saves
|
|
||||||
// this from having to know the domain. They are literal 1.0 because that is the GL
|
|
||||||
// default and glPatchParameterfv - their only setter - is a stub in this frontend
|
|
||||||
// (MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means making
|
|
||||||
// the levels a parameter here AND part of what makes a built program stale, exactly as
|
|
||||||
// PATCH_VERTICES already is; the two must move together, so they are named together.
|
|
||||||
//
|
|
||||||
// The same stage, for the same reason, that DirectVulkan synthesizes in
|
|
||||||
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
|
|
||||||
// tessellation stages. Kept as two generators rather than one because the two targets
|
|
||||||
// disagree on everything but the algorithm: desktop GLSL 450 against ESSL, a fixed
|
|
||||||
// gl_PerVertex shape that Vulkan matches structurally against a mirrored one, and a
|
|
||||||
// VkShaderModule against a driver shader object.
|
|
||||||
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
|
|
||||||
const String& inPerVertexMembers,
|
|
||||||
const String& outPerVertexMembers);
|
|
||||||
// Prefix of the writeonly half a read+write image uniform is split into (see
|
// Prefix of the writeonly half a read+write image uniform is split into (see
|
||||||
// SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
|
// SplitReadWriteImageUniforms); the suffix is the image's own name.
|
||||||
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
||||||
// The three names SplitReadWriteImageUniforms renames a rewritten image declaration
|
|
||||||
// under, one per REPAIR it can apply. Which one a stage picks is decided by that stage's
|
|
||||||
// own accesses, so two stages that use an image the same way arrive at the SAME name and
|
|
||||||
// two that use it differently arrive at different ones - which is exactly the property
|
|
||||||
// the rename exists for, at no cost to the stages that agree. Exposed for the tests.
|
|
||||||
constexpr const char* IMAGE_READONLY_ALIAS_PREFIX = "mg_imageRo_";
|
|
||||||
constexpr const char* IMAGE_WRITEONLY_ALIAS_PREFIX = "mg_imageWo_";
|
|
||||||
constexpr const char* IMAGE_SPLIT_READ_ALIAS_PREFIX = "mg_imageRw_";
|
|
||||||
// ESSL refuses an image variable that carries a format qualifier other than r32f /
|
// ESSL refuses an image variable that carries a format qualifier other than r32f /
|
||||||
// r32i / r32ui unless it also carries `readonly` or `writeonly` (GLSL ES 3.10 4.9 /
|
// r32i / r32ui unless it also carries `readonly` or `writeonly` (GLSL ES 3.10 4.9 /
|
||||||
// 3.20 4.10; glslang enforces it verbatim in ParseHelper.cpp's layoutObjectCheck).
|
// 3.20 4.10; glslang enforces it verbatim in ParseHelper.cpp's layoutObjectCheck).
|
||||||
@@ -405,74 +182,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// bare declaration, so the frontend raises no error and the illegal ESSL only shows
|
// bare declaration, so the frontend raises no error and the illegal ESSL only shows
|
||||||
// up as a device compile failure - and then as a silently no-op draw.
|
// up as a device compile failure - and then as a silently no-op draw.
|
||||||
//
|
//
|
||||||
// Restores a legal declaration, and RENAMES it after the repair it applied while doing so:
|
// Restores a legal declaration:
|
||||||
// * loaded only -> add `readonly`, rename under IMAGE_READONLY_ALIAS_PREFIX
|
// * loaded only -> add `readonly`
|
||||||
// * stored only -> add `writeonly`, rename under IMAGE_WRITEONLY_ALIAS_PREFIX
|
// * stored only -> add `writeonly`
|
||||||
// * both -> emit TWO declarations on the same binding and of the
|
// * both -> emit TWO declarations on the same binding and of the
|
||||||
// same type, `coherent readonly
|
// same type, `readonly <name>` and `writeonly
|
||||||
// <IMAGE_SPLIT_READ_ALIAS_PREFIX><name>` and `coherent
|
// <IMAGE_WRITE_ALIAS_PREFIX><name>`, and point every
|
||||||
// writeonly <IMAGE_WRITE_ALIAS_PREFIX><that name>`, point
|
// imageStore at the second one. Several image variables
|
||||||
// every imageStore at the second one, and follow each of
|
// may share an image unit as long as they have the same
|
||||||
// those stores with `memoryBarrierImage();`. Several image
|
// type and format, which is exactly what the pair is.
|
||||||
// variables may share an image unit as long as they have
|
|
||||||
// the same type and format, which is exactly what the pair
|
|
||||||
// is.
|
|
||||||
//
|
|
||||||
// The rename is the other half of the repair and applies to all three cases. The qualifier
|
|
||||||
// chosen above is a decision about ONE STAGE's accesses, and GLSL requires a uniform
|
|
||||||
// declared in two stages to be declared identically - so a shader that stores an image from
|
|
||||||
// the vertex stage and loads it from the fragment stage came out of here `writeonly` in one
|
|
||||||
// and `readonly` in the other. Adreno merges the two same-named declarations and silently
|
|
||||||
// drops the vertex-stage STORES: no GL error, no link log, LINK_STATUS = 1, and the image
|
|
||||||
// still reads back its initial contents
|
|
||||||
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation; a raw-ES probe
|
|
||||||
// isolated the trigger to the same-name/mismatched-qualifier pair, and only when both
|
|
||||||
// carry `coherent`). Renaming leaves no cross-stage variable to merge.
|
|
||||||
//
|
|
||||||
// The name is keyed on the REPAIR, not on the stage, and that distinction is the whole
|
|
||||||
// point: two stages that use an image the same way emit byte-identical declarations, so
|
|
||||||
// letting them keep one shared name costs nothing and merging them is correct, while two
|
|
||||||
// stages that use it differently land on different prefixes and cannot be merged at all.
|
|
||||||
// A per-STAGE tag also satisfied the first requirement but violated the second: it made
|
|
||||||
// the SAME image a distinct uniform in every stage that named it, and Adreno allocates
|
|
||||||
// image LOCATIONS per distinct uniform. KHR-GL43.shading_language_420pack.
|
|
||||||
// binding_images_texture_type_* declares three read+write images in each of its five
|
|
||||||
// stages; merged that is 6 image uniforms, per-stage-tagged it is 30, and the Adreno 830
|
|
||||||
// linker answered "Error: Image Image location or component exceeds max allowed. Error:
|
|
||||||
// Linking failed." - which, the frontend having already published LINK_STATUS = TRUE from
|
|
||||||
// glslang's link, surfaced only as every draw silently doing nothing and the images
|
|
||||||
// reading back zero. Mali and Mesa link the same text, so nothing but a device gate
|
|
||||||
// catches this.
|
|
||||||
//
|
|
||||||
// A declaration SPIRV-Cross already tagged `readonly` or `writeonly` needs no qualifier
|
|
||||||
// repair, but it is NOT stage-independent: that tag is derived from the accesses of the
|
|
||||||
// stage being emitted, so an image stored in the vertex stage and loaded in the fragment
|
|
||||||
// stage arrives here as `coherent writeonly g_image` and `coherent readonly g_image` -
|
|
||||||
// one name, two spellings, which is exactly the pair Adreno merges. Those declarations
|
|
||||||
// are therefore renamed too, keyed on the qualifier they already carry (readonly ->
|
|
||||||
// IMAGE_READONLY_ALIAS_PREFIX, writeonly -> IMAGE_WRITEONLY_ALIAS_PREFIX) and with
|
|
||||||
// nothing but the identifier changed. Stages that agree still reach the same alias and
|
|
||||||
// stay merged, so this costs no shader an extra image uniform.
|
|
||||||
//
|
|
||||||
// The declarations this pass still leaves untouched keep their names: one carrying BOTH
|
|
||||||
// readonly and writeonly (a spelling no access analysis produces, so it came from the
|
|
||||||
// application and is identical everywhere), and one carrying NEITHER, which is legal only
|
|
||||||
// for the r32f/r32i/r32ui formats and is likewise spelled the same in every stage.
|
|
||||||
//
|
|
||||||
// The `coherent` on both halves of the pair is load-bearing, not decoration: GLSL only
|
|
||||||
// guarantees a write through one image variable is visible to a read through a DIFFERENT
|
|
||||||
// one when both are coherent, and the split is what makes a same-variable
|
|
||||||
// read-after-write cross-variable. The single-declaration repairs above do not get it -
|
|
||||||
// nothing aliases them.
|
|
||||||
//
|
|
||||||
// The barrier is the other half of the same problem, and coherent alone did not cover it:
|
|
||||||
// visibility is not ORDER. Within one invocation the ES compiler sees a write to one
|
|
||||||
// variable and a read of another it has no reason to believe alias, and is free to serve
|
|
||||||
// the read from before the write - which is what advanced-memory-order's store/load/
|
|
||||||
// compare loop measured on Adreno. memoryBarrierImage() orders exactly those two, is core
|
|
||||||
// GLSL ES 3.10 in every stage, and is not an execution barrier, so it is legal in
|
|
||||||
// non-uniform control flow. It costs something in a shader that stores to a read+write
|
|
||||||
// image in a loop, which is why it is confined to the split pair.
|
|
||||||
//
|
//
|
||||||
// Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so
|
// Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so
|
||||||
// a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
|
// a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
|
||||||
@@ -482,14 +200,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
//
|
//
|
||||||
// Runs on the transpiled ESSL, so it must see the bindings the frontend units were
|
// Runs on the transpiled ESSL, so it must see the bindings the frontend units were
|
||||||
// already rewritten to and must run before those bindings are stripped - see the call
|
// already rewritten to and must run before those bindings are stripped - see the call
|
||||||
// site in Managers.cpp. Its output is a function of the emitted text alone - it needs no
|
// site in Managers.cpp.
|
||||||
// stage and no per-program state - so it adds nothing to BuildEsslTranslationKey either.
|
String SplitReadWriteImageUniforms(const String& glslCode);
|
||||||
//
|
|
||||||
// `outSplitCount`, when given, receives the number of declarations that were actually
|
|
||||||
// doubled - i.e. exactly how many image uniforms this stage gained over what the
|
|
||||||
// application declared. Zero for every shader but a handful, and the only number the
|
|
||||||
// budget note above can be reported with.
|
|
||||||
String SplitReadWriteImageUniforms(const String& glslCode, Uint* outSplitCount = nullptr);
|
|
||||||
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
|
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
|
||||||
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
|
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
|
||||||
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
|
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
#include "BackendObject_DirectVulkan.h"
|
#include "BackendObject_DirectVulkan.h"
|
||||||
#include "MG_Backend/BackendObject.h"
|
#include "MG_Backend/BackendObject.h"
|
||||||
#include "DirectVulkan.h"
|
#include "DirectVulkan.h"
|
||||||
#include "SubgroupSupportPolicy.h"
|
|
||||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||||
#include "MG_State/GLState/Core.h"
|
#include "MG_State/GLState/Core.h"
|
||||||
#include "MG_State/GLState/TextureState/TextureState.h"
|
#include "MG_State/GLState/TextureState/TextureState.h"
|
||||||
@@ -562,13 +561,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||||
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
||||||
}
|
}
|
||||||
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64), and stays opt-in even on a
|
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a
|
||||||
// device that HAS shaderFloat64. Every `double` in a shader compiles and runs either way
|
// shader compiles and runs already - it is narrowed to 32 bits before the module
|
||||||
// - narrowed to 32 bits where the device has no 64-bit floats, kept whole where it does -
|
// reaches this backend - so an application that simply uses doubles needs nothing
|
||||||
// so an application that simply uses doubles needs nothing advertised. What the extension
|
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
|
||||||
// additionally promises is the whole GL_ARB_gpu_shader_fp64 SURFACE (glUniform*d
|
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
|
||||||
// conformance, the fp64 built-ins, the state queries), and turning the string on is a
|
// make an application that checks the string take a path MobileGL cannot honour.
|
||||||
// decision about all of it rather than about the shader path alone.
|
|
||||||
if (MG_Config::Features.AdvertiseFp64) {
|
if (MG_Config::Features.AdvertiseFp64) {
|
||||||
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
|
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
|
||||||
}
|
}
|
||||||
@@ -706,14 +704,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
|
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
|
||||||
// run without a renderer; no timer query is advertised then. Rebuilding
|
// run without a renderer; no timer query is advertised then. Rebuilding
|
||||||
// the whole list keeps re-runs idempotent.
|
// the whole list keeps re-runs idempotent.
|
||||||
// The opt-in emulated compute path (SubgroupSupportPolicy.h) carries the
|
|
||||||
// extension by itself on devices with no native subgroup support at all; a
|
|
||||||
// device with native subgroups always advertises - and uses - those.
|
|
||||||
const Bool subgroupSupportAdvertised =
|
|
||||||
m_vulkanCaps.SupportsShaderSubgroup ||
|
|
||||||
ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup);
|
|
||||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||||
subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||||
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
|
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
|
||||||
}
|
}
|
||||||
@@ -848,38 +840,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;
|
||||||
@@ -906,22 +866,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
||||||
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
|
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
|
||||||
m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers);
|
m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers);
|
||||||
// Same shape as the image-uniform limits three lines above: maxClipDistances is reported
|
m_dynamicParameters.MaxClipDistances = m_vulkanCaps.MaxClipDistances;
|
||||||
// by every device, but declaring ClipDistance in a module needs the shaderClipDistance
|
|
||||||
// FEATURE, which VulkanRenderer enables exactly where the physical device has it. Without
|
|
||||||
// it the limit describes a capacity no shader may use, so report none.
|
|
||||||
m_dynamicParameters.MaxClipDistances =
|
|
||||||
m_vulkanCaps.SupportsShaderClipDistance ? std::max(m_vulkanCaps.MaxClipDistances, 0) : 0;
|
|
||||||
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
|
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
|
||||||
// Assigned explicitly rather than left to the struct's defaults, like every other
|
|
||||||
// parameter here, so a second fill cannot inherit a stale value. GL_UNDEFINED_VERTEX is
|
|
||||||
// the truthful answer for DirectVulkan and a legal one (GL 4.6 table 23.65): which vertex
|
|
||||||
// provokes is chosen per pipeline by VulkanRenderer::SelectProvokingVertexMode out of
|
|
||||||
// VK_EXT_provoking_vertex, provokingVertexModePerPipeline and the topology, so there is no
|
|
||||||
// one convention to name. Vulkan's own default is FIRST, which is the opposite of the
|
|
||||||
// GL_LAST_VERTEX_CONVENTION this used to claim unconditionally.
|
|
||||||
m_dynamicParameters.LayerProvokingVertex = GL_UNDEFINED_VERTEX;
|
|
||||||
m_dynamicParameters.ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
|
||||||
m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth;
|
m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth;
|
||||||
m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
|
m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
|
||||||
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
|
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
|
||||||
@@ -966,34 +912,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The device feature the whole fp64 story hangs off. With it, a module keeps its
|
// Never, on any device, and no longer for the reason it used to be. It used to track
|
||||||
// OpCapability Float64 and real doubles reach the driver; without it the transpile
|
// shaderFloat64 because a `dvec3` input needed the Float64 capability to exist in the
|
||||||
// narrows every 64-bit float to 32 (ShaderTranspiler::DemoteFloat64Pass), because
|
// module at all; a 64-bit vertex FETCH was already impossible (VK_FORMAT_R64*_SFLOAT is
|
||||||
// VUID-VkShaderModuleCreateInfo-pCode-08740 forbids the capability outright and no
|
// optional and lavapipe reports zero bufferFeatures for all four), so the attribute
|
||||||
// pipeline could be built from such a module. lavapipe reports it; Adreno and Mali both
|
// arrived as its 32-bit word pair and PackDoubleVertexInputsPass bitcast it back.
|
||||||
// report VK_FALSE, so on every real mobile device this is false and the demotion runs
|
|
||||||
// exactly as it always has.
|
|
||||||
m_dynamicParameters.SupportsShaderFloat64 = m_vulkanCaps.SupportsShaderFloat64;
|
|
||||||
// Never, on any device, and DELIBERATELY NOT COUPLED to the line above even though it
|
|
||||||
// once tracked the same feature. It used to, because a `dvec` input needed Float64 to
|
|
||||||
// exist in the module at all; a 64-bit vertex FETCH was already impossible
|
|
||||||
// (VK_FORMAT_R64*_SFLOAT is optional and lavapipe reports zero bufferFeatures for all
|
|
||||||
// four), so the attribute arrived as its 32-bit word pair and PackDoubleVertexInputsPass
|
|
||||||
// bitcast it back.
|
|
||||||
//
|
//
|
||||||
// Re-coupling it does not work, and the reason is worth recording because it is not
|
// The shader half of that is gone: every 64-bit float is narrowed before any module
|
||||||
// obvious: this flag decides the VkFormat from the VAO ATTRIBUTE alone, and the attribute
|
// reaches a backend (ShaderTranspiler::DemoteFloat64Pass), so there is no `double` input
|
||||||
// does not know what the shader declared. glVertexAttribFormat(GL_DOUBLE) against a plain
|
// left to bitcast INTO, and feeding a UINT-formatted attribute to what is now a `float`
|
||||||
// `in vec4` is not only legal but the common case
|
// input would be silent garbage. Reconstructing the value would mean decoding the
|
||||||
// (KHR-GL43.vertex_attrib_binding.basic-input-case4 does exactly that, and case5 adds
|
// IEEE-754 double bit pattern in the shader - software fp64, which is precisely what the
|
||||||
// normalized=GL_TRUE), and advanced-bindingUpdate feeds a dvec3 the same way - GL defines
|
// demotion exists to avoid - and on Espryt it would additionally need the ES driver to
|
||||||
// all of them as "doubles in memory, converted to float". Turning the flag on turns the
|
// fetch 2N uint components where the application declared N doubles, which a dvec3 or
|
||||||
// narrowing OFF for every one of them and the attributes come back unfetched.
|
// dvec4 cannot even express within one attribute location.
|
||||||
//
|
//
|
||||||
// What keeps the two halves honest instead is a per-MODULE decision: a vertex module that
|
// So glVertexAttribLFormat / glVertexAttribLPointer are declined here exactly as they
|
||||||
// declares a 64-bit float INPUT is demoted whole, even where the backend has native fp64,
|
// already were on Espryt and on every real mobile device (Adreno and Mali both report
|
||||||
// so `dvec` inputs are `vec` inputs on this backend exactly as they always were. See
|
// shaderFloat64 == VK_FALSE), and for the same visible reason. A `dvec3` INPUT still
|
||||||
// ShaderCompiler::SanitizeAndOptimizeBinary.
|
// compiles and draws - it is a `vec3` after demotion - as long as the application feeds
|
||||||
|
// it with glVertexAttribPointer(GL_FLOAT) rather than 64-bit data.
|
||||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||||
@@ -1003,18 +941,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_dynamicParameters.SubgroupSupportedFeatures =
|
m_dynamicParameters.SubgroupSupportedFeatures =
|
||||||
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
|
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
|
||||||
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
|
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
|
||||||
} else if (ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup)) {
|
|
||||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP on a device with no native subgroups: the
|
|
||||||
// advertised values describe the 32-lane virtual subgroup the compute
|
|
||||||
// lowering implements (SubgroupSupportPolicy.h / EmulateSubgroupsPass).
|
|
||||||
// GL requires the advertisement and the execution to agree, and on this
|
|
||||||
// path the emulation is what executes; only the compute stage is offered.
|
|
||||||
m_dynamicParameters.SubgroupSize = kEmulatedSubgroupSize;
|
|
||||||
m_dynamicParameters.SubgroupSupportedStages = kEmulatedSubgroupStages;
|
|
||||||
m_dynamicParameters.SubgroupSupportedFeatures = kEmulatedSubgroupFeatures;
|
|
||||||
m_dynamicParameters.SubgroupQuadOperationsInAllStages = false;
|
|
||||||
MGLOG_I("DirectVulkan: emulating 32-lane compute subgroups "
|
|
||||||
"(MOBILEGL_MAGMA_EMULATE_SUBGROUP, no native subgroup support)");
|
|
||||||
} else {
|
} else {
|
||||||
m_dynamicParameters.SubgroupSize = 0;
|
m_dynamicParameters.SubgroupSize = 0;
|
||||||
m_dynamicParameters.SubgroupSupportedStages = 0;
|
m_dynamicParameters.SubgroupSupportedStages = 0;
|
||||||
|
|||||||
@@ -69,12 +69,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// slot's ownership unambiguous.
|
// slot's ownership unambiguous.
|
||||||
Uint64 programLifetimeId = 0;
|
Uint64 programLifetimeId = 0;
|
||||||
Uint32 backendStateVersion = 0;
|
Uint32 backendStateVersion = 0;
|
||||||
// glShaderStorageBlockBinding deliberately does NOT bump the backend state
|
|
||||||
// version, and the pipeline composite is unnamed so the in-place patch in
|
|
||||||
// DirectVulkan::ShaderStorageBlockBinding can never reach its slot - the
|
|
||||||
// mirror replay bumps only the program's block-binding version. Without this
|
|
||||||
// key the composite's slot kept serving the pre-rebind block.binding.
|
|
||||||
Uint32 blockBindingVersion = 0;
|
|
||||||
Vector<StorageBlockResource> storageBlocks;
|
Vector<StorageBlockResource> storageBlocks;
|
||||||
Vector<BufferVariableResource> bufferVariables;
|
Vector<BufferVariableResource> bufferVariables;
|
||||||
GLint computeWorkGroupSize[3] = {1, 1, 1};
|
GLint computeWorkGroupSize[3] = {1, 1, 1};
|
||||||
@@ -162,33 +156,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
|
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
|
||||||
const Uint64 programLifetimeId = program.GetLifetimeId();
|
const Uint64 programLifetimeId = program.GetLifetimeId();
|
||||||
const Uint32 backendStateVersion = program.GetBackendStateVersion();
|
const Uint32 backendStateVersion = program.GetBackendStateVersion();
|
||||||
const Uint32 blockBindingVersion = program.GetBlockBindingVersion();
|
|
||||||
// The lifetime id must match too: a new program that reuses a deleted
|
// The lifetime id must match too: a new program that reuses a deleted
|
||||||
// program's name and happens to land on the same backendStateVersion (both
|
// program's name and happens to land on the same backendStateVersion (both
|
||||||
// count from zero) would otherwise be served the dead program's reflection.
|
// count from zero) would otherwise be served the dead program's reflection.
|
||||||
if (cache.programLifetimeId == programLifetimeId &&
|
if (cache.programLifetimeId == programLifetimeId &&
|
||||||
cache.backendStateVersion == backendStateVersion &&
|
cache.backendStateVersion == backendStateVersion &&
|
||||||
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
|
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
|
||||||
if (cache.blockBindingVersion != blockBindingVersion) {
|
|
||||||
// Only the block bindings moved (glShaderStorageBlockBinding, or the
|
|
||||||
// pipeline composite's mirror replay - neither touches the backend
|
|
||||||
// state version): the reflection itself is unchanged, so re-apply the
|
|
||||||
// overrides by name instead of re-running spirv-reflect. Overrides
|
|
||||||
// only ever accumulate, so a block without one still holds its
|
|
||||||
// declared binding.
|
|
||||||
for (auto& block : cache.storageBlocks) {
|
|
||||||
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
|
|
||||||
if (rebound >= 0) block.binding = static_cast<Uint32>(rebound);
|
|
||||||
}
|
|
||||||
cache.blockBindingVersion = blockBindingVersion;
|
|
||||||
}
|
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
cache = {};
|
cache = {};
|
||||||
cache.programLifetimeId = programLifetimeId;
|
cache.programLifetimeId = programLifetimeId;
|
||||||
cache.backendStateVersion = backendStateVersion;
|
cache.backendStateVersion = backendStateVersion;
|
||||||
cache.blockBindingVersion = blockBindingVersion;
|
|
||||||
|
|
||||||
Vector<SpvReflectShaderModule> modules;
|
Vector<SpvReflectShaderModule> modules;
|
||||||
Vector<Bool> validModules;
|
Vector<Bool> validModules;
|
||||||
@@ -632,15 +611,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);
|
||||||
|
|||||||
@@ -33,32 +33,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
using SpvcSession = MG_Util::ShaderTranspiler::SpvcSession;
|
using SpvcSession = MG_Util::ShaderTranspiler::SpvcSession;
|
||||||
using SessionUsageBit = MG_Util::ShaderTranspiler::SessionUsageBit;
|
using SessionUsageBit = MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||||
|
|
||||||
// Local size of a compute module, read from OpExecutionMode LocalSize; all-zero
|
|
||||||
// when absent. The compile chain pins SPIR-V 1.3, where a literal local size
|
|
||||||
// always reaches the module as this execution mode (LocalSizeId does not exist
|
|
||||||
// yet).
|
|
||||||
struct ComputeLocalSize {
|
|
||||||
Uint32 x = 0;
|
|
||||||
Uint32 y = 0;
|
|
||||||
Uint32 z = 0;
|
|
||||||
Uint64 Total() const { return static_cast<Uint64>(x) * y * z; }
|
|
||||||
};
|
|
||||||
ComputeLocalSize TryGetComputeLocalSize(const Vector<Uint>& spirv) {
|
|
||||||
constexpr SizeT kHeaderWords = 5;
|
|
||||||
constexpr Uint32 kOpExecutionMode = 16;
|
|
||||||
constexpr Uint32 kModeLocalSize = 17;
|
|
||||||
for (SizeT offset = kHeaderWords; offset < spirv.size();) {
|
|
||||||
const Uint32 wordCount = spirv[offset] >> 16u;
|
|
||||||
const Uint32 opcode = spirv[offset] & 0xffffu;
|
|
||||||
if (wordCount == 0 || offset + wordCount > spirv.size()) break;
|
|
||||||
if (opcode == kOpExecutionMode && wordCount >= 6 && spirv[offset + 2] == kModeLocalSize) {
|
|
||||||
return {spirv[offset + 3], spirv[offset + 4], spirv[offset + 5]};
|
|
||||||
}
|
|
||||||
offset += wordCount;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DescriptorKey {
|
struct DescriptorKey {
|
||||||
ProgramFactory::DescriptorBindingKind kind = ProgramFactory::DescriptorBindingKind::None;
|
ProgramFactory::DescriptorBindingKind kind = ProgramFactory::DescriptorBindingKind::None;
|
||||||
String name;
|
String name;
|
||||||
@@ -83,7 +57,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool isMember = false;
|
Bool isMember = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
ShaderStage PickClipFixupStage(const Vector<ShaderStage>& stages);
|
ShaderStage PickClipFixupStage(const Vector<SharedPtr<ShaderObject>>& shaders);
|
||||||
|
|
||||||
Bool IsVec4Float32(spvtools::opt::IRContext* context, Uint32 typeId, Uint32* outFloatTypeId) {
|
Bool IsVec4Float32(spvtools::opt::IRContext* context, Uint32 typeId, Uint32* outFloatTypeId) {
|
||||||
auto* vecInst = context->get_def_use_mgr()->GetDef(typeId);
|
auto* vecInst = context->get_def_use_mgr()->GetDef(typeId);
|
||||||
@@ -614,15 +588,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
void ReflectStageInterface(ShaderStage targetStage,
|
void ReflectStageInterface(ShaderStage targetStage,
|
||||||
Bool reflectInputs,
|
Bool reflectInputs,
|
||||||
const Vector<ShaderStage>& stages,
|
const Vector<SharedPtr<ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
StageInterfaceSummary& outSummary,
|
StageInterfaceSummary& outSummary,
|
||||||
Uint programExternalIndex,
|
Uint programExternalIndex,
|
||||||
const char* stageLabel) {
|
const char* stageLabel) {
|
||||||
outSummary.slotSignatures.fill(0);
|
outSummary.slotSignatures.fill(0);
|
||||||
|
|
||||||
for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||||
if (stages[moduleIndex] != targetStage) {
|
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != targetStage) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,11 +664,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ValidateRasterizationStageInterface(const Vector<ShaderStage>& stages,
|
void ValidateRasterizationStageInterface(const Vector<SharedPtr<ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
ProgramFactory::VkProgramObject& entry,
|
ProgramFactory::VkProgramObject& entry,
|
||||||
Uint programExternalIndex) {
|
Uint programExternalIndex) {
|
||||||
const ShaderStage producerStage = PickClipFixupStage(stages);
|
const ShaderStage producerStage = PickClipFixupStage(shaders);
|
||||||
entry.rasterizationProducerStage = producerStage;
|
entry.rasterizationProducerStage = producerStage;
|
||||||
entry.producerOutputComponentCount = 0;
|
entry.producerOutputComponentCount = 0;
|
||||||
entry.fragmentInputComponentCount = 0;
|
entry.fragmentInputComponentCount = 0;
|
||||||
@@ -703,8 +677,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bool hasFragmentStage = false;
|
Bool hasFragmentStage = false;
|
||||||
for (const ShaderStage stage : stages) {
|
for (const auto& shader : shaders) {
|
||||||
if (stage == ShaderStage::Fragment) {
|
if (shader && shader->GetShaderStage() == ShaderStage::Fragment) {
|
||||||
hasFragmentStage = true;
|
hasFragmentStage = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -715,9 +689,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
StageInterfaceSummary producerOutputs{};
|
StageInterfaceSummary producerOutputs{};
|
||||||
StageInterfaceSummary fragmentInputs{};
|
StageInterfaceSummary fragmentInputs{};
|
||||||
ReflectStageInterface(producerStage, false, stages, spirv, producerOutputs, programExternalIndex,
|
ReflectStageInterface(producerStage, false, shaders, spirv, producerOutputs, programExternalIndex,
|
||||||
"producer");
|
"producer");
|
||||||
ReflectStageInterface(ShaderStage::Fragment, true, stages, spirv, fragmentInputs, programExternalIndex,
|
ReflectStageInterface(ShaderStage::Fragment, true, shaders, spirv, fragmentInputs, programExternalIndex,
|
||||||
"fragment");
|
"fragment");
|
||||||
entry.producerOutputComponentCount = CountOccupiedStageInterfaceSlots(producerOutputs);
|
entry.producerOutputComponentCount = CountOccupiedStageInterfaceSlots(producerOutputs);
|
||||||
entry.fragmentInputComponentCount = CountOccupiedStageInterfaceSlots(fragmentInputs);
|
entry.fragmentInputComponentCount = CountOccupiedStageInterfaceSlots(fragmentInputs);
|
||||||
@@ -1719,12 +1693,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
ShaderStage PickClipFixupStage(const Vector<ShaderStage>& stages) {
|
ShaderStage PickClipFixupStage(const Vector<SharedPtr<ShaderObject>>& shaders) {
|
||||||
Bool hasGeometry = false;
|
Bool hasGeometry = false;
|
||||||
Bool hasTessEval = false;
|
Bool hasTessEval = false;
|
||||||
Bool hasVertex = false;
|
Bool hasVertex = false;
|
||||||
|
|
||||||
for (const ShaderStage stage : stages) {
|
for (const auto& shader : shaders) {
|
||||||
|
if (!shader) continue;
|
||||||
|
const auto stage = shader->GetShaderStage();
|
||||||
hasGeometry |= (stage == ShaderStage::Geometry);
|
hasGeometry |= (stage == ShaderStage::Geometry);
|
||||||
hasTessEval |= (stage == ShaderStage::TessEval);
|
hasTessEval |= (stage == ShaderStage::TessEval);
|
||||||
hasVertex |= (stage == ShaderStage::Vertex);
|
hasVertex |= (stage == ShaderStage::Vertex);
|
||||||
@@ -2092,15 +2068,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
case SpvImageFormatR11fG11fB10f: return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
|
case SpvImageFormatR11fG11fB10f: return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
|
||||||
case SpvImageFormatR16f: return VK_FORMAT_R16_SFLOAT;
|
case SpvImageFormatR16f: return VK_FORMAT_R16_SFLOAT;
|
||||||
case SpvImageFormatRgba16: return VK_FORMAT_R16G16B16A16_UNORM;
|
case SpvImageFormatRgba16: return VK_FORMAT_R16G16B16A16_UNORM;
|
||||||
// A2**B**10G10R10, matching MGToVk::ConvertTextureInternalFormatToVkFormat's RGB10A2.
|
case SpvImageFormatRgb10A2: return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
|
||||||
// This value becomes the storage image VIEW's format while the image itself was created
|
|
||||||
// from the texture's internal format, so the two must name the same bit layout or the
|
|
||||||
// shader reads the texel through a different component order than the host wrote it.
|
|
||||||
// GL_RGB10_A2 with GL_UNSIGNED_INT_2_10_10_10_REV puts R in bits 0-9, G in 10-19, B in
|
|
||||||
// 20-29 and A in 30-31, which is Vulkan's A2B10G10R10; A2R10G10B10 transposes R and B.
|
|
||||||
// KHR-GL43.shader_image_load_store.basic-allFormats-store read back [2,1,0,3] for an
|
|
||||||
// rgb10_a2ui image stored as [0,1,2,3] while these two converters disagreed.
|
|
||||||
case SpvImageFormatRgb10A2: return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
|
||||||
case SpvImageFormatRg16: return VK_FORMAT_R16G16_UNORM;
|
case SpvImageFormatRg16: return VK_FORMAT_R16G16_UNORM;
|
||||||
case SpvImageFormatRg8: return VK_FORMAT_R8G8_UNORM;
|
case SpvImageFormatRg8: return VK_FORMAT_R8G8_UNORM;
|
||||||
case SpvImageFormatR16: return VK_FORMAT_R16_UNORM;
|
case SpvImageFormatR16: return VK_FORMAT_R16_UNORM;
|
||||||
@@ -2123,7 +2091,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
case SpvImageFormatRgba16ui: return VK_FORMAT_R16G16B16A16_UINT;
|
case SpvImageFormatRgba16ui: return VK_FORMAT_R16G16B16A16_UINT;
|
||||||
case SpvImageFormatRgba8ui: return VK_FORMAT_R8G8B8A8_UINT;
|
case SpvImageFormatRgba8ui: return VK_FORMAT_R8G8B8A8_UINT;
|
||||||
case SpvImageFormatR32ui: return VK_FORMAT_R32_UINT;
|
case SpvImageFormatR32ui: return VK_FORMAT_R32_UINT;
|
||||||
case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2B10G10R10_UINT_PACK32; // see Rgb10A2 above
|
case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2R10G10B10_UINT_PACK32;
|
||||||
case SpvImageFormatRg32ui: return VK_FORMAT_R32G32_UINT;
|
case SpvImageFormatRg32ui: return VK_FORMAT_R32G32_UINT;
|
||||||
case SpvImageFormatRg16ui: return VK_FORMAT_R16G16_UINT;
|
case SpvImageFormatRg16ui: return VK_FORMAT_R16G16_UINT;
|
||||||
case SpvImageFormatRg8ui: return VK_FORMAT_R8G8_UINT;
|
case SpvImageFormatRg8ui: return VK_FORMAT_R8G8_UINT;
|
||||||
@@ -2314,15 +2282,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramFactory::ReflectVertexInputs(const Vector<ShaderStage>& stages,
|
void ProgramFactory::ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const {
|
VkProgramObject& entry) const {
|
||||||
entry.activeVertexInputLocationMask = 0;
|
entry.activeVertexInputLocationMask = 0;
|
||||||
entry.vertexInputTypes.fill(0);
|
entry.vertexInputTypes.fill(0);
|
||||||
entry.readsBaseVertexBuiltin = false;
|
entry.readsBaseVertexBuiltin = false;
|
||||||
|
|
||||||
for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||||
if (stages[moduleIndex] != ShaderStage::Vertex) {
|
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Vertex) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2399,13 +2367,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// evaluation stages. Rather than guess which one is last, every non-fragment, non-compute
|
// evaluation stages. Rather than guess which one is last, every non-fragment, non-compute
|
||||||
// module is asked - one writer anywhere means this program's draws need a multi-viewport
|
// module is asked - one writer anywhere means this program's draws need a multi-viewport
|
||||||
// pipeline, and a false positive costs only a wider viewportCount.
|
// pipeline, and a false positive costs only a wider viewportCount.
|
||||||
void ProgramFactory::ReflectViewportIndexUsage(const Vector<ShaderStage>& stages,
|
void ProgramFactory::ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const {
|
VkProgramObject& entry) const {
|
||||||
entry.writesViewportIndexBuiltin = false;
|
entry.writesViewportIndexBuiltin = false;
|
||||||
|
|
||||||
for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||||
const ShaderStage stage = stages[moduleIndex];
|
if (!shaders[moduleIndex]) continue;
|
||||||
|
const ShaderStage stage = shaders[moduleIndex]->GetShaderStage();
|
||||||
if (stage == ShaderStage::Fragment || stage == ShaderStage::Compute) continue;
|
if (stage == ShaderStage::Fragment || stage == ShaderStage::Compute) continue;
|
||||||
|
|
||||||
const auto& module = spirv[moduleIndex];
|
const auto& module = spirv[moduleIndex];
|
||||||
@@ -2433,15 +2402,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramFactory::ReflectFragmentOutputs(const Vector<ShaderStage>& stages,
|
void ProgramFactory::ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const {
|
VkProgramObject& entry) const {
|
||||||
entry.activeFragmentOutputLocationMask = 0;
|
entry.activeFragmentOutputLocationMask = 0;
|
||||||
entry.fragmentOutputTypes.fill(0);
|
entry.fragmentOutputTypes.fill(0);
|
||||||
entry.fragmentReplacesDepth = false;
|
entry.fragmentReplacesDepth = false;
|
||||||
|
|
||||||
for (SizeT moduleIndex = 0; moduleIndex < stages.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||||
if (stages[moduleIndex] != ShaderStage::Fragment) {
|
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Fragment) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3147,12 +3116,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto& entry = m_cache[hash];
|
auto& entry = m_cache[hash];
|
||||||
entry.hash = hash;
|
entry.hash = hash;
|
||||||
entry.lastUsedFrame = m_frameCounter;
|
entry.lastUsedFrame = m_frameCounter;
|
||||||
// The EXECUTABLE's stage list, not GetAttachedShaders(): `spirv` is a link artifact with
|
auto& shaders = program.GetAttachedShaders();
|
||||||
// one module per linked stage, while the attach list is live and grows on
|
|
||||||
// glAttachShader, which GL 4.6 core 7.3 says does not reach the executable until the
|
|
||||||
// next link. Sizing this loop by the attach list therefore ran it past the end of both
|
|
||||||
// `spirv` and `moduleSpirvs` for any program attached to after it linked.
|
|
||||||
const Vector<ShaderStage> stages = program.GetLinkedShaderStages();
|
|
||||||
auto& spirv = program.GetGeneratedSpirv();
|
auto& spirv = program.GetGeneratedSpirv();
|
||||||
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
|
||||||
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
|
const Bool enableSpirvValidation = program.GetSpirvValidationEnabled();
|
||||||
@@ -3160,17 +3124,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShaderStage fixupStage = PickClipFixupStage(stages);
|
const ShaderStage fixupStage = PickClipFixupStage(shaders);
|
||||||
|
|
||||||
// Both lists come from the same Link(), so they agree by construction; the min() is what
|
for (SizeT i = 0; i < shaders.size(); ++i) {
|
||||||
// makes that an assumption this loop does not have to bet the process on.
|
|
||||||
const SizeT moduleCount = std::min(stages.size(), spirv.size());
|
|
||||||
for (SizeT i = 0; i < moduleCount; ++i) {
|
|
||||||
auto& spv = spirv[i];
|
auto& spv = spirv[i];
|
||||||
if (spv.empty()) continue;
|
if (spv.empty()) continue;
|
||||||
|
|
||||||
// Apply position fixup if needed
|
// Apply position fixup if needed
|
||||||
if (fixupStage != ShaderStage::Unknown && stages[i] == fixupStage) {
|
if (fixupStage != ShaderStage::Unknown && shaders[i] && shaders[i]->GetShaderStage() == fixupStage) {
|
||||||
const Vector<Uint>* fixupInput = &spv;
|
const Vector<Uint>* fixupInput = &spv;
|
||||||
Vector<Uint> xfbSpirv;
|
Vector<Uint> xfbSpirv;
|
||||||
if ((flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
|
if ((flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
|
||||||
@@ -3186,89 +3147,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
moduleSpirvs[i] = spv;
|
moduleSpirvs[i] = spv;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & ProgramFactory::CompileOptionBit::ExplicitLod0Sampling) && stages[i] == ShaderStage::Fragment) {
|
if ((flags & ProgramFactory::CompileOptionBit::ExplicitLod0Sampling) && shaders[i] &&
|
||||||
|
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
|
||||||
Vector<Uint> explicitLodSpirv;
|
Vector<Uint> explicitLodSpirv;
|
||||||
if (TransformSpirvForExplicitLod0Sampling(moduleSpirvs[i], explicitLodSpirv)) {
|
if (TransformSpirvForExplicitLod0Sampling(moduleSpirvs[i], explicitLodSpirv)) {
|
||||||
moduleSpirvs[i] = Move(explicitLodSpirv);
|
moduleSpirvs[i] = Move(explicitLodSpirv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & ProgramFactory::CompileOptionBit::FragCoordYFlip) && stages[i] == ShaderStage::Fragment) {
|
if ((flags & ProgramFactory::CompileOptionBit::FragCoordYFlip) && shaders[i] &&
|
||||||
|
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
|
||||||
Vector<Uint> fragCoordSpirv;
|
Vector<Uint> fragCoordSpirv;
|
||||||
if (TransformSpirvForFragCoordYFlip(moduleSpirvs[i], fragCoordSpirv, m_defaultFramebufferHeight)) {
|
if (TransformSpirvForFragCoordYFlip(moduleSpirvs[i], fragCoordSpirv, m_defaultFramebufferHeight)) {
|
||||||
moduleSpirvs[i] = Move(fragCoordSpirv);
|
moduleSpirvs[i] = Move(fragCoordSpirv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL_KHR_shader_subgroup handling (SubgroupSupportPolicy.h). Native subgroup
|
|
||||||
// operations execute natively; module repairs keep the GL contract intact
|
|
||||||
// around them. The opt-in emulation path replaces them only on devices with no
|
|
||||||
// subgroup support at all (MOBILEGL_MAGMA_EMULATE_SUBGROUP).
|
|
||||||
if (stages[i] == 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) {
|
|
||||||
Vector<Uint> emulatedSpirv;
|
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::EmulateSubgroupsForVulkan(
|
|
||||||
moduleSpirvs[i], emulatedSpirv,
|
|
||||||
m_subgroupPolicy.maxComputeSharedMemoryBytes, enableSpirvValidation)) {
|
|
||||||
moduleSpirvs[i] = std::move(emulatedSpirv);
|
|
||||||
} else {
|
|
||||||
MGLOG_E("ProgramFactory: subgroup emulation failed for program %u; the "
|
|
||||||
"module keeps subgroup operations the device cannot execute",
|
|
||||||
program.GetExternalIndex());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// iterationRP under-declares its cross-subgroup scratch
|
|
||||||
// (prefixSumCache[32] for 512 invocations); on a sub-16-lane device
|
|
||||||
// grow that one fingerprinted array to what the topology needs.
|
|
||||||
if (m_subgroupPolicy.fixIterationRPSubgroupScratch) {
|
|
||||||
Vector<Uint> patchedSpirv;
|
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::FixIterationRPSubgroupScratchForVulkan(
|
|
||||||
moduleSpirvs[i], patchedSpirv, m_subgroupPolicy.nativeSubgroupSize,
|
|
||||||
m_subgroupPolicy.maxComputeSharedMemoryBytes,
|
|
||||||
enableSpirvValidation)) {
|
|
||||||
moduleSpirvs[i] = std::move(patchedSpirv);
|
|
||||||
} else {
|
|
||||||
MGLOG_E("ProgramFactory: iterationRP subgroup scratch patch failed for "
|
|
||||||
"program %u; the pack's declared array sizes stay in effect",
|
|
||||||
program.GetExternalIndex());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// gl_NumSubgroups must agree with the gl_SubgroupID range GL promises;
|
|
||||||
// derive it from the workgroup dimensions and gl_SubgroupSize instead of
|
|
||||||
// trusting a driver builtin that can disagree with the topology the same
|
|
||||||
// dispatch emits (Adreno reports 1 while emitting IDs 0..7 for a
|
|
||||||
// 512-invocation, 64-wide workgroup). The ceil() partition this derives
|
|
||||||
// is pinned by REQUIRE_FULL_SUBGROUPS at pipeline creation whenever the
|
|
||||||
// workgroup shape makes that flag legal (see the stage setup below).
|
|
||||||
if (m_subgroupPolicy.deriveNumSubgroups) {
|
|
||||||
Vector<Uint> derivedNumSubgroupsSpirv;
|
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::DeriveNumSubgroupsForVulkan(
|
|
||||||
moduleSpirvs[i], derivedNumSubgroupsSpirv, enableSpirvValidation)) {
|
|
||||||
moduleSpirvs[i] = std::move(derivedNumSubgroupsSpirv);
|
|
||||||
} else {
|
|
||||||
MGLOG_E("ProgramFactory: failed to derive gl_NumSubgroups for program %u; "
|
|
||||||
"compute shaders may observe a driver-inconsistent subgroup count",
|
|
||||||
program.GetExternalIndex());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vulkan's SPIR-V environment has no rectangle image dimension, so a
|
// Vulkan's SPIR-V environment has no rectangle image dimension, so a
|
||||||
// GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really
|
// GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really
|
||||||
// stored as - which addresses [0,1] where the application addressed texels.
|
// stored as - which addresses [0,1] where the application addressed texels.
|
||||||
@@ -3307,7 +3201,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// The unsupported-device counterpart of this rebase (warning when a shader reads
|
// The unsupported-device counterpart of this rebase (warning when a shader reads
|
||||||
// the builtin but shaderDrawParameters is missing) rides along with
|
// the builtin but shaderDrawParameters is missing) rides along with
|
||||||
// ReflectVertexInputs, which already reflects this stage.
|
// ReflectVertexInputs, which already reflects this stage.
|
||||||
if (stages[i] == ShaderStage::Vertex && m_shaderDrawParametersEnabled) {
|
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex &&
|
||||||
|
m_shaderDrawParametersEnabled) {
|
||||||
Vector<Uint> rebasedSpirv;
|
Vector<Uint> rebasedSpirv;
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
|
||||||
rebasedSpirv, enableSpirvValidation)) {
|
rebasedSpirv, enableSpirvValidation)) {
|
||||||
@@ -3324,7 +3219,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// through CompileOptionBit::ZeroBaseVertex, so the indexed variant of the same
|
// through CompileOptionBit::ZeroBaseVertex, so the indexed variant of the same
|
||||||
// program keeps the native builtin and stays correct for glDrawElementsBaseVertex
|
// program keeps the native builtin and stays correct for glDrawElementsBaseVertex
|
||||||
// and for the baseVertex word of an indexed indirect command.
|
// and for the baseVertex word of an indexed indirect command.
|
||||||
if (stages[i] == ShaderStage::Vertex && (flags & CompileOptionBit::ZeroBaseVertex)) {
|
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex &&
|
||||||
|
(flags & CompileOptionBit::ZeroBaseVertex)) {
|
||||||
Vector<Uint> zeroedSpirv;
|
Vector<Uint> zeroedSpirv;
|
||||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i],
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i],
|
||||||
zeroedSpirv, enableSpirvValidation)) {
|
zeroedSpirv, enableSpirvValidation)) {
|
||||||
@@ -3347,7 +3243,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// committed to R32G32{,B32A32}_UINT for the attribute, so a module still declaring
|
// committed to R32G32{,B32A32}_UINT for the attribute, so a module still declaring
|
||||||
// `in double` would reconcile to Unknown and build a pipeline with a UINT format under a
|
// `in double` would reconcile to Unknown and build a pipeline with a UINT format under a
|
||||||
// double input - garbage with no diagnostic anywhere.
|
// double input - garbage with no diagnostic anywhere.
|
||||||
if (stages[i] == ShaderStage::Vertex) {
|
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) {
|
||||||
Vector<Uint> packedSpirv;
|
Vector<Uint> packedSpirv;
|
||||||
const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan(
|
const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan(
|
||||||
moduleSpirvs[i], packedSpirv, enableSpirvValidation);
|
moduleSpirvs[i], packedSpirv, enableSpirvValidation);
|
||||||
@@ -3386,17 +3282,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs);
|
const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs);
|
||||||
MOBILEGL_ASSERT(remapOk, "ProgramFactory::GetOrCreateProgram: descriptor binding remap failed");
|
MOBILEGL_ASSERT(remapOk, "ProgramFactory::GetOrCreateProgram: descriptor binding remap failed");
|
||||||
|
|
||||||
for (SizeT i = 0; i < moduleCount; ++i) {
|
for (SizeT i = 0; i < shaders.size(); ++i) {
|
||||||
auto& moduleSpv = moduleSpirvs[i];
|
auto& moduleSpv = moduleSpirvs[i];
|
||||||
if (moduleSpv.empty()) continue;
|
if (moduleSpv.empty()) continue;
|
||||||
|
|
||||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||||
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
|
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
||||||
#else
|
#else
|
||||||
// Final module the driver receives; also checked in the INFO-level CI/test
|
// Final module the driver receives; also checked in the INFO-level CI/test
|
||||||
// lanes, where the DEBUG gate above is compiled out.
|
// lanes, where the DEBUG gate above is compiled out.
|
||||||
if (enableSpirvValidation) {
|
if (enableSpirvValidation) {
|
||||||
ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex());
|
ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex());
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -3408,31 +3304,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VK_VERIFY(vkCreateShaderModule(m_device, &smci, nullptr, &module), "vkCreateShaderModule");
|
VK_VERIFY(vkCreateShaderModule(m_device, &smci, nullptr, &module), "vkCreateShaderModule");
|
||||||
|
|
||||||
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
|
VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
|
||||||
ShaderStage shaderStage = stages[i];
|
ShaderStage shaderStage = shaders[i]->GetShaderStage();
|
||||||
stage.stage = ToVkStage(shaderStage);
|
stage.stage = ToVkStage(shaderStage);
|
||||||
stage.module = module;
|
stage.module = module;
|
||||||
stage.pName = "main";
|
stage.pName = "main";
|
||||||
// Pin the full-subgroup launch the derived gl_NumSubgroups assumes. Legal
|
|
||||||
// exactly when the computeFullSubgroups feature is enabled and local_size_x is
|
|
||||||
// a multiple of the subgroup size (VUID-VkPipelineShaderStageCreateInfo-
|
|
||||||
// flags-02759/-02785), and only worth requesting while the resulting subgroup
|
|
||||||
// count fits the device's maxComputeWorkgroupSubgroups (lavapipe caps it at
|
|
||||||
// 32, below a 512-invocation dispatch's 64). With the bit set, "Full
|
|
||||||
// Subgroups" guarantees every subgroup launches with all invocations active,
|
|
||||||
// making the subgroup count exactly invocations / size. Shapes the flag
|
|
||||||
// cannot cover (e.g. 32x16 on a 64-wide device) fall back to the driver's
|
|
||||||
// own - spec-encouraged - tight partitioning, which the DriverPost witness
|
|
||||||
// verifies per device.
|
|
||||||
if (shaderStage == ShaderStage::Compute && m_subgroupPolicy.requireFullSubgroups &&
|
|
||||||
!m_subgroupPolicy.emulateSubgroups && m_subgroupPolicy.nativeSubgroupSize != 0) {
|
|
||||||
const ComputeLocalSize localSize = TryGetComputeLocalSize(moduleSpv);
|
|
||||||
const Uint64 fullSubgroupCount =
|
|
||||||
localSize.Total() / m_subgroupPolicy.nativeSubgroupSize;
|
|
||||||
if (localSize.x != 0 && localSize.x % m_subgroupPolicy.nativeSubgroupSize == 0 &&
|
|
||||||
fullSubgroupCount <= m_subgroupPolicy.maxComputeWorkgroupSubgroups) {
|
|
||||||
stage.flags |= VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.modules.push_back(module);
|
entry.modules.push_back(module);
|
||||||
entry.stages.push_back(stage);
|
entry.stages.push_back(stage);
|
||||||
@@ -3443,12 +3318,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
// Reflect and create layout as part of the program object
|
// Reflect and create layout as part of the program object
|
||||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||||
ValidateRasterizationStageInterface(stages, moduleSpirvs, entry, program.GetExternalIndex());
|
ValidateRasterizationStageInterface(shaders, moduleSpirvs, entry, program.GetExternalIndex());
|
||||||
#endif
|
#endif
|
||||||
ReflectVertexInputs(stages, moduleSpirvs, entry);
|
ReflectVertexInputs(shaders, moduleSpirvs, entry);
|
||||||
ReflectViewportIndexUsage(stages, moduleSpirvs, entry);
|
ReflectViewportIndexUsage(shaders, moduleSpirvs, entry);
|
||||||
ReflectFragmentOutputs(stages, moduleSpirvs, entry);
|
ReflectFragmentOutputs(shaders, moduleSpirvs, entry);
|
||||||
ReflectPassthroughTessControlNeed(stages, moduleSpirvs, entry);
|
ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry);
|
||||||
ReflectLayout(program, moduleSpirvs, entry);
|
ReflectLayout(program, moduleSpirvs, entry);
|
||||||
// A failed remap means the modules kept glslang's per-stage auto-mapped binding numbers -
|
// A failed remap means the modules kept glslang's per-stage auto-mapped binding numbers -
|
||||||
// no cross-stage unification, no set->0 normalisation - so the bindings this layout
|
// no cross-stage unification, no set->0 normalisation - so the bindings this layout
|
||||||
@@ -3660,7 +3535,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProgramFactory::ReflectPassthroughTessControlNeed(
|
void ProgramFactory::ReflectPassthroughTessControlNeed(
|
||||||
const Vector<ShaderStage>& stages,
|
const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const {
|
VkProgramObject& entry) const {
|
||||||
entry.needsPassthroughTessControl = false;
|
entry.needsPassthroughTessControl = false;
|
||||||
@@ -3669,8 +3544,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool hasTessEval = false;
|
Bool hasTessEval = false;
|
||||||
Bool hasTessControl = false;
|
Bool hasTessControl = false;
|
||||||
SizeT tessEvalModuleIndex = 0;
|
SizeT tessEvalModuleIndex = 0;
|
||||||
for (SizeT i = 0; i < stages.size(); ++i) {
|
for (SizeT i = 0; i < shaders.size(); ++i) {
|
||||||
const ShaderStage stage = stages[i];
|
if (!shaders[i]) continue;
|
||||||
|
const auto stage = shaders[i]->GetShaderStage();
|
||||||
if (stage == ShaderStage::TessControl) hasTessControl = true;
|
if (stage == ShaderStage::TessControl) hasTessControl = true;
|
||||||
if (stage == ShaderStage::TessEval) {
|
if (stage == ShaderStage::TessEval) {
|
||||||
hasTessEval = true;
|
hasTessEval = true;
|
||||||
|
|||||||
@@ -372,39 +372,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
|
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// How this factory's compute modules implement GL_KHR_shader_subgroup. Computed
|
|
||||||
// once at renderer initialization (SubgroupSupportPolicy.h + the device's
|
|
||||||
// subgroup properties) so lowering can never disagree with the advertised
|
|
||||||
// capabilities. Native subgroup operations always execute natively; the two
|
|
||||||
// repair passes patch modules AROUND them, and the emulation only replaces them
|
|
||||||
// on opted-in devices with no subgroup support at all.
|
|
||||||
struct SubgroupLoweringPolicy {
|
|
||||||
Bool emulateSubgroups = false; // MOBILEGL_MAGMA_EMULATE_SUBGROUP, no-native-support devices
|
|
||||||
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 requireFullSubgroups = false; // computeFullSubgroups enabled on the device
|
|
||||||
Uint32 nativeSubgroupSize = 0;
|
|
||||||
// Full-subgroup launches are bounded by this device limit; a dispatch whose
|
|
||||||
// workgroup needs more subgroups than this cannot request the flag.
|
|
||||||
Uint32 maxComputeWorkgroupSubgroups = 0;
|
|
||||||
// VkPhysicalDeviceLimits::maxComputeSharedMemorySize; bounds the scratch the
|
|
||||||
// emulation pass may add (0 falls back to the Vulkan minimum, 16384).
|
|
||||||
Uint32 maxComputeSharedMemoryBytes = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
|
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
|
||||||
Bool shaderDrawParametersEnabled,
|
Bool shaderDrawParametersEnabled,
|
||||||
Bool unformattedFloatStorageImagesEnabled,
|
Bool unformattedFloatStorageImagesEnabled,
|
||||||
Bool enableSpirvValidation,
|
Bool enableSpirvValidation,
|
||||||
UpdateAfterBindLimits updateAfterBindLimits,
|
UpdateAfterBindLimits updateAfterBindLimits)
|
||||||
SubgroupLoweringPolicy subgroupPolicy)
|
|
||||||
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
||||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
||||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
|
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
|
||||||
m_enableSpirvValidation(enableSpirvValidation),
|
m_enableSpirvValidation(enableSpirvValidation),
|
||||||
m_updateAfterBindLimits(updateAfterBindLimits),
|
m_updateAfterBindLimits(updateAfterBindLimits) {
|
||||||
m_subgroupPolicy(subgroupPolicy) {
|
|
||||||
VkProgramObject::s_device = device;
|
VkProgramObject::s_device = device;
|
||||||
}
|
}
|
||||||
// Destroys the pass-through tessellation control modules. Runs while the device is
|
// Destroys the pass-through tessellation control modules. Runs while the device is
|
||||||
@@ -499,17 +476,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static TextureTarget UniformTypeToTextureTarget(GLenum glType);
|
static TextureTarget UniformTypeToTextureTarget(GLenum glType);
|
||||||
// `stages` is ALWAYS ProgramObject::GetLinkedShaderStages() - one entry per module of
|
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
// `spirv`, at the same index. Taking the stages rather than the shader objects is what
|
|
||||||
// keeps the program's live attach list, which is a longer and differently-indexed list
|
|
||||||
// the moment a glAttachShader lands after the link, from being passed here by mistake.
|
|
||||||
void ReflectVertexInputs(const Vector<ShaderStage>& stages,
|
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const;
|
VkProgramObject& entry) const;
|
||||||
void ReflectViewportIndexUsage(const Vector<ShaderStage>& stages,
|
void ReflectViewportIndexUsage(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const;
|
VkProgramObject& entry) const;
|
||||||
void ReflectFragmentOutputs(const Vector<ShaderStage>& stages,
|
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const;
|
VkProgramObject& entry) const;
|
||||||
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
|
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
|
||||||
@@ -517,7 +490,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
|
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
|
||||||
// modules. Const and reflection-only: it decides nothing about the pipeline, it only
|
// modules. Const and reflection-only: it decides nothing about the pipeline, it only
|
||||||
// records what the evaluation stage's input interface is made of.
|
// records what the evaluation stage's input interface is made of.
|
||||||
void ReflectPassthroughTessControlNeed(const Vector<ShaderStage>& stages,
|
void ReflectPassthroughTessControlNeed(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||||
const Vector<Vector<Uint>>& spirv,
|
const Vector<Vector<Uint>>& spirv,
|
||||||
VkProgramObject& entry) const;
|
VkProgramObject& entry) const;
|
||||||
|
|
||||||
@@ -538,7 +511,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// the factory lets each reflected layout choose ordinary descriptors when its
|
// the factory lets each reflected layout choose ordinary descriptors when its
|
||||||
// own counts would exceed the update-after-bind budget.
|
// own counts would exceed the update-after-bind budget.
|
||||||
UpdateAfterBindLimits m_updateAfterBindLimits{};
|
UpdateAfterBindLimits m_updateAfterBindLimits{};
|
||||||
SubgroupLoweringPolicy m_subgroupPolicy{};
|
|
||||||
// See SetDefaultFramebufferHeight. 0 means "not known yet"; the FragCoordYFlip bit is
|
// See SetDefaultFramebufferHeight. 0 means "not known yet"; the FragCoordYFlip bit is
|
||||||
// never set before the swapchain exists, so no variant can be compiled against it.
|
// never set before the swapchain exists, so no variant can be compiled against it.
|
||||||
Uint32 m_defaultFramebufferHeight = 0;
|
Uint32 m_defaultFramebufferHeight = 0;
|
||||||
|
|||||||
@@ -542,14 +542,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
outImageInfo = {
|
outImageInfo = {
|
||||||
.sampler = m_samplerManager->GetOrCreateSampler(*samplerBindingOverride.sampler,
|
.sampler = m_samplerManager->GetOrCreateSampler(*samplerBindingOverride.sampler,
|
||||||
*samplerBindingOverride.texture,
|
*samplerBindingOverride.texture),
|
||||||
samplerBindingOverride.forceNearestFiltering,
|
|
||||||
resource->sampledLevelCount),
|
|
||||||
.imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ?
|
.imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ?
|
||||||
samplerBindingOverride.imageView :
|
samplerBindingOverride.imageView :
|
||||||
(resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView),
|
(resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView),
|
||||||
.imageLayout = samplerBindingOverride.imageLayout != VK_IMAGE_LAYOUT_UNDEFINED ?
|
.imageLayout = resource->layout,
|
||||||
samplerBindingOverride.imageLayout : resource->layout,
|
|
||||||
};
|
};
|
||||||
return outImageInfo.sampler != VK_NULL_HANDLE;
|
return outImageInfo.sampler != VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
@@ -1272,87 +1269,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool UniformManager::SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
|
|
||||||
GLint imageLevel, GLenum imageAccess) {
|
|
||||||
return imageAccess != GL_READ_ONLY && imageLevel >= samplerBaseLevel && imageLevel <= samplerMaxLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool UniformManager::CollectSamplerImageFeedback(
|
|
||||||
const MG_State::GLState::ProgramObject& program,
|
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
|
||||||
Vector<SamplerImageFeedbackBinding>& outBindings) const {
|
|
||||||
outBindings.clear();
|
|
||||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr,
|
|
||||||
"CollectSamplerImageFeedback: GL context is null");
|
|
||||||
if (programObj.declinedDescriptors) return true;
|
|
||||||
|
|
||||||
for (const Uint32 samplerBinding : programObj.activeBindings) {
|
|
||||||
if (samplerBinding >= m_maxBindings ||
|
|
||||||
programObj.bindingKinds[samplerBinding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const Uint32 samplerCount = BindingDescriptorCount(programObj, samplerBinding);
|
|
||||||
for (Uint32 samplerElement = 0; samplerElement < samplerCount; ++samplerElement) {
|
|
||||||
MG_State::GLState::ITextureObject* sampledTexture = nullptr;
|
|
||||||
const MG_State::GLState::SamplerObject* sampledSampler = nullptr;
|
|
||||||
if (!ResolveSampledBinding(program, programObj, samplerBinding, samplerElement,
|
|
||||||
sampledTexture, sampledSampler) ||
|
|
||||||
sampledTexture == nullptr || sampledSampler == nullptr ||
|
|
||||||
MG_State::GLState::SamplesAsIncompleteTexture(sampledTexture, sampledSampler)) {
|
|
||||||
// ResolveSamplerDescriptor uses a fallback in these cases, which cannot
|
|
||||||
// alias the image-unit binding of the original texture.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Multisample source images intentionally omit TRANSFER_SRC usage. Keep their existing
|
|
||||||
// direct binding instead of turning otherwise valid sampler2DMS/image2DMS dispatches
|
|
||||||
// into failed dispatches; a correct snapshot for them needs a same-sample-count path.
|
|
||||||
const TextureTarget sampledTarget = sampledTexture->GetTarget();
|
|
||||||
if (sampledTarget == TextureTarget::Texture2DMultisample ||
|
|
||||||
sampledTarget == TextureTarget::Texture2DMultisampleArray) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const auto& levelRange = sampledTexture->GetLevelRange();
|
|
||||||
Bool aliasesWritableImage = false;
|
|
||||||
for (const Uint32 imageBinding : programObj.activeBindings) {
|
|
||||||
if (imageBinding >= m_maxBindings ||
|
|
||||||
programObj.bindingKinds[imageBinding] != ProgramFactory::DescriptorBindingKind::StorageImage) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (imageBinding >= programObj.samplerUniformLocationByBinding.size()) return false;
|
|
||||||
const Int baseLocation = programObj.samplerUniformLocationByBinding[imageBinding];
|
|
||||||
if (baseLocation < 0) return false;
|
|
||||||
const Uint32 imageCount = BindingDescriptorCount(programObj, imageBinding);
|
|
||||||
for (Uint32 imageElement = 0; imageElement < imageCount; ++imageElement) {
|
|
||||||
const Int location = ResolveDescriptorElementLocation(program, baseLocation, imageElement);
|
|
||||||
if (location < 0) return false;
|
|
||||||
const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
|
|
||||||
if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const auto& image = MG_State::pGLContext->GetImageTextureBinding(imageUnit);
|
|
||||||
// A sampler view exposes all layers of its target; equal texture plus an
|
|
||||||
// overlapping mip therefore aliases the writable image subresource.
|
|
||||||
if (image.Texture.get() == sampledTexture &&
|
|
||||||
SamplerOverlapsWritableImageSubresource(levelRange.x(), levelRange.y(),
|
|
||||||
image.Level, image.Access)) {
|
|
||||||
aliasesWritableImage = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (aliasesWritableImage) break;
|
|
||||||
}
|
|
||||||
if (aliasesWritableImage) {
|
|
||||||
outBindings.push_back({.samplerBinding = samplerBinding,
|
|
||||||
.samplerElement = samplerElement,
|
|
||||||
.texture = sampledTexture,
|
|
||||||
.sampler = sampledSampler,
|
|
||||||
.numericDomain = programObj.samplerNumericDomainByBinding[samplerBinding]});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
Uint32 arrayElement, UboBindResult& out) const {
|
Uint32 arrayElement, UboBindResult& out) const {
|
||||||
@@ -1726,8 +1642,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 frameIndex,
|
Uint32 frameIndex,
|
||||||
VkPipelineBindPoint bindPoint,
|
VkPipelineBindPoint bindPoint,
|
||||||
const SamplerBindingOverride* samplerBindingOverride,
|
const SamplerBindingOverride* samplerBindingOverride,
|
||||||
Bool samplerDescriptorsUnchangedHint,
|
Bool samplerDescriptorsUnchangedHint) {
|
||||||
const Vector<SamplerBindingOverride>* samplerBindingOverrides) {
|
|
||||||
// This program has a descriptor MobileGL could not resolve (see
|
// This program has a descriptor MobileGL could not resolve (see
|
||||||
// VkProgramObject::declinedDescriptors). Refusing here is the whole of the decline: the
|
// VkProgramObject::declinedDescriptors). Refusing here is the whole of the decline: the
|
||||||
// binding is still declared in the layout, so the pipeline is consistent with the shader
|
// binding is still declared in the layout, so the pipeline is consistent with the shader
|
||||||
@@ -1754,8 +1669,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// sampler binding, and an unchanged (buffer, range) for the single
|
// sampler binding, and an unchanged (buffer, range) for the single
|
||||||
// dynamic UBO covers the rest - except the dynamic offset, which rebinding
|
// dynamic UBO covers the rest - except the dynamic offset, which rebinding
|
||||||
// the SAME set delivers without any descriptor write.
|
// the SAME set delivers without any descriptor write.
|
||||||
const Bool cacheable = samplerBindingOverride == nullptr &&
|
const Bool cacheable = (samplerBindingOverride == nullptr);
|
||||||
(samplerBindingOverrides == nullptr || samplerBindingOverrides->empty());
|
|
||||||
if (cacheable && samplerDescriptorsUnchangedHint && m_fastRebindMemo.valid &&
|
if (cacheable && samplerDescriptorsUnchangedHint && m_fastRebindMemo.valid &&
|
||||||
m_fastRebindMemo.frameIndex == frameIndex &&
|
m_fastRebindMemo.frameIndex == frameIndex &&
|
||||||
m_fastRebindMemo.programLifetimeId == program.GetLifetimeId() &&
|
m_fastRebindMemo.programLifetimeId == program.GetLifetimeId() &&
|
||||||
@@ -1979,23 +1893,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const SizeT firstImageInfoIndex = imageInfos.size();
|
const SizeT firstImageInfoIndex = imageInfos.size();
|
||||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||||
VkDescriptorImageInfo imageInfo{};
|
VkDescriptorImageInfo imageInfo{};
|
||||||
const SamplerBindingOverride* overrideForElement =
|
Bool hasImage = false;
|
||||||
overrideThisBinding && element == 0 ? samplerBindingOverride : nullptr;
|
if (overrideThisBinding && element == 0) {
|
||||||
if (overrideForElement == nullptr && samplerBindingOverrides != nullptr) {
|
hasImage = ResolveSamplerDescriptorOverride(*samplerBindingOverride, imageInfo);
|
||||||
const auto overrideIt = std::find_if(
|
} else {
|
||||||
samplerBindingOverrides->begin(), samplerBindingOverrides->end(),
|
hasImage = ResolveSamplerDescriptor(commandBuffer, program, programObj, binding, element,
|
||||||
[binding, element](const SamplerBindingOverride& candidate) {
|
imageInfo, samplerDescriptorsUnchangedHint);
|
||||||
return candidate.binding == binding && candidate.element == element;
|
|
||||||
});
|
|
||||||
if (overrideIt != samplerBindingOverrides->end()) {
|
|
||||||
overrideForElement = &*overrideIt;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const Bool hasImage = overrideForElement != nullptr
|
|
||||||
? ResolveSamplerDescriptorOverride(*overrideForElement, imageInfo)
|
|
||||||
: ResolveSamplerDescriptor(commandBuffer, program, programObj, binding,
|
|
||||||
element, imageInfo,
|
|
||||||
samplerDescriptorsUnchangedHint);
|
|
||||||
if (!hasImage) {
|
if (!hasImage) {
|
||||||
MGLOG_E_ONCE(
|
MGLOG_E_ONCE(
|
||||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
|
"UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u "
|
||||||
|
|||||||
@@ -26,20 +26,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
public:
|
public:
|
||||||
struct SamplerBindingOverride {
|
struct SamplerBindingOverride {
|
||||||
Uint32 binding = 0;
|
Uint32 binding = 0;
|
||||||
Uint32 element = 0;
|
|
||||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||||
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||||
VkImageView imageView = VK_NULL_HANDLE;
|
VkImageView imageView = VK_NULL_HANDLE;
|
||||||
VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
||||||
Bool forceNearestFiltering = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SamplerImageFeedbackBinding {
|
|
||||||
Uint32 samplerBinding = 0;
|
|
||||||
Uint32 samplerElement = 0;
|
|
||||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
|
||||||
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
|
||||||
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
|
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
|
||||||
@@ -90,12 +79,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
|
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
||||||
Bool CollectSamplerImageFeedback(
|
|
||||||
const MG_State::GLState::ProgramObject& program,
|
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
|
||||||
Vector<SamplerImageFeedbackBinding>& outBindings) const;
|
|
||||||
static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
|
|
||||||
GLint imageLevel, GLenum imageAccess);
|
|
||||||
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
|
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
|
||||||
// every input of every combined-image-sampler resolution is unchanged since the
|
// every input of every combined-image-sampler resolution is unchanged since the
|
||||||
// previous draw's resolve - same (texture, sampler) per binding, texture params
|
// previous draw's resolve - same (texture, sampler) per binding, texture params
|
||||||
@@ -108,8 +91,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 frameIndex,
|
Uint32 frameIndex,
|
||||||
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||||
const SamplerBindingOverride* samplerBindingOverride = nullptr,
|
const SamplerBindingOverride* samplerBindingOverride = nullptr,
|
||||||
Bool samplerDescriptorsUnchangedHint = false,
|
Bool samplerDescriptorsUnchangedHint = false);
|
||||||
const Vector<SamplerBindingOverride>* samplerBindingOverrides = nullptr);
|
|
||||||
|
|
||||||
// Pure format-policy helper kept public for host regression tests. Formatted storage
|
// Pure format-policy helper kept public for host regression tests. Formatted storage
|
||||||
// images use their shader qualifier; transformed float images use glBindImageTexture's
|
// images use their shader qualifier; transformed float images use glBindImageTexture's
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
#include "VertexInputStateFactory.h"
|
#include "VertexInputStateFactory.h"
|
||||||
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
|
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
|
||||||
#include <MG_Backend/BackendObjects.h>
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
@@ -108,34 +107,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
VkFormat sourceVkFormat =
|
const VkFormat sourceVkFormat =
|
||||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
||||||
VertexStreamConversion conversion = VertexStreamConversion::None;
|
|
||||||
// Gated on the SAME flag ToVkVertexFormat gates its 64-bit path on, and that is
|
|
||||||
// load-bearing rather than belt-and-braces: the narrowing is only correct because the
|
|
||||||
// shader's `dvec` input is a `vec` by the time the pipeline is built, and what
|
|
||||||
// guarantees that is the flag being clear. It is clear on every backend today, and a
|
|
||||||
// program with a 64-bit float vertex input is demoted WHOLE for the same reason even
|
|
||||||
// where the device has native fp64 (ProgramSpirvTask::GenerateSpirv). With the flag
|
|
||||||
// set, a dvec3/dvec4 would be declined by ToVkVertexFormat AND left 64-bit in the
|
|
||||||
// module, so a float32 stream would be fed to a Float64 input.
|
|
||||||
const Bool narrowFloat64Arrays =
|
|
||||||
MG_Backend::pActiveBackendObject == nullptr ||
|
|
||||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes;
|
|
||||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED && attr.Type == DataType::Float64 && narrowFloat64Arrays) {
|
|
||||||
// No native 64-bit fetch here (see ToVkVertexFormat's Float64 case), but the
|
|
||||||
// source bytes are ordinary IEEE-754 doubles and DemoteFloat64Pass has already
|
|
||||||
// narrowed every dvec input to a vec, so the array is narrowed to match rather
|
|
||||||
// than dropped. Mirrors what DirectGLES does for the same state.
|
|
||||||
const VkFormat narrowedFormat = ToFloat32VertexFormat(attr.Size);
|
|
||||||
if (narrowedFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(narrowedFormat)) {
|
|
||||||
sourceVkFormat = narrowedFormat;
|
|
||||||
conversion = VertexStreamConversion::Float64ToFloat32;
|
|
||||||
MGLOG_W_ONCE("Vertex attribute location=%u is a 64-bit (GL_DOUBLE) array; fetching it at "
|
|
||||||
"float32 precision through format=%d (size=%d long=%s)",
|
|
||||||
location, static_cast<Int>(narrowedFormat), attr.Size, attr.IsLong ? "true" : "false");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
||||||
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||||
"enabled but cannot be mapped to a VkFormat",
|
"enabled but cannot be mapped to a VkFormat",
|
||||||
@@ -145,7 +118,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VkFormat vkFormat = sourceVkFormat;
|
VkFormat vkFormat = sourceVkFormat;
|
||||||
if (conversion == VertexStreamConversion::None && !SupportsVertexBufferFormat(vkFormat)) {
|
VertexStreamConversion conversion = VertexStreamConversion::None;
|
||||||
|
if (!SupportsVertexBufferFormat(vkFormat)) {
|
||||||
if (IsScaledIntegerVertexFormat(vkFormat)) {
|
if (IsScaledIntegerVertexFormat(vkFormat)) {
|
||||||
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
|
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
|
||||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
||||||
@@ -214,8 +188,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (sourceStride != 0) {
|
if (sourceStride != 0) {
|
||||||
if (conversion == VertexStreamConversion::Repack) {
|
if (conversion == VertexStreamConversion::Repack) {
|
||||||
stride = static_cast<Uint32>(attribByteSize);
|
stride = static_cast<Uint32>(attribByteSize);
|
||||||
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32 ||
|
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
|
||||||
conversion == VertexStreamConversion::Float64ToFloat32) {
|
|
||||||
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,10 +287,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||||
it = m_cache.erase(it);
|
it = m_cache.erase(it);
|
||||||
// Invalidate every VAO's state-pointer memo: the erased node's
|
// Invalidate every VAO's state-pointer memo: the erased node's
|
||||||
// address may be reused by a future insert. Advance through the
|
// address may be reused by a future insert.
|
||||||
// process-wide source so the value stays unique across factory
|
++m_evictionEpoch;
|
||||||
// instances (see the member comment).
|
|
||||||
m_evictionEpoch = ++s_evictionEpochSource;
|
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
@@ -357,20 +328,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there
|
// for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there
|
||||||
// while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
|
// while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
|
||||||
// so they always agree without extra plumbing.
|
// so they always agree without extra plumbing.
|
||||||
//
|
|
||||||
// ... as long as the shader half still runs. It does not when the backend has declared
|
|
||||||
// no 64-bit vertex attribute support: DemoteFloat64Pass has already narrowed every
|
|
||||||
// `dvec` input to a `vec` by then, so PackDoubleVertexInputsPass finds nothing to pack
|
|
||||||
// and a UINT-formatted attribute would be fed to a float input - garbage with no
|
|
||||||
// diagnostic anywhere. Declining here hands the attribute to the caller's
|
|
||||||
// Float64ToFloat32 fallback instead, which narrows the source doubles to match the
|
|
||||||
// demoted `vec` input - the same thing DirectGLES does for the same state. The
|
|
||||||
// frontend RECORDS the format either way, so this gate is the only thing standing
|
|
||||||
// between a legal glVertexAttribLFormat and a mismatched pipeline.
|
|
||||||
if (MG_Backend::pActiveBackendObject == nullptr ||
|
|
||||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
|
||||||
return VK_FORMAT_UNDEFINED;
|
|
||||||
}
|
|
||||||
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
|
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 1: return VK_FORMAT_R32G32_UINT;
|
case 1: return VK_FORMAT_R32G32_UINT;
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
None = 0,
|
None = 0,
|
||||||
Repack,
|
Repack,
|
||||||
ScaledIntegerToFloat32,
|
ScaledIntegerToFloat32,
|
||||||
// GL_DOUBLE source data narrowed to a tightly packed float32 stream: the fetch half
|
|
||||||
// of the fp64 demotion the shader side already does unconditionally.
|
|
||||||
Float64ToFloat32,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BackendVertexInputState {
|
struct BackendVertexInputState {
|
||||||
@@ -128,17 +125,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// construction); a memo is honored only while its recorded epoch
|
// construction); a memo is honored only while its recorded epoch
|
||||||
// matches, so an evicted entry can never be dereferenced through a
|
// matches, so an evicted entry can never be dereferenced through a
|
||||||
// stale memo.
|
// stale memo.
|
||||||
//
|
Uint64 m_evictionEpoch = 1;
|
||||||
// Drawn from a process-wide source, never a per-instance counter: the VAO
|
|
||||||
// memos outlive this factory (they live on pGLContext's VAOs, the renderer
|
|
||||||
// is destroyed and recreated on EGL surface release/re-create), so a fresh
|
|
||||||
// factory restarting at a dead factory's epoch value would honor its
|
|
||||||
// dangling entry pointers. The constructor takes a value strictly greater
|
|
||||||
// than anything a predecessor ever stamped, so a dead factory's memo can
|
|
||||||
// never compare equal here - the same never-reused idiom as the lifetime ids.
|
|
||||||
// Single-threaded like the rest of the factory (renderer-thread only).
|
|
||||||
static inline Uint64 s_evictionEpochSource = 0;
|
|
||||||
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
|
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -166,15 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
|
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
|
||||||
dst.mask |= src.mask;
|
dst.mask |= src.mask;
|
||||||
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||||
// The whole colour story travels together (same rule as
|
|
||||||
// VkRenderPassManager::QueueRenderbufferClear): a glClearBufferiv/uiv
|
|
||||||
// payload carries its value in colorInt/colorUint and its branch selector
|
|
||||||
// in colorEncoding - dropping them here would leave the pending clear
|
|
||||||
// reading as an all-zero float one.
|
|
||||||
dst.color = src.color;
|
dst.color = src.color;
|
||||||
dst.colorEncoding = src.colorEncoding;
|
|
||||||
dst.colorInt = src.colorInt;
|
|
||||||
dst.colorUint = src.colorUint;
|
|
||||||
}
|
}
|
||||||
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||||
dst.depth = src.depth;
|
dst.depth = src.depth;
|
||||||
|
|||||||
@@ -831,7 +831,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// recreated since (texture + renderbuffer image epochs), and no pending clear (which alters
|
// recreated since (texture + renderbuffer image epochs), and no pending clear (which alters
|
||||||
// load ops). Any of these differing forces the full recompute below. Portable to VK 1.1.
|
// load ops). Any of these differing forces the full recompute below. Portable to VK 1.1.
|
||||||
if (activeRenderPass != nullptr && m_rpFastValid && m_rpFastFbo == &fbo &&
|
if (activeRenderPass != nullptr && m_rpFastValid && m_rpFastFbo == &fbo &&
|
||||||
m_rpFastFboLifetimeId == fbo.GetLifetimeId() &&
|
|
||||||
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
|
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
|
||||||
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
|
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
|
||||||
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
|
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
|
||||||
@@ -856,7 +855,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// epochs AFTER ComputeHash: its attachment SyncTexture can create an image (bump the epoch).
|
// epochs AFTER ComputeHash: its attachment SyncTexture can create an image (bump the epoch).
|
||||||
m_rpFastValid = true;
|
m_rpFastValid = true;
|
||||||
m_rpFastFbo = &fbo;
|
m_rpFastFbo = &fbo;
|
||||||
m_rpFastFboLifetimeId = fbo.GetLifetimeId();
|
|
||||||
m_rpFastFboVersion = fbo.GetObjectVersion();
|
m_rpFastFboVersion = fbo.GetObjectVersion();
|
||||||
m_rpFastSwapchainIndex = swapchainImageIndex;
|
m_rpFastSwapchainIndex = swapchainImageIndex;
|
||||||
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
||||||
@@ -1509,23 +1507,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ClearAttachmentPayload clearPayload{};
|
ClearAttachmentPayload clearPayload{};
|
||||||
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
||||||
if (pending.hasInlinePayload) {
|
if (pending.hasInlinePayload) {
|
||||||
// The inline payload was snapshotted when the entry was CREATED, but the
|
clearPayload = pending.inlinePayload;
|
||||||
// clear VALUE is not part of the entry's hash - a cache hit with a newer
|
|
||||||
// glClear would replay the creation-time value and drop the new one (the
|
|
||||||
// texture path below is immune because it re-reads the live payload).
|
|
||||||
// Same defense as ClearAttachmentsOnActiveRenderPass: prefer the live
|
|
||||||
// pending clear, fall back to the snapshot only when none is queued.
|
|
||||||
if (s_renderPassManager != nullptr &&
|
|
||||||
s_renderPassManager->GetPendingRenderbufferClear(pending.renderbuffer, clearPayload)) {
|
|
||||||
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0 && pending.renderbuffer != nullptr &&
|
|
||||||
MG_Util::GetBaseInternalFormatComponentCount(pending.renderbuffer->GetInternalFormat()) ==
|
|
||||||
3) {
|
|
||||||
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
|
|
||||||
ForceOpaqueClearAlpha(clearPayload);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
clearPayload = pending.inlinePayload;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (pending.key.texture == nullptr ||
|
if (pending.key.texture == nullptr ||
|
||||||
!s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) {
|
!s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) {
|
||||||
|
|||||||
@@ -289,11 +289,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
|
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
|
||||||
Bool m_rpFastValid = false;
|
Bool m_rpFastValid = false;
|
||||||
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
|
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
|
||||||
// The FBO's never-reused lifetime id joins the raw pointer + Uint16 version:
|
|
||||||
// a deleted FBO reallocated at the same address whose fresh setup performed
|
|
||||||
// the same number of version bumps would otherwise compare equal (both count
|
|
||||||
// from 0), serving the dead framebuffer's pass to the new object.
|
|
||||||
Uint64 m_rpFastFboLifetimeId = 0;
|
|
||||||
Uint16 m_rpFastFboVersion = 0;
|
Uint16 m_rpFastFboVersion = 0;
|
||||||
Uint32 m_rpFastSwapchainIndex = 0;
|
Uint32 m_rpFastSwapchainIndex = 0;
|
||||||
Uint64 m_rpFastTexEpoch = 0;
|
Uint64 m_rpFastTexEpoch = 0;
|
||||||
|
|||||||
@@ -1291,158 +1291,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool VkTextureManager::SnapshotTextureForSampling(VkCommandBuffer commandBuffer,
|
|
||||||
MG_State::GLState::ITextureObject& texture,
|
|
||||||
SamplerNumericDomain numericDomain,
|
|
||||||
VkPipelineStageFlags consumerShaderStageMask,
|
|
||||||
SampledTextureSnapshot& outSnapshot) {
|
|
||||||
outSnapshot = {};
|
|
||||||
TextureResource* source = SyncTextureAndGetDescriptor(texture);
|
|
||||||
if (source == nullptr || source->image == VK_NULL_HANDLE || source->sampleCount != VK_SAMPLE_COUNT_1_BIT ||
|
|
||||||
source->sampledLevelCount == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VkFormat sampledFormat = ResolveSampledImageViewFormat(source->format, numericDomain);
|
|
||||||
if (sampledFormat == VK_FORMAT_UNDEFINED ||
|
|
||||||
!AreSampledImageViewFormatsCompatible(source->format, sampledFormat)) {
|
|
||||||
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d cannot create sampled view format=%d from image format=%d",
|
|
||||||
texture.GetExternalIndex(), static_cast<Int>(sampledFormat), static_cast<Int>(source->format));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (sampledFormat != source->format &&
|
|
||||||
(source->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) {
|
|
||||||
MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d needs unavailable mutable image format=%d for sampled view=%d",
|
|
||||||
texture.GetExternalIndex(), static_cast<Int>(source->format), static_cast<Int>(sampledFormat));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
VkImageType imageType = VK_IMAGE_TYPE_2D;
|
|
||||||
switch (source->viewType) {
|
|
||||||
case VK_IMAGE_VIEW_TYPE_1D:
|
|
||||||
case VK_IMAGE_VIEW_TYPE_1D_ARRAY:
|
|
||||||
imageType = VK_IMAGE_TYPE_1D;
|
|
||||||
break;
|
|
||||||
case VK_IMAGE_VIEW_TYPE_3D:
|
|
||||||
imageType = VK_IMAGE_TYPE_3D;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
TextureResource snapshot{};
|
|
||||||
VkImageCreateInfo imageInfo{};
|
|
||||||
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
|
||||||
imageInfo.flags = source->imageCreateFlags;
|
|
||||||
imageInfo.imageType = imageType;
|
|
||||||
imageInfo.extent = {source->extent.width, source->extent.height, source->depth};
|
|
||||||
imageInfo.mipLevels = source->mipLevels;
|
|
||||||
imageInfo.arrayLayers = source->arrayLayers;
|
|
||||||
imageInfo.format = source->format;
|
|
||||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
||||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
||||||
imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
||||||
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
||||||
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
||||||
|
|
||||||
// Keep the temporary's view-format list just as narrow as the source's sampler use. This
|
|
||||||
// has no storage-image usage, so unlike an app image binding the exact list is knowable.
|
|
||||||
Vector<VkFormat> viewFormats;
|
|
||||||
VkImageFormatListCreateInfo formatListInfo{};
|
|
||||||
if (m_imageFormatListSupported && (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
|
||||||
viewFormats.push_back(source->format);
|
|
||||||
if (sampledFormat != source->format) {
|
|
||||||
viewFormats.push_back(sampledFormat);
|
|
||||||
}
|
|
||||||
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
|
|
||||||
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
|
|
||||||
formatListInfo.pViewFormats = viewFormats.data();
|
|
||||||
imageInfo.pNext = &formatListInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
VmaAllocationCreateInfo allocationInfo{};
|
|
||||||
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
|
|
||||||
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
|
||||||
const VkResult createResult =
|
|
||||||
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &snapshot.image, &snapshot.allocation, nullptr);
|
|
||||||
if (createResult != VK_SUCCESS) {
|
|
||||||
MGLOG_E_ONCE("SnapshotTextureForSampling: vmaCreateImage failed result=%d textureId=%d", createResult,
|
|
||||||
texture.GetExternalIndex());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshot.extent = source->extent;
|
|
||||||
snapshot.depth = source->depth;
|
|
||||||
snapshot.arrayLayers = source->arrayLayers;
|
|
||||||
snapshot.mipLevels = source->mipLevels;
|
|
||||||
snapshot.sampledBaseMipLevel = source->sampledBaseMipLevel;
|
|
||||||
snapshot.sampledLevelCount = source->sampledLevelCount;
|
|
||||||
snapshot.format = source->format;
|
|
||||||
snapshot.aspect = source->aspect;
|
|
||||||
snapshot.viewType = source->viewType;
|
|
||||||
snapshot.sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
|
||||||
snapshot.imageCreateFlags = imageInfo.flags;
|
|
||||||
snapshot.usageFlags = imageInfo.usage;
|
|
||||||
|
|
||||||
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
|
|
||||||
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
|
|
||||||
const VkImageAspectFlags sampledAspect =
|
|
||||||
ResolveSampledImageViewAspectMask(snapshot.aspect, texture.GetDepthStencilTextureMode());
|
|
||||||
snapshot.sampledView = CreateImageView(snapshot.image, sampledFormat, sampledAspect, snapshot.viewType,
|
|
||||||
snapshot.sampledBaseMipLevel, snapshot.sampledLevelCount, 0,
|
|
||||||
snapshot.arrayLayers, &sampledComponents);
|
|
||||||
if (snapshot.sampledView == VK_NULL_HANDLE) {
|
|
||||||
MGLOG_E_ONCE("SnapshotTextureForSampling: failed to create sampled view textureId=%d", texture.GetExternalIndex());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
VkPipelineStageFlags sourceStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
|
||||||
VkAccessFlags sourceAccessMask = 0;
|
|
||||||
const VkImageLayout sourceLayout = source->layout;
|
|
||||||
GetImageTransitionSourceState(sourceLayout, sourceStageMask, sourceAccessMask);
|
|
||||||
if (!TransitionImageLayout(commandBuffer, source->image, source->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
||||||
sourceStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, sourceAccessMask,
|
|
||||||
VK_ACCESS_TRANSFER_READ_BIT, source->aspect, 0, source->mipLevels) ||
|
|
||||||
!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
||||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
|
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
|
|
||||||
snapshot.sampledLevelCount)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector<VkImageCopy> copyRegions;
|
|
||||||
copyRegions.reserve(snapshot.sampledLevelCount);
|
|
||||||
for (Uint32 level = snapshot.sampledBaseMipLevel;
|
|
||||||
level < snapshot.sampledBaseMipLevel + snapshot.sampledLevelCount; ++level) {
|
|
||||||
VkImageCopy copy{};
|
|
||||||
copy.srcSubresource = {source->aspect, level, 0, source->arrayLayers};
|
|
||||||
copy.dstSubresource = {snapshot.aspect, level, 0, snapshot.arrayLayers};
|
|
||||||
copy.extent = {std::max(source->extent.width >> level, 1u),
|
|
||||||
std::max(source->extent.height >> level, 1u),
|
|
||||||
std::max(source->depth >> level, 1u)};
|
|
||||||
copyRegions.push_back(copy);
|
|
||||||
}
|
|
||||||
vkCmdCopyImage(commandBuffer, source->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, snapshot.image,
|
|
||||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<Uint32>(copyRegions.size()), copyRegions.data());
|
|
||||||
|
|
||||||
if (!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout,
|
|
||||||
ResolveSampledReadOnlyLayout(snapshot.aspect), VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
||||||
consumerShaderStageMask, VK_ACCESS_TRANSFER_WRITE_BIT,
|
|
||||||
VK_ACCESS_SHADER_READ_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel,
|
|
||||||
snapshot.sampledLevelCount) ||
|
|
||||||
!TransitionImageLayout(commandBuffer, source->image, source->layout, sourceLayout,
|
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT, consumerShaderStageMask,
|
|
||||||
VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
|
|
||||||
source->aspect, 0, source->mipLevels)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
StampResourceRecordingUse(*source);
|
|
||||||
outSnapshot = {.imageView = snapshot.sampledView, .layout = snapshot.layout};
|
|
||||||
DeferResourceRelease(Move(snapshot));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
|
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
|
||||||
m_storageImageTextures.insert(MakeTextureIdentity(&texture));
|
m_storageImageTextures.insert(MakeTextureIdentity(&texture));
|
||||||
}
|
}
|
||||||
@@ -1494,7 +1342,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||||
const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u;
|
const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u;
|
||||||
return resource.syncedContentVersion != texture.GetContentVersion() ||
|
return resource.syncedContentVersion != texture.GetContentVersion() ||
|
||||||
resource.syncedShapeVersion != texture.GetShapeVersion() ||
|
|
||||||
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() ||
|
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() ||
|
||||||
resource.syncedMipLevelCount != mipLevelCount;
|
resource.syncedMipLevelCount != mipLevelCount;
|
||||||
}
|
}
|
||||||
@@ -1594,16 +1441,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
|
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
|
||||||
TextureResource &outResource) {
|
TextureResource &outResource) {
|
||||||
// Cross-draw fast path: if the resource is already built and neither the texture's
|
// Cross-draw fast path: if the resource is already built and neither the texture's
|
||||||
// pixel content (bumped in MarkStorageDirty), its SHAPE (bumped in BumpShapeVersion)
|
// pixel content (bumped in MarkStorageDirty) nor its params changed since the last
|
||||||
// nor its params changed since the last sync, there is nothing to re-check or
|
// sync, there is nothing to re-check or re-upload - skip CheckMipmapCompleteness,
|
||||||
// re-upload - skip CheckMipmapCompleteness, SyncTextureResource, SyncTextureViews and
|
// SyncTextureResource, SyncTextureViews and the per-level dirty scan. Layout is
|
||||||
// the per-level dirty scan. Layout is maintained separately by the transition path, so
|
// maintained separately by the transition path, so the resource still reflects truth.
|
||||||
// the resource still reflects truth. The shape version is NOT redundant with the
|
|
||||||
// content one: glTexImage2D(..., nullptr) re-specifies a level's size or format
|
|
||||||
// without dirtying a texel, which is exactly how a re-specified image-unit texture used
|
|
||||||
// to keep reporting its old imageSize().
|
|
||||||
const Uint64 syncingContentVersion = texture.GetContentVersion();
|
const Uint64 syncingContentVersion = texture.GetContentVersion();
|
||||||
const Uint64 syncingShapeVersion = texture.GetShapeVersion();
|
|
||||||
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
|
||||||
const Uint32 syncingMipLevelCount =
|
const Uint32 syncingMipLevelCount =
|
||||||
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
|
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
|
||||||
@@ -1615,7 +1457,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
|
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
|
||||||
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
|
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
|
||||||
outResource.syncedContentVersion == syncingContentVersion &&
|
outResource.syncedContentVersion == syncingContentVersion &&
|
||||||
outResource.syncedShapeVersion == syncingShapeVersion &&
|
|
||||||
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
|
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
|
||||||
outResource.syncedMipLevelCount == syncingMipLevelCount) {
|
outResource.syncedMipLevelCount == syncingMipLevelCount) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1636,12 +1477,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// From here down the size is VULKAN geometry, not GL's: a 1D array's layer count moves
|
|
||||||
// out of the height it occupies GL-side and into z, which is the slot
|
|
||||||
// TryResolveTextureShapeInfo reads arrayLayers from and the only one that leaves
|
|
||||||
// extent.height at the 1 a VK_IMAGE_TYPE_1D image is required to have.
|
|
||||||
texelSize = ToVulkanLevelExtent(texture.GetTarget(), texelSize);
|
|
||||||
|
|
||||||
if (!SyncTextureResource(texture, uploadTarget, texelSize, byteSize, mipLevelCount, outResource)) {
|
if (!SyncTextureResource(texture, uploadTarget, texelSize, byteSize, mipLevelCount, outResource)) {
|
||||||
MGLOG_D("%s: SyncTextureResource failed", __func__);
|
MGLOG_D("%s: SyncTextureResource failed", __func__);
|
||||||
return false;
|
return false;
|
||||||
@@ -1673,7 +1508,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (!hasDirtyMipLevel) {
|
if (!hasDirtyMipLevel) {
|
||||||
outResource.syncedContentVersion = syncingContentVersion;
|
outResource.syncedContentVersion = syncingContentVersion;
|
||||||
outResource.syncedMipLevelCount = syncingMipLevelCount;
|
outResource.syncedMipLevelCount = syncingMipLevelCount;
|
||||||
outResource.syncedShapeVersion = syncingShapeVersion;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1683,7 +1517,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
outResource.syncedContentVersion = syncingContentVersion;
|
outResource.syncedContentVersion = syncingContentVersion;
|
||||||
outResource.syncedMipLevelCount = syncingMipLevelCount;
|
outResource.syncedMipLevelCount = syncingMipLevelCount;
|
||||||
outResource.syncedShapeVersion = syncingShapeVersion;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1863,19 +1696,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);
|
||||||
}
|
}
|
||||||
@@ -2021,13 +1841,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
texture.GetExternalIndex(),
|
texture.GetExternalIndex(),
|
||||||
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||||
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
|
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
|
||||||
// The preserved image was written by GPU work that may still be in flight
|
|
||||||
// (preserve requires layout != UNDEFINED); park it on the deferred ring
|
|
||||||
// like every other destruction path instead of letting the unique_ptr
|
|
||||||
// destroy it synchronously under the GPU.
|
|
||||||
if (preservedResource) {
|
|
||||||
DeferResourceRelease(Move(*preservedResource));
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2050,12 +1863,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static_cast<Int>(imageInfo.samples), static_cast<Int>(imageInfo.format));
|
static_cast<Int>(imageInfo.samples), static_cast<Int>(imageInfo.format));
|
||||||
resource.image = VK_NULL_HANDLE;
|
resource.image = VK_NULL_HANDLE;
|
||||||
resource.allocation = nullptr;
|
resource.allocation = nullptr;
|
||||||
// Same as the probe failure above: the preserved live image must go through
|
|
||||||
// the deferred ring, never a synchronous destructor while frames that
|
|
||||||
// reference it are still in flight.
|
|
||||||
if (preservedResource) {
|
|
||||||
DeferResourceRelease(Move(*preservedResource));
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
|
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
|
||||||
@@ -2551,13 +2358,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
uploadItem.target = target;
|
uploadItem.target = target;
|
||||||
uploadItem.level = level;
|
uploadItem.level = level;
|
||||||
uploadItem.baseArrayLayer = ResolveUploadArrayLayer(target);
|
uploadItem.baseArrayLayer = ResolveUploadArrayLayer(target);
|
||||||
// Vulkan geometry, like the image this stages into (see SyncTexture): a 1D
|
uploadItem.texelSize = texelSize;
|
||||||
// array's layers move from y to z, where the copy loop's depthSelectsArrayLayer
|
|
||||||
// branch turns them into layerCount. The shadow needs no repacking to follow -
|
|
||||||
// one layer of a 1D array IS one row of `width` texels, so the tight-packed
|
|
||||||
// per-layer copy the swapped size describes reads the same bytes in the same
|
|
||||||
// order as the row-major level it replaces.
|
|
||||||
uploadItem.texelSize = ToVulkanLevelExtent(mipmapTexture.GetTarget(), texelSize);
|
|
||||||
uploadItem.source = source;
|
uploadItem.source = source;
|
||||||
uploadItem.offset = stagingSize;
|
uploadItem.offset = stagingSize;
|
||||||
uploadItem.uploadByteSize = byteSize;
|
uploadItem.uploadByteSize = byteSize;
|
||||||
@@ -2595,23 +2396,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
uploadItem.uploadByteSize = rectTexels * uploadItem.texelBytes;
|
uploadItem.uploadByteSize = rectTexels * uploadItem.texelBytes;
|
||||||
}
|
}
|
||||||
// The boxes came out of the shadow in GL coordinates, where a 1D
|
|
||||||
// array's layer is the y. They have to follow texelSize across to z or
|
|
||||||
// they would address rows of an image that now has exactly one, and
|
|
||||||
// the staging walk would read the wrong bytes for them. Every byte
|
|
||||||
// count computed above is a product of the three extents, so moving
|
|
||||||
// the axes leaves all of them alone - and an OFFSET lands on a zero y,
|
|
||||||
// not on the extent's one, which is why this is spelled out rather than
|
|
||||||
// handed to ToVulkanLevelExtent.
|
|
||||||
if (mipmapTexture.GetTarget() == TextureTarget::Texture1DArray) {
|
|
||||||
uploadItem.regionLo = {uploadItem.regionLo.x(), 0, uploadItem.regionLo.y()};
|
|
||||||
uploadItem.regionSize = {uploadItem.regionSize.x(), 1,
|
|
||||||
uploadItem.regionSize.y()};
|
|
||||||
for (auto& rect : uploadItem.rects) {
|
|
||||||
rect.lo = {rect.lo.x(), 0, rect.lo.y()};
|
|
||||||
rect.hi = {rect.hi.x(), 1, rect.hi.y()};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (formatInfo.expandRgbToRgba) {
|
if (formatInfo.expandRgbToRgba) {
|
||||||
|
|||||||
@@ -22,25 +22,6 @@ class ITextureObject;
|
|||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
enum class SamplerNumericDomain : Uint8;
|
enum class SamplerNumericDomain : Uint8;
|
||||||
|
|
||||||
// A GL 1D-ARRAY level keeps its LAYER COUNT in the state-side HEIGHT: that is what
|
|
||||||
// glTexImage2D(GL_TEXTURE_1D_ARRAY, width, layers) means, and the frontend records the level
|
|
||||||
// as {width, layers, 1} (see GL_Texture.cpp's AllocateStorage and the completeness walk in
|
|
||||||
// TextureObject.cpp, which shrinks only x down the chain). Vulkan packs it the other way: a
|
|
||||||
// 1D array is a VK_IMAGE_TYPE_1D image whose extent.height MUST be 1 and whose layers live in
|
|
||||||
// arrayLayers - i.e. in the slot this backend reads out of z. So every place that turns a GL
|
|
||||||
// level size into Vulkan image geometry has to move the count across first, and every GL-space
|
|
||||||
// sub-box that rides along with it has to move its y the same way. DirectGLES performs the
|
|
||||||
// identical remap onto the ES 2D array it maps 1D arrays to (GetBackendUploadSize).
|
|
||||||
//
|
|
||||||
// Applied to nothing else: a 2D array, a cube array and a 3D texture all already carry their
|
|
||||||
// depth/layer count in z, which is where the Vulkan side expects it.
|
|
||||||
inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glTexelSize) {
|
|
||||||
if (stateTarget == TextureTarget::Texture1DArray) {
|
|
||||||
return {glTexelSize.x(), 1, glTexelSize.y()};
|
|
||||||
}
|
|
||||||
return glTexelSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
class VkTextureManager {
|
class VkTextureManager {
|
||||||
public:
|
public:
|
||||||
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
// Monotonic epoch bumped whenever a texture VkImage is (re)created. The render-pass
|
||||||
@@ -225,12 +206,6 @@ public:
|
|||||||
// as defense-in-depth: any path that grows the level set (which resizes the sampled view)
|
// as defense-in-depth: any path that grows the level set (which resizes the sampled view)
|
||||||
// busts the skip even if it failed to bump the content version.
|
// busts the skip even if it failed to bump the content version.
|
||||||
Uint32 syncedMipLevelCount = 0;
|
Uint32 syncedMipLevelCount = 0;
|
||||||
// Snapshot of ITextureObject::GetShapeVersion() at the last successful sync. The content
|
|
||||||
// version alone does NOT cover a re-specification: glTexImage2D(..., nullptr) on an
|
|
||||||
// already-defined level changes its size or format and dirties no texel, so it moves the
|
|
||||||
// shape version and nothing else. Without this in the early-out key the image, its views
|
|
||||||
// and therefore imageSize() all keep answering with the texture's PREVIOUS shape.
|
|
||||||
Uint64 syncedShapeVersion = 0;
|
|
||||||
|
|
||||||
TextureResource() = default;
|
TextureResource() = default;
|
||||||
TextureResource(const TextureResource&) = delete;
|
TextureResource(const TextureResource&) = delete;
|
||||||
@@ -262,7 +237,6 @@ public:
|
|||||||
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
||||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||||
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Reset() {
|
void Reset() {
|
||||||
@@ -326,7 +300,6 @@ public:
|
|||||||
syncedTextureParamsVersion = 0;
|
syncedTextureParamsVersion = 0;
|
||||||
syncedContentVersion = 0;
|
syncedContentVersion = 0;
|
||||||
syncedMipLevelCount = 0;
|
syncedMipLevelCount = 0;
|
||||||
syncedShapeVersion = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
~TextureResource() {
|
~TextureResource() {
|
||||||
@@ -337,11 +310,6 @@ public:
|
|||||||
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
|
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SampledTextureSnapshot {
|
|
||||||
VkImageView imageView = VK_NULL_HANDLE;
|
|
||||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
||||||
};
|
|
||||||
|
|
||||||
Bool Initialize(const InitInfo& initInfo);
|
Bool Initialize(const InitInfo& initInfo);
|
||||||
void Shutdown();
|
void Shutdown();
|
||||||
void BeginFrame(Uint32 frameIndex);
|
void BeginFrame(Uint32 frameIndex);
|
||||||
@@ -375,13 +343,6 @@ public:
|
|||||||
VkImageLayout newLayout);
|
VkImageLayout newLayout);
|
||||||
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||||
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
|
||||||
// Copies the complete sampler-visible mip range into a transient sampled image. The source is
|
|
||||||
// restored to its prior layout, so image-store descriptors continue to name the original image.
|
|
||||||
// The transient ownership is tied to the current frame slot and is safe through its submission.
|
|
||||||
Bool SnapshotTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture,
|
|
||||||
SamplerNumericDomain numericDomain,
|
|
||||||
VkPipelineStageFlags consumerShaderStageMask,
|
|
||||||
SampledTextureSnapshot& outSnapshot);
|
|
||||||
|
|
||||||
// Recording-generation bookkeeping for the pre-pass command stream. The
|
// Recording-generation bookkeeping for the pre-pass command stream. The
|
||||||
// generation advances every time the frame command buffer (re)begins
|
// generation advances every time the frame command buffer (re)begins
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
#include "VulkanRenderer.h"
|
#include "VulkanRenderer.h"
|
||||||
|
|
||||||
#include "MG_Backend/DirectVulkan/SubgroupSupportPolicy.h"
|
|
||||||
#include "MG_Backend/DirectGLES/Utils.h"
|
#include "MG_Backend/DirectGLES/Utils.h"
|
||||||
#include "VertexInputStateFactory.h"
|
#include "VertexInputStateFactory.h"
|
||||||
#include "VertexInputStateBuilder.h"
|
#include "VertexInputStateBuilder.h"
|
||||||
@@ -972,38 +971,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The fetch half of the 64-bit vertex narrowing, whose shader half is guaranteed by
|
|
||||||
// SupportsFloat64VertexAttributes staying false on this backend: any program with a Float64
|
|
||||||
// vertex INPUT is demoted whole, native fp64 or not, so the input is always a 32-bit one. The
|
|
||||||
// source bytes are ordinary IEEE-754 doubles, so a GL_DOUBLE array is deinterleaved into a
|
|
||||||
// tightly packed float32 stream rather than dropped. `normalized` is not consulted - GL
|
|
||||||
// ignores it for floating-point array types.
|
|
||||||
static Bool ConvertFloat64VertexStreamToFloat32(
|
|
||||||
const MG_State::GLState::VertexAttribute& attribute,
|
|
||||||
const Uint8* sourceData,
|
|
||||||
SizeT sourceStride,
|
|
||||||
SizeT elementCount,
|
|
||||||
Vector<Float>& outData) {
|
|
||||||
if (sourceData == nullptr || attribute.Size < 1 || attribute.Size > 4 || sourceStride == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SizeT componentCount = static_cast<SizeT>(attribute.Size);
|
|
||||||
outData.resize(elementCount * componentCount);
|
|
||||||
for (SizeT element = 0; element < elementCount; ++element) {
|
|
||||||
const Uint8* sourceElement = sourceData + element * sourceStride;
|
|
||||||
Float* destinationElement = outData.data() + element * componentCount;
|
|
||||||
for (SizeT component = 0; component < componentCount; ++component) {
|
|
||||||
// GL byte strides and offsets are arbitrary, so no component carries an 8-byte
|
|
||||||
// alignment guarantee; copy it out before narrowing it.
|
|
||||||
Double value = 0.0;
|
|
||||||
Memcpy(&value, sourceElement + component * sizeof(Double), sizeof(Double));
|
|
||||||
destinationElement[component] = static_cast<Float>(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Bool RepackVertexStream(const Uint8* sourceData,
|
static Bool RepackVertexStream(const Uint8* sourceData,
|
||||||
SizeT sourceStride,
|
SizeT sourceStride,
|
||||||
SizeT elementSize,
|
SizeT elementSize,
|
||||||
@@ -3091,23 +3058,11 @@ void main() {
|
|||||||
}
|
}
|
||||||
PipelineFactory::SetSuppressBlendedDepthWrite(suppressBlendedDepthWrite);
|
PipelineFactory::SetSuppressBlendedDepthWrite(suppressBlendedDepthWrite);
|
||||||
}
|
}
|
||||||
ProgramFactory::SubgroupLoweringPolicy subgroupPolicy{};
|
|
||||||
subgroupPolicy.emulateSubgroups = ShouldEmulateSubgroups(m_nativeSubgroupSupported);
|
|
||||||
subgroupPolicy.fixIterationRPSubgroupScratch =
|
|
||||||
m_nativeSubgroupSupported && ShouldFixIterationRPSubgroupScratch();
|
|
||||||
subgroupPolicy.fixIterationRPBarrier = ShouldFixIterationRPBarrier();
|
|
||||||
subgroupPolicy.deriveNumSubgroups =
|
|
||||||
m_nativeSubgroupSupported && ShouldDeriveNumSubgroups();
|
|
||||||
subgroupPolicy.requireFullSubgroups = m_computeFullSubgroupsFeatureEnabled;
|
|
||||||
subgroupPolicy.nativeSubgroupSize = m_nativeSubgroupSize;
|
|
||||||
subgroupPolicy.maxComputeWorkgroupSubgroups = m_maxComputeWorkgroupSubgroups;
|
|
||||||
subgroupPolicy.maxComputeSharedMemoryBytes =
|
|
||||||
m_physicalDevice.properties.limits.maxComputeSharedMemorySize;
|
|
||||||
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
|
m_programFactory = MakeUnique<ProgramFactory>(m_device, m_config, maxProgramBindings,
|
||||||
m_shaderDrawParametersFeatureEnabled,
|
m_shaderDrawParametersFeatureEnabled,
|
||||||
m_unformattedFloatStorageImagesEnabled,
|
m_unformattedFloatStorageImagesEnabled,
|
||||||
MG_Config::Features.EnableSpirvValidation,
|
MG_Config::Features.EnableSpirvValidation,
|
||||||
m_updateAfterBindLimits, subgroupPolicy);
|
m_updateAfterBindLimits);
|
||||||
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
|
||||||
// The swapchain already exists at this point (Initialize creates it first), so seed the
|
// The swapchain already exists at this point (Initialize creates it first), so seed the
|
||||||
// height the factory could not be told about from CreateSwapchain.
|
// height the factory could not be told about from CreateSwapchain.
|
||||||
@@ -3356,11 +3311,6 @@ void main() {
|
|||||||
indexView.indexByteSize > bufferSize - indexView.indexByteOffset) {
|
indexView.indexByteSize > bufferSize - indexView.indexByteOffset) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Recorded-but-unexecuted GPU writes (XFB capture, SSBO, storage texel
|
|
||||||
// buffer) land in the coherent mapping this scan is about to read;
|
|
||||||
// submit-and-wait first, exactly like the restart-index rewrite does.
|
|
||||||
// A no-op unless the gpu-write flag is set.
|
|
||||||
indexBufferShared->SyncGpuWrites();
|
|
||||||
indexBufferShared->SyncPersistentMappedRange();
|
indexBufferShared->SyncPersistentMappedRange();
|
||||||
indexBytes = indexBufferShared->MappedData() + indexView.indexByteOffset;
|
indexBytes = indexBufferShared->MappedData() + indexView.indexByteOffset;
|
||||||
} else {
|
} else {
|
||||||
@@ -3598,16 +3548,6 @@ void main() {
|
|||||||
const Uint8* sourceData, SizeT sourceStride,
|
const Uint8* sourceData, SizeT sourceStride,
|
||||||
SizeT elementSize, SizeT elementCount,
|
SizeT elementSize, SizeT elementCount,
|
||||||
BufferSlice& outSlice) -> Bool {
|
BufferSlice& outSlice) -> Bool {
|
||||||
// A resolved stride of 0 is the binding model's "never advance" (see the
|
|
||||||
// factory's layout notes): exactly one element is converted and every vertex
|
|
||||||
// reads it. That single element is read at offset 0, so the stride is never
|
|
||||||
// actually used - but both converters reject 0 as a degenerate input, which
|
|
||||||
// made the documented single-element conversion unreachable and silently
|
|
||||||
// dropped every draw using such a binding. Substitute the element's own
|
|
||||||
// size; the caller's cache key still carries the distinct stride 0.
|
|
||||||
if (sourceStride == 0 && elementCount == 1) {
|
|
||||||
sourceStride = elementSize;
|
|
||||||
}
|
|
||||||
const void* uploadData = nullptr;
|
const void* uploadData = nullptr;
|
||||||
VkDeviceSize uploadSize = 0;
|
VkDeviceSize uploadSize = 0;
|
||||||
switch (conversion) {
|
switch (conversion) {
|
||||||
@@ -3627,14 +3567,6 @@ void main() {
|
|||||||
uploadData = m_vertexConversionScratch.data();
|
uploadData = m_vertexConversionScratch.data();
|
||||||
uploadSize = static_cast<VkDeviceSize>(m_vertexConversionScratch.size() * sizeof(Float));
|
uploadSize = static_cast<VkDeviceSize>(m_vertexConversionScratch.size() * sizeof(Float));
|
||||||
break;
|
break;
|
||||||
case VertexInputStateFactory::VertexStreamConversion::Float64ToFloat32:
|
|
||||||
if (!ConvertFloat64VertexStreamToFloat32(attribute, sourceData, sourceStride, elementCount,
|
|
||||||
m_vertexConversionScratch)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
uploadData = m_vertexConversionScratch.data();
|
|
||||||
uploadSize = static_cast<VkDeviceSize>(m_vertexConversionScratch.size() * sizeof(Float));
|
|
||||||
break;
|
|
||||||
case VertexInputStateFactory::VertexStreamConversion::None:
|
case VertexInputStateFactory::VertexStreamConversion::None:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3749,12 +3681,6 @@ void main() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A GPU-written source (XFB capture, SSBO, storage texel buffer) has its
|
|
||||||
// bytes produced by commands that are merely RECORDED at this point, and
|
|
||||||
// MappedData() aliases the coherent GPU memory they will write into -
|
|
||||||
// converting now would read pre-write garbage. Submit-and-wait first,
|
|
||||||
// mirroring the restart-index rewrite; a flag-test no-op otherwise.
|
|
||||||
sourceBufferShared->SyncGpuWrites();
|
|
||||||
sourceBufferShared->SyncPersistentMappedRange();
|
sourceBufferShared->SyncPersistentMappedRange();
|
||||||
const SizeT availableElementCount =
|
const SizeT availableElementCount =
|
||||||
sourceStride == 0 ? 1 : 1 + (sourceSize - baseOffset - elementSize) / sourceStride;
|
sourceStride == 0 ? 1 : 1 + (sourceSize - baseOffset - elementSize) / sourceStride;
|
||||||
@@ -4048,7 +3974,7 @@ void main() {
|
|||||||
// Skips the per-draw GetBackendResource chase into a cold resource object.
|
// Skips the per-draw GetBackendResource chase into a cold resource object.
|
||||||
Bool sliceStillValid = false;
|
Bool sliceStillValid = false;
|
||||||
const Uint64 frameSerial = m_bufferManager.GetFrameSerial();
|
const Uint64 frameSerial = m_bufferManager.GetFrameSerial();
|
||||||
if (indexMemo->indexFrameSerial == frameSerial && !indexMemo->indexBufferMapped &&
|
if (indexMemo->indexFrameSerial == frameSerial &&
|
||||||
indexMemo->indexSliceEpochCounter == m_bufferManager.GetSliceEpochCounter()) {
|
indexMemo->indexSliceEpochCounter == m_bufferManager.GetSliceEpochCounter()) {
|
||||||
sliceStillValid = true;
|
sliceStillValid = true;
|
||||||
}
|
}
|
||||||
@@ -4111,9 +4037,6 @@ void main() {
|
|||||||
indexMemo->indexVkBuffer = slice.buffer;
|
indexMemo->indexVkBuffer = slice.buffer;
|
||||||
indexMemo->indexSliceOffset = slice.offset;
|
indexMemo->indexSliceOffset = slice.offset;
|
||||||
indexMemo->indexFrameSerial = m_bufferManager.GetFrameSerial();
|
indexMemo->indexFrameSerial = m_bufferManager.GetFrameSerial();
|
||||||
// A host-mapped EBO can mutate its shadow with no epoch bump; the hit
|
|
||||||
// path declines on this flag (mirror of anyBufferMapped).
|
|
||||||
indexMemo->indexBufferMapped = indexBufferShared->IsMapped();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const VkDeviceSize indexBindOffset =
|
const VkDeviceSize indexBindOffset =
|
||||||
@@ -4767,11 +4690,10 @@ void main() {
|
|||||||
// link-time properties, so this is safe to fold into a pipeline keyed on the program hash.
|
// link-time properties, so this is safe to fold into a pipeline keyed on the program hash.
|
||||||
static Bool ProgramCapturesXfbFromGeometryStage(const MG_State::GLState::ProgramObject& program) {
|
static Bool ProgramCapturesXfbFromGeometryStage(const MG_State::GLState::ProgramObject& program) {
|
||||||
if (program.GetTransformFeedbackVaryingCount() == 0) return false;
|
if (program.GetTransformFeedbackVaryingCount() == 0) return false;
|
||||||
// Both halves are link-time properties, so both are asked of the LAST LINK. Reading the
|
for (const auto& shader : program.GetAttachedShaders()) {
|
||||||
// live attach list would let a glAttachShader that has not been linked in yet - which GL
|
if (shader && shader->GetShaderStage() == ShaderStage::Geometry) return true;
|
||||||
// 4.6 core 7.3 says changes nothing about what the program runs - flip a property this
|
}
|
||||||
// pipeline is cached under, for an executable with no geometry stage in it.
|
return false;
|
||||||
return program.HasLinkedShaderStage(ShaderStage::Geometry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VkPipeline VulkanRenderer::GetOrCreatePipeline(
|
VkPipeline VulkanRenderer::GetOrCreatePipeline(
|
||||||
@@ -5490,81 +5412,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Bool VulkanRenderer::PrepareSamplerImageFeedbackSnapshots(
|
|
||||||
FrameContext::FrameData& frame,
|
|
||||||
const MG_State::GLState::ProgramObject& program,
|
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
|
||||||
VkPipelineStageFlags consumerShaderStageMask) {
|
|
||||||
auto& feedbackBindings = m_samplerImageFeedbackScratch;
|
|
||||||
auto& overrides = m_samplerImageBindingOverridesScratch;
|
|
||||||
overrides.clear();
|
|
||||||
if (!programObj.hasStorageImages) {
|
|
||||||
feedbackBindings.clear();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!m_uniformManager->CollectSamplerImageFeedback(program, programObj, feedbackBindings)) {
|
|
||||||
MGLOG_E_ONCE("%s: failed to collect sampler/image feedback for program=%u", __func__,
|
|
||||||
program.GetExternalIndex());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (feedbackBindings.empty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Copy and layout barriers cannot be recorded inside a render pass. A graphics draw only
|
|
||||||
// gets here after an actual sampled/writable-image mip overlap was found, so ordinary
|
|
||||||
// graphics draws retain the active pass.
|
|
||||||
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
|
|
||||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SnapshotCacheEntry {
|
|
||||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
|
||||||
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
|
|
||||||
VkTextureManager::SampledTextureSnapshot snapshot{};
|
|
||||||
};
|
|
||||||
Vector<SnapshotCacheEntry> snapshotCache;
|
|
||||||
snapshotCache.reserve(feedbackBindings.size());
|
|
||||||
overrides.reserve(feedbackBindings.size());
|
|
||||||
for (const auto& feedback : feedbackBindings) {
|
|
||||||
VkTextureManager::SampledTextureSnapshot snapshot{};
|
|
||||||
const auto existing = std::find_if(
|
|
||||||
snapshotCache.begin(), snapshotCache.end(), [&feedback](const SnapshotCacheEntry& candidate) {
|
|
||||||
return candidate.texture == feedback.texture && candidate.numericDomain == feedback.numericDomain;
|
|
||||||
});
|
|
||||||
if (existing != snapshotCache.end()) {
|
|
||||||
snapshot = existing->snapshot;
|
|
||||||
} else {
|
|
||||||
if (!m_textureManager->SnapshotTextureForSampling(frame.commandBuffer, *feedback.texture,
|
|
||||||
feedback.numericDomain, consumerShaderStageMask,
|
|
||||||
snapshot) ||
|
|
||||||
snapshot.imageView == VK_NULL_HANDLE) {
|
|
||||||
MGLOG_E_ONCE("%s: failed to snapshot textureId=%d for sampler binding=%u element=%u", __func__,
|
|
||||||
feedback.texture != nullptr ? feedback.texture->GetExternalIndex() : 0,
|
|
||||||
feedback.samplerBinding, feedback.samplerElement);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
snapshotCache.push_back({.texture = feedback.texture,
|
|
||||||
.numericDomain = feedback.numericDomain,
|
|
||||||
.snapshot = snapshot});
|
|
||||||
}
|
|
||||||
overrides.push_back({
|
|
||||||
.binding = feedback.samplerBinding,
|
|
||||||
.element = feedback.samplerElement,
|
|
||||||
.texture = feedback.texture,
|
|
||||||
.sampler = feedback.sampler,
|
|
||||||
.imageView = snapshot.imageView,
|
|
||||||
.imageLayout = snapshot.layout,
|
|
||||||
.forceNearestFiltering = feedback.numericDomain == SamplerNumericDomain::SignedInteger ||
|
|
||||||
feedback.numericDomain == SamplerNumericDomain::UnsignedInteger,
|
|
||||||
});
|
|
||||||
if (program.GetExternalIndex() == 194 && feedback.texture->GetExternalIndex() == 75) {
|
|
||||||
MGLOG_D_ONCE("sampler/image feedback snapshot: program=194 texture=75 binding=%u element=%u view=%p",
|
|
||||||
feedback.samplerBinding, feedback.samplerElement, snapshot.imageView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The scissor rectangle Vulkan needs for ARB_viewport_array index `index`. Vulkan has no
|
// The scissor rectangle Vulkan needs for ARB_viewport_array index `index`. Vulkan has no
|
||||||
// per-viewport scissor-test TOGGLE - a scissor rectangle always applies - so an index whose
|
// per-viewport scissor-test TOGGLE - a scissor rectangle always applies - so an index whose
|
||||||
@@ -5784,22 +5632,6 @@ void main() {
|
|||||||
if (program.GetBackendStateVersion() != snap.programVersion) {
|
if (program.GetBackendStateVersion() != snap.programVersion) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// glBegin/EndTransformFeedback moves no key this fast path otherwise observes
|
|
||||||
// (the design makes capture a compile-option FLAG precisely because no version
|
|
||||||
// bumps, VulkanRenderer.h's pipeline-memo note) - but the snapshot bakes that
|
|
||||||
// flag into resolvedTransformFlags and the pipeline. Recompute the one dynamic
|
|
||||||
// bit (the full path's exact predicate) and decline on a mismatch, or the first
|
|
||||||
// captured draw after glBeginTransformFeedback would bind the undecorated
|
|
||||||
// variant and silently capture nothing while the CPU bookkeeping advances.
|
|
||||||
const Bool wantsXfbCapture = m_transformFeedbackFeatureEnabled &&
|
|
||||||
MG_State::pGLContext->IsTransformFeedbackActive() &&
|
|
||||||
program.GetTransformFeedbackVaryingCount() > 0;
|
|
||||||
const Bool snapHasXfbCapture =
|
|
||||||
static_cast<Bool>(ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags) &
|
|
||||||
ProgramFactory::CompileOptionBit::XfbCapture);
|
|
||||||
if (wantsXfbCapture != snapHasXfbCapture) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// A changed VAO does NOT decline: the VAO only feeds the pipeline's vertex
|
// A changed VAO does NOT decline: the VAO only feeds the pipeline's vertex
|
||||||
// input state (re-resolved below through the layout-keyed memo, so N VAOs
|
// input state (re-resolved below through the layout-keyed memo, so N VAOs
|
||||||
// sharing one attribute layout share one pipeline) and the vertex/index
|
// sharing one attribute layout share one pipeline) and the vertex/index
|
||||||
@@ -5813,7 +5645,6 @@ void main() {
|
|||||||
const auto& drawFbo =
|
const auto& drawFbo =
|
||||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||||
if (static_cast<const void*>(drawFbo.get()) != snap.drawFbo ||
|
if (static_cast<const void*>(drawFbo.get()) != snap.drawFbo ||
|
||||||
drawFbo->GetLifetimeId() != snap.drawFboLifetimeId ||
|
|
||||||
drawFbo->GetObjectVersion() != snap.fboVersion) {
|
drawFbo->GetObjectVersion() != snap.fboVersion) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -5997,14 +5828,8 @@ void main() {
|
|||||||
}
|
}
|
||||||
const Uint64 samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
const Uint64 samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||||
if (samplingResolutionGeneration != snap.samplingResolutionGeneration) {
|
if (samplingResolutionGeneration != snap.samplingResolutionGeneration) {
|
||||||
// Decline, not re-arm: snap.resolvedTransformFlags bakes the
|
snap.samplingResolutionGeneration = samplingResolutionGeneration;
|
||||||
// ExplicitLod0Sampling verdict, which reads the effective sampler's
|
samplerDescriptorsUnchanged = false;
|
||||||
// filters/aniso/LOD range - exactly the state this counter tracks.
|
|
||||||
// Re-arming the stamp here would rebuild the descriptors but keep the
|
|
||||||
// stale SPIR-V variant forever (every later draw compares equal again).
|
|
||||||
// Same shape as the erase-epoch declines above; costs one full-path draw
|
|
||||||
// per sampler/shape change, and the full path's LOD memo re-probes.
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything the full path would re-resolve is provably unchanged - or, for
|
// Everything the full path would re-resolve is provably unchanged - or, for
|
||||||
@@ -6184,18 +6009,11 @@ void main() {
|
|||||||
const Uint64 lodProgramLifetimeId = program.GetLifetimeId();
|
const Uint64 lodProgramLifetimeId = program.GetLifetimeId();
|
||||||
const Uint32 lodProgramVersion = program.GetBackendStateVersion();
|
const Uint32 lodProgramVersion = program.GetBackendStateVersion();
|
||||||
const Uint64 lodBindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
const Uint64 lodBindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||||
// The probe also reads the EFFECTIVE sampler's filters/aniso/LOD range
|
|
||||||
// (ProgramSamplesOnlySingleLevelTextures), and those setters bump ONLY the
|
|
||||||
// sampling-resolution generation - not the texture params version the sum
|
|
||||||
// below covers. Without this key a filter/aniso change would keep serving
|
|
||||||
// the stale verdict.
|
|
||||||
const Uint64 lodSamplingGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
|
||||||
Bool lodMemoHit = false;
|
Bool lodMemoHit = false;
|
||||||
if (m_lastLodDecisionValid && m_lastSampledSetValid &&
|
if (m_lastLodDecisionValid && m_lastSampledSetValid &&
|
||||||
m_lastLodProgramLifetimeId == lodProgramLifetimeId &&
|
m_lastLodProgramLifetimeId == lodProgramLifetimeId &&
|
||||||
m_lastLodProgramVersion == lodProgramVersion &&
|
m_lastLodProgramVersion == lodProgramVersion &&
|
||||||
m_lastLodBindGeneration == lodBindGeneration &&
|
m_lastLodBindGeneration == lodBindGeneration && m_lastLodBaseFlags == transformFlags &&
|
||||||
m_lastLodSamplingGeneration == lodSamplingGeneration && m_lastLodBaseFlags == transformFlags &&
|
|
||||||
m_lastSampledSetProgramLifetimeId == lodProgramLifetimeId &&
|
m_lastSampledSetProgramLifetimeId == lodProgramLifetimeId &&
|
||||||
m_lastSampledSetProgramVersion == lodProgramVersion &&
|
m_lastSampledSetProgramVersion == lodProgramVersion &&
|
||||||
m_lastSampledSetBindGeneration == lodBindGeneration) {
|
m_lastSampledSetBindGeneration == lodBindGeneration) {
|
||||||
@@ -6220,7 +6038,6 @@ void main() {
|
|||||||
m_lastLodProgramLifetimeId = lodProgramLifetimeId;
|
m_lastLodProgramLifetimeId = lodProgramLifetimeId;
|
||||||
m_lastLodProgramVersion = lodProgramVersion;
|
m_lastLodProgramVersion = lodProgramVersion;
|
||||||
m_lastLodBindGeneration = lodBindGeneration;
|
m_lastLodBindGeneration = lodBindGeneration;
|
||||||
m_lastLodSamplingGeneration = lodSamplingGeneration;
|
|
||||||
m_lastLodBaseFlags = baseFlags;
|
m_lastLodBaseFlags = baseFlags;
|
||||||
m_lastLodResultFlags = transformFlags;
|
m_lastLodResultFlags = transformFlags;
|
||||||
m_lastLodParamsSum = 0; // filled below once the sampled set is known
|
m_lastLodParamsSum = 0; // filled below once the sampled set is known
|
||||||
@@ -6277,11 +6094,6 @@ void main() {
|
|||||||
MGLOG_E_ONCE("SetupDraw skipped: storage image preparation failed");
|
MGLOG_E_ONCE("SetupDraw skipped: storage image preparation failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj,
|
|
||||||
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT)) {
|
|
||||||
MGLOG_E_ONCE("SetupDraw skipped: sampler/image feedback snapshot failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||||
|
|
||||||
@@ -6526,9 +6338,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
|
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
|
||||||
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(),
|
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex());
|
||||||
VK_PIPELINE_BIND_POINT_GRAPHICS, nullptr, false,
|
|
||||||
m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch);
|
|
||||||
if (!boundUniforms) {
|
if (!boundUniforms) {
|
||||||
MGLOG_E_ONCE("SetupDraw skipped: BindProgramUniformBuffers failed");
|
MGLOG_E_ONCE("SetupDraw skipped: BindProgramUniformBuffers failed");
|
||||||
return false;
|
return false;
|
||||||
@@ -6564,7 +6374,6 @@ void main() {
|
|||||||
snap.vaoLifetimeId = vao.GetLifetimeId();
|
snap.vaoLifetimeId = vao.GetLifetimeId();
|
||||||
snap.vaoConfigVersion = vao.GetConfigVersion();
|
snap.vaoConfigVersion = vao.GetConfigVersion();
|
||||||
snap.drawFbo = drawFbo.get();
|
snap.drawFbo = drawFbo.get();
|
||||||
snap.drawFboLifetimeId = drawFbo->GetLifetimeId();
|
|
||||||
snap.fboVersion = drawFbo->GetObjectVersion();
|
snap.fboVersion = drawFbo->GetObjectVersion();
|
||||||
snap.drawFboIsDefault = drawFboIsDefault;
|
snap.drawFboIsDefault = drawFboIsDefault;
|
||||||
snap.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin);
|
snap.viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin);
|
||||||
@@ -6652,11 +6461,6 @@ void main() {
|
|||||||
MGLOG_E_ONCE("DispatchCompute skipped: storage image preparation failed");
|
MGLOG_E_ONCE("DispatchCompute skipped: storage image preparation failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj,
|
|
||||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT)) {
|
|
||||||
MGLOG_E_ONCE("DispatchCompute skipped: sampler/image feedback snapshot failed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
|
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
|
||||||
if (pipeline == VK_NULL_HANDLE) {
|
if (pipeline == VK_NULL_HANDLE) {
|
||||||
@@ -6668,8 +6472,7 @@ void main() {
|
|||||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
||||||
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
|
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
|
||||||
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(),
|
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(),
|
||||||
VK_PIPELINE_BIND_POINT_COMPUTE, nullptr, false,
|
VK_PIPELINE_BIND_POINT_COMPUTE);
|
||||||
m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch);
|
|
||||||
if (!boundUniforms) {
|
if (!boundUniforms) {
|
||||||
MGLOG_E_ONCE("DispatchCompute skipped: BindProgramUniformBuffers failed");
|
MGLOG_E_ONCE("DispatchCompute skipped: BindProgramUniformBuffers failed");
|
||||||
return;
|
return;
|
||||||
@@ -6704,11 +6507,6 @@ void main() {
|
|||||||
MGLOG_E_ONCE("DispatchComputeIndirect skipped: storage image preparation failed");
|
MGLOG_E_ONCE("DispatchComputeIndirect skipped: storage image preparation failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj,
|
|
||||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT)) {
|
|
||||||
MGLOG_E_ONCE("DispatchComputeIndirect skipped: sampler/image feedback snapshot failed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
|
const VkPipeline pipeline = GetOrCreateComputePipeline(programObj);
|
||||||
if (pipeline == VK_NULL_HANDLE) {
|
if (pipeline == VK_NULL_HANDLE) {
|
||||||
@@ -6720,8 +6518,7 @@ void main() {
|
|||||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
||||||
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
|
const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers(
|
||||||
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(),
|
frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(),
|
||||||
VK_PIPELINE_BIND_POINT_COMPUTE, nullptr, false,
|
VK_PIPELINE_BIND_POINT_COMPUTE);
|
||||||
m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch);
|
|
||||||
if (!boundUniforms) {
|
if (!boundUniforms) {
|
||||||
MGLOG_E_ONCE("DispatchComputeIndirect skipped: BindProgramUniformBuffers failed");
|
MGLOG_E_ONCE("DispatchComputeIndirect skipped: BindProgramUniformBuffers failed");
|
||||||
return;
|
return;
|
||||||
@@ -8910,7 +8707,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.
|
||||||
@@ -8924,35 +8721,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:
|
||||||
@@ -8960,14 +8735,13 @@ 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::Texture1DArray:
|
|
||||||
case TextureTarget::Texture2DArray:
|
case TextureTarget::Texture2DArray:
|
||||||
case TextureTarget::Texture2DMultisampleArray:
|
case TextureTarget::Texture2DMultisampleArray:
|
||||||
case TextureTarget::TextureCubeMap:
|
case TextureTarget::TextureCubeMap:
|
||||||
@@ -8975,35 +8749,27 @@ 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.
|
||||||
//
|
outEndpoint.slicesAreDepth = false;
|
||||||
// GL_TEXTURE_1D_ARRAY belongs here too, and needs no remap: this backend STORES it
|
outEndpoint.baseSlice = baseSlice;
|
||||||
// as a VK_IMAGE_TYPE_1D image whose layers live in arrayLayers (ToVulkanLevelExtent
|
outEndpoint.availableSlices = resource.arrayLayers;
|
||||||
// moves the count across), and GL 4.6 core 18.3.2 ADDRESSES it as a stack of slices
|
|
||||||
// on z with an image height of 1 - so the frontend's y/height are already the 0/1
|
|
||||||
// Vulkan requires and the layer lands in baseArrayLayer either way.
|
|
||||||
outMapping.slicesAreDepth = false;
|
|
||||||
outMapping.baseSlice = baseSlice;
|
|
||||||
outMapping.availableSlices = image.arrayLayers;
|
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
// GL_TEXTURE_BUFFER has no image at all. Declined rather than mis-addressed.
|
// GL_TEXTURE_1D_ARRAY carries its layers on the Y axis (srcY/srcHeight), which
|
||||||
|
// would have to be remapped against a Vulkan extent that also has to stay height 1
|
||||||
|
// for a VK_IMAGE_TYPE_1D image; GL_TEXTURE_BUFFER has no image at all. Declined
|
||||||
|
// rather than mis-addressed.
|
||||||
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.
|
||||||
@@ -9020,9 +8786,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9035,42 +8801,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
|
||||||
@@ -9086,29 +8818,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,
|
||||||
@@ -9131,10 +8863,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);
|
||||||
@@ -9145,53 +8877,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;
|
||||||
@@ -9202,15 +8921,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__);
|
||||||
@@ -9222,15 +8941,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__);
|
||||||
@@ -9240,18 +8959,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 "
|
||||||
@@ -9262,8 +8981,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;
|
||||||
@@ -9271,14 +8990,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__);
|
||||||
@@ -9289,14 +9008,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__);
|
||||||
@@ -9858,7 +9577,7 @@ void main() {
|
|||||||
VkImageAspectFlags imageAspect, Uint32 mipLevel,
|
VkImageAspectFlags imageAspect, Uint32 mipLevel,
|
||||||
Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width,
|
Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width,
|
||||||
GLsizei height, GLenum format, GLenum type, void* pixels,
|
GLsizei height, GLenum format, GLenum type, void* pixels,
|
||||||
Bool defaultFramebufferOrientation, Uint32 sourceLayerCount) {
|
Bool defaultFramebufferOrientation) {
|
||||||
const Bool wantDepth = format != GL_STENCIL_INDEX;
|
const Bool wantDepth = format != GL_STENCIL_INDEX;
|
||||||
const Bool wantStencil = format != GL_DEPTH_COMPONENT;
|
const Bool wantStencil = format != GL_DEPTH_COMPONENT;
|
||||||
auto& frame = m_frameContext.GetCurrent();
|
auto& frame = m_frameContext.GetCurrent();
|
||||||
@@ -9937,10 +9656,6 @@ void main() {
|
|||||||
if (!mapped) return;
|
if (!mapped) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// See the header: a stack of one-row layers and a single multi-row layer copy out to the
|
|
||||||
// same tightly-packed bytes, so only the region's shape splits the two cases.
|
|
||||||
const Uint32 copyLayerCount = std::max<Uint32>(sourceLayerCount, 1u);
|
|
||||||
const Uint32 copyRowCount = copyLayerCount > 1u ? 1u : copyExtent.height;
|
|
||||||
VkBufferImageCopy regions[2]{};
|
VkBufferImageCopy regions[2]{};
|
||||||
Uint32 regionCount = 0;
|
Uint32 regionCount = 0;
|
||||||
if (wantDepth) {
|
if (wantDepth) {
|
||||||
@@ -9949,9 +9664,9 @@ void main() {
|
|||||||
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||||
region.imageSubresource.mipLevel = mipLevel;
|
region.imageSubresource.mipLevel = mipLevel;
|
||||||
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
||||||
region.imageSubresource.layerCount = copyLayerCount;
|
region.imageSubresource.layerCount = 1;
|
||||||
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
|
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
|
||||||
region.imageExtent = {copyExtent.width, copyRowCount, 1};
|
region.imageExtent = {copyExtent.width, copyExtent.height, 1};
|
||||||
}
|
}
|
||||||
if (wantStencil) {
|
if (wantStencil) {
|
||||||
auto& region = regions[regionCount++];
|
auto& region = regions[regionCount++];
|
||||||
@@ -9959,9 +9674,9 @@ void main() {
|
|||||||
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
|
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||||
region.imageSubresource.mipLevel = mipLevel;
|
region.imageSubresource.mipLevel = mipLevel;
|
||||||
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
||||||
region.imageSubresource.layerCount = copyLayerCount;
|
region.imageSubresource.layerCount = 1;
|
||||||
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
|
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
|
||||||
region.imageExtent = {copyExtent.width, copyRowCount, 1};
|
region.imageExtent = {copyExtent.width, copyExtent.height, 1};
|
||||||
}
|
}
|
||||||
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
|
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
|
||||||
regionCount, regions);
|
regionCount, regions);
|
||||||
@@ -10196,17 +9911,9 @@ void main() {
|
|||||||
? static_cast<Uint32>(textureUploadTarget) -
|
? static_cast<Uint32>(textureUploadTarget) -
|
||||||
static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX)
|
static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX)
|
||||||
: 0;
|
: 0;
|
||||||
// A 1D array's levelSize.y() is its LAYER count, and those layers are the rows
|
|
||||||
// GL wants back - but in Vulkan they are array layers of a one-row image, not
|
|
||||||
// rows of layer 0, so the read has to be told which of the two it is looking at.
|
|
||||||
const Uint32 sourceLayers =
|
|
||||||
textureObject->GetTarget() == TextureTarget::Texture1DArray
|
|
||||||
? static_cast<Uint32>(std::max<Int>(levelSize.y(), 1))
|
|
||||||
: 1u;
|
|
||||||
ReadDepthStencilImageToClient(resource->image, resource->format, &resource->layout, resource->aspect,
|
ReadDepthStencilImageToClient(resource->image, resource->format, &resource->layout, resource->aspect,
|
||||||
static_cast<Uint32>(level), arrayLayer, 0, 0, levelSize.x(),
|
static_cast<Uint32>(level), arrayLayer, 0, 0, levelSize.x(),
|
||||||
levelSize.y(), format, type, pixels,
|
levelSize.y(), format, type, pixels);
|
||||||
/*defaultFramebufferOrientation=*/false, sourceLayers);
|
|
||||||
} else {
|
} else {
|
||||||
MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: color query of a non-color texture");
|
MGLOG_E_ONCE("DirectVulkan::GetTexImage skipped: color query of a non-color texture");
|
||||||
}
|
}
|
||||||
@@ -10224,19 +9931,12 @@ void main() {
|
|||||||
// destination layout (GL 3.3 section 6.1.4).
|
// destination layout (GL 3.3 section 6.1.4).
|
||||||
const auto imageTextureTarget = textureObject->GetTarget();
|
const auto imageTextureTarget = textureObject->GetTarget();
|
||||||
const Bool is3dImage = imageTextureTarget == TextureTarget::Texture3D;
|
const Bool is3dImage = imageTextureTarget == TextureTarget::Texture3D;
|
||||||
const Bool is1dArrayImage = imageTextureTarget == TextureTarget::Texture1DArray;
|
const Bool isArrayImage = imageTextureTarget == TextureTarget::Texture1DArray ||
|
||||||
const Bool isArrayImage = is1dArrayImage ||
|
|
||||||
imageTextureTarget == TextureTarget::Texture2DArray ||
|
imageTextureTarget == TextureTarget::Texture2DArray ||
|
||||||
imageTextureTarget == TextureTarget::TextureCubeMapArray;
|
imageTextureTarget == TextureTarget::TextureCubeMapArray;
|
||||||
const GLsizei depthSlices = is3dImage ? std::max<GLsizei>(texelSize.z(), 1) : 1;
|
const GLsizei depthSlices = is3dImage ? std::max<GLsizei>(texelSize.z(), 1) : 1;
|
||||||
const GLsizei arrayLayers = isArrayImage ? static_cast<GLsizei>(resource->arrayLayers) : 1;
|
const GLsizei arrayLayers = isArrayImage ? static_cast<GLsizei>(resource->arrayLayers) : 1;
|
||||||
// A 1D array level comes back as ONE two-dimensional image whose rows are its layers
|
const GLsizei sliceCount = std::max<GLsizei>(depthSlices * arrayLayers, 1);
|
||||||
// (GL 4.6 core 8.11.4), so its layers are already counted by `height` above and must not
|
|
||||||
// multiply the slice count the way a 2D-array's or a cube-array's do. Vulkan still keeps
|
|
||||||
// them in arrayLayers on a one-row image, which is what the copy region below says - the
|
|
||||||
// two describe the same tightly-packed bytes.
|
|
||||||
const GLsizei sliceCount =
|
|
||||||
std::max<GLsizei>(depthSlices * (is1dArrayImage ? 1 : arrayLayers), 1);
|
|
||||||
if (bufSize >= 0) {
|
if (bufSize >= 0) {
|
||||||
const Int dstChannels = GetReadbackChannelCount(format);
|
const Int dstChannels = GetReadbackChannelCount(format);
|
||||||
if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) {
|
if ((type == GL_UNSIGNED_BYTE || type == GL_FLOAT) && dstChannels > 0) {
|
||||||
@@ -10289,8 +9989,7 @@ void main() {
|
|||||||
copyRegion.imageSubresource.mipLevel = static_cast<Uint32>(level);
|
copyRegion.imageSubresource.mipLevel = static_cast<Uint32>(level);
|
||||||
copyRegion.imageSubresource.baseArrayLayer = 0;
|
copyRegion.imageSubresource.baseArrayLayer = 0;
|
||||||
copyRegion.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers);
|
copyRegion.imageSubresource.layerCount = static_cast<Uint32>(arrayLayers);
|
||||||
copyRegion.imageExtent = {static_cast<Uint32>(width),
|
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height),
|
||||||
is1dArrayImage ? 1u : static_cast<Uint32>(height),
|
|
||||||
static_cast<Uint32>(depthSlices)};
|
static_cast<Uint32>(depthSlices)};
|
||||||
vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
vkCmdCopyImageToBuffer(frame.commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||||
readback.GetHandle(), 1, ©Region);
|
readback.GetHandle(), 1, ©Region);
|
||||||
@@ -12948,73 +12647,6 @@ void main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Native subgroup topology, and VK_EXT_subgroup_size_control's
|
|
||||||
// computeFullSubgroups feature. REQUIRE_FULL_SUBGROUPS on a compute stage is what
|
|
||||||
// turns the derived gl_NumSubgroups (DeriveNumSubgroupsPass) from
|
|
||||||
// encouraged-but-unspecified driver behaviour into a spec guarantee: with the bit
|
|
||||||
// set and local_size_x a multiple of the subgroup size, every subgroup launches
|
|
||||||
// full, so the subgroup count is exactly invocations / size ("Full Subgroups",
|
|
||||||
// VUID-VkPipelineShaderStageCreateInfo-flags-02759/-02785).
|
|
||||||
m_nativeSubgroupSize = 0;
|
|
||||||
m_nativeSubgroupSupported = false;
|
|
||||||
m_computeFullSubgroupsFeatureEnabled = false;
|
|
||||||
if (getPhysicalDeviceProperties2 != nullptr) {
|
|
||||||
VkPhysicalDeviceSubgroupProperties subgroupProperties{};
|
|
||||||
subgroupProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES;
|
|
||||||
VkPhysicalDeviceProperties2 subgroupPropertyQuery{};
|
|
||||||
subgroupPropertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
|
||||||
subgroupPropertyQuery.pNext = &subgroupProperties;
|
|
||||||
getPhysicalDeviceProperties2(m_physicalDevice.handle, &subgroupPropertyQuery);
|
|
||||||
// Mirrors the loader's HasUsableShaderSubgroupSupport gate, including the
|
|
||||||
// MOBILEGL_DISABLE_SUBGROUP escape hatch, so the module lowerings can never
|
|
||||||
// disagree with the advertised capabilities.
|
|
||||||
const Bool usableSubgroups =
|
|
||||||
subgroupProperties.subgroupSize > 0 &&
|
|
||||||
(subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 &&
|
|
||||||
(subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0;
|
|
||||||
if (usableSubgroups && !MG_Config::Features.DisableSubgroup) {
|
|
||||||
m_nativeSubgroupSize = subgroupProperties.subgroupSize;
|
|
||||||
m_nativeSubgroupSupported = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
VkPhysicalDeviceSubgroupSizeControlFeaturesEXT subgroupSizeControlFeatures{};
|
|
||||||
subgroupSizeControlFeatures.sType =
|
|
||||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES_EXT;
|
|
||||||
m_maxComputeWorkgroupSubgroups = 0;
|
|
||||||
if (m_nativeSubgroupSupported &&
|
|
||||||
IsExtensionSupported(availableExtensions, VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME) &&
|
|
||||||
getPhysicalDeviceFeatures2 != nullptr) {
|
|
||||||
VkPhysicalDeviceFeatures2 featureQuery{};
|
|
||||||
featureQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
|
||||||
featureQuery.pNext = &subgroupSizeControlFeatures;
|
|
||||||
getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery);
|
|
||||||
if (getPhysicalDeviceProperties2 != nullptr) {
|
|
||||||
VkPhysicalDeviceSubgroupSizeControlPropertiesEXT subgroupSizeControlProperties{};
|
|
||||||
subgroupSizeControlProperties.sType =
|
|
||||||
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES_EXT;
|
|
||||||
VkPhysicalDeviceProperties2 propertyQuery{};
|
|
||||||
propertyQuery.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
|
||||||
propertyQuery.pNext = &subgroupSizeControlProperties;
|
|
||||||
getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery);
|
|
||||||
m_maxComputeWorkgroupSubgroups =
|
|
||||||
subgroupSizeControlProperties.maxComputeWorkgroupSubgroups;
|
|
||||||
}
|
|
||||||
if (subgroupSizeControlFeatures.computeFullSubgroups == VK_TRUE) {
|
|
||||||
if (!IsExtensionAlreadyEnabled(enabledDeviceExtensions,
|
|
||||||
VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME)) {
|
|
||||||
enabledDeviceExtensions.push_back(VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME);
|
|
||||||
}
|
|
||||||
// Only the full-subgroups guarantee is wanted; required/varying subgroup
|
|
||||||
// sizes stay unrequested.
|
|
||||||
subgroupSizeControlFeatures.subgroupSizeControl = VK_FALSE;
|
|
||||||
subgroupSizeControlFeatures.pNext = const_cast<void*>(deviceCreateInfo.pNext);
|
|
||||||
deviceCreateInfo.pNext = &subgroupSizeControlFeatures;
|
|
||||||
m_computeFullSubgroupsFeatureEnabled = true;
|
|
||||||
MGLOG_I("Enabled optional device extension: %s (computeFullSubgroups)",
|
|
||||||
VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// VK_EXT_transform_feedback backs GL transform feedback capture.
|
// VK_EXT_transform_feedback backs GL transform feedback capture.
|
||||||
m_transformFeedbackFeatureEnabled = false;
|
m_transformFeedbackFeatureEnabled = false;
|
||||||
VkPhysicalDeviceTransformFeedbackFeaturesEXT transformFeedbackFeatures{};
|
VkPhysicalDeviceTransformFeedbackFeaturesEXT transformFeedbackFeatures{};
|
||||||
@@ -14021,15 +13653,6 @@ void main() {
|
|||||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||||
VK_VERIFY(vkCreateComputePipelines(m_device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline),
|
VK_VERIFY(vkCreateComputePipelines(m_device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline),
|
||||||
"GetOrCreateComputePipeline, vkCreateComputePipelines");
|
"GetOrCreateComputePipeline, vkCreateComputePipelines");
|
||||||
// A failed creation must never be memoized - same contract as
|
|
||||||
// PipelineFactory::GetOrCreatePipeline: caching the null would serve it back
|
|
||||||
// for the rest of the process and every dispatch of this program would be
|
|
||||||
// silently skipped. Retrying costs one failed vkCreateComputePipelines per
|
|
||||||
// dispatch, which is the correct price.
|
|
||||||
if (pipeline == VK_NULL_HANDLE) {
|
|
||||||
MGLOG_E("GetOrCreateComputePipeline: vkCreateComputePipelines failed; not caching the failure");
|
|
||||||
return VK_NULL_HANDLE;
|
|
||||||
}
|
|
||||||
m_computePipelines.emplace(programObj.hash, pipeline);
|
m_computePipelines.emplace(programObj.hash, pipeline);
|
||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -217,15 +216,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// depth/stencil image, which this renderer stores display-side-up: the copy rect then
|
// depth/stencil image, which this renderer stores display-side-up: the copy rect then
|
||||||
// has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on
|
// has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on
|
||||||
// the way back, exactly as the colour ReadPixels path does.
|
// the way back, exactly as the colour ReadPixels path does.
|
||||||
// `sourceLayerCount` above 1 says the `height` rows the client is owed are stored as that
|
|
||||||
// many ARRAY LAYERS of a one-row image rather than as rows of one layer - the shape a GL
|
|
||||||
// 1D array has in Vulkan. The two produce byte-identical tightly-packed readbacks, so
|
|
||||||
// only the copy region differs; everything after it is written against `height`.
|
|
||||||
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
|
||||||
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
|
||||||
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||||
void* pixels, Bool defaultFramebufferOrientation = false,
|
void* pixels, Bool defaultFramebufferOrientation = false);
|
||||||
Uint32 sourceLayerCount = 1);
|
|
||||||
// Same-extent depth blit between images of different depth formats: host
|
// Same-extent depth blit between images of different depth formats: host
|
||||||
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
|
||||||
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
|
||||||
@@ -560,16 +554,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool m_samplerAnisotropyFeatureEnabled = false;
|
Bool m_samplerAnisotropyFeatureEnabled = false;
|
||||||
Bool m_shaderDrawParametersExtensionEnabled = false;
|
Bool m_shaderDrawParametersExtensionEnabled = false;
|
||||||
Bool m_shaderDrawParametersFeatureEnabled = false;
|
Bool m_shaderDrawParametersFeatureEnabled = false;
|
||||||
// Native subgroup topology, queried at device creation for the compute-module
|
|
||||||
// subgroup repairs (SubgroupSupportPolicy.h) and the REQUIRE_FULL_SUBGROUPS
|
|
||||||
// stage flag; 0 / false when the device has no usable compute subgroups or
|
|
||||||
// MOBILEGL_DISABLE_SUBGROUP forced them off.
|
|
||||||
Uint32 m_nativeSubgroupSize = 0;
|
|
||||||
Bool m_nativeSubgroupSupported = false;
|
|
||||||
Bool m_computeFullSubgroupsFeatureEnabled = false;
|
|
||||||
// VkPhysicalDeviceSubgroupSizeControlProperties::maxComputeWorkgroupSubgroups;
|
|
||||||
// 0 when the extension (and therefore the full-subgroups flag) is unavailable.
|
|
||||||
Uint32 m_maxComputeWorkgroupSubgroups = 0;
|
|
||||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||||
// Set only after descriptor-indexing feature AND property queries prove that
|
// Set only after descriptor-indexing feature AND property queries prove that
|
||||||
// update-after-bind is legal for every descriptor category this renderer emits.
|
// update-after-bind is legal for every descriptor category this renderer emits.
|
||||||
@@ -813,10 +797,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 m_lastLodProgramVersion = 0;
|
Uint32 m_lastLodProgramVersion = 0;
|
||||||
Uint64 m_lastLodBindGeneration = 0;
|
Uint64 m_lastLodBindGeneration = 0;
|
||||||
Uint64 m_lastLodParamsSum = 0;
|
Uint64 m_lastLodParamsSum = 0;
|
||||||
// Sampling-resolution generation at probe time. The probe reads the effective
|
|
||||||
// sampler's filters/aniso/LOD range, whose setters bump only this counter -
|
|
||||||
// the params-version sum above never moves for them.
|
|
||||||
Uint64 m_lastLodSamplingGeneration = 0;
|
|
||||||
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
|
||||||
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
|
||||||
|
|
||||||
@@ -854,11 +834,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 vaoLifetimeId = 0;
|
Uint64 vaoLifetimeId = 0;
|
||||||
Uint32 vaoConfigVersion = 0;
|
Uint32 vaoConfigVersion = 0;
|
||||||
const void* drawFbo = nullptr;
|
const void* drawFbo = nullptr;
|
||||||
// Never-reused lifetime id beside the raw pointer + Uint16 version: a
|
|
||||||
// deleted FBO recycled at the same address with the same fresh version
|
|
||||||
// count would otherwise compare equal (same ABA as the render-pass
|
|
||||||
// manager's fast-path memo).
|
|
||||||
Uint64 drawFboLifetimeId = 0;
|
|
||||||
Uint16 fboVersion = 0;
|
Uint16 fboVersion = 0;
|
||||||
Bool drawFboIsDefault = false;
|
Bool drawFboIsDefault = false;
|
||||||
Uint renderStateVersion = 0;
|
Uint renderStateVersion = 0;
|
||||||
@@ -954,8 +929,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// already sampleable.
|
// already sampleable.
|
||||||
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
|
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
|
||||||
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
||||||
Vector<UniformManager::SamplerImageFeedbackBinding> m_samplerImageFeedbackScratch;
|
|
||||||
Vector<UniformManager::SamplerBindingOverride> m_samplerImageBindingOverridesScratch;
|
|
||||||
Vector<VkBuffer> m_vertexBuffersScratch;
|
Vector<VkBuffer> m_vertexBuffersScratch;
|
||||||
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
||||||
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
|
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
|
||||||
@@ -1082,14 +1055,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkBuffer indexVkBuffer = VK_NULL_HANDLE;
|
VkBuffer indexVkBuffer = VK_NULL_HANDLE;
|
||||||
VkDeviceSize indexSliceOffset = 0;
|
VkDeviceSize indexSliceOffset = 0;
|
||||||
Uint64 indexFrameSerial = 0;
|
Uint64 indexFrameSerial = 0;
|
||||||
// The EBO carried a host map when the slice was recorded - the mirror of
|
|
||||||
// anyBufferMapped on the vertex half. A shadow-backed (non-adopted)
|
|
||||||
// persistent map mutates its shadow with no API call and no epoch bump, so
|
|
||||||
// the one-compare rescue must decline and re-run the acquire, whose
|
|
||||||
// SyncPersistentMappedRange is the push-down. A map taken AFTER the record
|
|
||||||
// is already covered: AcquirePersistentMap bumps the slice epoch for the
|
|
||||||
// request itself, adopted or declined.
|
|
||||||
Bool indexBufferMapped = false;
|
|
||||||
|
|
||||||
// Bound per draw (first bindingCount elements).
|
// Bound per draw (first bindingCount elements).
|
||||||
VkBuffer vkBuffers[kMaxBindings] = {};
|
VkBuffer vkBuffers[kMaxBindings] = {};
|
||||||
@@ -1183,14 +1148,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
FrameContext::FrameData& frame,
|
FrameContext::FrameData& frame,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj);
|
const ProgramFactory::VkProgramObject& programObj);
|
||||||
// Vulkan forbids a sampled descriptor and writable storage descriptor from naming the
|
|
||||||
// same image subresource in one shader operation. Snapshot only the sampler side; the
|
|
||||||
// storage descriptor continues to name the application texture.
|
|
||||||
Bool PrepareSamplerImageFeedbackSnapshots(
|
|
||||||
FrameContext::FrameData& frame,
|
|
||||||
const MG_State::GLState::ProgramObject& program,
|
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
|
||||||
VkPipelineStageFlags consumerShaderStageMask);
|
|
||||||
|
|
||||||
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
||||||
// bias, line width, stencil), gated behind one render-state-parameters-version
|
// bias, line width, stencil), gated behind one render-state-parameters-version
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/SubgroupSupportPolicy.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 <Config.h>
|
|
||||||
#include <Includes.h>
|
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
|
||||||
// The single decision point for how DirectVulkan implements GL_KHR_shader_subgroup,
|
|
||||||
// shared by capability advertisement (BackendObject) and module lowering
|
|
||||||
// (VulkanRenderer / ProgramFactory) so the two can never disagree.
|
|
||||||
//
|
|
||||||
// Native subgroups are the implementation whenever the device has them, whatever
|
|
||||||
// their width - subgroup operations execute on the hardware paths they were made
|
|
||||||
// for. Module-level repairs keep the GL contract intact around them:
|
|
||||||
// - FixIterationRPSubgroupScratchPass patches the one known pack bug: iterationRP's
|
|
||||||
// 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
|
|
||||||
// (gl_NumSubgroups) with the value the rest of the topology implies.
|
|
||||||
// The 32-lane shared-memory emulation (EmulateSubgroupsPass) is a LAST RESORT for
|
|
||||||
// devices with no subgroup support at all, and only when the user opts in with
|
|
||||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP=1; it never replaces available native operations.
|
|
||||||
|
|
||||||
inline constexpr Uint32 kEmulatedSubgroupSize = 32u;
|
|
||||||
inline constexpr Uint32 kEmulatedSubgroupStages = GL_COMPUTE_SHADER_BIT;
|
|
||||||
inline constexpr Uint32 kEmulatedSubgroupFeatures =
|
|
||||||
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_VOTE_BIT_KHR |
|
|
||||||
GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR | GL_SUBGROUP_FEATURE_BALLOT_BIT_KHR |
|
|
||||||
GL_SUBGROUP_FEATURE_SHUFFLE_BIT_KHR | GL_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT_KHR |
|
|
||||||
GL_SUBGROUP_FEATURE_CLUSTERED_BIT_KHR | GL_SUBGROUP_FEATURE_QUAD_BIT_KHR;
|
|
||||||
|
|
||||||
inline Bool ShouldEmulateSubgroups(const Bool nativeSubgroupSupported) {
|
|
||||||
return MG_Config::Features.MagmaEmulateSubgroup && !nativeSubgroupSupported &&
|
|
||||||
!MG_Config::Features.DisableSubgroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Bool ShouldFixIterationRPSubgroupScratch() {
|
|
||||||
// Auto is ON: the patch is fingerprint-gated to iterationRP's reduction and
|
|
||||||
// grows one under-declared array; every other module passes through untouched.
|
|
||||||
return MG_Config::Features.FixIterationRPSubgroupScratch !=
|
|
||||||
MG_Config::QuirkOverride::ForceOff;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Bool ShouldFixIterationRPBarrier() {
|
|
||||||
return MG_Config::Features.IterationRPFixBarrier;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Bool ShouldDeriveNumSubgroups() {
|
|
||||||
// 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
|
|
||||||
// with REQUIRE_FULL_SUBGROUPS - the driver builtin is the value with no
|
|
||||||
// cross-driver guarantee (Adreno returns 1 for an 8-subgroup dispatch).
|
|
||||||
return MG_Config::Features.DeriveNumSubgroups != MG_Config::QuirkOverride::ForceOff;
|
|
||||||
}
|
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
|
||||||
@@ -10,6 +10,9 @@
|
|||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||||
#include <MG_Util/Converters/MGToStr/GLExtensionConverter.h>
|
#include <MG_Util/Converters/MGToStr/GLExtensionConverter.h>
|
||||||
|
#if defined(MOBILEGL_ENABLE_DILIGENT)
|
||||||
|
#include <MG_Backend/Diligent/BackendObject_Diligent.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend {
|
namespace MobileGL::MG_Backend {
|
||||||
void LogBackendInfo() {
|
void LogBackendInfo() {
|
||||||
@@ -55,6 +58,11 @@ namespace MobileGL::MG_Backend {
|
|||||||
case BackendType::DirectVulkan:
|
case BackendType::DirectVulkan:
|
||||||
pActiveBackendObject = MakeUnique<DirectVulkan::BackendObject_DirectVulkan>();
|
pActiveBackendObject = MakeUnique<DirectVulkan::BackendObject_DirectVulkan>();
|
||||||
break;
|
break;
|
||||||
|
#if defined(MOBILEGL_ENABLE_DILIGENT)
|
||||||
|
case BackendType::DiligentVulkan:
|
||||||
|
pActiveBackendObject = MakeUnique<DiligentBackend::BackendObject_Diligent>();
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
case BackendType::Unknown:
|
case BackendType::Unknown:
|
||||||
default:
|
default:
|
||||||
MGLOG_W("Unknown backend type, defaulting to unknown backend");
|
MGLOG_W("Unknown backend type, defaulting to unknown backend");
|
||||||
|
|||||||
@@ -44,5 +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(ShaderCache)
|
|
||||||
add_subdirectory(Transpile)
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
|
||||||
|
|
||||||
add_executable(
|
|
||||||
TranslationCacheBench
|
|
||||||
TranslationCacheBench.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(TranslationCacheBench PRIVATE
|
|
||||||
${MGL_ROOT}/include
|
|
||||||
${MGL_ROOT}/MobileGL
|
|
||||||
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(
|
|
||||||
TranslationCacheBench PRIVATE
|
|
||||||
benchmark::benchmark
|
|
||||||
${LINK_LIBRARIES}
|
|
||||||
)
|
|
||||||
|
|
||||||
add_test(NAME TranslationCacheBench COMMAND TranslationCacheBench --benchmark_counters_tabular=true)
|
|
||||||
set_tests_properties(TranslationCacheBench PROPERTIES LABELS benchmark)
|
|
||||||
@@ -1,457 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_Benchmark/ShaderCache/TranslationCacheBench.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-level shader translation memo is worth, measured on the workload that
|
|
||||||
// motivated it: the KHR-GL33.texture_swizzle.smoke_* shape, where one case builds 2592
|
|
||||||
// programs out of a handful of distinct sources.
|
|
||||||
//
|
|
||||||
// Four pairs of cases, each Off/On:
|
|
||||||
//
|
|
||||||
// ProgramLink - the whole glCompileShader + glLinkProgram path for one program, with
|
|
||||||
// FRESH SHADER OBJECTS every iteration. This is the CTS shape exactly,
|
|
||||||
// and it is the headline case now. It used to be the PESSIMISTIC one:
|
|
||||||
// a hit still paid for both glslang parses, because the parse happens
|
|
||||||
// at glCompileShader - a different entry point from the one L1
|
|
||||||
// memoizes - and fresh shader objects meant ShaderCompileAdoptionMap
|
|
||||||
// could not hand the earlier parse over either. L1c is what closed
|
|
||||||
// that: the compile half of the memo recognises each stage's source
|
|
||||||
// and publishes its verdict without parsing, so on a hit this case now
|
|
||||||
// constructs no glslang object at all.
|
|
||||||
//
|
|
||||||
// SharedShaderLink - the same program population with the shader objects KEPT ALIVE, so
|
|
||||||
// the parses happen once outside the measured loop whatever the cache
|
|
||||||
// does. That makes it the CONTROL for L1c rather than a target: its
|
|
||||||
// numbers should not move, and if they do, L1c has added cost to a
|
|
||||||
// path it was supposed to leave alone.
|
|
||||||
//
|
|
||||||
// DeferredParseLink - the shape where L1c could LOSE: a constant vertex source (which
|
|
||||||
// hits L1c and therefore skips its parse) against a fresh fragment
|
|
||||||
// source every iteration (which makes the PROGRAM key miss, so the
|
|
||||||
// skipped parse has to happen inside the link after all). Same parse
|
|
||||||
// count either way, so the pair should land within noise; see its own
|
|
||||||
// header below.
|
|
||||||
//
|
|
||||||
// EsslTranspile - the DirectGLES backend segment: the SPIR-V pass chain plus
|
|
||||||
// SPIRV-Cross. Runs the driver-INDEPENDENT half of the real chain (the
|
|
||||||
// passes SyncToBackend runs unconditionally, plus the two stage-gated
|
|
||||||
// ones a fragment module reaches) so the miss path costs what
|
|
||||||
// production costs; the capability-gated passes need a live ES driver
|
|
||||||
// and are not reachable from a benchmark process.
|
|
||||||
//
|
|
||||||
// Every On case runs with a warm cache: the first iteration misses and every one after it
|
|
||||||
// hits, which is exactly the steady state of a 2592-program smoke case.
|
|
||||||
|
|
||||||
#include <benchmark/benchmark.h>
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include "Config.h"
|
|
||||||
#include "Includes.h"
|
|
||||||
#include "Init.h"
|
|
||||||
#include "MG_Impl/GLImpl/Program/GL_Program.h"
|
|
||||||
#include "MG_State/GLState/Core.h"
|
|
||||||
#include "MG_State/GLState/ProgramState/ProgramTranslationCache.h"
|
|
||||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
|
||||||
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
|
||||||
#include "MG_Util/ShaderTranspiler/TranslationCache.h"
|
|
||||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
|
||||||
|
|
||||||
using namespace MobileGL;
|
|
||||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
const char* kVertexSource = R"(#version 460
|
|
||||||
layout(location = 0) in vec3 aPos;
|
|
||||||
out vec3 vPos;
|
|
||||||
out vec2 vUv;
|
|
||||||
void main() {
|
|
||||||
vPos = aPos;
|
|
||||||
vUv = aPos.xy * 0.5 + 0.5;
|
|
||||||
gl_Position = vec4(aPos, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// Shaped after gl3cTextureSwizzleTests.cpp's template: a sampler of one type, one
|
|
||||||
// TEXTURE_ACCESS, one CHANNEL, and an output whose BASIC_TYPE is the only thing that
|
|
||||||
// varies within a case. Padded with enough real arithmetic that the translation chain
|
|
||||||
// is doing work rather than measuring fixed overheads.
|
|
||||||
// `padLines` = 0 is the honest CTS size: gl3cTextureSwizzleTests' smoke template is a
|
|
||||||
// handful of lines, and that is the workload the memo exists for. The padded variant is
|
|
||||||
// kept alongside it because a shaderpack stage is orders of magnitude bigger, and the
|
|
||||||
// two bracket the ratio the cache is worth in practice.
|
|
||||||
String SwizzleLikeFragment(const String& prefix, const int padLines) {
|
|
||||||
String source = "#version 460\n";
|
|
||||||
source += "in vec3 vPos;\n";
|
|
||||||
source += "in vec2 vUv;\n";
|
|
||||||
source += "layout(location = 0) out " + prefix + "vec4 fragColor;\n";
|
|
||||||
source += "uniform sampler2D uTex;\n";
|
|
||||||
source += "uniform vec4 uTint;\n";
|
|
||||||
source += "uniform mat4 uModel;\n";
|
|
||||||
source += "uniform float uArr[8];\n";
|
|
||||||
source += "void main() {\n";
|
|
||||||
source += " vec4 s = texture(uTex, vUv);\n";
|
|
||||||
source += " float acc = s.r;\n";
|
|
||||||
for (int i = 0; i < padLines; ++i) {
|
|
||||||
source += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0) * cos(acc);\n";
|
|
||||||
}
|
|
||||||
source += " for (int i = 0; i < 8; ++i) acc += uArr[i];\n";
|
|
||||||
source += " vec4 p = uModel * vec4(vPos, 1.0);\n";
|
|
||||||
source += " fragColor = " + prefix + "vec4((s + uTint) * acc + p);\n";
|
|
||||||
source += "}\n";
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
class CacheModeScope {
|
|
||||||
public:
|
|
||||||
explicit CacheModeScope(const Bool enabled)
|
|
||||||
: m_saved(MG_Config::Features.ShaderTranslationCache) {
|
|
||||||
MG_Config::Features.ShaderTranslationCache =
|
|
||||||
enabled ? MG_Config::QuirkOverride::ForceOn : MG_Config::QuirkOverride::ForceOff;
|
|
||||||
}
|
|
||||||
~CacheModeScope() { MG_Config::Features.ShaderTranslationCache = m_saved; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
const MG_Config::QuirkOverride m_saved;
|
|
||||||
};
|
|
||||||
|
|
||||||
class SyncCompileScope {
|
|
||||||
public:
|
|
||||||
SyncCompileScope() : m_saved(MG_Config::Features.AsyncShaderCompile) {
|
|
||||||
MG_Config::Features.AsyncShaderCompile = MG_Config::QuirkOverride::ForceOff;
|
|
||||||
}
|
|
||||||
~SyncCompileScope() { MG_Config::Features.AsyncShaderCompile = m_saved; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
const MG_Config::QuirkOverride m_saved;
|
|
||||||
};
|
|
||||||
|
|
||||||
// One program, built the way the CTS builds one: fresh shader objects every time.
|
|
||||||
void LinkOneProgram(const String& vertexSource, const String& fragmentSource) {
|
|
||||||
using namespace MG_Impl::GLImpl;
|
|
||||||
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
|
|
||||||
const char* vsText = vertexSource.c_str();
|
|
||||||
ShaderSource(vs, 1, &vsText, nullptr);
|
|
||||||
CompileShader(vs);
|
|
||||||
|
|
||||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
|
||||||
const char* fsText = fragmentSource.c_str();
|
|
||||||
ShaderSource(fs, 1, &fsText, nullptr);
|
|
||||||
CompileShader(fs);
|
|
||||||
|
|
||||||
const GLuint program = CreateProgram();
|
|
||||||
AttachShader(program, vs);
|
|
||||||
AttachShader(program, fs);
|
|
||||||
LinkProgram(program);
|
|
||||||
benchmark::DoNotOptimize(program);
|
|
||||||
|
|
||||||
DeleteProgram(program);
|
|
||||||
DeleteShader(vs);
|
|
||||||
DeleteShader(fs);
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector<Uint32> BuildSanitizedFragmentSpirv(const String& fragmentSource) {
|
|
||||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fragmentSource};
|
|
||||||
auto shader = ShaderCompiler::CompileShader(attrib);
|
|
||||||
if (!shader) return {};
|
|
||||||
ProgramAttrib programAttrib{.shaders = {shader.value()}};
|
|
||||||
auto program = ShaderCompiler::LinkProgram(programAttrib);
|
|
||||||
if (!program) return {};
|
|
||||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *program.value()};
|
|
||||||
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
|
||||||
if (!binary || binary->empty()) return {};
|
|
||||||
Vector<Uint32> sanitized;
|
|
||||||
if (!ShaderCompiler::SanitizeAndOptimizeBinary(binary->front(), sanitized)) return {};
|
|
||||||
return sanitized;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The driver-independent part of BackendProgramObjectImpl::TranspileSpirvToEssl, in the
|
|
||||||
// same order. What is missing is only the capability-gated passes (viewport lowering,
|
|
||||||
// multisample clamping, noperspective emulation, the image-format bake), which cannot
|
|
||||||
// fire without a live ES driver to arm them.
|
|
||||||
Bool TranspileLikeDirectGles(const Vector<Uint32>& spirv, const Uint esslVersion, String& outEssl) {
|
|
||||||
Vector<Uint32> a;
|
|
||||||
const Vector<Uint32>* effective = &spirv;
|
|
||||||
if (ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(*effective, a, false) && !a.empty()) {
|
|
||||||
effective = &a;
|
|
||||||
}
|
|
||||||
Vector<Uint32> b;
|
|
||||||
if (ShaderCompiler::LowerRectImages(*effective, b, false) && !b.empty()) effective = &b;
|
|
||||||
Vector<Uint32> c;
|
|
||||||
if (ShaderCompiler::Lower1DArrayImagesForEssl(*effective, c, false) && !c.empty()) effective = &c;
|
|
||||||
Vector<Uint32> d;
|
|
||||||
if (ShaderCompiler::LegalizeFragmentOutputIndexingForEssl(*effective, d, false) && !d.empty()) {
|
|
||||||
effective = &d;
|
|
||||||
}
|
|
||||||
|
|
||||||
SpvcSession session(*effective, SessionUsageBit::Transpile);
|
|
||||||
spvc_compiler_options options;
|
|
||||||
if (session.CreateOptions(&options) != SPVC_SUCCESS) return false;
|
|
||||||
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, esslVersion);
|
|
||||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
|
||||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
|
||||||
session.SetOptions(options);
|
|
||||||
const char* result = nullptr;
|
|
||||||
session.Compile(&result);
|
|
||||||
if (!result) return false;
|
|
||||||
outEssl = result;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
EsslTranslationKeyInputs EsslInputsFor(const Vector<Uint32>& spirv) {
|
|
||||||
EsslTranslationKeyInputs inputs;
|
|
||||||
inputs.spirv = &spirv;
|
|
||||||
inputs.shaderType = GL_FRAGMENT_SHADER;
|
|
||||||
inputs.maxColorTextureSamples = 4;
|
|
||||||
inputs.maxIntegerSamples = 1;
|
|
||||||
inputs.maxDepthTextureSamples = 4;
|
|
||||||
inputs.advertisedMaxSamples = 4;
|
|
||||||
inputs.esslVersion = 320;
|
|
||||||
return inputs;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
// L1, in situ: the full glCompileShader + glLinkProgram path for a repeated program.
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
// Arg(0) = the CTS smoke size; Arg(120) = a heavy stage, bracketing the ratio.
|
|
||||||
static void BM_ProgramLink_CacheOff(benchmark::State& state) {
|
|
||||||
MobileGL::Initialize();
|
|
||||||
const SyncCompileScope sync;
|
|
||||||
const CacheModeScope cache(false);
|
|
||||||
const String vs = kVertexSource;
|
|
||||||
const String fs = SwizzleLikeFragment("", static_cast<int>(state.range(0)));
|
|
||||||
for (auto _ : state) {
|
|
||||||
LinkOneProgram(vs, fs);
|
|
||||||
}
|
|
||||||
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
|
|
||||||
}
|
|
||||||
BENCHMARK(BM_ProgramLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
|
||||||
|
|
||||||
static void BM_ProgramLink_CacheOn(benchmark::State& state) {
|
|
||||||
MobileGL::Initialize();
|
|
||||||
const SyncCompileScope sync;
|
|
||||||
const CacheModeScope cache(true);
|
|
||||||
const String vs = kVertexSource;
|
|
||||||
const String fs = SwizzleLikeFragment("", static_cast<int>(state.range(0)));
|
|
||||||
LinkOneProgram(vs, fs); // prime, so the measured loop is the steady state
|
|
||||||
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
|
|
||||||
const TranslationCacheStats parseBefore = GetShaderParseVerdictCache().Stats();
|
|
||||||
for (auto _ : state) {
|
|
||||||
LinkOneProgram(vs, fs);
|
|
||||||
}
|
|
||||||
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
|
|
||||||
const TranslationCacheStats parseStats = GetShaderParseVerdictCache().Stats();
|
|
||||||
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
|
|
||||||
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
|
|
||||||
// Two stages per iteration, so a clean run shows L1c_hits == 2 * iterations and zero
|
|
||||||
// misses: every glCompileShader in the loop skipped its parse.
|
|
||||||
state.counters["L1c_hits"] = static_cast<double>(parseStats.hits - parseBefore.hits);
|
|
||||||
state.counters["L1c_misses"] = static_cast<double>(parseStats.misses - parseBefore.misses);
|
|
||||||
}
|
|
||||||
BENCHMARK(BM_ProgramLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
// L1, the shape the memo actually exists for: MANY PROGRAMS OUT OF THE SAME SHADERS.
|
|
||||||
//
|
|
||||||
// The pair above deletes its shader objects every iteration, which forces a fresh glslang
|
|
||||||
// parse per iteration no matter what the link does - glCompileShader parses, and that is a
|
|
||||||
// DIFFERENT entry point from the one L1 memoizes. It is a real workload (what an application
|
|
||||||
// that never reuses a shader object pays) but it is the pessimistic one, and the residual it
|
|
||||||
// leaves is the parse, not the link.
|
|
||||||
//
|
|
||||||
// This pair keeps the shader objects alive, so the parses happen once before the measured
|
|
||||||
// loop and the L1 hit then skips the link, mapIO, the SPIR-V, the reflection and the routing
|
|
||||||
// outright.
|
|
||||||
//
|
|
||||||
// SINCE L1c THIS IS THE CONTROL, NOT THE TARGET. Nothing inside the measured loop calls
|
|
||||||
// glCompileShader, so L1c cannot fire here at all - which is exactly what makes the pair
|
|
||||||
// useful: it is the shape that says whether the compile-side memo has slowed the LINK path
|
|
||||||
// down. Its numbers should be indistinguishable from the pre-L1c ones.
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
namespace {
|
|
||||||
struct SharedShaders {
|
|
||||||
GLuint vs = 0;
|
|
||||||
GLuint fs = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
SharedShaders MakeSharedShaders(const String& vertexSource, const String& fragmentSource) {
|
|
||||||
using namespace MG_Impl::GLImpl;
|
|
||||||
SharedShaders shaders;
|
|
||||||
shaders.vs = CreateShader(GL_VERTEX_SHADER);
|
|
||||||
const char* vsText = vertexSource.c_str();
|
|
||||||
ShaderSource(shaders.vs, 1, &vsText, nullptr);
|
|
||||||
CompileShader(shaders.vs);
|
|
||||||
shaders.fs = CreateShader(GL_FRAGMENT_SHADER);
|
|
||||||
const char* fsText = fragmentSource.c_str();
|
|
||||||
ShaderSource(shaders.fs, 1, &fsText, nullptr);
|
|
||||||
CompileShader(shaders.fs);
|
|
||||||
return shaders;
|
|
||||||
}
|
|
||||||
|
|
||||||
void LinkFromSharedShaders(const SharedShaders& shaders) {
|
|
||||||
using namespace MG_Impl::GLImpl;
|
|
||||||
const GLuint program = CreateProgram();
|
|
||||||
AttachShader(program, shaders.vs);
|
|
||||||
AttachShader(program, shaders.fs);
|
|
||||||
LinkProgram(program);
|
|
||||||
benchmark::DoNotOptimize(program);
|
|
||||||
DeleteProgram(program);
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
static void BM_SharedShaderLink_CacheOff(benchmark::State& state) {
|
|
||||||
MobileGL::Initialize();
|
|
||||||
const SyncCompileScope sync;
|
|
||||||
const CacheModeScope cache(false);
|
|
||||||
const SharedShaders shaders =
|
|
||||||
MakeSharedShaders(kVertexSource, SwizzleLikeFragment("", static_cast<int>(state.range(0))));
|
|
||||||
for (auto _ : state) {
|
|
||||||
LinkFromSharedShaders(shaders);
|
|
||||||
}
|
|
||||||
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
|
|
||||||
}
|
|
||||||
BENCHMARK(BM_SharedShaderLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
|
||||||
|
|
||||||
static void BM_SharedShaderLink_CacheOn(benchmark::State& state) {
|
|
||||||
MobileGL::Initialize();
|
|
||||||
const SyncCompileScope sync;
|
|
||||||
const CacheModeScope cache(true);
|
|
||||||
const SharedShaders shaders =
|
|
||||||
MakeSharedShaders(kVertexSource, SwizzleLikeFragment("", static_cast<int>(state.range(0))));
|
|
||||||
LinkFromSharedShaders(shaders); // prime, so the measured loop is the steady state
|
|
||||||
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
|
|
||||||
for (auto _ : state) {
|
|
||||||
LinkFromSharedShaders(shaders);
|
|
||||||
}
|
|
||||||
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
|
|
||||||
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
|
|
||||||
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
|
|
||||||
}
|
|
||||||
BENCHMARK(BM_SharedShaderLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
// L2, component: the DirectGLES SPIR-V pass chain plus SPIRV-Cross for one stage.
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
static void BM_EsslTranspile_CacheOff(benchmark::State& state) {
|
|
||||||
MobileGL::Initialize();
|
|
||||||
const Vector<Uint32> spirv =
|
|
||||||
BuildSanitizedFragmentSpirv(SwizzleLikeFragment("", static_cast<int>(state.range(0))));
|
|
||||||
if (spirv.empty()) {
|
|
||||||
state.SkipWithError("could not build the fragment module");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String essl;
|
|
||||||
for (auto _ : state) {
|
|
||||||
if (!TranspileLikeDirectGles(spirv, 320, essl)) {
|
|
||||||
state.SkipWithError("transpile failed");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
benchmark::DoNotOptimize(essl.data());
|
|
||||||
}
|
|
||||||
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
|
|
||||||
}
|
|
||||||
BENCHMARK(BM_EsslTranspile_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
|
||||||
|
|
||||||
static void BM_EsslTranspile_CacheOn(benchmark::State& state) {
|
|
||||||
MobileGL::Initialize();
|
|
||||||
const Vector<Uint32> spirv =
|
|
||||||
BuildSanitizedFragmentSpirv(SwizzleLikeFragment("", static_cast<int>(state.range(0))));
|
|
||||||
if (spirv.empty()) {
|
|
||||||
state.SkipWithError("could not build the fragment module");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BoundedTranslationCache<EsslTranslationResult> cache("bench L2", 64, 8u << 20);
|
|
||||||
const EsslTranslationKeyInputs inputs = EsslInputsFor(spirv);
|
|
||||||
for (auto _ : state) {
|
|
||||||
const TranslationCacheKey key = BuildEsslTranslationKey(inputs);
|
|
||||||
EsslTranslationResultPtr hit = cache.Find(key);
|
|
||||||
if (!hit) {
|
|
||||||
auto payload = MakeShared<EsslTranslationResult>();
|
|
||||||
if (!TranspileLikeDirectGles(spirv, inputs.esslVersion, payload->essl)) {
|
|
||||||
state.SkipWithError("transpile failed");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
cache.Insert(key, EsslTranslationResultPtr(payload), EsslTranslationResultBytes(*payload));
|
|
||||||
hit = payload;
|
|
||||||
}
|
|
||||||
benchmark::DoNotOptimize(hit->essl.data());
|
|
||||||
}
|
|
||||||
const TranslationCacheStats stats = cache.Stats();
|
|
||||||
state.counters["L2_hits"] = static_cast<double>(stats.hits);
|
|
||||||
state.counters["L2_misses"] = static_cast<double>(stats.misses);
|
|
||||||
}
|
|
||||||
BENCHMARK(BM_EsslTranspile_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
// L1c, the shape where it could LOSE rather than win: the DEFERRED PARSE.
|
|
||||||
// ---------------------------------------------------------------------------------------
|
|
||||||
// A stage whose compile hits L1c holds no AST, so if the program-level key then MISSES, the
|
|
||||||
// parse it skipped has to happen anyway - inside the link, via ClaimParsedShader. The parse
|
|
||||||
// is moved, not removed, and this pair is what says whether moving it costs anything.
|
|
||||||
//
|
|
||||||
// The shape forces exactly that, every iteration: one CONSTANT vertex source (hits L1c after
|
|
||||||
// the first iteration) linked against a FRESH fragment source each time (misses L1c, and
|
|
||||||
// makes the program key miss too). So:
|
|
||||||
//
|
|
||||||
// cache off - two parses at glCompileShader, then the link.
|
|
||||||
// cache on - one parse at glCompileShader (the fragment), one deferred parse inside the
|
|
||||||
// link (the vertex), then the link.
|
|
||||||
//
|
|
||||||
// The parse count is identical, so these two should land within noise of each other. If the
|
|
||||||
// On arm is materially SLOWER, L1c is charging for something - the per-compile key build and
|
|
||||||
// hash over the full preprocessed source, or the loss of the claim-CAS reuse - and that cost
|
|
||||||
// shows up here and nowhere else.
|
|
||||||
//
|
|
||||||
// The distinct fragment sources also churn both front-end levels through their FIFO caps,
|
|
||||||
// which is the eviction behaviour a real shaderpack load produces; over a long run the
|
|
||||||
// constant vertex entry is occasionally evicted by that churn and re-inserted, so the L1c
|
|
||||||
// hit rate reported below is high but not exactly 1.0 per iteration.
|
|
||||||
namespace {
|
|
||||||
String UniqueFragmentSource(const Uint64 serial, const int padLines) {
|
|
||||||
return SwizzleLikeFragment("", padLines) +
|
|
||||||
"\n// unique-" + std::to_string(serial) + "\n";
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
static void BM_DeferredParseLink_CacheOff(benchmark::State& state) {
|
|
||||||
MobileGL::Initialize();
|
|
||||||
const SyncCompileScope sync;
|
|
||||||
const CacheModeScope cache(false);
|
|
||||||
const String vs = kVertexSource;
|
|
||||||
Uint64 serial = 0;
|
|
||||||
for (auto _ : state) {
|
|
||||||
LinkOneProgram(vs, UniqueFragmentSource(serial++, static_cast<int>(state.range(0))));
|
|
||||||
}
|
|
||||||
state.SetLabel("MOBILEGL_SHADER_CACHE=0");
|
|
||||||
}
|
|
||||||
BENCHMARK(BM_DeferredParseLink_CacheOff)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
|
||||||
|
|
||||||
static void BM_DeferredParseLink_CacheOn(benchmark::State& state) {
|
|
||||||
MobileGL::Initialize();
|
|
||||||
const SyncCompileScope sync;
|
|
||||||
const CacheModeScope cache(true);
|
|
||||||
const String vs = kVertexSource;
|
|
||||||
Uint64 serial = 0;
|
|
||||||
LinkOneProgram(vs, UniqueFragmentSource(~0ull, static_cast<int>(state.range(0)))); // prime the vertex entry
|
|
||||||
const TranslationCacheStats before = MG_State::GLState::GetProgramTranslationCache().Stats();
|
|
||||||
const TranslationCacheStats parseBefore = GetShaderParseVerdictCache().Stats();
|
|
||||||
for (auto _ : state) {
|
|
||||||
LinkOneProgram(vs, UniqueFragmentSource(serial++, static_cast<int>(state.range(0))));
|
|
||||||
}
|
|
||||||
const TranslationCacheStats stats = MG_State::GLState::GetProgramTranslationCache().Stats();
|
|
||||||
const TranslationCacheStats parseStats = GetShaderParseVerdictCache().Stats();
|
|
||||||
// Expected shape: L1 all misses (every program is new), L1c one hit (vertex) and one miss
|
|
||||||
// (fragment) per iteration.
|
|
||||||
state.counters["L1_hits"] = static_cast<double>(stats.hits - before.hits);
|
|
||||||
state.counters["L1_misses"] = static_cast<double>(stats.misses - before.misses);
|
|
||||||
state.counters["L1c_hits"] = static_cast<double>(parseStats.hits - parseBefore.hits);
|
|
||||||
state.counters["L1c_misses"] = static_cast<double>(parseStats.misses - parseBefore.misses);
|
|
||||||
}
|
|
||||||
BENCHMARK(BM_DeferredParseLink_CacheOn)->Arg(0)->Arg(120)->Unit(benchmark::kMicrosecond);
|
|
||||||
|
|
||||||
BENCHMARK_MAIN();
|
|
||||||
@@ -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
@@ -13,7 +13,6 @@
|
|||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToStr/BufferEnumConverter.h>
|
#include <MG_Util/Converters/MGToStr/BufferEnumConverter.h>
|
||||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
||||||
Bool ValidateBufferTarget(BufferTarget target) {
|
Bool ValidateBufferTarget(BufferTarget target) {
|
||||||
@@ -68,13 +67,6 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
|||||||
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
||||||
pointCount = std::min<SizeT>(pointCount, 4);
|
pointCount = std::min<SizeT>(pointCount, 4);
|
||||||
}
|
}
|
||||||
if (target == BufferTarget::AtomicCounter) {
|
|
||||||
// GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, which is NOT the state layer's array
|
|
||||||
// size: a counter buffer reaches a shader only as a lowered storage block, so the
|
|
||||||
// reserved range is the ceiling, and glGetIntegerv advertises the same number.
|
|
||||||
pointCount = std::min<SizeT>(
|
|
||||||
pointCount, static_cast<SizeT>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
|
|
||||||
}
|
|
||||||
return pointCount;
|
return pointCount;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -45,11 +45,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();
|
||||||
if (!ValidateProgramForExecution(currentProgram, functionName)) return false;
|
if (!ValidateProgramForExecution(currentProgram, functionName)) return false;
|
||||||
|
|
||||||
// Of the EXECUTABLE, not the live attach list: attaching a compute shader to an
|
if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
||||||
// already-linked graphics program does not give that program a compute stage to
|
|
||||||
// dispatch (GL 4.6 core 7.3), and letting the dispatch through on the strength of the
|
|
||||||
// attach hands the backend a program whose SPIR-V has no compute module in it.
|
|
||||||
if (!currentProgram->HasLinkedShaderStage(ShaderStage::Compute)) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
@@ -112,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->HasLinkedShaderStage(ShaderStage::Geometry)) {
|
|
||||||
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) {
|
||||||
@@ -137,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
|
||||||
@@ -166,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,58 +176,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
|
||||||
|
|
||||||
// GL 4.6 core 10.1: the tessellation pipeline's only input primitive is GL_PATCHES, and
|
|
||||||
// GL_PATCHES has no meaning without it. Both directions are INVALID_OPERATION, and
|
|
||||||
// neither was implemented - which is two of the four sites
|
|
||||||
// KHR-GL43.transform_feedback.api_errors_test checks with one shared message string.
|
|
||||||
// The EVALUATION stage is what decides: a control stage cannot run without one, and a
|
|
||||||
// program carrying only an evaluation stage still tessellates, through GL's
|
|
||||||
// fixed-function pass-through control stage (11.2.2).
|
|
||||||
// Asked of the LAST LINK, not the live attach list (GL 4.6 core 7.3): attaching a
|
|
||||||
// tessellation evaluation shader to an already-linked program does not put it in the
|
|
||||||
// executable, so reading the live list here would reject every non-GL_PATCHES draw
|
|
||||||
// against a program that does not tessellate - and keep rejecting them, since a detach
|
|
||||||
// is likewise deferred to the next link.
|
|
||||||
const Bool tessellationActive = currentProgram && currentProgram->HasLinkedShaderStage(ShaderStage::TessEval);
|
|
||||||
if (tessellationActive && mode != GL_PATCHES) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>(
|
|
||||||
"MG_Impl/GLImpl", functionName,
|
|
||||||
"A program with a tessellation evaluation shader can only be drawn with GL_PATCHES."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!tessellationActive && mode == GL_PATCHES) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
|
||||||
"GL_PATCHES requires an active tessellation evaluation shader."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A geometry stage only accepts the primitive types that decompose into its declared
|
// A geometry stage only accepts the primitive types that decompose into its declared
|
||||||
// input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES
|
// input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES
|
||||||
// is the tessellation pipeline's input and reaches the geometry stage already
|
// is the tessellation pipeline's input and reaches the geometry stage already
|
||||||
// converted, so it is not constrained here.
|
// converted, so it is not constrained here.
|
||||||
//
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||||
// "Is there a geometry stage at all" has to be asked of the STAGE, never of the input
|
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
|
||||||
// primitive: GL_NONE and GL_POINTS are both 0, so a `layout(points) in` geometry shader
|
if (gsInput != GL_NONE && mode != GL_PATCHES) {
|
||||||
// is indistinguishable from no geometry shader by its reflected input type alone. The
|
|
||||||
// sentinel test this replaces therefore skipped the whole rule for exactly the geometry
|
|
||||||
// shaders whose input is the most restrictive one - every mode but GL_POINTS was
|
|
||||||
// accepted (KHR-GL43.transform_feedback.api_errors_test draws a points-in geometry
|
|
||||||
// program with GL_LINES and requires INVALID_OPERATION).
|
|
||||||
//
|
|
||||||
// And it has to be asked of the LAST LINK: gsInputPrimitive is a link artifact, so
|
|
||||||
// pairing it with the live attach list would re-point the very same 0-aliasing rather
|
|
||||||
// than remove it. In the window after glAttachShader(GS) on a linked program the live
|
|
||||||
// list says "geometry present" while the artifact still reads GL_NONE == GL_POINTS, and
|
|
||||||
// the switch below would silently reject every mode but GL_POINTS.
|
|
||||||
const Bool geometryActive = currentProgram && currentProgram->HasLinkedShaderStage(ShaderStage::Geometry);
|
|
||||||
const GLenum gsInput = geometryActive ? currentProgram->GetGeometryInputType() : GL_NONE;
|
|
||||||
if (geometryActive && mode != GL_PATCHES) {
|
|
||||||
Bool compatible = false;
|
Bool compatible = false;
|
||||||
switch (gsInput) {
|
switch (gsInput) {
|
||||||
case GL_POINTS:
|
case GL_POINTS:
|
||||||
@@ -288,20 +216,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// While transform feedback is active the draw's primitive type must match
|
// While transform feedback is active the draw's primitive type must match
|
||||||
// the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader
|
// the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader
|
||||||
// the constraint moves to the shader's output primitive type instead, so
|
// the constraint moves to the shader's output primitive type instead, so
|
||||||
// the draw mode itself is unconstrained here - and a TESSELLATION EVALUATION
|
// the draw mode itself is unconstrained here. A paused span is exempt: it
|
||||||
// stage relocates it exactly the same way (GL 4.6 core 13.2.2 names both):
|
// captures nothing, so there is nothing for the mode to be incompatible with
|
||||||
// what is captured is the tessellator's output primitive, and the draw mode
|
// (GL 4.6 core 13.2.3).
|
||||||
// can only ever be GL_PATCHES. A paused span is exempt: it captures nothing,
|
|
||||||
// so there is nothing for the mode to be incompatible with (GL 4.6 core 13.2.3).
|
|
||||||
const auto& feedbackProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
|
|
||||||
// Both stage tests are asked of the last link, for the same reason as the two guards
|
|
||||||
// above: what relocates the constraint is a stage the program actually RUNS, and an
|
|
||||||
// attach that has not been linked in yet gives it none.
|
|
||||||
const Bool feedbackModeIsProgramDriven =
|
|
||||||
feedbackProgram && (feedbackProgram->HasLinkedShaderStage(ShaderStage::Geometry) ||
|
|
||||||
feedbackProgram->HasLinkedShaderStage(ShaderStage::TessEval));
|
|
||||||
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||||
!MG_State::pGLContext->IsTransformFeedbackPaused() && !feedbackModeIsProgramDriven) {
|
!MG_State::pGLContext->IsTransformFeedbackPaused() &&
|
||||||
|
!(MG_State::pGLContext->GetTransformFeedbackProgram() &&
|
||||||
|
MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) {
|
||||||
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
|
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
|
||||||
Bool compatible = false;
|
Bool compatible = false;
|
||||||
switch (feedbackMode) {
|
switch (feedbackMode) {
|
||||||
@@ -382,23 +303,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL 4.6 core 10.9: inside a conditional block whose predicate did not pass, the drawing
|
|
||||||
// commands, Clear, ClearBuffer* and the compute dispatches are DISCARDED. The gate sits on the
|
|
||||||
// wrappers that ISSUE the backend call rather than at the top of each entry point, so that
|
|
||||||
// everything a real driver would still do inside the block - argument validation and the
|
|
||||||
// errors it raises - happens exactly as it does outside one, and only the command itself is
|
|
||||||
// dropped. It is deliberately not on the frontend's transform-feedback accounting either:
|
|
||||||
// that mirrors what the capture stage would have written, and a conditional block around a
|
|
||||||
// capturing draw has no test coverage in either direction.
|
|
||||||
static Bool ConditionalRenderDiscardsCommand() {
|
|
||||||
return MG_State::pGLContext->ConditionalRenderDiscardsCommands();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Clear_Backend(GLbitfield mask) {
|
void Clear_Backend(GLbitfield mask) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
|
MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +314,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
|
MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +322,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,7 +330,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
|
||||||
basevertex);
|
basevertex);
|
||||||
}
|
}
|
||||||
@@ -433,7 +338,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
|
MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +345,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,7 +353,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,7 +361,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +368,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,7 +376,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
|
||||||
maxdrawcount, stride);
|
maxdrawcount, stride);
|
||||||
}
|
}
|
||||||
@@ -486,7 +385,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
|
MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
|
||||||
stride);
|
stride);
|
||||||
}
|
}
|
||||||
@@ -496,7 +394,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
|
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
|
||||||
basevertex);
|
basevertex);
|
||||||
}
|
}
|
||||||
@@ -506,7 +403,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
|
MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,7 +412,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
|
||||||
mode, count, type, indices, instancecount, basevertex, baseinstance);
|
mode, count, type, indices, instancecount, basevertex, baseinstance);
|
||||||
}
|
}
|
||||||
@@ -526,7 +421,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
|
||||||
basevertex);
|
basevertex);
|
||||||
}
|
}
|
||||||
@@ -536,7 +430,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
|
||||||
instancecount, baseinstance);
|
instancecount, baseinstance);
|
||||||
}
|
}
|
||||||
@@ -546,7 +439,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,7 +446,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
|
MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
|
||||||
}
|
}
|
||||||
void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
|
||||||
@@ -562,7 +453,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
|
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
|
||||||
baseinstance);
|
baseinstance);
|
||||||
}
|
}
|
||||||
@@ -571,7 +461,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
|
MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,7 +468,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
|
MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,9 +496,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// GL 4.3 added both dispatches to the conditional-render set (GL 4.6 core 10.9), which is
|
|
||||||
// exactly what KHR-GL43.compute_shader.conditional-dispatching checks.
|
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,7 +547,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||||
if (ConditionalRenderDiscardsCommand()) return;
|
|
||||||
dispatchComputeIndirect(indirect);
|
dispatchComputeIndirect(indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,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);
|
||||||
@@ -833,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);
|
||||||
@@ -848,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,
|
||||||
@@ -857,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);
|
||||||
@@ -865,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;
|
||||||
@@ -889,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;
|
||||||
@@ -911,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);
|
||||||
@@ -919,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);
|
||||||
@@ -927,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) {
|
||||||
@@ -941,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);
|
||||||
@@ -949,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);
|
||||||
@@ -960,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);
|
||||||
|
|||||||
@@ -725,8 +725,8 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, LoadName, GLuint name) DECLARE_GL_FUNCTION_S
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushName, GLuint name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushName, name)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PushName, GLuint name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PushName, name)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopName) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopName)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PopName) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PopName)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ClampColor, GLenum target, GLenum clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClampColor, target, clamp)
|
DECLARE_GL_FUNCTION_HEAD(void, ClampColor, GLenum target, GLenum clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClampColor, target, clamp)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginConditionalRender, id, mode)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginConditionalRender, GLuint id, GLenum mode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginConditionalRender, id, mode)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, EndConditionalRender) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndConditionalRender)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndConditionalRender, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndConditionalRender)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI1i, GLuint index, GLint x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI1i, index, x)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI1i, GLuint index, GLint x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI1i, index, x)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI2i, GLuint index, GLint x, GLint y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI2i, index, x, y)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI2i, GLuint index, GLint x, GLint y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI2i, index, x, y)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI3i, GLuint index, GLint x, GLint y, GLint z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI3i, index, x, y, z)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribI3i, GLuint index, GLint x, GLint y, GLint z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribI3i, index, x, y, z)
|
||||||
@@ -982,7 +982,7 @@ DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdoub
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysInstancedBaseInstance, GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysInstancedBaseInstance, mode, first, count, instancecount, baseinstance)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseInstance, mode, count, type, indices, instancecount, baseinstance)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertexBaseInstance, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertexBaseInstance, mode, count, type, indices, instancecount, basevertex, baseinstance)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveAtomicCounterBufferiv, GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveAtomicCounterBufferiv, program, bufferIndex, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackInstanced, mode, id, instancecount)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStreamInstanced, mode, id, stream, instancecount)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
DECLARE_GL_FUNCTION_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -2613,26 +2608,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
void ClearBufferfi_Backend(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||||
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
|
||||||
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
|
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfi(buffer, drawbuffer, depth, stencil);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
void ClearBufferfv_Backend(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
|
||||||
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
|
||||||
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value);
|
MG_Backend::gBackendFunctionsTable.GL.ClearBufferfv(buffer, drawbuffer, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
void ClearBufferuiv_Backend(GLenum buffer, GLint drawbuffer, const GLuint* value) {
|
||||||
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
|
||||||
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value);
|
MG_Backend::gBackendFunctionsTable.GL.ClearBufferuiv(buffer, drawbuffer, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) {
|
void ClearBufferiv_Backend(GLenum buffer, GLint drawbuffer, const GLint* value) {
|
||||||
// GL 4.6 core 10.9 makes ClearBuffer* conditional alongside the drawing commands.
|
|
||||||
if (MG_State::pGLContext->ConditionalRenderDiscardsCommands()) return;
|
|
||||||
MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value);
|
MG_Backend::gBackendFunctionsTable.GL.ClearBufferiv(buffer, drawbuffer, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3161,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
@@ -47,29 +46,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared with the glslang resource table for the same reason as the atomic-counter
|
constexpr GLint kFrontendMaxComputeUniformComponents = 1024;
|
||||||
// limits below: gl_MaxComputeUniformComponents expands from BuildTBuiltInResource.
|
constexpr GLint kFrontendMaxComputeAtomicCounters = 8;
|
||||||
constexpr GLint kFrontendMaxComputeUniformComponents =
|
constexpr GLint kFrontendMaxComputeAtomicCounterBuffers = 8;
|
||||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_COMPUTE_UNIFORM_COMPONENTS);
|
|
||||||
// Every atomic-counter limit is shared with the glslang resource table
|
|
||||||
// (BuildTBuiltInResource) through MG_Util/ShaderTranspiler/Types.h: GL 4.6 requires
|
|
||||||
// glGetIntegerv and the gl_MaxAtomicCounter* built-in constants to agree, and the two
|
|
||||||
// used to be independent tables that disagreed on both the binding count and the buffer
|
|
||||||
// size. Never move one of these without the other.
|
|
||||||
constexpr GLint kFrontendMaxComputeAtomicCounters =
|
|
||||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
|
|
||||||
constexpr GLint kFrontendMaxComputeAtomicCounterBuffers =
|
|
||||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
|
|
||||||
constexpr GLint kFrontendMaxComputeSharedMemorySize = 32768;
|
constexpr GLint kFrontendMaxComputeSharedMemorySize = 32768;
|
||||||
constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024;
|
constexpr GLint kFrontendMaxComputeWorkGroupInvocations = 1024;
|
||||||
constexpr GLint kFrontendMaxCombinedAtomicCounters =
|
constexpr GLint kFrontendMaxCombinedAtomicCounters = 8;
|
||||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
|
constexpr GLint kFrontendMaxFragmentAtomicCounters = 8;
|
||||||
constexpr GLint kFrontendMaxCombinedAtomicCounterBuffers =
|
|
||||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
|
|
||||||
constexpr GLint kFrontendMaxFragmentAtomicCounters =
|
|
||||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTERS_PER_STAGE);
|
|
||||||
constexpr GLint kFrontendMaxFragmentAtomicCounterBuffers =
|
|
||||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE);
|
|
||||||
constexpr GLint kFrontendMaxGeometryAtomicCounters = 0;
|
constexpr GLint kFrontendMaxGeometryAtomicCounters = 0;
|
||||||
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
|
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
|
||||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
|
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
|
||||||
@@ -83,11 +66,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
|
constexpr GLint kFrontendMaxTessControlAtomicCounterBuffers = 0;
|
||||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
|
constexpr GLint kFrontendMaxTessEvaluationAtomicCounterBuffers = 0;
|
||||||
constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
|
constexpr GLint kFrontendMaxVertexAtomicCounterBuffers = 0;
|
||||||
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: the byte offset ceiling a counter may be declared
|
// One atomic counter is a uint, and a buffer never has to hold more counters than the
|
||||||
// at. The matching binding count is applied in GetIndexedBufferQueryPointCount, so that
|
// combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes.
|
||||||
// the getter, the indexed queries and glBindBufferBase all share one ceiling.
|
|
||||||
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
|
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
|
||||||
static_cast<GLint>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_SIZE);
|
kFrontendMaxCombinedAtomicCounters * static_cast<GLint>(sizeof(GLuint));
|
||||||
// KHR_debug minima (GL 4.6 table 23.66); the debug entry points are stubs, but the
|
// KHR_debug minima (GL 4.6 table 23.66); the debug entry points are stubs, but the
|
||||||
// limits they advertise still have to be legal.
|
// limits they advertise still have to be legal.
|
||||||
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
|
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
|
||||||
@@ -121,16 +103,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
constexpr GLint kFrontendSubpixelBits = 4;
|
constexpr GLint kFrontendSubpixelBits = 4;
|
||||||
constexpr GLint kFrontendMaxSamples = 4;
|
constexpr GLint kFrontendMaxSamples = 4;
|
||||||
|
|
||||||
// The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile
|
|
||||||
// pipeline (CaptureCompileEnv floors the same driver answers at them, and
|
|
||||||
// BuildTBuiltInResource expands gl_MaxComputeWorkGroup* from the result), because a
|
|
||||||
// shader is allowed to compare the built-in constant against this query.
|
|
||||||
constexpr GLint GetMinComputeWorkGroupCount(GLuint index) {
|
constexpr GLint GetMinComputeWorkGroupCount(GLuint index) {
|
||||||
return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_COUNT[index]) : 0;
|
return index < 3 ? 65535 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr GLint GetMinComputeWorkGroupSize(GLuint index) {
|
constexpr GLint GetMinComputeWorkGroupSize(GLuint index) {
|
||||||
return index < 3 ? static_cast<GLint>(MG_Util::ShaderTranspiler::MIN_COMPUTE_WORK_GROUP_SIZE[index]) : 0;
|
return index < 2 ? 1024 : (index == 2 ? 64 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
|
GLint GetMaxCombinedUniformComponents(GLint maxDefaultUniformComponents, GLint maxUniformBlocks,
|
||||||
@@ -208,16 +186,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
||||||
return std::min(frontendCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
return std::min(frontendCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
||||||
}
|
}
|
||||||
if (bufferTarget == BufferTarget::AtomicCounter) {
|
|
||||||
// The counter family's binding count is NOT the state layer's array size: a
|
|
||||||
// counter buffer only reaches a shader as a lowered storage block, so what an
|
|
||||||
// implementation can serve is the reserved range, and that number is also what
|
|
||||||
// glslang compiles a layout(binding = N) atomic_uint against. Clamped here so
|
|
||||||
// GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, the indexed getters' index check and
|
|
||||||
// glBindBufferBase's all report the same ceiling.
|
|
||||||
return std::min(frontendCount,
|
|
||||||
static_cast<SizeT>(MG_Util::ShaderTranspiler::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS));
|
|
||||||
}
|
|
||||||
return frontendCount;
|
return frontendCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,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;
|
||||||
@@ -471,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;
|
||||||
@@ -1572,15 +1511,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_LINE_WIDTH:
|
case GL_LINE_WIDTH:
|
||||||
*params = static_cast<GLint>(MG_State::pGLContext->GetLineWidth());
|
*params = static_cast<GLint>(MG_State::pGLContext->GetLineWidth());
|
||||||
return;
|
return;
|
||||||
|
case GL_LAYER_PROVOKING_VERTEX:
|
||||||
|
*params = GL_LAST_VERTEX_CONVENTION;
|
||||||
|
return;
|
||||||
case GL_LOGIC_OP_MODE:
|
case GL_LOGIC_OP_MODE:
|
||||||
*params = static_cast<GLint>(MG_Util::ConvertLogicOperationToGLEnum(MG_State::pGLContext->GetLogicOp()));
|
*params = static_cast<GLint>(MG_Util::ConvertLogicOperationToGLEnum(MG_State::pGLContext->GetLogicOp()));
|
||||||
return;
|
return;
|
||||||
case GL_MAX_COMBINED_ATOMIC_COUNTERS:
|
case GL_MAX_COMBINED_ATOMIC_COUNTERS:
|
||||||
*params = kFrontendMaxCombinedAtomicCounters;
|
*params = kFrontendMaxCombinedAtomicCounters;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS:
|
|
||||||
*params = kFrontendMaxCombinedAtomicCounterBuffers;
|
|
||||||
return;
|
|
||||||
case GL_MAX_COMBINED_UNIFORM_BLOCKS:
|
case GL_MAX_COMBINED_UNIFORM_BLOCKS:
|
||||||
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
|
*params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks);
|
||||||
return;
|
return;
|
||||||
@@ -1596,11 +1535,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_MAX_FRAGMENT_ATOMIC_COUNTERS:
|
case GL_MAX_FRAGMENT_ATOMIC_COUNTERS:
|
||||||
*params = kFrontendMaxFragmentAtomicCounters;
|
*params = kFrontendMaxFragmentAtomicCounters;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS:
|
|
||||||
*params = kFrontendMaxFragmentAtomicCounterBuffers;
|
|
||||||
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;
|
||||||
@@ -1626,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;
|
||||||
@@ -1661,11 +1597,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample) ? GL_TRUE : GL_FALSE;
|
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::Multisample) ? GL_TRUE : GL_FALSE;
|
||||||
return;
|
return;
|
||||||
case GL_MIN_MAP_BUFFER_ALIGNMENT:
|
case GL_MIN_MAP_BUFFER_ALIGNMENT:
|
||||||
// The same constant the map paths align to (MG_State/GLState/BufferState/
|
*params = 64; // TODO
|
||||||
// PipeResource.h), never a literal: this number is a PROMISE about the pointers
|
|
||||||
// glMapBuffer and glMapBufferRange return, and the two used to be unrelated - the
|
|
||||||
// query said 64 while the pointers came out of a std::vector aligned to 16.
|
|
||||||
*params = static_cast<GLint>(MG_State::GLState::MIN_MAP_BUFFER_ALIGNMENT);
|
|
||||||
return;
|
return;
|
||||||
case GL_MAX_LABEL_LENGTH:
|
case GL_MAX_LABEL_LENGTH:
|
||||||
*params = 256; // TODO
|
*params = 256; // TODO
|
||||||
@@ -1701,18 +1633,16 @@ 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
|
||||||
return;
|
return;
|
||||||
case GL_MAX_UNIFORM_LOCATIONS:
|
case GL_MAX_UNIFORM_LOCATIONS:
|
||||||
// The same constant the link's location allocator enforces - see ProgramObject.
|
*params = 1024 * 4; // TODO
|
||||||
*params = MG_State::GLState::ProgramObject::MAX_UNIFORM_LOCATIONS;
|
|
||||||
return;
|
return;
|
||||||
case GL_MAX_VARYING_COMPONENTS:
|
case GL_MAX_VARYING_COMPONENTS:
|
||||||
*params = kFrontendMaxVaryingComponents;
|
*params = kFrontendMaxVaryingComponents;
|
||||||
@@ -1732,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;
|
||||||
@@ -2042,24 +1972,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_UNIFORM_BUFFER_START:
|
case GL_UNIFORM_BUFFER_START:
|
||||||
RecordIndexedOnlyGetterError(__func__, pname);
|
RecordIndexedOnlyGetterError(__func__, pname);
|
||||||
return;
|
return;
|
||||||
// glBindBufferBase/Range set the GENERIC binding point too (GL 4.6 core 6.1.1), and this
|
|
||||||
// is the one indexed-buffer family whose non-indexed query was never answered - so it
|
|
||||||
// fell through to INVALID_ENUM and left the caller's variable holding whatever was in its
|
|
||||||
// stack slot. _START/_SIZE stay indexed-only, exactly like their uniform-buffer siblings.
|
|
||||||
case GL_ATOMIC_COUNTER_BUFFER_BINDING:
|
|
||||||
if (const auto& obj =
|
|
||||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::AtomicCounter).GetBoundObject()) {
|
|
||||||
*params = static_cast<GLint>(obj->GetExternalIndex());
|
|
||||||
} else {
|
|
||||||
*params = 0;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
case GL_ATOMIC_COUNTER_BUFFER_START:
|
|
||||||
RecordIndexedOnlyGetterError(__func__, pname);
|
|
||||||
return;
|
|
||||||
case GL_ATOMIC_COUNTER_BUFFER_SIZE:
|
|
||||||
RecordIndexedOnlyGetterError(__func__, pname);
|
|
||||||
return;
|
|
||||||
case GL_UNPACK_ALIGNMENT:
|
case GL_UNPACK_ALIGNMENT:
|
||||||
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackAlignment);
|
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::UnpackAlignment);
|
||||||
return;
|
return;
|
||||||
@@ -2114,6 +2026,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
params[3] = vp.w();
|
params[3] = vp.w();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
|
||||||
|
*params = GL_LAST_VERTEX_CONVENTION;
|
||||||
|
return;
|
||||||
case GL_MAX_ELEMENT_INDEX:
|
case GL_MAX_ELEMENT_INDEX:
|
||||||
*params = 1024 * 1024; // TODO
|
*params = 1024 * 1024; // TODO
|
||||||
return;
|
return;
|
||||||
@@ -2201,22 +2116,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_MAX_CLIP_DISTANCES:
|
case GL_MAX_CLIP_DISTANCES:
|
||||||
*params = dynamicParameters.MaxClipDistances;
|
*params = dynamicParameters.MaxClipDistances;
|
||||||
break;
|
break;
|
||||||
// Both were a hard-coded GL_LAST_VERTEX_CONVENTION, derived from nothing. GL 4.6 table
|
|
||||||
// 23.65 permits GL_UNDEFINED_VERTEX for either, and that is what the backends report
|
|
||||||
// wherever they do not actually pin a convention - claiming one is a statement about
|
|
||||||
// which vertex of a primitive supplies gl_Layer / gl_ViewportIndex, and DirectGLES
|
|
||||||
// rasterizes only viewport 0 on a driver without GL_OES_viewport_array while
|
|
||||||
// DirectVulkan picks its provoking mode per pipeline. KHR-GLxx.viewport_array.query
|
|
||||||
// accepts all four values, and .provoking_vertex - which failed on both devices, in
|
|
||||||
// OPPOSITE directions - stops verifying as soon as either answer is undefined.
|
|
||||||
case GL_LAYER_PROVOKING_VERTEX:
|
|
||||||
*params = static_cast<GLint>(dynamicParameters.LayerProvokingVertex);
|
|
||||||
break;
|
|
||||||
case GL_VIEWPORT_INDEX_PROVOKING_VERTEX:
|
|
||||||
*params = static_cast<GLint>(dynamicParameters.ViewportIndexProvokingVertex);
|
|
||||||
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,
|
||||||
@@ -2246,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;
|
||||||
@@ -2273,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;
|
||||||
@@ -2306,19 +2207,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
static_cast<Uint64>(INT32_MAX)));
|
static_cast<Uint64>(INT32_MAX)));
|
||||||
break;
|
break;
|
||||||
case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS:
|
case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS:
|
||||||
// NOT the frontend's binding-point array size: GetIndexedBufferQueryPointCount
|
|
||||||
// clamps this family to the range a lowered counter block can actually be served
|
|
||||||
// from, which is the same number glslang compiles a layout(binding = N) atomic_uint
|
|
||||||
// against and the same one glBindBufferBase validates an index against.
|
|
||||||
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
|
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
|
||||||
break;
|
break;
|
||||||
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
|
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
|
||||||
// The conformance suite splits this evenly across every advertised binding point and
|
// The conformance suite splits this evenly across every advertised binding point and
|
||||||
// binds all of them in one glBindBuffersRange
|
// binds all of them in one glBindBuffersRange
|
||||||
// (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide -
|
// (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide:
|
||||||
// a zero-sized range is INVALID_VALUE before BindBufferRange binds anything. The
|
// 32 bytes over 36 binding points is a zero-sized range, which BindBufferRange
|
||||||
// shared constant is 16384 over 8 binding points, which divides.
|
// rejects with INVALID_VALUE before it binds anything. Floor the advertised size at
|
||||||
*params = kFrontendMaxAtomicCounterBufferSize;
|
// one counter per binding point.
|
||||||
|
*params = std::max<GLint>(
|
||||||
|
kFrontendMaxAtomicCounterBufferSize,
|
||||||
|
static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter) * sizeof(GLuint)));
|
||||||
break;
|
break;
|
||||||
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
||||||
*params = dynamicParameters.MaxTextureBufferSize;
|
*params = dynamicParameters.MaxTextureBufferSize;
|
||||||
@@ -2440,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
|
||||||
|
|||||||
@@ -21,9 +21,6 @@
|
|||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
// The flattened uniform type these helpers used to take as a raw glslang::TType*
|
|
||||||
// pointing into the TProgram's pool allocator. See ProgramObject::TypeFacts.
|
|
||||||
using TypeFactsRef = const MG_State::GLState::ProgramObject::TypeFacts&;
|
|
||||||
static GLint BoolToGLInt(bool value) {
|
static GLint BoolToGLInt(bool value) {
|
||||||
return value ? GL_TRUE : GL_FALSE;
|
return value ? GL_TRUE : GL_FALSE;
|
||||||
}
|
}
|
||||||
@@ -226,14 +223,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLint GetOpaqueUniformUnitLimit(const TypeFactsRef type) {
|
GLint GetOpaqueUniformUnitLimit(const glslang::TType* type) {
|
||||||
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
|
||||||
if (type.isImage) return dynamicParameters.MaxImageUnits;
|
if (type && type->isImage()) return dynamicParameters.MaxImageUnits;
|
||||||
if (type.isTexture) return dynamicParameters.MaxCombinedTextureImageUnits;
|
if (type && type->isTexture()) return dynamicParameters.MaxCombinedTextureImageUnits;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ValidateOpaqueUniformUnit(const char* functionName, const TypeFactsRef type, GLint unit) {
|
bool ValidateOpaqueUniformUnit(const char* functionName, const glslang::TType* type, GLint unit) {
|
||||||
const GLint limit = GetOpaqueUniformUnitLimit(type);
|
const GLint limit = GetOpaqueUniformUnitLimit(type);
|
||||||
if (unit < 0 || unit >= limit) {
|
if (unit < 0 || unit >= limit) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -528,10 +525,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_UNIFORM_ARRAY_STRIDE:
|
case GL_UNIFORM_ARRAY_STRIDE:
|
||||||
case GL_UNIFORM_MATRIX_STRIDE:
|
case GL_UNIFORM_MATRIX_STRIDE:
|
||||||
case GL_UNIFORM_IS_ROW_MAJOR:
|
case GL_UNIFORM_IS_ROW_MAJOR:
|
||||||
// GL 4.2 / ARB_shader_atomic_counters adds this one to the accepted set. Leaving it
|
|
||||||
// out did not merely lose the answer: the leftover GL_INVALID_ENUM is what made
|
|
||||||
// KHR-GL43.shader_atomic_counters.basic-program-query force a FAIL.
|
|
||||||
case GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX:
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -587,11 +580,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_UNIFORM_IS_ROW_MAJOR:
|
case GL_UNIFORM_IS_ROW_MAJOR:
|
||||||
params[i] = programObject->GetActiveUniformIsRowMajor(idx);
|
params[i] = programObject->GetActiveUniformIsRowMajor(idx);
|
||||||
break;
|
break;
|
||||||
case GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX:
|
|
||||||
// Index into the GL_ACTIVE_ATOMIC_COUNTER_BUFFERS list, -1 for every uniform
|
|
||||||
// that is not an atomic counter (GL 4.6 core table 7.6).
|
|
||||||
params[i] = programObject->GetActiveUniformAtomicCounterBufferIndex(idx);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -654,13 +642,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GL_ACTIVE_ATOMIC_COUNTER_BUFFERS:
|
case GL_ACTIVE_ATOMIC_COUNTER_BUFFERS:
|
||||||
// Counter BUFFERS, not counters, and glslang's own getNumAtomicCounters() answers
|
*params = programObject->GetActiveAtomicCounterCount();
|
||||||
// neither: the relaxed parse has already turned every atomic_uint into a plain uint
|
|
||||||
// member of a synthesized storage block by the time it builds its reflection, so it
|
|
||||||
// reports zero. The interface-query model recovers the buffers from those blocks and
|
|
||||||
// is what glGetProgramInterfaceiv(GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES)
|
|
||||||
// already answers - the two queries are required to agree.
|
|
||||||
*params = ProgramInterface::GetActiveResourceCount(*programObject, GL_ATOMIC_COUNTER_BUFFER);
|
|
||||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||||
break;
|
break;
|
||||||
case GL_ACTIVE_ATTRIBUTES:
|
case GL_ACTIVE_ATTRIBUTES:
|
||||||
@@ -680,9 +662,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||||
break;
|
break;
|
||||||
case GL_ACTIVE_UNIFORM_BLOCKS: // GL >= 3.1
|
case GL_ACTIVE_UNIFORM_BLOCKS: // GL >= 3.1
|
||||||
// Uniform blocks only. GetActiveUniformBlocksCount() is the internal block space,
|
*params = programObject->GetActiveUniformBlocksCount();
|
||||||
// which also carries the storage blocks and the synthesized atomic counter blocks.
|
|
||||||
*params = programObject->GetGlUniformBlockCount();
|
|
||||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||||
break;
|
break;
|
||||||
case GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH: // ditto.
|
case GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH: // ditto.
|
||||||
@@ -702,11 +682,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
|
||||||
break;
|
break;
|
||||||
case GL_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3
|
case GL_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3
|
||||||
// "a linked program object with a compute shader" is one whose EXECUTABLE has the
|
if (!programObject->GetLinkStatus() || programObject->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
||||||
// stage: the local size below is a link artifact, so an attached-but-not-yet-linked
|
|
||||||
// compute shader would answer this query with the previous link's (absent) value
|
|
||||||
// instead of the INVALID_OPERATION GL 4.6 core 7.13 asks for.
|
|
||||||
if (!programObject->GetLinkStatus() || !programObject->HasLinkedShaderStage(ShaderStage::Compute)) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
@@ -880,14 +856,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// demotion makes a dmat4 a mat4 in the shader and a mat4-shaped slot here - but because it
|
// demotion makes a dmat4 a mat4 in the shader and a mat4-shaped slot here - but because it
|
||||||
// is ROUTED differently: the caller's component-by-component EbtDouble branch has to widen
|
// is ROUTED differently: the caller's component-by-component EbtDouble branch has to widen
|
||||||
// each float back to the queried type, and it undoes the same padding itself.
|
// each float back to the queried type, and it undoes the same padding itself.
|
||||||
// Float matrices only, in both senses: a DOUBLE matrix never comes through here, whether its
|
Bool TryGatherFloatMatrixColumns(const glslang::TType* ttype, const char* pBase, void* params) {
|
||||||
// program was demoted (components are floats, the query is not) or kept its doubles (the
|
if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false;
|
||||||
// column stride is a dvec4's, and the caller's converting branch already walks it component
|
const Int columns = ttype->getMatrixCols();
|
||||||
// by component with the right one).
|
const Int rows = ttype->getMatrixRows();
|
||||||
Bool TryGatherFloatMatrixColumns(const TypeFactsRef ttype, const char* pBase, void* params) {
|
|
||||||
if (!ttype.isMatrix || ttype.isDouble) return false;
|
|
||||||
const Int columns = ttype.matrixCols;
|
|
||||||
const Int rows = ttype.matrixRows;
|
|
||||||
for (Int column = 0; column < columns; ++column) {
|
for (Int column = 0; column < columns; ++column) {
|
||||||
Memcpy(static_cast<char*>(params) + static_cast<SizeT>(column) * rows * sizeof(GLfloat),
|
Memcpy(static_cast<char*>(params) + static_cast<SizeT>(column) * rows * sizeof(GLfloat),
|
||||||
pBase + static_cast<SizeT>(column) * 4 * sizeof(GLfloat), rows * sizeof(GLfloat));
|
pBase + static_cast<SizeT>(column) * 4 * sizeof(GLfloat), rows * sizeof(GLfloat));
|
||||||
@@ -896,12 +868,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bytes a uniform actually occupies in the global UBO. It is the tight GL type size for
|
// Bytes a uniform actually occupies in the global UBO. It is the tight GL type size for
|
||||||
// everything except a matrix, whose padded columns make it wider, and a `double` on a
|
// everything except a float matrix, whose padded columns make it wider. The rule itself
|
||||||
// program whose modules were demoted, where it is half. The rule itself lives on
|
// lives on ProgramObject, because the pipeline composite's uniform refresh needs the same
|
||||||
// ProgramObject, because the pipeline composite's uniform refresh needs the same one and
|
// one and two copies of a layout rule is one too many.
|
||||||
// two copies of a layout rule is one too many.
|
SizeT UniformStorageSpanInBytes(const glslang::TType* ttype, SizeT tightSize) {
|
||||||
SizeT UniformStorageSpanInBytes(const TypeFactsRef ttype, SizeT tightSize, const Bool nativeFloat64) {
|
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize);
|
||||||
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize, nativeFloat64);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetUniform_State(GLuint program, GLint location, void* params) {
|
void GetUniform_State(GLuint program, GLint location, void* params) {
|
||||||
@@ -933,9 +904,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto offset = programObject->GetUniformOffset(location);
|
auto offset = programObject->GetUniformOffset(location);
|
||||||
auto size = programObject->GetUniformSizesInBytes(location);
|
auto size = programObject->GetUniformSizesInBytes(location);
|
||||||
char* pUBO = (char*)programObject->MapUBO();
|
char* pUBO = (char*)programObject->MapUBO();
|
||||||
const auto& ttype = programObject->GetUniformTypeFacts(location);
|
auto* ttype = programObject->GetUniformTType(location);
|
||||||
const Bool nativeFloat64 = programObject->UsesNativeFloat64();
|
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||||
const SizeT span = UniformStorageSpanInBytes(ttype, size, nativeFloat64);
|
|
||||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
offset + span > programObject->GetUBOSize()) {
|
offset + span > programObject->GetUBOSize()) {
|
||||||
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||||
@@ -945,9 +915,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) {
|
if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) {
|
||||||
// Never more than the uniform actually occupies. `size` is the GL type size,
|
// Never more than the uniform actually occupies. `size` is the GL type size,
|
||||||
// which on a DEMOTED program is twice a `double` uniform's storage - its 64-bit
|
// which for a `double` uniform is twice its storage - every 64-bit float is
|
||||||
// floats were narrowed before the module reached a backend, so the slot holds
|
// narrowed before the module reaches a backend, so the slot holds floats. The
|
||||||
// floats. The typed entry points (glGetUniformdv and friends) go through
|
// typed entry points (glGetUniformdv and friends) go through
|
||||||
// GetUniformScalar_State, which converts component by component; this raw
|
// GetUniformScalar_State, which converts component by component; this raw
|
||||||
// copy has no type to convert with, so it is bounded rather than converted.
|
// copy has no type to convert with, so it is bounded rather than converted.
|
||||||
Memcpy(params, pUBO + offset, std::min<SizeT>(size, span));
|
Memcpy(params, pUBO + offset, std::min<SizeT>(size, span));
|
||||||
@@ -988,9 +958,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto offset = programObject->GetUniformOffset(location);
|
auto offset = programObject->GetUniformOffset(location);
|
||||||
auto size = programObject->GetUniformSizesInBytes(location);
|
auto size = programObject->GetUniformSizesInBytes(location);
|
||||||
char* pUBO = static_cast<char*>(programObject->MapUBO());
|
char* pUBO = static_cast<char*>(programObject->MapUBO());
|
||||||
const auto& ttype = programObject->GetUniformTypeFacts(location);
|
auto* ttype = programObject->GetUniformTType(location);
|
||||||
const Bool nativeFloat64 = programObject->UsesNativeFloat64();
|
const SizeT span = UniformStorageSpanInBytes(ttype, size);
|
||||||
const SizeT span = UniformStorageSpanInBytes(ttype, size, nativeFloat64);
|
|
||||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
offset + span > programObject->GetUBOSize()) {
|
offset + span > programObject->GetUBOSize()) {
|
||||||
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
MGLOG_E_ONCE("%s: uniform at program %u location %d has no backing storage; returning nothing", __func__,
|
||||||
@@ -1002,38 +971,28 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
|
if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A double-precision uniform is the one case where the stored component type can differ
|
// A double-precision uniform is the one case where the stored component type differs
|
||||||
// from the DECLARED one for a non-opaque uniform: on a DEMOTED program the shader's
|
// from the DECLARED one for a non-opaque uniform: the shader's 64-bit floats are
|
||||||
// 64-bit floats were narrowed to 32 before the module reached the backend
|
// narrowed to 32 bits before the module reaches a backend
|
||||||
// (ShaderTranspiler::DemoteFloat64Pass), so what is in the global UBO is a float per
|
// (ShaderTranspiler::DemoteFloat64Pass), so what is in the global UBO is a float per
|
||||||
// component, laid out exactly like the float-typed twin of this uniform - std140
|
// component, laid out exactly like the float-typed twin of this uniform - std140
|
||||||
// 16-byte column stride for a matrix included. Reading it as a GLdouble would return
|
// 16-byte column stride for a matrix included. Reading it as a GLdouble would return
|
||||||
// two components reinterpreted as one. A program that KEPT its doubles stores real ones
|
// two components reinterpreted as one. Read component by component and let GL's
|
||||||
// at the dvec4 column stride instead, so the width and the stride both move; everything
|
|
||||||
// else about this walk is the same. Read component by component either way and let GL's
|
|
||||||
// conversion rules (7.6: round to nearest for the integer queries) apply; the value
|
// conversion rules (7.6: round to nearest for the integer queries) apply; the value
|
||||||
// widens back to the queried type, having lost precision - where it lost any - at the
|
// widens back to the queried type, having lost precision at the glUniform*d that
|
||||||
// glUniform*d that stored it and not here.
|
// stored it and not here.
|
||||||
if (ttype.isDouble) {
|
if (ttype->getBasicType() == glslang::EbtDouble) {
|
||||||
const Int columns = ttype.isMatrix ? ttype.matrixCols : 1;
|
const Int columns = ttype->isMatrix() ? ttype->getMatrixCols() : 1;
|
||||||
const Int rows = ttype.isMatrix ? ttype.matrixRows
|
const Int rows = ttype->isMatrix() ? ttype->getMatrixRows()
|
||||||
: (ttype.isVector ? ttype.vectorSize : 1);
|
: (ttype->isVector() ? ttype->getVectorSize() : 1);
|
||||||
// A non-matrix is one tightly packed run and never reaches the stride at all.
|
// std140 gives every matrix column its own 16-byte slot; a non-matrix is one
|
||||||
const SizeT columnStride =
|
// tightly packed run and never reaches the stride at all.
|
||||||
MG_State::GLState::ProgramObject::UniformMatrixColumnStride(ttype, nativeFloat64);
|
const SizeT columnStride = 4 * sizeof(GLfloat);
|
||||||
const SizeT componentSize = nativeFloat64 ? sizeof(GLdouble) : sizeof(GLfloat);
|
|
||||||
for (Int column = 0; column < columns; ++column) {
|
for (Int column = 0; column < columns; ++column) {
|
||||||
for (Int row = 0; row < rows; ++row) {
|
for (Int row = 0; row < rows; ++row) {
|
||||||
GLdouble component = 0.0;
|
GLfloat component = 0.0f;
|
||||||
if (nativeFloat64) {
|
Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLfloat),
|
||||||
Memcpy(&component, pUBO + offset + column * columnStride + row * componentSize,
|
sizeof(component));
|
||||||
sizeof(GLdouble));
|
|
||||||
} else {
|
|
||||||
GLfloat narrow = 0.0f;
|
|
||||||
Memcpy(&narrow, pUBO + offset + column * columnStride + row * componentSize,
|
|
||||||
sizeof(narrow));
|
|
||||||
component = static_cast<GLdouble>(narrow);
|
|
||||||
}
|
|
||||||
if constexpr (std::is_integral_v<T>) {
|
if constexpr (std::is_integral_v<T>) {
|
||||||
// Rounded to the nearest integer and clamped into the queried type's
|
// Rounded to the nearest integer and clamped into the queried type's
|
||||||
// range, so a negative double read through glGetUniformuiv is 0
|
// range, so a negative double read through glGetUniformuiv is 0
|
||||||
@@ -1098,20 +1057,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read fresh every link, never latched in a static: the capability is
|
static Bool allowVSOnlyPrograms;
|
||||||
// per-backend, and a latch would freeze it across a backend teardown +
|
static Bool initialized = false;
|
||||||
// re-initialization (the previous function-static memo here never even set
|
if (!initialized) {
|
||||||
// its own initialized flag, so it re-read every call anyway - this makes
|
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||||
// the always-fresh behavior the stated one). A struct-field read per
|
if (!activeBackendObject) {
|
||||||
// glLinkProgram costs nothing.
|
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
|
||||||
|
allowVSOnlyPrograms = (Int)rendererInfo.StaticBackendCapability.AllowVSOnlyPrograms;
|
||||||
|
}
|
||||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||||
if (!activeBackendObject) {
|
if (activeBackendObject) {
|
||||||
MGLOG_E_ONCE("activeBackendObject is not initialized!");
|
programObject->SetMaxFragmentOutputColorNumber(activeBackendObject->GetDynamicParameters().MaxDrawBuffers);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const Bool allowVSOnlyPrograms =
|
|
||||||
activeBackendObject->GetRendererInfo().StaticBackendCapability.AllowVSOnlyPrograms;
|
|
||||||
programObject->SetMaxFragmentOutputColorNumber(activeBackendObject->GetDynamicParameters().MaxDrawBuffers);
|
|
||||||
programObject->Link(!allowVSOnlyPrograms);
|
programObject->Link(!allowVSOnlyPrograms);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1232,8 +1192,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
|
Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize);
|
||||||
programObject.MarkUBOContentDirty();
|
programObject.MarkUBOContentDirty();
|
||||||
} else {
|
} else {
|
||||||
const auto& ttype = programObject.GetUniformTypeFacts(location);
|
auto* ttype = programObject.GetUniformTType(location);
|
||||||
if (!ttype.isTexture && !ttype.isImage) return;
|
if (!ttype->isTexture() && !ttype->isImage()) return;
|
||||||
if constexpr (!std::is_same_v<std::remove_cv_t<T>, GLint> || ItemCount != 1) {
|
if constexpr (!std::is_same_v<std::remove_cv_t<T>, GLint> || ItemCount != 1) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -1304,45 +1264,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether the program a uniform write is about to land in stores 64-bit floats at their
|
// glUniform*d / glUniformMatrix*dv. Neither needs a layout of its own any more: the
|
||||||
// declared width. Answered off the PROGRAM, never off the live backend: it describes the
|
// transpile chain narrows every 64-bit float in the shader to 32 bits
|
||||||
// modules that were actually built for it, and a backend with native fp64 still demotes a
|
|
||||||
// program whose vertex stage declares a Float64 input (see ProgramSpirvTask::GenerateSpirv).
|
|
||||||
// Nullptr - no current program, or a name that is not a program - answers false and lets the
|
|
||||||
// callee record the same error it always did.
|
|
||||||
Bool CurrentProgramUsesNativeFloat64() {
|
|
||||||
if (MG_State::pGLContext == nullptr) return false;
|
|
||||||
const auto& programObject = MG_State::pGLContext->GetProgramForUniform();
|
|
||||||
return programObject != nullptr && programObject->UsesNativeFloat64();
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool NamedProgramUsesNativeFloat64(GLuint program) {
|
|
||||||
const auto& programObject = TryToGetProgramObject(program);
|
|
||||||
return programObject != nullptr && programObject->GetLinkStatus() && programObject->UsesNativeFloat64();
|
|
||||||
}
|
|
||||||
|
|
||||||
// glUniform*d / glUniformMatrix*dv. On a DEMOTED program neither needs a layout of its own:
|
|
||||||
// the transpile chain narrowed every 64-bit float in the shader to 32
|
|
||||||
// (ShaderTranspiler::DemoteFloat64Pass) and the global UBO is laid out by reflecting that
|
// (ShaderTranspiler::DemoteFloat64Pass) and the global UBO is laid out by reflecting that
|
||||||
// demoted module, so a double uniform's storage IS a float uniform's - same offset, same
|
// demoted module, so a double uniform's storage IS a float uniform's - same offset, same
|
||||||
// 4-byte components, same std140 column padding for matrices. Narrowing here, at the one
|
// 4-byte components, same std140 column padding for matrices. Narrowing here, at the one
|
||||||
// place the 64-bit value enters, and then handing the bytes to the ordinary float upload
|
// place the 64-bit value enters, and then handing the bytes to the ordinary float upload
|
||||||
// path is what keeps the two in step; a separate double-shaped layout there would write
|
// path is what keeps the two in step; a separate double-shaped layout here would write
|
||||||
// 8-byte components into 4-byte slots and silently address the wrong ones.
|
// 8-byte components into 4-byte slots and silently address the wrong ones.
|
||||||
//
|
//
|
||||||
// The narrowing is the same static_cast the demoted shader's own arithmetic performs, so the
|
// The narrowing is the same static_cast the shader's own arithmetic now performs, so the
|
||||||
// value the shader reads is the value glUniform*d was given, at float precision.
|
// value the shader reads is the value glUniform*d was given, at float precision.
|
||||||
//
|
|
||||||
// On a program that KEPT its doubles the reverse is true and for the same reason: its global
|
|
||||||
// UBO really does hold 8-byte components, so narrowing would leave a float bit pattern in the
|
|
||||||
// low half of a double slot - which is not a precision loss but a garbage value. The 64-bit
|
|
||||||
// values go through unchanged then, and the upload path is width-agnostic (it is templated on
|
|
||||||
// the component type and bounded by the uniform's own slot span).
|
|
||||||
//
|
|
||||||
// Note TryToGetProgramObject / GetProgramForUniform run TWICE on this path, once for the
|
|
||||||
// width question and once inside the call below. That is a lookup and a join on an entry
|
|
||||||
// point no shader pack uses; the alternative is duplicating both functions' whole validation
|
|
||||||
// sequence here, which is the thing that must not drift.
|
|
||||||
template <GLsizei ItemCount>
|
template <GLsizei ItemCount>
|
||||||
void UniformvNarrowed_State(GLint location, GLsizei count, const GLdouble* value) {
|
void UniformvNarrowed_State(GLint location, GLsizei count, const GLdouble* value) {
|
||||||
if (value == nullptr || count <= 0) {
|
if (value == nullptr || count <= 0) {
|
||||||
@@ -1351,10 +1283,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
Uniformv_State<ItemCount>(location, count, reinterpret_cast<const GLfloat*>(value));
|
Uniformv_State<ItemCount>(location, count, reinterpret_cast<const GLfloat*>(value));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (location != -1 && CurrentProgramUsesNativeFloat64()) {
|
|
||||||
Uniformv_State<ItemCount>(location, count, value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
||||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||||
Uniformv_State<ItemCount>(location, count, narrowed.data());
|
Uniformv_State<ItemCount>(location, count, narrowed.data());
|
||||||
@@ -1366,10 +1294,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
ProgramUniformv_State<ItemCount>(program, location, count, reinterpret_cast<const GLfloat*>(value));
|
ProgramUniformv_State<ItemCount>(program, location, count, reinterpret_cast<const GLfloat*>(value));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (location != -1 && NamedProgramUsesNativeFloat64(program)) {
|
|
||||||
ProgramUniformv_State<ItemCount>(program, location, count, value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * ItemCount);
|
||||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||||
ProgramUniformv_State<ItemCount>(program, location, count, narrowed.data());
|
ProgramUniformv_State<ItemCount>(program, location, count, narrowed.data());
|
||||||
@@ -1421,63 +1345,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// glUniformMatrix*dv / glProgramUniformMatrix*dv on a program that KEPT its doubles. Same
|
// glUniformMatrix*dv / glProgramUniformMatrix*dv. Narrowed to the float form and handed
|
||||||
// walk as UniformMatrixfv_Object down to the last branch, and deliberately a copy of it
|
// straight to it: after DemoteFloat64Pass a `dmat4` uniform is a `mat4` in the shader and a
|
||||||
// rather than a template over the component type: the two differ in exactly one number that
|
// mat4-shaped slot in the global UBO, columns padded to a vec4 and all. Everything else
|
||||||
// is not derivable from the component type alone - std140 pads a double matrix's column out
|
// about the call - transpose handling, the array-element walk, the opaque-uniform refusal -
|
||||||
// to a dvec4 (32 bytes) unless the column is a dvec2, which is already 16 - and folding that
|
// is then the one implementation both spellings share.
|
||||||
// into the float version would put a per-call branch on the hot glUniformMatrix4fv path
|
|
||||||
// Minecraft calls thousands of times a frame for a case no shader pack ever takes.
|
|
||||||
template <typename Program>
|
|
||||||
void UniformMatrixdvNative_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
|
||||||
const GLdouble* value, Int columns, Int rows,
|
|
||||||
const String& ownerDescription) {
|
|
||||||
const SizeT columnStride = rows <= 2 ? 2 * sizeof(GLdouble) : 4 * sizeof(GLdouble);
|
|
||||||
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
|
||||||
GLdouble column[4] = {};
|
|
||||||
for (GLint matrix = 0; matrix < count; ++matrix) {
|
|
||||||
if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) break;
|
|
||||||
if (!programObject.IsValidUniformLocation(location + matrix)) {
|
|
||||||
RecordInvalidUniformLocationError("glUniformMatrixdv", location + matrix, ownerDescription);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (programObject.IsUniformOpaqueAtLocation(location + matrix)) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "glUniformMatrixdv",
|
|
||||||
"Opaque uniforms cannot be set with matrix Uniform calls."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const GLdouble* source = value + static_cast<SizeT>(matrix) * componentCount;
|
|
||||||
for (Int c = 0; c < columns; ++c) {
|
|
||||||
for (Int r = 0; r < rows; ++r) {
|
|
||||||
column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r];
|
|
||||||
}
|
|
||||||
const SizeT byteOffset = static_cast<SizeT>(c) * columnStride;
|
|
||||||
switch (rows) {
|
|
||||||
case 2: Uniform_State<2>(programObject, location + matrix, column, byteOffset); break;
|
|
||||||
case 3: Uniform_State<3>(programObject, location + matrix, column, byteOffset); break;
|
|
||||||
default: Uniform_State<4>(programObject, location + matrix, column, byteOffset); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// glUniformMatrix*dv / glProgramUniformMatrix*dv. On a DEMOTED program this narrows to the
|
|
||||||
// float form and hands it straight over: after DemoteFloat64Pass a `dmat4` uniform is a
|
|
||||||
// `mat4` in the shader and a mat4-shaped slot in the global UBO, columns padded to a vec4
|
|
||||||
// and all. Everything else about the call - transpose handling, the array-element walk, the
|
|
||||||
// opaque-uniform refusal - is then the one implementation both spellings share. A program
|
|
||||||
// that kept its doubles gets the same walk at double width and the wider column stride.
|
|
||||||
template <typename Program>
|
template <typename Program>
|
||||||
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose,
|
||||||
const GLdouble* value, Int columns, Int rows) {
|
const GLdouble* value, Int columns, Int rows) {
|
||||||
if (value == nullptr || count <= 0) return;
|
if (value == nullptr || count <= 0) return;
|
||||||
if (programObject.UsesNativeFloat64()) {
|
|
||||||
UniformMatrixdvNative_Object(programObject, location, count, transpose, value, columns, rows,
|
|
||||||
"the current program object");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
const SizeT componentCount = static_cast<SizeT>(columns) * static_cast<SizeT>(rows);
|
||||||
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * componentCount);
|
Vector<GLfloat> narrowed(static_cast<SizeT>(count) * componentCount);
|
||||||
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast<GLfloat>(value[i]);
|
||||||
@@ -1819,10 +1695,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return GL_INVALID_INDEX;
|
return GL_INVALID_INDEX;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGlUniformBlockIndex, not GetUniformBlockIndex: the latter answers in the internal
|
const auto& index = programObject->GetUniformBlockIndex(uniformBlockName);
|
||||||
// block space, which also resolves storage blocks and the synthesized atomic counter
|
|
||||||
// blocks. Neither is a uniform block (GL 4.6 core 7.6), so both are GL_INVALID_INDEX here.
|
|
||||||
const auto index = programObject->GetGlUniformBlockIndex(uniformBlockName);
|
|
||||||
MGLOG_D("GBI prog=%u name='%s' -> %d", program, uniformBlockName ? uniformBlockName : "(null)", (Int)index);
|
MGLOG_D("GBI prog=%u name='%s' -> %d", program, uniformBlockName ? uniformBlockName : "(null)", (Int)index);
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
@@ -1836,7 +1709,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"Program object" + std::to_string(program) + " that has been linked."));
|
"Program object" + std::to_string(program) + " that has been linked."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
|
if (!programObject->IsActiveUniformBlock(uniformBlockIndex)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
@@ -1847,11 +1720,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
std::to_string(program) + "."));
|
std::to_string(program) + "."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The GL_UNIFORM_BLOCK index space skips the storage and atomic counter blocks the
|
|
||||||
// block-keyed tables still carry; translate before touching them.
|
|
||||||
const Uint blockIndex = static_cast<Uint>(programObject->BlockIndexFromGlUniformBlock(uniformBlockIndex));
|
|
||||||
MGLOG_D("UBB prog=%u idx=%u binding=%u", program, uniformBlockIndex, uniformBlockBinding);
|
MGLOG_D("UBB prog=%u idx=%u binding=%u", program, uniformBlockIndex, uniformBlockBinding);
|
||||||
programObject->SetUniformBlockBinding(blockIndex, uniformBlockBinding);
|
programObject->SetUniformBlockBinding(uniformBlockIndex, uniformBlockBinding);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetActiveUniformBlockiv_State(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint* params) {
|
void GetActiveUniformBlockiv_State(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint* params) {
|
||||||
@@ -1863,7 +1733,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"Program object" + std::to_string(program) + " that has been linked."));
|
"Program object" + std::to_string(program) + " that has been linked."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
|
if (!programObject->IsActiveUniformBlock(uniformBlockIndex)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
@@ -1874,68 +1744,61 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
std::to_string(program) + "."));
|
std::to_string(program) + "."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The GL_UNIFORM_BLOCK index space skips the storage and atomic counter blocks the
|
|
||||||
// block-keyed tables still carry; every accessor below is indexed by the block space.
|
|
||||||
const Uint blockIndex = static_cast<Uint>(programObject->BlockIndexFromGlUniformBlock(uniformBlockIndex));
|
|
||||||
switch (pname) {
|
switch (pname) {
|
||||||
case GL_UNIFORM_BLOCK_DATA_SIZE: {
|
case GL_UNIFORM_BLOCK_DATA_SIZE: {
|
||||||
*params = (GLint)programObject->GetUBOSizeAt(blockIndex);
|
*params = (GLint)programObject->GetUBOSizeAt(uniformBlockIndex);
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_DATA_SIZE = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_DATA_SIZE = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GL_UNIFORM_BLOCK_NAME_LENGTH: {
|
case GL_UNIFORM_BLOCK_NAME_LENGTH: {
|
||||||
*params = (GLint)programObject->GetUniformBlockName(blockIndex).length() + 1;
|
*params = (GLint)programObject->GetUniformBlockName(uniformBlockIndex).length() + 1;
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_NAME_LENGTH = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_NAME_LENGTH = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS: {
|
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS: {
|
||||||
*params = programObject->GetUniformBlockActiveUniformCount(blockIndex);
|
*params = programObject->GetUniformBlockActiveUniformCount(uniformBlockIndex);
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GL_UNIFORM_BLOCK_BINDING: {
|
case GL_UNIFORM_BLOCK_BINDING: {
|
||||||
*params = static_cast<GLint>(programObject->GetUniformBlockBinding(blockIndex));
|
*params = static_cast<GLint>(programObject->GetUniformBlockBinding(uniformBlockIndex));
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_BINDING = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_BINDING = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER:
|
case GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER:
|
||||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangVertex));
|
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangVertex));
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER:
|
case GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||||
*params =
|
*params =
|
||||||
BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangTessControl));
|
BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangTessControl));
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
case GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||||
*params =
|
*params =
|
||||||
BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangTessEvaluation));
|
BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangTessEvaluation));
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER:
|
case GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER:
|
||||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangGeometry));
|
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangGeometry));
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER:
|
case GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER:
|
||||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangFragment));
|
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangFragment));
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
case GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER:
|
case GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER:
|
||||||
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(blockIndex, EShLangCompute));
|
*params = BoolToGLInt(programObject->IsUniformBlockReferencedByStage(uniformBlockIndex, EShLangCompute));
|
||||||
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = %d", __func__, *params);
|
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = %d", __func__, *params);
|
||||||
break;
|
break;
|
||||||
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: {
|
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: {
|
||||||
// Member entries of an arrayed block are recorded against the first instance;
|
// Member entries of an arrayed block are recorded against the first instance;
|
||||||
// every instance of the array reports that shared member set (matches
|
// every instance of the array reports that shared member set (matches
|
||||||
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, which scans with the same owner index).
|
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, which scans with the same owner index).
|
||||||
//
|
const Int ownerIndex = static_cast<Int>(programObject->GetUniformBlockMemberOwnerIndex(uniformBlockIndex));
|
||||||
// Both sides of the comparison are BLOCK indices: GetUniformBlockMemberOwnerIndex
|
|
||||||
// answers in that space, so the scan uses GetActiveUniformOwnerBlockIndex rather
|
|
||||||
// than the GL_UNIFORM_BLOCK-space GetActiveUniformBlockIndex.
|
|
||||||
const Int ownerIndex = static_cast<Int>(programObject->GetUniformBlockMemberOwnerIndex(blockIndex));
|
|
||||||
GLint uniformIndexCount = 0;
|
GLint uniformIndexCount = 0;
|
||||||
for (Uint uniformIndex = 0; uniformIndex < programObject->GetUniformCount(); ++uniformIndex) {
|
for (Uint uniformIndex = 0; uniformIndex < programObject->GetUniformCount(); ++uniformIndex) {
|
||||||
if (programObject->GetActiveUniformOwnerBlockIndex(uniformIndex) != ownerIndex) {
|
if (programObject->GetActiveUniformBlockIndex(uniformIndex) != ownerIndex) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
params[uniformIndexCount++] = static_cast<GLint>(uniformIndex);
|
params[uniformIndexCount++] = static_cast<GLint>(uniformIndex);
|
||||||
@@ -1965,7 +1828,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
" is not a program object that has been linked."));
|
" is not a program object that has been linked."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!programObject->IsActiveGlUniformBlock(uniformBlockIndex)) {
|
if (!programObject->IsActiveUniformBlock(uniformBlockIndex)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
@@ -1975,8 +1838,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"not the index of an active uniform block in program."));
|
"not the index of an active uniform block in program."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto& name = programObject->GetUniformBlockName(
|
const auto& name = programObject->GetUniformBlockName(uniformBlockIndex);
|
||||||
static_cast<Uint>(programObject->BlockIndexFromGlUniformBlock(uniformBlockIndex)));
|
|
||||||
CopyStr(bufSize, length, uniformBlockName, name.c_str(), (GLsizei)name.length());
|
CopyStr(bufSize, length, uniformBlockName, name.c_str(), (GLsizei)name.length());
|
||||||
MGLOG_D("%s: \"%s\" at uniformBlockIndex %02d, length = %d", __func__, uniformBlockName, uniformBlockIndex,
|
MGLOG_D("%s: \"%s\" at uniformBlockIndex %02d, length = %d", __func__, uniformBlockName, uniformBlockIndex,
|
||||||
length ? *length : 0);
|
length ? *length : 0);
|
||||||
@@ -2974,73 +2836,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name);
|
return ProgramInterface::GetResourceLocationIndex(*programObject, programInterface, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL 4.6 §7.7. Every property this reports is one the GL_ATOMIC_COUNTER_BUFFER interface
|
|
||||||
// already carries, so this is a rename of glGetProgramResourceiv's props onto the older
|
|
||||||
// entry point's - and the two are required to agree, which is only true while both read the
|
|
||||||
// same model. It was a silent stub: it wrote nothing, raised nothing, and left every probe
|
|
||||||
// reading its own uninitialised output.
|
|
||||||
static Bool TryMapActiveAtomicCounterBufferProp(GLenum pname, GLenum& outProp) {
|
|
||||||
switch (pname) {
|
|
||||||
case GL_ATOMIC_COUNTER_BUFFER_BINDING:
|
|
||||||
outProp = GL_BUFFER_BINDING;
|
|
||||||
return true;
|
|
||||||
case GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE:
|
|
||||||
outProp = GL_BUFFER_DATA_SIZE;
|
|
||||||
return true;
|
|
||||||
case GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS:
|
|
||||||
outProp = GL_NUM_ACTIVE_VARIABLES;
|
|
||||||
return true;
|
|
||||||
case GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES:
|
|
||||||
outProp = GL_ACTIVE_VARIABLES;
|
|
||||||
return true;
|
|
||||||
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER:
|
|
||||||
outProp = GL_REFERENCED_BY_VERTEX_SHADER;
|
|
||||||
return true;
|
|
||||||
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_CONTROL_SHADER:
|
|
||||||
outProp = GL_REFERENCED_BY_TESS_CONTROL_SHADER;
|
|
||||||
return true;
|
|
||||||
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
|
||||||
outProp = GL_REFERENCED_BY_TESS_EVALUATION_SHADER;
|
|
||||||
return true;
|
|
||||||
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_GEOMETRY_SHADER:
|
|
||||||
outProp = GL_REFERENCED_BY_GEOMETRY_SHADER;
|
|
||||||
return true;
|
|
||||||
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER:
|
|
||||||
outProp = GL_REFERENCED_BY_FRAGMENT_SHADER;
|
|
||||||
return true;
|
|
||||||
case GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_COMPUTE_SHADER:
|
|
||||||
outProp = GL_REFERENCED_BY_COMPUTE_SHADER;
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GetActiveAtomicCounterBufferiv(GLuint program, GLuint bufferIndex, GLenum pname, GLint* params) {
|
|
||||||
auto& programObject = TryToGetProgramForInterfaceQuery(program, __func__);
|
|
||||||
if (!programObject) return;
|
|
||||||
GLenum prop = GL_NONE;
|
|
||||||
if (!TryMapActiveAtomicCounterBufferProp(pname, prop)) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidEnum,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
|
||||||
"pname is not an active atomic counter buffer property."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Vector<GLint> values;
|
|
||||||
if (!ProgramInterface::GetResourceProp(*programObject, GL_ATOMIC_COUNTER_BUFFER, bufferIndex, prop, values)) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
|
||||||
"bufferIndex is not an active atomic counter buffer index."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (params == nullptr) return;
|
|
||||||
// GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES is the only multi-value property
|
|
||||||
// here, and the caller sized its array from _ACTIVE_ATOMIC_COUNTERS.
|
|
||||||
for (SizeT i = 0; i < values.size(); ++i) params[i] = values[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// GL 4.6 §7.6.2: <storageBlockIndex> is an active shader storage block index of <program>
|
// GL 4.6 §7.6.2: <storageBlockIndex> is an active shader storage block index of <program>
|
||||||
// - that is, exactly what glGetProgramResourceIndex(GL_SHADER_STORAGE_BLOCK) returned.
|
// - that is, exactly what glGetProgramResourceIndex(GL_SHADER_STORAGE_BLOCK) returned.
|
||||||
// Since wave 2 that index is the interface-query layer's, so this is where the one index
|
// Since wave 2 that index is the interface-query layer's, so this is where the one index
|
||||||
|
|||||||
@@ -140,7 +140,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
||||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
||||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
||||||
void GetActiveAtomicCounterBufferiv(GLuint program, GLuint bufferIndex, GLenum pname, GLint* params);
|
|
||||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||||
void Uniform1d(GLint location, GLdouble v0);
|
void Uniform1d(GLint location, GLdouble v0);
|
||||||
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value);
|
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
|
// "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
|
||||||
// atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource
|
// atomic-counter binding point. That block IS the GL_ATOMIC_COUNTER_BUFFER resource
|
||||||
// and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs.
|
// and its trailing number IS GL_BUFFER_BINDING; its members stay GL_UNIFORMs.
|
||||||
constexpr const char* kAtomicCounterBlockPrefix = MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX;
|
constexpr const char* kAtomicCounterBlockPrefix = "gl_AtomicCounterBlock";
|
||||||
|
|
||||||
enum class BlockKind {
|
enum class BlockKind {
|
||||||
Uniform, // a real GL uniform block
|
Uniform, // a real GL uniform block
|
||||||
@@ -81,18 +81,19 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// The enumerated spelling of an array resource is "name[0]". glslang already applies
|
// The enumerated spelling of an array resource is "name[0]". glslang already applies
|
||||||
// that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to
|
// that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to
|
||||||
// stage inputs/outputs, so those get it here.
|
// stage inputs/outputs, so those get it here.
|
||||||
String WithArraySuffix(const String& name, const ProgramObject::TypeFacts& type) {
|
String WithArraySuffix(const String& name, const glslang::TType* type) {
|
||||||
if (!type.isArray || EndsWithZeroSubscript(name)) return name;
|
if (type == nullptr || !type->isArray() || EndsWithZeroSubscript(name)) return name;
|
||||||
return name + "[0]";
|
return name + "[0]";
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL_ARRAY_SIZE: element count for a sized array, 0 for a runtime-sized one
|
// GL_ARRAY_SIZE: element count for a sized array, 0 for a runtime-sized one
|
||||||
// (a shader storage block's unsized trailing member), 1 for a non-array.
|
// (a shader storage block's unsized trailing member), 1 for a non-array.
|
||||||
// `record.arraySize` is already the sized-array/reflected-size resolution; the only
|
GLint ArraySizeOf(const glslang::TType* type, GLint reflectedSize) {
|
||||||
// extra rule here is GL's 0 for a runtime-sized array.
|
if (type != nullptr && type->isArray()) {
|
||||||
GLint ArraySizeOf(const ProgramObject::ResourceReflection& record) {
|
if (!type->isSizedArray()) return 0;
|
||||||
if (record.type.isArray && !record.type.isSizedArray) return 0;
|
return type->getOuterArraySize();
|
||||||
return record.arraySize;
|
}
|
||||||
|
return reflectedSize < 1 ? 1 : reflectedSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two spellings name the same resource when they are equal, or differ only by the
|
// Two spellings name the same resource when they are equal, or differ only by the
|
||||||
@@ -173,21 +174,22 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
return static_cast<GLint>(element);
|
return static_cast<GLint>(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockKind ClassifyBlock(const ProgramObject::BlockReflection& block) {
|
BlockKind ClassifyBlock(const glslang::TObjectReflection& block) {
|
||||||
if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) {
|
if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) {
|
||||||
return BlockKind::GlobalUbo;
|
return BlockKind::GlobalUbo;
|
||||||
}
|
}
|
||||||
if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter;
|
if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter;
|
||||||
if (block.type.isBuffer) return BlockKind::Storage;
|
const glslang::TType* type = block.getType();
|
||||||
|
if (type != nullptr && type->getQualifier().storage == glslang::EvqBuffer) return BlockKind::Storage;
|
||||||
return BlockKind::Uniform;
|
return BlockKind::Uniform;
|
||||||
}
|
}
|
||||||
|
|
||||||
// std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to
|
// std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to
|
||||||
// uniform matrices. 0 for a non-matrix.
|
// uniform matrices. 0 for a non-matrix.
|
||||||
GLint MatrixStrideOf(const ProgramObject::TypeFacts& type) {
|
GLint MatrixStrideOf(const glslang::TType* type) {
|
||||||
if (!type.isMatrix) return 0;
|
if (type == nullptr || !type->isMatrix()) return 0;
|
||||||
const bool rowMajor = type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor);
|
const bool rowMajor = type->getQualifier().layoutMatrix == glslang::ElmRowMajor;
|
||||||
const int strideVectorComponents = rowMajor ? type.matrixCols : type.matrixRows;
|
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows();
|
||||||
constexpr int scalarSize = 4;
|
constexpr int scalarSize = 4;
|
||||||
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
|
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
|
||||||
: (strideVectorComponents == 2) ? 2 * scalarSize
|
: (strideVectorComponents == 2) ? 2 * scalarSize
|
||||||
@@ -195,9 +197,9 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
return (vectorAlignment + 15) & ~15;
|
return (vectorAlignment + 15) & ~15;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLint IsRowMajorOf(const ProgramObject::TypeFacts& type) {
|
GLint IsRowMajorOf(const glslang::TType* type) {
|
||||||
if (!type.isMatrix) return 0;
|
if (type == nullptr || !type->isMatrix()) return 0;
|
||||||
return type.layoutMatrix == static_cast<Int>(glslang::ElmRowMajor) ? 1 : 0;
|
return type->getQualifier().layoutMatrix == glslang::ElmRowMajor ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
GLint MappedLocation(Int rawLocation) {
|
GLint MappedLocation(Int rawLocation) {
|
||||||
@@ -225,12 +227,12 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// Note the union is used even when it is empty: an array element nobody dereferenced has
|
// Note the union is used even when it is empty: an array element nobody dereferenced has
|
||||||
// no member bits and is genuinely referenced by nobody, which is the whole point - falling
|
// no member bits and is genuinely referenced by nobody, which is the whole point - falling
|
||||||
// back to the block's own mask there would restore the over-approximation.
|
// back to the block's own mask there would restore the over-approximation.
|
||||||
Vector<Uint32> BuildBlockStagesFromMembers(const ProgramObject::LinkArtifacts& reflection,
|
Vector<Uint32> BuildBlockStagesFromMembers(const glslang::TProgram& reflection, Int blockCount) {
|
||||||
Int blockCount) {
|
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
||||||
Vector<Uint32> stagesByBlock(static_cast<SizeT>(blockCount < 0 ? 0 : blockCount), 0u);
|
Vector<Uint32> stagesByBlock(static_cast<SizeT>(blockCount < 0 ? 0 : blockCount), 0u);
|
||||||
const Int uniformCount = static_cast<Int>(reflection.uniformReflection.size());
|
const Int uniformCount = mutableReflection.getNumUniformVariables();
|
||||||
for (Int index = 0; index < uniformCount; ++index) {
|
for (Int index = 0; index < uniformCount; ++index) {
|
||||||
const auto& uniform = reflection.uniformReflection[index];
|
const auto& uniform = mutableReflection.getUniform(index);
|
||||||
const Int owner = uniform.index;
|
const Int owner = uniform.index;
|
||||||
if (owner < 0 || owner >= blockCount) continue;
|
if (owner < 0 || owner >= blockCount) continue;
|
||||||
stagesByBlock[static_cast<SizeT>(owner)] |= static_cast<Uint32>(uniform.stages);
|
stagesByBlock[static_cast<SizeT>(owner)] |= static_cast<Uint32>(uniform.stages);
|
||||||
@@ -248,7 +250,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// ss[1] and requires both to report the fragment stage, which only glslang's own
|
// ss[1] and requires both to report the fragment stage, which only glslang's own
|
||||||
// (deliberately over-approximating) block mask gets right. Storage and atomic-counter
|
// (deliberately over-approximating) block mask gets right. Storage and atomic-counter
|
||||||
// blocks therefore keep that mask untouched.
|
// blocks therefore keep that mask untouched.
|
||||||
Uint32 UniformBlockStages(const ProgramObject::BlockReflection& block, const Vector<Uint32>& stagesFromMembers,
|
Uint32 UniformBlockStages(const glslang::TObjectReflection& block, const Vector<Uint32>& stagesFromMembers,
|
||||||
Int tIndex) {
|
Int tIndex) {
|
||||||
String arrayBase;
|
String arrayBase;
|
||||||
Uint element = 0;
|
Uint element = 0;
|
||||||
@@ -262,15 +264,15 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
return stagesFromMembers[static_cast<SizeT>(tIndex)];
|
return stagesFromMembers[static_cast<SizeT>(tIndex)];
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildBlocks(ProgramObject& program, const ProgramObject::LinkArtifacts& reflection, Model& model,
|
void BuildBlocks(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
||||||
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
|
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
|
||||||
const Int blockCount = static_cast<Int>(reflection.blockReflection.size());
|
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks();
|
||||||
blockKind.assign(blockCount, BlockKind::Uniform);
|
blockKind.assign(blockCount, BlockKind::Uniform);
|
||||||
blockInterfaceIndex.assign(blockCount, -1);
|
blockInterfaceIndex.assign(blockCount, -1);
|
||||||
const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);
|
const Vector<Uint32> stagesFromMembers = BuildBlockStagesFromMembers(reflection, blockCount);
|
||||||
|
|
||||||
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
|
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
|
||||||
const auto& block = reflection.blockReflection[tIndex];
|
const auto& block = const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex);
|
||||||
const BlockKind kind = ClassifyBlock(block);
|
const BlockKind kind = ClassifyBlock(block);
|
||||||
blockKind[tIndex] = kind;
|
blockKind[tIndex] = kind;
|
||||||
if (kind == BlockKind::AtomicCounter) {
|
if (kind == BlockKind::AtomicCounter) {
|
||||||
@@ -291,7 +293,7 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 -
|
// glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 -
|
||||||
// exactly the same rule GL_UNIFORM_BLOCK follows through
|
// exactly the same rule GL_UNIFORM_BLOCK follows through
|
||||||
// GetUniformBlockBinding below).
|
// GetUniformBlockBinding below).
|
||||||
const GLint declared = block.binding;
|
const GLint declared = block.getBinding();
|
||||||
resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name);
|
resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name);
|
||||||
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
|
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
|
||||||
if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound);
|
if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound);
|
||||||
@@ -305,53 +307,38 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// GL_UNIFORM_BLOCK keeps the index space glUniformBlockBinding and
|
// GL_UNIFORM_BLOCK keeps the index space glUniformBlockBinding and
|
||||||
// glGetActiveUniformBlockiv already use, so an index handed out here is usable
|
// glGetActiveUniformBlockiv already use, so an index handed out here is usable
|
||||||
// with them (which is exactly what the CTS does).
|
// with them (which is exactly what the CTS does).
|
||||||
const Int glBlockCount = program.GetGlUniformBlockCount();
|
const Int glBlockCount = program.GetActiveUniformBlocksCount();
|
||||||
for (Int glIndex = 0; glIndex < glBlockCount; ++glIndex) {
|
for (Int glIndex = 0; glIndex < glBlockCount; ++glIndex) {
|
||||||
// The block-space index the block-keyed accessors want; the two spaces differ
|
|
||||||
// whenever the program also has a storage or atomic counter block, which
|
|
||||||
// glslang files under the same reflection list (no EShReflectionSeparateBuffers).
|
|
||||||
const Int blockIndex = program.BlockIndexFromGlUniformBlock(static_cast<Uint>(glIndex));
|
|
||||||
Resource resource;
|
Resource resource;
|
||||||
resource.name = program.GetUniformBlockName(static_cast<Uint>(blockIndex));
|
resource.name = program.GetUniformBlockName(glIndex);
|
||||||
resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(static_cast<Uint>(blockIndex)));
|
resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(glIndex));
|
||||||
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(static_cast<Uint>(blockIndex)));
|
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
|
||||||
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(blockIndex));
|
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
|
||||||
if (tIndex >= 0 && tIndex < blockCount) {
|
if (tIndex >= 0 && tIndex < blockCount) {
|
||||||
resource.stages = UniformBlockStages(reflection.blockReflection[tIndex],
|
resource.stages = UniformBlockStages(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex),
|
||||||
stagesFromMembers, tIndex);
|
stagesFromMembers, tIndex);
|
||||||
}
|
}
|
||||||
model.uniformBlocks.push_back(Move(resource));
|
model.uniformBlocks.push_back(Move(resource));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildUniformsAndBufferVariables(ProgramObject& program,
|
void BuildUniformsAndBufferVariables(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
||||||
const ProgramObject::LinkArtifacts& reflection, Model& model,
|
|
||||||
const Vector<BlockKind>& blockKind,
|
const Vector<BlockKind>& blockKind,
|
||||||
const Vector<Int>& blockInterfaceIndex) {
|
const Vector<Int>& blockInterfaceIndex) {
|
||||||
// Walks the TPROGRAM uniform space, not the GL one. A buffer variable is not a GL
|
const Uint uniformCount = program.GetUniformCount();
|
||||||
// uniform (GL 4.6 core 7.3.1) and DoReflection therefore keeps it out of the GL
|
for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) {
|
||||||
// active-uniform index space - but GL_BUFFER_VARIABLE still has to enumerate it, and
|
const Int tIndex = program.TProgramUniformIndex(glIndex);
|
||||||
// this is the only place that does. GL uniforms keep their GL index as their
|
const auto& refl = const_cast<glslang::TProgram&>(reflection).getUniform(tIndex);
|
||||||
// GL_UNIFORM resource index: the GL space is a subsequence of this one, so pushing
|
const glslang::TType* type = refl.getType();
|
||||||
// the GL-visible entries in this order preserves the correspondence.
|
|
||||||
const Int tUniformCount = static_cast<Int>(reflection.uniformReflection.size());
|
|
||||||
for (Int tIndex = 0; tIndex < tUniformCount; ++tIndex) {
|
|
||||||
const auto& refl = ProgramObject::UniformAtIn(reflection, tIndex);
|
|
||||||
const auto& type = refl.type;
|
|
||||||
const Int owner = refl.index;
|
const Int owner = refl.index;
|
||||||
const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
|
const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
|
||||||
? blockKind[owner]
|
? blockKind[owner]
|
||||||
: BlockKind::GlobalUbo;
|
: BlockKind::GlobalUbo;
|
||||||
const Int glIndex = program.GlUniformIndexFromTProgram(tIndex);
|
|
||||||
// Everything except a buffer variable is enumerated through the GL space, so a
|
|
||||||
// uniform the relaxed parse swept out of it (a declared-but-dead default-block
|
|
||||||
// one) stays out of GL_UNIFORM too.
|
|
||||||
if (kind != BlockKind::Storage && glIndex < 0) continue;
|
|
||||||
|
|
||||||
Resource resource;
|
Resource resource;
|
||||||
resource.name = refl.name;
|
resource.name = refl.name;
|
||||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
resource.arraySize = ArraySizeOf(refl);
|
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||||
resource.stages = static_cast<Uint32>(refl.stages);
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
|
|
||||||
if (kind == BlockKind::Storage) {
|
if (kind == BlockKind::Storage) {
|
||||||
@@ -379,12 +366,11 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
resource.atomicCounterBufferIndex = blockInterfaceIndex[owner];
|
resource.atomicCounterBufferIndex = blockInterfaceIndex[owner];
|
||||||
resource.location = -1;
|
resource.location = -1;
|
||||||
} else {
|
} else {
|
||||||
const Uint glUniformIndex = static_cast<Uint>(glIndex);
|
resource.blockIndex = program.GetActiveUniformBlockIndex(glIndex);
|
||||||
resource.blockIndex = program.GetActiveUniformBlockIndex(glUniformIndex);
|
resource.offset = program.GetActiveUniformOffset(glIndex);
|
||||||
resource.offset = program.GetActiveUniformOffset(glUniformIndex);
|
resource.arrayStride = program.GetActiveUniformArrayStride(glIndex);
|
||||||
resource.arrayStride = program.GetActiveUniformArrayStride(glUniformIndex);
|
resource.matrixStride = program.GetActiveUniformMatrixStride(glIndex);
|
||||||
resource.matrixStride = program.GetActiveUniformMatrixStride(glUniformIndex);
|
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glIndex);
|
||||||
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glUniformIndex);
|
|
||||||
// A member of a named uniform block has no location, whatever the
|
// A member of a named uniform block has no location, whatever the
|
||||||
// frontend's own location table says (it hands one out to every uniform
|
// frontend's own location table says (it hands one out to every uniform
|
||||||
// so glUniform* can address block members through the global UBO).
|
// so glUniform* can address block members through the global UBO).
|
||||||
@@ -403,16 +389,12 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
static_cast<GLuint>(i));
|
static_cast<GLuint>(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (SizeT glBlockIndex = 0; glBlockIndex < model.uniformBlocks.size(); ++glBlockIndex) {
|
for (SizeT blockIndex = 0; blockIndex < model.uniformBlocks.size(); ++blockIndex) {
|
||||||
// Members of an arrayed block are reflected once, against instance [0].
|
// Members of an arrayed block are reflected once, against instance [0].
|
||||||
// GetUniformBlockMemberOwnerIndex takes and answers BLOCK indices, while
|
const Int owner = static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex)));
|
||||||
// Resource::blockIndex is a GL_UNIFORM_BLOCK index, so translate both ways.
|
|
||||||
const Int blockIndex = program.BlockIndexFromGlUniformBlock(static_cast<Uint>(glBlockIndex));
|
|
||||||
const Int owner = program.GlUniformBlockIndexFromBlock(
|
|
||||||
static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex))));
|
|
||||||
for (SizeT i = 0; i < model.uniforms.size(); ++i) {
|
for (SizeT i = 0; i < model.uniforms.size(); ++i) {
|
||||||
if (model.uniforms[i].blockIndex == owner) {
|
if (model.uniforms[i].blockIndex == owner) {
|
||||||
model.uniformBlocks[glBlockIndex].activeVariables.push_back(static_cast<GLuint>(i));
|
model.uniformBlocks[blockIndex].activeVariables.push_back(static_cast<GLuint>(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,13 +414,17 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// program that redeclares `out gl_PerVertex { vec4 gl_Position; }` still carries
|
// program that redeclares `out gl_PerVertex { vec4 gl_Position; }` still carries
|
||||||
// gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they
|
// gl_PointSize and gl_ClipDistance through the block-unwrapping reflection, and they
|
||||||
// are not part of its output interface.
|
// are not part of its output interface.
|
||||||
Bool IsHiddenBlockMember(const ProgramObject::TypeFacts& type) { return type.isVoid; }
|
Bool IsHiddenBlockMember(const glslang::TType* type) {
|
||||||
|
return type != nullptr && type->getBasicType() == glslang::EbtVoid;
|
||||||
|
}
|
||||||
|
|
||||||
void BuildStageIO(ProgramObject& program, const ProgramObject::LinkArtifacts& reflection, Model& model) {
|
void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) {
|
||||||
const Int inputCount = static_cast<Int>(reflection.pipeInputReflection.size());
|
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
||||||
|
|
||||||
|
const Int inputCount = mutableReflection.getNumPipeInputs();
|
||||||
for (Int index = 0; index < inputCount; ++index) {
|
for (Int index = 0; index < inputCount; ++index) {
|
||||||
const auto& refl = reflection.pipeInputReflection[index];
|
const auto& refl = mutableReflection.getPipeInput(index);
|
||||||
const auto& type = refl.type;
|
const glslang::TType* type = refl.getType();
|
||||||
if (IsHiddenBlockMember(type)) continue;
|
if (IsHiddenBlockMember(type)) continue;
|
||||||
Resource resource;
|
Resource resource;
|
||||||
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
|
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
|
||||||
@@ -446,10 +432,10 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name);
|
const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name);
|
||||||
resource.name = WithArraySuffix(glName, type);
|
resource.name = WithArraySuffix(glName, type);
|
||||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
resource.arraySize = ArraySizeOf(refl);
|
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||||
resource.location = program.GetAttributeLocation(refl.name);
|
resource.location = program.GetAttributeLocation(refl.name);
|
||||||
if (resource.location < 0) resource.location = MappedLocation(refl.location);
|
if (resource.location < 0) resource.location = MappedLocation(static_cast<Int>(refl.layoutLocation()));
|
||||||
resource.isPerPatch = type.isPatch ? 1 : 0;
|
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
|
||||||
resource.stages = static_cast<Uint32>(refl.stages);
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
model.programInputs.push_back(Move(resource));
|
model.programInputs.push_back(Move(resource));
|
||||||
}
|
}
|
||||||
@@ -461,16 +447,16 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
// carries its own layout(location=N)), and a location then manufactures a color
|
// carries its own layout(location=N)), and a location then manufactures a color
|
||||||
// index of 0 where GL requires -1
|
// index of 0 where GL requires -1
|
||||||
// (KHR-GL43.program_interface_query.separate-programs-tess-control).
|
// (KHR-GL43.program_interface_query.separate-programs-tess-control).
|
||||||
const Bool lastStageIsFragment = reflection.lastStageIsFragment;
|
const Bool lastStageIsFragment = mutableReflection.getIntermediate(EShLangFragment) != nullptr;
|
||||||
const Int outputCount = static_cast<Int>(reflection.pipeOutputReflection.size());
|
const Int outputCount = mutableReflection.getNumPipeOutputs();
|
||||||
for (Int index = 0; index < outputCount; ++index) {
|
for (Int index = 0; index < outputCount; ++index) {
|
||||||
const auto& refl = reflection.pipeOutputReflection[index];
|
const auto& refl = mutableReflection.getPipeOutput(index);
|
||||||
const auto& type = refl.type;
|
const glslang::TType* type = refl.getType();
|
||||||
if (IsHiddenBlockMember(type)) continue;
|
if (IsHiddenBlockMember(type)) continue;
|
||||||
Resource resource;
|
Resource resource;
|
||||||
resource.name = WithArraySuffix(refl.name, type);
|
resource.name = WithArraySuffix(refl.name, type);
|
||||||
resource.type = static_cast<GLenum>(refl.glDefineType);
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
resource.arraySize = ArraySizeOf(refl);
|
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||||
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
|
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
|
||||||
if (resource.location < 0 || !lastStageIsFragment) {
|
if (resource.location < 0 || !lastStageIsFragment) {
|
||||||
// A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a
|
// A built-in output (gl_FragDepth, gl_SampleMask) has no location, and a
|
||||||
@@ -481,11 +467,11 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
|
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
|
||||||
// glBindFragDataLocationIndexed wins; otherwise the shader's
|
// glBindFragDataLocationIndexed wins; otherwise the shader's
|
||||||
// layout(index = N), which the frag-data maps never saw.
|
// layout(index = N), which the frag-data maps never saw.
|
||||||
if (resource.locationIndex == 0 && type.hasIndex) {
|
if (resource.locationIndex == 0 && type != nullptr && type->getQualifier().hasIndex()) {
|
||||||
resource.locationIndex = static_cast<GLint>(type.layoutIndex);
|
resource.locationIndex = static_cast<GLint>(type->getQualifier().layoutIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resource.isPerPatch = type.isPatch ? 1 : 0;
|
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
|
||||||
resource.stages = static_cast<Uint32>(refl.stages);
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
model.programOutputs.push_back(Move(resource));
|
model.programOutputs.push_back(Move(resource));
|
||||||
}
|
}
|
||||||
@@ -525,14 +511,15 @@ namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
|||||||
Model BuildModel(ProgramObject& program) {
|
Model BuildModel(ProgramObject& program) {
|
||||||
Model model;
|
Model model;
|
||||||
if (!program.GetLinkStatus()) return model;
|
if (!program.GetLinkStatus()) return model;
|
||||||
const ProgramObject::LinkArtifacts& reflection = program.GetLinkReflection();
|
const glslang::TProgram* reflection = program.GetReflection();
|
||||||
|
if (reflection == nullptr) return model;
|
||||||
model.valid = true;
|
model.valid = true;
|
||||||
|
|
||||||
Vector<BlockKind> blockKind;
|
Vector<BlockKind> blockKind;
|
||||||
Vector<Int> blockInterfaceIndex;
|
Vector<Int> blockInterfaceIndex;
|
||||||
BuildBlocks(program, reflection, model, blockKind, blockInterfaceIndex);
|
BuildBlocks(program, *reflection, model, blockKind, blockInterfaceIndex);
|
||||||
BuildUniformsAndBufferVariables(program, reflection, model, blockKind, blockInterfaceIndex);
|
BuildUniformsAndBufferVariables(program, *reflection, model, blockKind, blockInterfaceIndex);
|
||||||
BuildStageIO(program, reflection, model);
|
BuildStageIO(program, *reflection, model);
|
||||||
BuildXfb(program, model);
|
BuildXfb(program, model);
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -565,75 +505,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
queryObject->ended = true;
|
queryObject->ended = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BeginConditionalRender(GLuint id, GLenum mode) {
|
|
||||||
// GL 4.6 core 10.9's eight modes. The _INVERTED half flips the sense of the predicate;
|
|
||||||
// the BY_REGION half only narrows WHERE an implementation is permitted to discard, so
|
|
||||||
// treating it as its whole-framebuffer sibling is what an implementation without region
|
|
||||||
// granularity does. The _NO_WAIT half is a permission to render rather than stall, not an
|
|
||||||
// obligation - see the resolve below.
|
|
||||||
Bool inverted = false;
|
|
||||||
switch (mode) {
|
|
||||||
case GL_QUERY_WAIT:
|
|
||||||
case GL_QUERY_NO_WAIT:
|
|
||||||
case GL_QUERY_BY_REGION_WAIT:
|
|
||||||
case GL_QUERY_BY_REGION_NO_WAIT:
|
|
||||||
inverted = false;
|
|
||||||
break;
|
|
||||||
case GL_QUERY_WAIT_INVERTED:
|
|
||||||
case GL_QUERY_NO_WAIT_INVERTED:
|
|
||||||
case GL_QUERY_BY_REGION_WAIT_INVERTED:
|
|
||||||
case GL_QUERY_BY_REGION_NO_WAIT_INVERTED:
|
|
||||||
inverted = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "mode is not a conditional render mode.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MG_State::pGLContext->IsConditionalRenderActive()) {
|
|
||||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is already active.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
|
||||||
const auto* queryObject = FindQueryObjectLocked(id);
|
|
||||||
// A generated NAME is not yet a query object; it becomes one at its first use with a
|
|
||||||
// target (the same rule glIsQuery answers by).
|
|
||||||
if (!queryObject || (!queryObject->created && queryObject->target == 0)) {
|
|
||||||
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "id is not the name of a query object.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (queryObject->active) {
|
|
||||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "The query object is still active.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (queryObject->target != GL_SAMPLES_PASSED && queryObject->target != GL_ANY_SAMPLES_PASSED &&
|
|
||||||
queryObject->target != GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
|
||||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
|
|
||||||
"Conditional rendering requires an occlusion query object.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolved ONCE, here, and by WAITING even for the _NO_WAIT modes: the spec lets those
|
|
||||||
// render instead of stalling, so always waiting is conforming and is the only choice that
|
|
||||||
// gives the whole block one deterministic verdict. Reading it per command instead would
|
|
||||||
// let a result that lands mid-block change the answer half way through.
|
|
||||||
Uint64 samplesPassed = 0;
|
|
||||||
if (!GetQueryObjectValue(id, GL_QUERY_RESULT, __FUNCTION__, samplesPassed)) return;
|
|
||||||
const Bool passed = samplesPassed != 0;
|
|
||||||
MG_State::pGLContext->BeginConditionalRender(id, mode, inverted ? passed : !passed);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EndConditionalRender() {
|
|
||||||
if (!MG_State::pGLContext->IsConditionalRenderActive()) {
|
|
||||||
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Conditional rendering is not active.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MG_State::pGLContext->EndConditionalRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
void GetQueryiv(GLenum target, GLenum pname, GLint* params) {
|
void GetQueryiv(GLenum target, GLenum pname, GLint* params) {
|
||||||
if (!params) {
|
if (!params) {
|
||||||
return;
|
return;
|
||||||
@@ -777,39 +648,4 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
|
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
|
||||||
GetQueryiv(target, pname, params);
|
GetQueryiv(target, pname, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DestroyAllQueryObjects() {
|
|
||||||
// Detach the registry under the lock, release outside it - same discipline
|
|
||||||
// (and the same accepted teardown race) as DestroyAllSyncObjects. Without
|
|
||||||
// this drain, every query the app left undeleted survived full library
|
|
||||||
// teardown in the process-global registry: the objects and their backend
|
|
||||||
// wrappers leaked across Destroy/Initialize cycles, stale ids kept
|
|
||||||
// answering IsQuery == GL_TRUE in the re-initialized library, and a later
|
|
||||||
// glDeleteQueries could hand the OLD backend's handle to a DIFFERENT
|
|
||||||
// backend's DeleteBackendQuery, which casts it to the wrong wrapper type.
|
|
||||||
UnorderedMap<GLuint, QueryObject*> orphans;
|
|
||||||
{
|
|
||||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
|
||||||
orphans.swap(g_liveQueryObjects);
|
|
||||||
g_activeTimeElapsedQueryId = 0;
|
|
||||||
g_activePrimitivesWrittenQueryId = 0;
|
|
||||||
g_activePrimitivesGeneratedQueryId = 0;
|
|
||||||
g_activeSamplesPassedQueryId = 0;
|
|
||||||
}
|
|
||||||
if (orphans.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Backend handles must be released by the backend that created them, so
|
|
||||||
// this runs while the function table is still populated. Both backends'
|
|
||||||
// DeleteBackendQuery are generation-guarded, so a handle whose renderer
|
|
||||||
// or ES context is already gone frees only the wrapper.
|
|
||||||
const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery;
|
|
||||||
for (const auto& [_, queryObject] : orphans) {
|
|
||||||
if (deleteBackendQuery && queryObject->backendHandle) {
|
|
||||||
deleteBackendQuery(queryObject->backendHandle);
|
|
||||||
}
|
|
||||||
delete queryObject;
|
|
||||||
}
|
|
||||||
MGLOG_D("DestroyAllQueryObjects: reclaimed %zu query object(s) the app left undeleted", orphans.size());
|
|
||||||
}
|
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -29,18 +29,4 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||||
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||||
void QueryCounter(GLuint id, GLenum target);
|
void QueryCounter(GLuint id, GLenum target);
|
||||||
// Conditional rendering (GL 4.6 core 10.9). Implemented here rather than beside the drawing
|
|
||||||
// entry points because the predicate is a QUERY OBJECT's result, and the object registry -
|
|
||||||
// with the lock that guards it - lives in this file.
|
|
||||||
void BeginConditionalRender(GLuint id, GLenum mode);
|
|
||||||
void EndConditionalRender();
|
|
||||||
// Destroys every still-registered query object exactly as DeleteQueries would.
|
|
||||||
// GL requires queries to die with their context; called only from full library
|
|
||||||
// teardown (DestroyImpl), where no context survives on any thread, so the
|
|
||||||
// process-global registry can be drained wholesale. Must run while the backend
|
|
||||||
// function table is still populated: each backend handle has to be released by
|
|
||||||
// the backend that created it, never by a later re-initialized one (whose
|
|
||||||
// DeleteBackendQuery would cast the wrapper to the wrong backend's type).
|
|
||||||
// Same contract as DestroyAllSyncObjects.
|
|
||||||
void DestroyAllQueryObjects();
|
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -661,42 +614,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"Compressed texture formats are not supported."));
|
"Compressed texture formats are not supported."));
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL_TEXTURE_WIDTH of a buffer texture: how many texels of the texture's internal format fit
|
// glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain. The only
|
||||||
// in the buffer range it addresses, CLAMPED to GL_MAX_TEXTURE_BUFFER_SIZE. Attaching a larger
|
// other storage type the state layer knows is GL_TEXTURE_BUFFER (TextureStorageType is
|
||||||
// buffer is legal (GL 4.6 core 8.9) - the texture simply addresses the first
|
// {Mipmap, Buffer}), whose level geometry this stack does not track yet. Report that instead
|
||||||
// MAX_TEXTURE_BUFFER_SIZE texels of it, and that clamped count is what WIDTH reports.
|
// of throwing: THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes
|
||||||
//
|
// the process down, which is never an acceptable answer to a query - see the same reasoning
|
||||||
// GL_TEXTURE_BUFFER_SIZE is deliberately NOT clamped the same way: it reports the range in
|
// above for the compressed-format path.
|
||||||
// basic machine units exactly as glTexBuffer/glTexBufferRange were given it. Swapping the two
|
|
||||||
// fails KHR-GL43.texture_buffer.texture_buffer_max_size in the opposite direction.
|
|
||||||
GLint GetBufferTextureTexelWidth(const MG_State::GLState::ITextureObject* textureObject) {
|
|
||||||
const SizeT texelByteSize = MG_Util::GetSizedInternalFormatSizeInBytes(textureObject->GetFormat());
|
|
||||||
// A format with no known footprint has no texel count to report; answering 0 beats
|
|
||||||
// dividing by it.
|
|
||||||
if (texelByteSize == 0) return 0;
|
|
||||||
const auto* bufferTextureObject =
|
|
||||||
static_cast<const MG_State::GLState::TextureObjectBuffer*>(textureObject);
|
|
||||||
const SizeT texelCount = bufferTextureObject->GetBufferRangeSizeInBytes() / texelByteSize;
|
|
||||||
const SizeT maxTexelCount = static_cast<SizeT>(
|
|
||||||
std::max(0, MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxTextureBufferSize));
|
|
||||||
return static_cast<GLint>(std::min(texelCount, maxTexelCount));
|
|
||||||
}
|
|
||||||
|
|
||||||
// glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain, and (since
|
|
||||||
// the buffer-texture arms above) out of the attached buffer range for GL_TEXTURE_BUFFER. This
|
|
||||||
// is what is left: a storage class with no level geometry at all. Report it instead of
|
|
||||||
// throwing - THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes the
|
|
||||||
// process down, which is never an acceptable answer to a query - see the same reasoning above
|
|
||||||
// for the compressed-format path.
|
|
||||||
void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) {
|
void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) {
|
||||||
MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for this texture's "
|
MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for texture-buffer "
|
||||||
"storage class; recording GL_INVALID_OPERATION instead of terminating",
|
"storage; recording GL_INVALID_OPERATION instead of terminating",
|
||||||
caller, MG_Util::ConvertGLEnumToString(pname).c_str());
|
caller, MG_Util::ConvertGLEnumToString(pname).c_str());
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
MakeUnique<GenericErrorInfo>(
|
MakeUnique<GenericErrorInfo>(
|
||||||
"MG_Impl/GLImpl", caller,
|
"MG_Impl/GLImpl", caller,
|
||||||
"Level queries are not supported for this texture's storage class."));
|
"Level queries are not supported for texture-buffer storage."));
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -712,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(
|
||||||
@@ -775,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2261,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) {
|
||||||
@@ -2427,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) {
|
||||||
@@ -2522,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) {
|
||||||
@@ -3080,15 +2935,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = textureObject->GetSamplerObject()->GetMaxAnisotropy();
|
*params = textureObject->GetSamplerObject()->GetMaxAnisotropy();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// GL 4.6 core 8.11 lists this among the parameters EVERY GetTexParameter form answers.
|
|
||||||
// It was handled by the iv/Iiv/Iuiv getters and missed by this one, so the float query
|
|
||||||
// raised GL_INVALID_ENUM and left the caller's float untouched - which is what
|
|
||||||
// KHR-GL4x.shader_image_load_store.basic-api-texParam reads back.
|
|
||||||
case GL_IMAGE_FORMAT_COMPATIBILITY_TYPE:
|
|
||||||
if (params) {
|
|
||||||
*params = static_cast<GLfloat>(GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case GL_DEPTH_STENCIL_TEXTURE_MODE:
|
case GL_DEPTH_STENCIL_TEXTURE_MODE:
|
||||||
if (params) {
|
if (params) {
|
||||||
*params = static_cast<GLfloat>(textureObject->GetDepthStencilTextureMode());
|
*params = static_cast<GLfloat>(textureObject->GetDepthStencilTextureMode());
|
||||||
@@ -3138,9 +2984,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
|
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TextureStorageType::Buffer:
|
|
||||||
*params = GetBufferTextureTexelWidth(textureObject.get());
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||||
break;
|
break;
|
||||||
@@ -3156,9 +2999,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
|
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TextureStorageType::Buffer:
|
|
||||||
*params = 1; // a buffer texture is one-dimensional
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||||
break;
|
break;
|
||||||
@@ -3174,9 +3014,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
|
*params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TextureStorageType::Buffer:
|
|
||||||
*params = 1; // a buffer texture is one-dimensional
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
|
||||||
break;
|
break;
|
||||||
@@ -3246,31 +3083,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GL_TEXTURE_BUFFER_SIZE:
|
|
||||||
case GL_TEXTURE_BUFFER_OFFSET: {
|
|
||||||
// GL 4.6 core 8.9: both describe the window of the attached buffer a GL_TEXTURE_BUFFER
|
|
||||||
// texture addresses, so there is nothing to report for any other storage - which is
|
|
||||||
// INVALID_OPERATION, the same shape GL_TEXTURE_COMPRESSED_IMAGE_SIZE guards itself with
|
|
||||||
// above.
|
|
||||||
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>(
|
|
||||||
"MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
|
|
||||||
"GL_TEXTURE_BUFFER_SIZE / GL_TEXTURE_BUFFER_OFFSET need a buffer texture."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (params) {
|
|
||||||
const auto* bufferTextureObject =
|
|
||||||
static_cast<MG_State::GLState::TextureObjectBuffer*>(textureObject.get());
|
|
||||||
// Basic machine units, and UNCLAMPED - see GetBufferTextureTexelWidth for why this
|
|
||||||
// half does not take the GL_MAX_TEXTURE_BUFFER_SIZE clamp that WIDTH does.
|
|
||||||
*params = static_cast<GLint>(pname == GL_TEXTURE_BUFFER_SIZE
|
|
||||||
? bufferTextureObject->GetBufferRangeSizeInBytes()
|
|
||||||
: bufferTextureObject->GetBufferRangeOffset());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
|
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
|
||||||
@@ -3310,9 +3122,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
|
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TextureStorageType::Buffer:
|
|
||||||
*params = (GLfloat)GetBufferTextureTexelWidth(textureObject.get());
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||||
break;
|
break;
|
||||||
@@ -3328,9 +3137,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
|
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TextureStorageType::Buffer:
|
|
||||||
*params = 1.0f; // a buffer texture is one-dimensional
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||||
break;
|
break;
|
||||||
@@ -3346,9 +3152,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
|
*params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TextureStorageType::Buffer:
|
|
||||||
*params = 1.0f; // a buffer texture is one-dimensional
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
|
||||||
break;
|
break;
|
||||||
@@ -3416,27 +3219,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GL_TEXTURE_BUFFER_SIZE:
|
|
||||||
case GL_TEXTURE_BUFFER_OFFSET: {
|
|
||||||
// See GetTexLevelParameteriv_State: both describe the attached buffer range of a
|
|
||||||
// GL_TEXTURE_BUFFER texture, so any other storage makes the query INVALID_OPERATION.
|
|
||||||
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>(
|
|
||||||
"MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
|
|
||||||
"GL_TEXTURE_BUFFER_SIZE / GL_TEXTURE_BUFFER_OFFSET need a buffer texture."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (params) {
|
|
||||||
const auto* bufferTextureObject =
|
|
||||||
static_cast<MG_State::GLState::TextureObjectBuffer*>(textureObject.get());
|
|
||||||
*params = static_cast<GLfloat>(pname == GL_TEXTURE_BUFFER_SIZE
|
|
||||||
? bufferTextureObject->GetBufferRangeSizeInBytes()
|
|
||||||
: bufferTextureObject->GetBufferRangeOffset());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
|
ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
|
||||||
@@ -3583,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;
|
||||||
@@ -3596,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3643,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>(
|
||||||
@@ -3669,166 +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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The per-axis extent of one endpoint's image AS THIS ENTRY POINT ADDRESSES IT, which is
|
|
||||||
// not always the level extent this frontend stores.
|
|
||||||
//
|
|
||||||
// GL 4.6 core 18.3.2 treats EVERY array texture as a stack of slices addressed by z, and
|
|
||||||
// gives a 1D array an image height of 1. This frontend stores a 1D array the way
|
|
||||||
// glTexImage2D(GL_TEXTURE_1D_ARRAY, w, layers) writes it instead - layers on y - so the
|
|
||||||
// two views have to be told apart here. Measuring y against the LAYER count is what let
|
|
||||||
// srcY = 14 on a 16-wide, 16-layer 1D array come back GL_NO_ERROR
|
|
||||||
// (KHR-GL43.copy_image.exceeding_boundaries, the src_test_case y variants); the CTS is
|
|
||||||
// unambiguous about the convention, forcing height = 1 for 1D and 1D_ARRAY and listing
|
|
||||||
// 1D_ARRAY as multilayer.
|
|
||||||
//
|
|
||||||
// A CUBE MAP is the other target whose z bound is not the level extent: this frontend
|
|
||||||
// keeps its six faces as six separate one-slice upload targets, so the level says 1 and
|
|
||||||
// the real bound is 6. A cube-map ARRAY is one upload target whose depth already counts
|
|
||||||
// layer-faces, and every remaining target is answered by the level extent verbatim.
|
|
||||||
IntVec3 GetCopyImageEndpointRegionBounds(const MG_Backend::CopyImageEndpoint& endpoint,
|
|
||||||
const IntVec3& levelSize) {
|
|
||||||
const TextureTarget target = (!endpoint.IsRenderbuffer() && endpoint.Texture)
|
|
||||||
? endpoint.Texture->GetTarget()
|
|
||||||
: TextureTarget::Unknown;
|
|
||||||
if (target == TextureTarget::TextureCubeMap) {
|
|
||||||
return {levelSize.x(), levelSize.y(), 6};
|
|
||||||
}
|
|
||||||
if (target == TextureTarget::Texture1DArray) {
|
|
||||||
return {levelSize.x(), 1, std::max(levelSize.y(), 1)};
|
|
||||||
}
|
|
||||||
return {levelSize.x(), levelSize.y(), std::max(levelSize.z(), 1)};
|
|
||||||
}
|
|
||||||
|
|
||||||
// GL 4.6 core 18.3.2 requires INVALID_VALUE when the region exceeds either image's
|
|
||||||
// boundaries. The only bounds-shaped call this validator used to make was
|
|
||||||
// ValidateCopyImageBlockAlignment, whose first line returns true for every UNCOMPRESSED
|
|
||||||
// format - so no uncompressed copy was bounded at all, and the z extent could not be
|
|
||||||
// bounded even in principle because srcZ/dstZ never reached the validator. Texture
|
|
||||||
// endpoints were covered only by accident, through the ES driver's own error, which the
|
|
||||||
// DirectGLES backend logs and swallows rather than reporting; a GL_RENDERBUFFER endpoint
|
|
||||||
// got neither (KHR-GL43.copy_image.exceeding_boundaries).
|
|
||||||
Bool ValidateCopyImageRegionBounds(const MG_Backend::CopyImageEndpoint& endpoint, const IntVec3& levelSize,
|
|
||||||
GLint x, GLint y, GLint z, GLsizei width, GLsizei height, GLsizei depth,
|
|
||||||
const char* endpointName) {
|
|
||||||
// An extent this frontend does not know cannot bound anything, and guessing would
|
|
||||||
// reject a copy GL allows. Every caller has already established that the level
|
|
||||||
// exists and that the image is complete, so this is a belt-and-braces guard.
|
|
||||||
if (levelSize.x() <= 0 || levelSize.y() <= 0) return true;
|
|
||||||
const IntVec3 bounds = GetCopyImageEndpointRegionBounds(endpoint, levelSize);
|
|
||||||
if (x >= 0 && y >= 0 && z >= 0 && static_cast<Int64>(x) + width <= bounds.x() &&
|
|
||||||
static_cast<Int64>(y) + height <= bounds.y() && static_cast<Int64>(z) + depth <= bounds.z()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidValue,
|
|
||||||
MakeUnique<GenericErrorInfo>(
|
|
||||||
"MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
|
|
||||||
std::format("The {} region [{}, {}, {}] + [{} x {} x {}] does not fit inside the {} x {} x {} "
|
|
||||||
"image.",
|
|
||||||
endpointName, x, y, z, width, height, depth, bounds.x(), bounds.y(), bounds.z())));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} // 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, GLint srcZ,
|
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, GLint dstZ,
|
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
|
||||||
@@ -3836,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) ||
|
||||||
@@ -3851,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) {
|
||||||
@@ -3868,55 +3505,43 @@ 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,
|
||||||
dstLevelSize.x(), dstLevelSize.y(), "destination")) {
|
dstLevelSize.x(), dstLevelSize.y(), "destination")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// One region extent, measured against both images: GL 4.6 core 18.3.2 gives the copy a
|
|
||||||
// single width/height/depth and requires it to fit in the source AND the destination.
|
|
||||||
if (!ValidateCopyImageRegionBounds(src, srcLevelSize, srcX, srcY, srcZ, srcWidth, srcHeight, srcDepth,
|
|
||||||
"source") ||
|
|
||||||
!ValidateCopyImageRegionBounds(dst, dstLevelSize, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth,
|
|
||||||
"destination")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4426,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"));
|
||||||
@@ -4465,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;
|
||||||
@@ -4492,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4768,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.
|
||||||
@@ -4842,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));
|
||||||
@@ -4855,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__);
|
||||||
@@ -4897,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);
|
||||||
@@ -4910,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));
|
||||||
@@ -5083,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
|
||||||
@@ -5119,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__);
|
||||||
@@ -5130,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),
|
||||||
@@ -6132,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) {
|
dstLevel, dstX, dstY, srcWidth, srcHeight, srcDepth)) {
|
||||||
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, srcZ, dst, dstTarget,
|
|
||||||
dstLevel, dstX, dstY, dstZ, 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)) {
|
||||||
|
|||||||
@@ -514,17 +514,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form,
|
// recorded DataType is always Float64 - what IsLong adds is that this is the *unconverted* form,
|
||||||
// as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion.
|
// as opposed to VertexAttribFormat(GL_DOUBLE), which asks for a float conversion.
|
||||||
//
|
//
|
||||||
// Whether the backend can FEED it at full precision is detected, not assumed: DirectVulkan
|
// Whether the backend can feed it is detected, not assumed: DirectVulkan needs shaderFloat64,
|
||||||
// needs shaderFloat64, and DirectGLES can never have it at all. What that costs is PRECISION,
|
// and DirectGLES can never have it at all. A backend without it declines here, loudly - GL error
|
||||||
// not the call and no longer the array: GL 4.6 core 10.3.2 defines no error for a well-formed
|
// plus a log line naming the reason - rather than accepting state no draw could honour and
|
||||||
// glVertexAttribLFormat, and a GL 4.3 context has 64-bit attributes in core, so declining the
|
// rendering garbage. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp.
|
||||||
// call would be non-conformant and would make the four pure state queries
|
|
||||||
// (VERTEX_ATTRIB_ARRAY_SIZE / _TYPE / _LONG / _RELATIVE_OFFSET) unanswerable
|
|
||||||
// (KHR-GL43.vertex_attrib_binding.basic-state1/3). The format is therefore RECORDED here and
|
|
||||||
// the array is NARROWED to float32 at draw, matching the fp64 demotion every shader already
|
|
||||||
// gets (DemoteFloat64Pass) - loudly, once, naming the cost. The matching startup POST row is in
|
|
||||||
// MG_Util/SelfTest/DriverPost.cpp; the draw-side narrowing is DirectGLES/Managers.cpp and, on
|
|
||||||
// DirectVulkan, VertexInputStateFactory's Float64 case.
|
|
||||||
static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
|
static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
|
||||||
GLuint attribindex, GLint size, GLenum type,
|
GLuint attribindex, GLint size, GLenum type,
|
||||||
GLuint relativeoffset) {
|
GLuint relativeoffset) {
|
||||||
@@ -535,11 +528,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!MG_Backend::pActiveBackendObject ||
|
if (!MG_Backend::pActiveBackendObject ||
|
||||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||||
MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
MGLOG_W_ONCE("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
||||||
"backend has no double-precision vertex attribute support - the format is recorded "
|
"backend has no double-precision vertex attribute support - see the "
|
||||||
"and queryable, and the array is FETCHED AT FLOAT32 PRECISION at draw (the same "
|
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
|
||||||
"narrowing the shader's dvec inputs already get); see the \"64-bit vertex "
|
|
||||||
"attributes\" / \"shaderFloat64\" POST row for what that costs",
|
|
||||||
attribindex);
|
attribindex);
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "VertexAttribLFormat",
|
||||||
|
"64-bit vertex attributes are not supported by this backend."));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type),
|
vao->SetAttributeFormatSeparate(attribindex, size, MG_Util::ConvertGLEnumToDataType(type),
|
||||||
|
|||||||
@@ -175,14 +175,14 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
|
|
||||||
struct ContextObject {
|
struct ContextObject {
|
||||||
Display* XDisplay = nullptr;
|
Display* XDisplay = nullptr;
|
||||||
EGLDisplay Display = EGL_NO_DISPLAY;
|
EGLDisplay Dpy = EGL_NO_DISPLAY;
|
||||||
EGLConfig Config = nullptr;
|
EGLConfig Config = nullptr;
|
||||||
EGLContext Context = EGL_NO_CONTEXT;
|
EGLContext Context = EGL_NO_CONTEXT;
|
||||||
const FBConfigInfo* FBConfig = nullptr;
|
const FBConfigInfo* FBConfig = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DrawableSurface {
|
struct DrawableSurface {
|
||||||
EGLDisplay Display = EGL_NO_DISPLAY;
|
EGLDisplay Dpy = EGL_NO_DISPLAY;
|
||||||
EGLSurface Surface = EGL_NO_SURFACE;
|
EGLSurface Surface = EGL_NO_SURFACE;
|
||||||
Uint32 Width = 0;
|
Uint32 Width = 0;
|
||||||
Uint32 Height = 0;
|
Uint32 Height = 0;
|
||||||
@@ -294,7 +294,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
if (width == surface.Width && height == surface.Height) {
|
if (width == surface.Width && height == surface.Height) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (EGLImpl::ResizePlatformWindowSurface(surface.Display, surface.Surface,
|
if (EGLImpl::ResizePlatformWindowSurface(surface.Dpy, surface.Surface,
|
||||||
static_cast<EGLint>(width),
|
static_cast<EGLint>(width),
|
||||||
static_cast<EGLint>(height))) {
|
static_cast<EGLint>(height))) {
|
||||||
surface.Width = width;
|
surface.Width = width;
|
||||||
@@ -324,7 +324,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
EGL_NONE,
|
EGL_NONE,
|
||||||
};
|
};
|
||||||
EGLSurface surface = EGLImpl::CreatePlatformWindowSurface(
|
EGLSurface surface = EGLImpl::CreatePlatformWindowSurface(
|
||||||
context.Display, context.Config, reinterpret_cast<void*>(drawable), attribs);
|
context.Dpy, context.Config, reinterpret_cast<void*>(drawable), attribs);
|
||||||
if (surface == EGL_NO_SURFACE) {
|
if (surface == EGL_NO_SURFACE) {
|
||||||
MGLOG_E_ONCE("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
|
MGLOG_E_ONCE("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
|
||||||
width, height);
|
width, height);
|
||||||
@@ -332,7 +332,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DrawableSurface record;
|
DrawableSurface record;
|
||||||
record.Display = context.Display;
|
record.Dpy = context.Dpy;
|
||||||
record.Surface = surface;
|
record.Surface = surface;
|
||||||
record.Width = width;
|
record.Width = width;
|
||||||
record.Height = height;
|
record.Height = height;
|
||||||
@@ -388,7 +388,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
|
|
||||||
ContextObject object;
|
ContextObject object;
|
||||||
object.XDisplay = dpy;
|
object.XDisplay = dpy;
|
||||||
object.Display = display;
|
object.Dpy = display;
|
||||||
object.Config = config;
|
object.Config = config;
|
||||||
object.Context = eglContext;
|
object.Context = eglContext;
|
||||||
object.FBConfig = fbconfig;
|
object.FBConfig = fbconfig;
|
||||||
@@ -895,7 +895,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (object->Context != EGL_NO_CONTEXT) {
|
if (object->Context != EGL_NO_CONTEXT) {
|
||||||
EGLImpl::DestroyContext(object->Display, object->Context);
|
EGLImpl::DestroyContext(object->Dpy, object->Context);
|
||||||
}
|
}
|
||||||
Contexts().erase(context);
|
Contexts().erase(context);
|
||||||
}
|
}
|
||||||
@@ -929,7 +929,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface,
|
if (!EGLImpl::MakeCurrent(object->Dpy, surface->Surface, surface->Surface,
|
||||||
object->Context)) {
|
object->Context)) {
|
||||||
MGLOG_E_ONCE("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
|
MGLOG_E_ONCE("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -962,7 +962,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SyncSurfaceSize(dpy, drawable, it->second);
|
SyncSurfaceSize(dpy, drawable, it->second);
|
||||||
EGLImpl::SwapBuffers(it->second.Display, it->second.Surface);
|
EGLImpl::SwapBuffers(it->second.Dpy, it->second.Surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
GLXDrawableHandle CreateWindow(Display*, GLXFBConfigHandle config, GLXDrawableHandle window,
|
GLXDrawableHandle CreateWindow(Display*, GLXFBConfigHandle config, GLXDrawableHandle window,
|
||||||
@@ -988,7 +988,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
|
|||||||
if (it == surfaces.end()) {
|
if (it == surfaces.end()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
EGLImpl::DestroySurface(it->second.Display, it->second.Surface);
|
EGLImpl::DestroySurface(it->second.Dpy, it->second.Surface);
|
||||||
surfaces.erase(it);
|
surfaces.erase(it);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
@@ -59,7 +54,6 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/AsyncCompileScenario.cpp
|
Scenarios/AsyncCompileScenario.cpp
|
||||||
Scenarios/XfbAfterClipDistanceScenario.cpp
|
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||||
Scenarios/ThreeChannelAttachmentScenario.cpp
|
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||||
Scenarios/SnormAttachmentScenario.cpp
|
|
||||||
Scenarios/PipelineFailureScenario.cpp
|
Scenarios/PipelineFailureScenario.cpp
|
||||||
Scenarios/AdvertisedLimitsScenario.cpp
|
Scenarios/AdvertisedLimitsScenario.cpp
|
||||||
Scenarios/PixelStoreSweepScenario.cpp
|
Scenarios/PixelStoreSweepScenario.cpp
|
||||||
@@ -74,37 +68,20 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/DoublePrecisionScenario.cpp
|
Scenarios/DoublePrecisionScenario.cpp
|
||||||
Scenarios/UniformInitializerScenario.cpp
|
Scenarios/UniformInitializerScenario.cpp
|
||||||
Scenarios/SwizzleAccessRoutineScenario.cpp
|
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||||
Scenarios/IterationRPFirstReductionScenario.cpp
|
|
||||||
Scenarios/IterationRPProgram203Scenario.cpp
|
|
||||||
Scenarios/IterationRPScratchFixScenario.cpp
|
|
||||||
Scenarios/ProgramPipelineScenario.cpp
|
Scenarios/ProgramPipelineScenario.cpp
|
||||||
Scenarios/ImageLoadStoreSsoScenario.cpp
|
Scenarios/ImageLoadStoreSsoScenario.cpp
|
||||||
Scenarios/ImageTargetKindScenario.cpp
|
Scenarios/ImageTargetKindScenario.cpp
|
||||||
Scenarios/ImageFormatQualifierScenario.cpp
|
Scenarios/ImageFormatQualifierScenario.cpp
|
||||||
Scenarios/NonCoreImageFormatScenario.cpp
|
|
||||||
Scenarios/ImageSizeAfterRespecScenario.cpp
|
|
||||||
Scenarios/SsboDeclarationFormScenario.cpp
|
Scenarios/SsboDeclarationFormScenario.cpp
|
||||||
Scenarios/Glsl420DeclarationScenario.cpp
|
Scenarios/Glsl420DeclarationScenario.cpp
|
||||||
Scenarios/IoBlockNameCollisionScenario.cpp
|
|
||||||
Scenarios/TessellationDrawModeScenario.cpp
|
|
||||||
Scenarios/GeometryDrawModeScenario.cpp
|
|
||||||
Scenarios/PostLinkAttachScenario.cpp
|
|
||||||
Scenarios/FormatlessImageBakeScenario.cpp
|
|
||||||
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
||||||
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
|
||||||
Scenarios/PackedWordReadbackScenario.cpp
|
|
||||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||||
Scenarios/LayeredTextureReadbackScenario.cpp
|
|
||||||
Scenarios/AtomicCounterScenario.cpp
|
|
||||||
Scenarios/SsboArrayDynamicIndexScenario.cpp
|
|
||||||
Scenarios/StorageBufferRegrowScenario.cpp
|
|
||||||
Scenarios/RelinkStageSetScenario.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||||
@@ -115,20 +92,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
|
||||||
@@ -137,10 +103,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
|
||||||
@@ -261,19 +223,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
|
||||||
|
|||||||
@@ -15,16 +15,6 @@
|
|||||||
#include <ostream>
|
#include <ostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
#if defined(_WIN32)
|
|
||||||
#define WIN32_LEAN_AND_MEAN
|
|
||||||
#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
|
|
||||||
|
|
||||||
// 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
|
||||||
// first, then glcorearb.h for the 3.x+ entry points. This binary links
|
// first, then glcorearb.h for the 3.x+ entry points. This binary links
|
||||||
// MobileGL_s, so every gl*/egl* below binds to MobileGL's implementation, not
|
// MobileGL_s, so every gl*/egl* below binds to MobileGL's implementation, not
|
||||||
@@ -42,7 +32,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>
|
||||||
@@ -63,83 +53,6 @@ namespace MGITest {
|
|||||||
constexpr int kSurfaceWidth = 128;
|
constexpr int kSurfaceWidth = 128;
|
||||||
constexpr int kSurfaceHeight = 96;
|
constexpr int kSurfaceHeight = 96;
|
||||||
|
|
||||||
#if defined(_WIN32)
|
|
||||||
HWND g_testWindow = nullptr;
|
|
||||||
|
|
||||||
HWND CreateTestWindow() {
|
|
||||||
static const wchar_t* const kClassName = L"MobileGLIntegrationTestWindow";
|
|
||||||
static bool registered = false;
|
|
||||||
if (!registered) {
|
|
||||||
WNDCLASSW windowClass{};
|
|
||||||
windowClass.lpfnWndProc = DefWindowProcW;
|
|
||||||
windowClass.hInstance = GetModuleHandleW(nullptr);
|
|
||||||
windowClass.lpszClassName = kClassName;
|
|
||||||
if (RegisterClassW(&windowClass) == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
registered = true;
|
|
||||||
}
|
|
||||||
return CreateWindowExW(0, kClassName, L"MobileGL Integration Test", WS_OVERLAPPEDWINDOW,
|
|
||||||
CW_USEDEFAULT, CW_USEDEFAULT, kSurfaceWidth, kSurfaceHeight, nullptr, nullptr,
|
|
||||||
GetModuleHandleW(nullptr), nullptr);
|
|
||||||
}
|
|
||||||
#elif defined(__ANDROID__)
|
|
||||||
AImageReader* g_imageReader = nullptr;
|
|
||||||
ANativeWindow* g_imageReaderWindow = nullptr;
|
|
||||||
|
|
||||||
void DrainImageReader(void*, AImageReader* reader) {
|
|
||||||
AImage* image = nullptr;
|
|
||||||
if (AImageReader_acquireNextImage(reader, &image) == AMEDIA_OK && image != nullptr) {
|
|
||||||
AImage_delete(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CreateImageReaderWindow() {
|
|
||||||
if (g_imageReaderWindow != nullptr) return true;
|
|
||||||
constexpr int kMaxImages = 4;
|
|
||||||
const media_status_t status = AImageReader_newWithUsage(
|
|
||||||
kSurfaceWidth, kSurfaceHeight, AIMAGE_FORMAT_RGBA_8888,
|
|
||||||
AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT,
|
|
||||||
kMaxImages, &g_imageReader);
|
|
||||||
if (status != AMEDIA_OK || g_imageReader == nullptr) return false;
|
|
||||||
|
|
||||||
AImageReader_ImageListener listener = {nullptr, DrainImageReader};
|
|
||||||
AImageReader_setImageListener(g_imageReader, &listener);
|
|
||||||
if (AImageReader_getWindow(g_imageReader, &g_imageReaderWindow) != AMEDIA_OK ||
|
|
||||||
g_imageReaderWindow == nullptr) {
|
|
||||||
AImageReader_setImageListener(g_imageReader, nullptr);
|
|
||||||
AImageReader_delete(g_imageReader);
|
|
||||||
g_imageReader = nullptr;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ANativeWindow_acquire(g_imageReaderWindow);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DestroyImageReaderWindow() {
|
|
||||||
if (g_imageReaderWindow != nullptr) {
|
|
||||||
ANativeWindow_release(g_imageReaderWindow);
|
|
||||||
g_imageReaderWindow = nullptr;
|
|
||||||
}
|
|
||||||
if (g_imageReader != nullptr) {
|
|
||||||
AImageReader_setImageListener(g_imageReader, nullptr);
|
|
||||||
AImageReader_delete(g_imageReader);
|
|
||||||
g_imageReader = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
bool UseWindowSurface() {
|
|
||||||
#if defined(_WIN32)
|
|
||||||
const char* value = std::getenv("MOBILEGL_ITEST_WINDOW_SURFACE");
|
|
||||||
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
|
||||||
#elif defined(__ANDROID__)
|
|
||||||
return true;
|
|
||||||
#else
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string EnvOr(const char* name, const char* fallback) {
|
std::string EnvOr(const char* name, const char* fallback) {
|
||||||
const char* value = std::getenv(name);
|
const char* value = std::getenv(name);
|
||||||
return (value != nullptr && value[0] != '\0') ? std::string(value) : std::string(fallback);
|
return (value != nullptr && value[0] != '\0') ? std::string(value) : std::string(fallback);
|
||||||
@@ -174,10 +87,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;
|
||||||
@@ -221,9 +134,8 @@ namespace MGITest {
|
|||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool useWindowSurface = UseWindowSurface();
|
|
||||||
const EGLint configAttribs[] = {EGL_SURFACE_TYPE,
|
const EGLint configAttribs[] = {EGL_SURFACE_TYPE,
|
||||||
useWindowSurface ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT,
|
EGL_PBUFFER_BIT,
|
||||||
EGL_RED_SIZE,
|
EGL_RED_SIZE,
|
||||||
8,
|
8,
|
||||||
EGL_GREEN_SIZE,
|
EGL_GREEN_SIZE,
|
||||||
@@ -240,9 +152,7 @@ namespace MGITest {
|
|||||||
EGLConfig config = nullptr;
|
EGLConfig config = nullptr;
|
||||||
EGLint configCount = 0;
|
EGLint configCount = 0;
|
||||||
if (eglChooseConfig(display, configAttribs, &config, 1, &configCount) != EGL_TRUE || configCount < 1) {
|
if (eglChooseConfig(display, configAttribs, &config, 1, &configCount) != EGL_TRUE || configCount < 1) {
|
||||||
outReason = WithEglError(useWindowSurface
|
outReason = WithEglError("eglChooseConfig found no pbuffer-capable RGBA8/D24 config");
|
||||||
? "eglChooseConfig found no window-capable RGBA8/D24 config"
|
|
||||||
: "eglChooseConfig found no pbuffer-capable RGBA8/D24 config");
|
|
||||||
return 4;
|
return 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,32 +166,10 @@ namespace MGITest {
|
|||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
EGLSurface surface = EGL_NO_SURFACE;
|
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
|
||||||
if (useWindowSurface) {
|
EGLSurface surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
|
||||||
#if defined(_WIN32)
|
|
||||||
if (g_testWindow == nullptr) g_testWindow = CreateTestWindow();
|
|
||||||
if (g_testWindow == nullptr) {
|
|
||||||
outReason = "failed to create the Windows integration-test window";
|
|
||||||
return 6;
|
|
||||||
}
|
|
||||||
surface = eglCreateWindowSurface(display, config, g_testWindow, nullptr);
|
|
||||||
#elif defined(__ANDROID__)
|
|
||||||
if (!CreateImageReaderWindow()) {
|
|
||||||
outReason = "failed to create the Android AImageReader integration-test window";
|
|
||||||
return 6;
|
|
||||||
}
|
|
||||||
surface = eglCreateWindowSurface(display, config, g_imageReaderWindow, nullptr);
|
|
||||||
#endif
|
|
||||||
} else {
|
|
||||||
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
|
|
||||||
surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
|
|
||||||
}
|
|
||||||
if (surface == EGL_NO_SURFACE) {
|
if (surface == EGL_NO_SURFACE) {
|
||||||
#if defined(__ANDROID__)
|
outReason = WithEglError("eglCreatePbufferSurface failed");
|
||||||
DestroyImageReaderWindow();
|
|
||||||
#endif
|
|
||||||
outReason = WithEglError(useWindowSurface ? "eglCreateWindowSurface failed"
|
|
||||||
: "eglCreatePbufferSurface failed");
|
|
||||||
return 6;
|
return 6;
|
||||||
}
|
}
|
||||||
// The step that brings the whole backend up (DirectVulkan creates its
|
// The step that brings the whole backend up (DirectVulkan creates its
|
||||||
@@ -603,14 +491,6 @@ namespace MGITest {
|
|||||||
if (m_context != nullptr) eglDestroyContext(display, static_cast<EGLContext>(m_context));
|
if (m_context != nullptr) eglDestroyContext(display, static_cast<EGLContext>(m_context));
|
||||||
if (m_surface != nullptr) eglDestroySurface(display, static_cast<EGLSurface>(m_surface));
|
if (m_surface != nullptr) eglDestroySurface(display, static_cast<EGLSurface>(m_surface));
|
||||||
eglTerminate(display);
|
eglTerminate(display);
|
||||||
#if defined(_WIN32)
|
|
||||||
if (g_testWindow != nullptr) {
|
|
||||||
DestroyWindow(g_testWindow);
|
|
||||||
g_testWindow = nullptr;
|
|
||||||
}
|
|
||||||
#elif defined(__ANDROID__)
|
|
||||||
DestroyImageReaderWindow();
|
|
||||||
#endif
|
|
||||||
m_context = nullptr;
|
m_context = nullptr;
|
||||||
m_surface = nullptr;
|
m_surface = nullptr;
|
||||||
m_display = nullptr;
|
m_display = 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,
|
||||||
|
|||||||
@@ -1,239 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/AtomicCounterScenario.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
|
|
||||||
//
|
|
||||||
// Scenario - ATOMIC COUNTERS, END TO END.
|
|
||||||
//
|
|
||||||
// GL_ATOMIC_COUNTER_BUFFER does not exist in ES, and glslang does not hand one to a backend
|
|
||||||
// either: its Vulkan-relaxed parse rewrites every atomic_uint into a uint member of a
|
|
||||||
// synthesized gl_AtomicCounterBlock_<N> STORAGE block. Making counters work therefore means
|
|
||||||
// closing two open ends that used to be missing entirely -
|
|
||||||
//
|
|
||||||
// * the block's shader-storage binding, which the IO mapper picked at random and which had no
|
|
||||||
// relation to the GL binding point N the application bound its buffer to (and could alias an
|
|
||||||
// SSBO the application binds itself), is moved to a slot reserved at the top of the driver's
|
|
||||||
// range; and
|
|
||||||
// * the buffer bound at GL_ATOMIC_COUNTER_BUFFER point N, which nothing in the ES backend ever
|
|
||||||
// read, is re-issued as a shader-storage binding at that reserved slot.
|
|
||||||
//
|
|
||||||
// Neither end alone is observable: with only the first the shader increments a block nobody
|
|
||||||
// bound a buffer to, with only the second the buffer lands where the shader does not look. The
|
|
||||||
// only thing that proves both is the VALUE, so every assertion here reads the counter back.
|
|
||||||
//
|
|
||||||
// Compute rather than a draw on purpose: the invocation count is exactly what was dispatched,
|
|
||||||
// while a fragment stage's is a property of the rasterizer (helper invocations, early depth).
|
|
||||||
// Conformance cases behind this: KHR-GL42/GL43.shader_atomic_counters.basic-usage-cs,
|
|
||||||
// .advanced-usage-multi-stage and .advanced-usage-draw-update-draw.
|
|
||||||
|
|
||||||
#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 {
|
|
||||||
|
|
||||||
// Two counters share binding 0 at DIFFERENT offsets and a third sits alone on binding 1.
|
|
||||||
// The offsets are what separates "the buffer arrived" from "the buffer arrived and the
|
|
||||||
// block is laid out the way GL says": a lowering that packed the members in declaration
|
|
||||||
// order without honouring `offset` would still pass a single-counter check.
|
|
||||||
constexpr const char* kCounterComputeSource = R"(#version 430 core
|
|
||||||
layout(local_size_x = 4) in;
|
|
||||||
layout(binding = 0, offset = 0) uniform atomic_uint g_first;
|
|
||||||
layout(binding = 0, offset = 4) uniform atomic_uint g_second;
|
|
||||||
layout(binding = 1, offset = 0) uniform atomic_uint g_other;
|
|
||||||
void main() {
|
|
||||||
atomicCounterIncrement(g_first);
|
|
||||||
atomicCounterIncrement(g_second);
|
|
||||||
atomicCounterIncrement(g_second);
|
|
||||||
atomicCounterIncrement(g_other);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr int kLocalSizeX = 4;
|
|
||||||
constexpr int kWorkGroups = 2;
|
|
||||||
constexpr unsigned int kInvocations = kLocalSizeX * kWorkGroups;
|
|
||||||
|
|
||||||
// Deliberately non-zero: the shader adds to whatever the application uploaded, so a seed
|
|
||||||
// that survives is also proof that the buffer's CPU-side contents reached the driver.
|
|
||||||
constexpr unsigned int kSeedFirst = 5;
|
|
||||||
constexpr unsigned int kSeedSecond = 100;
|
|
||||||
constexpr unsigned int kSeedOther = 7;
|
|
||||||
|
|
||||||
class AtomicCounterScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
GLint counters = 0;
|
|
||||||
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTERS, &counters);
|
|
||||||
GLint buffers = 0;
|
|
||||||
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS, &buffers);
|
|
||||||
if (counters < 3 || buffers < 2) {
|
|
||||||
GTEST_SKIP() << "GL_MAX_COMPUTE_ATOMIC_COUNTERS is " << counters
|
|
||||||
<< " and GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS is " << buffers
|
|
||||||
<< "; this needs 3 and 2";
|
|
||||||
}
|
|
||||||
if (!AtomicCountersAreWired()) {
|
|
||||||
GTEST_SKIP() << "atomic counter buffers are not wired up on " << Gl().BackendName()
|
|
||||||
<< " yet: glslang lowers them onto a storage block and that block's descriptor "
|
|
||||||
<< "is still resolved from the shader-storage binding points";
|
|
||||||
}
|
|
||||||
m_program = CompileComputeProgram(kCounterComputeSource);
|
|
||||||
ASSERT_NE(m_program, 0u) << m_buildLog;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
glUseProgram(0);
|
|
||||||
if (!m_buffers.empty()) glDeleteBuffers(static_cast<GLsizei>(m_buffers.size()), m_buffers.data());
|
|
||||||
if (m_program != 0) glDeleteProgram(m_program);
|
|
||||||
m_buffers.clear();
|
|
||||||
m_program = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Magma binds the lowered block as an ordinary storage-buffer descriptor resolved
|
|
||||||
// from GL_SHADER_STORAGE_BUFFER point N, so the counter buffer never reaches it. The
|
|
||||||
// frontend half (limits, reflection queries, the link-time offset rules) is
|
|
||||||
// backend-agnostic and is covered by the unit suites; only the VALUE is scoped here.
|
|
||||||
bool AtomicCountersAreWired() const { return Gl().BackendName() != "DirectVulkan"; }
|
|
||||||
|
|
||||||
unsigned int CompileComputeProgram(const char* source) {
|
|
||||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
|
||||||
glShaderSource(shader, 1, &source, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
GLint compiled = 0;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
||||||
if (compiled == GL_FALSE) {
|
|
||||||
char log[2048] = {};
|
|
||||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
|
||||||
m_buildLog = std::string("compute shader did not compile: ") + log;
|
|
||||||
glDeleteShader(shader);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
glAttachShader(program, shader);
|
|
||||||
glLinkProgram(program);
|
|
||||||
glDeleteShader(shader);
|
|
||||||
GLint linked = 0;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
if (linked == GL_FALSE) {
|
|
||||||
char log[2048] = {};
|
|
||||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
|
||||||
m_buildLog = std::string("compute program did not link: ") + log;
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A counter buffer of `count` uints, seeded and bound to atomic-counter point
|
|
||||||
// `binding`.
|
|
||||||
GLuint MakeCounterBuffer(GLuint binding, const std::vector<unsigned int>& seed) {
|
|
||||||
GLuint buffer = 0;
|
|
||||||
glGenBuffers(1, &buffer);
|
|
||||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
|
|
||||||
glBufferData(GL_ATOMIC_COUNTER_BUFFER,
|
|
||||||
static_cast<GLsizeiptr>(seed.size() * sizeof(unsigned int)), seed.data(),
|
|
||||||
GL_DYNAMIC_DRAW);
|
|
||||||
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, binding, buffer);
|
|
||||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
|
|
||||||
m_buffers.push_back(buffer);
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<unsigned int> ReadCounters(GLuint buffer, int count) {
|
|
||||||
std::vector<unsigned int> values(static_cast<std::size_t>(count), 0xDEADBEEFu);
|
|
||||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buffer);
|
|
||||||
glGetBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0,
|
|
||||||
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
|
|
||||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Dispatch() {
|
|
||||||
glUseProgram(m_program);
|
|
||||||
glDispatchCompute(kWorkGroups, 1, 1);
|
|
||||||
glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned int m_program = 0;
|
|
||||||
std::string m_buildLog;
|
|
||||||
std::vector<GLuint> m_buffers;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// The counter values a dispatch leaves behind, per binding point and per offset within one
|
|
||||||
// binding. Nothing in the ES backend used to touch BufferTarget::AtomicCounter at all, so
|
|
||||||
// before the wiring landed every one of these read back its seed unchanged.
|
|
||||||
TEST_F(AtomicCounterScenario, DispatchIncrementsTheBoundCounterBuffers) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
|
|
||||||
const GLuint zero = MakeCounterBuffer(0, {kSeedFirst, kSeedSecond});
|
|
||||||
const GLuint one = MakeCounterBuffer(1, {kSeedOther});
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "binding the counter buffers raised a GL error";
|
|
||||||
|
|
||||||
Dispatch();
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error";
|
|
||||||
|
|
||||||
const std::vector<unsigned int> zeroValues = ReadCounters(zero, 2);
|
|
||||||
const std::vector<unsigned int> oneValues = ReadCounters(one, 1);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << "reading the counters back raised a GL error";
|
|
||||||
|
|
||||||
EXPECT_EQ(zeroValues[0], kSeedFirst + kInvocations)
|
|
||||||
<< "binding 0 offset 0 read back " << zeroValues[0] << "; " << kSeedFirst
|
|
||||||
<< " means the shader's increments never reached the buffer the application bound";
|
|
||||||
EXPECT_EQ(zeroValues[1], kSeedSecond + 2 * kInvocations)
|
|
||||||
<< "binding 0 offset 4 read back " << zeroValues[1] << "; the seed means the counter at a NON-ZERO "
|
|
||||||
<< "offset was not carried through the lowering, even though offset 0 was";
|
|
||||||
EXPECT_EQ(oneValues[0], kSeedOther + kInvocations)
|
|
||||||
<< "binding 1 read back " << oneValues[0] << "; a counter buffer past the first binding point "
|
|
||||||
<< "resolves to a different reserved slot and is where an off-by-one shows up";
|
|
||||||
}
|
|
||||||
|
|
||||||
// A second dispatch continues from where the first left off, and a re-seed between them is
|
|
||||||
// visible to the shader. Both halves of the buffer's traffic have to work, in both
|
|
||||||
// directions: the increments are only observable through the readback path, and the re-seed
|
|
||||||
// is only observable if the upload reaches the driver AFTER the buffer has been GPU-written.
|
|
||||||
TEST_F(AtomicCounterScenario, CountersAccumulateAcrossDispatchesAndFollowAReseed) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
|
|
||||||
const GLuint zero = MakeCounterBuffer(0, {0u, 0u});
|
|
||||||
MakeCounterBuffer(1, {0u});
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
Dispatch();
|
|
||||||
Dispatch();
|
|
||||||
std::vector<unsigned int> values = ReadCounters(zero, 2);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
EXPECT_EQ(values[0], 2 * kInvocations) << "two dispatches did not accumulate";
|
|
||||||
EXPECT_EQ(values[1], 4 * kInvocations) << "two dispatches did not accumulate at offset 4";
|
|
||||||
|
|
||||||
const unsigned int reseed[2] = {1000u, 2000u};
|
|
||||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, zero);
|
|
||||||
glBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(reseed), reseed);
|
|
||||||
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "re-seeding the counter buffer raised a GL error";
|
|
||||||
|
|
||||||
Dispatch();
|
|
||||||
values = ReadCounters(zero, 2);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
EXPECT_EQ(values[0], reseed[0] + kInvocations) << "the re-seeded value did not reach the shader";
|
|
||||||
EXPECT_EQ(values[1], reseed[1] + 2 * kInvocations) << "the re-seeded value at offset 4 did not reach the shader";
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -299,99 +299,4 @@ void main() {
|
|||||||
EXPECT_EQ(FirstGLError(), 0u);
|
EXPECT_EQ(FirstGLError(), 0u);
|
||||||
}
|
}
|
||||||
|
|
||||||
// glGetTexLevelParameter used to refuse EVERY pname on a buffer texture: WIDTH/HEIGHT/DEPTH
|
|
||||||
// fell out of a mipmap-only switch as GL_INVALID_OPERATION, and GL_TEXTURE_BUFFER_SIZE /
|
|
||||||
// GL_TEXTURE_BUFFER_OFFSET were not in the switch at all, so they came back GL_INVALID_ENUM.
|
|
||||||
// KHR-GL43.texture_buffer wraps both queries in GLU_EXPECT_NO_ERROR, so the error alone fails
|
|
||||||
// the case before any value is compared.
|
|
||||||
//
|
|
||||||
// The two halves report DIFFERENT units and only one of them is clamped, which is the thing
|
|
||||||
// easiest to get backwards: WIDTH is a TEXEL count clamped to GL_MAX_TEXTURE_BUFFER_SIZE,
|
|
||||||
// BUFFER_SIZE is the range in basic machine units exactly as it was given.
|
|
||||||
TEST_F(BufferTextureScenario, LevelQueriesDescribeTheAttachedBufferRange) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
FirstGLError();
|
|
||||||
|
|
||||||
GLint offsetAlignment = 1;
|
|
||||||
glGetIntegerv(GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT, &offsetAlignment);
|
|
||||||
if (offsetAlignment < 1) offsetAlignment = 1;
|
|
||||||
GLint maxTexels = 0;
|
|
||||||
glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &maxTexels);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
ASSERT_GT(maxTexels, 0) << "an OpenGL 4.x context may not advertise a zero buffer-texture limit";
|
|
||||||
|
|
||||||
constexpr GLint kTexelBytes = 4; // GL_RGBA8
|
|
||||||
const GLsizeiptr rangeOffset = static_cast<GLsizeiptr>(offsetAlignment);
|
|
||||||
const GLsizeiptr rangeBytes = 32 * kTexelBytes;
|
|
||||||
// Deliberately bigger than the range, so a getter that answered out of the BUFFER rather
|
|
||||||
// than out of the texture's window would be caught.
|
|
||||||
const GLsizeiptr bufferBytes = rangeOffset + rangeBytes + 16 * kTexelBytes;
|
|
||||||
|
|
||||||
const std::vector<GLubyte> zeros(static_cast<size_t>(bufferBytes), 0);
|
|
||||||
GLuint buffer = 0;
|
|
||||||
glGenBuffers(1, &buffer);
|
|
||||||
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
|
||||||
glBufferData(GL_TEXTURE_BUFFER, bufferBytes, zeros.data(), GL_STATIC_DRAW);
|
|
||||||
|
|
||||||
GLuint texture = 0;
|
|
||||||
glGenTextures(1, &texture);
|
|
||||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
|
||||||
glTexBufferRange(GL_TEXTURE_BUFFER, GL_RGBA8, buffer, rangeOffset, rangeBytes);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "glTexBufferRange(GL_RGBA8) was refused";
|
|
||||||
|
|
||||||
const auto levelQuery = [](GLenum pname) {
|
|
||||||
GLint value = -1;
|
|
||||||
glGetTexLevelParameteriv(GL_TEXTURE_BUFFER, 0, pname, &value);
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
const auto levelQueryF = [](GLenum pname) {
|
|
||||||
GLfloat value = -1.0f;
|
|
||||||
glGetTexLevelParameterfv(GL_TEXTURE_BUFFER, 0, pname, &value);
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
EXPECT_EQ(levelQuery(GL_TEXTURE_WIDTH), static_cast<GLint>(rangeBytes / kTexelBytes))
|
|
||||||
<< "GL_TEXTURE_WIDTH is a texel count over the attached RANGE";
|
|
||||||
EXPECT_EQ(levelQuery(GL_TEXTURE_HEIGHT), 1);
|
|
||||||
EXPECT_EQ(levelQuery(GL_TEXTURE_DEPTH), 1);
|
|
||||||
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_SIZE), static_cast<GLint>(rangeBytes))
|
|
||||||
<< "GL_TEXTURE_BUFFER_SIZE reports basic machine units, not texels";
|
|
||||||
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_OFFSET), static_cast<GLint>(rangeOffset));
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << "a buffer-texture level query raised an error";
|
|
||||||
EXPECT_LE(levelQuery(GL_TEXTURE_WIDTH), maxTexels)
|
|
||||||
<< "GL_TEXTURE_WIDTH must stay clamped to GL_MAX_TEXTURE_BUFFER_SIZE";
|
|
||||||
|
|
||||||
// The float getter is a separate switch and has drifted from the integer one before.
|
|
||||||
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_WIDTH), static_cast<GLfloat>(rangeBytes / kTexelBytes));
|
|
||||||
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_HEIGHT), 1.0f);
|
|
||||||
EXPECT_FLOAT_EQ(levelQueryF(GL_TEXTURE_BUFFER_SIZE), static_cast<GLfloat>(rangeBytes));
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << "the float form of a buffer-texture level query raised an error";
|
|
||||||
|
|
||||||
// The whole-buffer form follows the buffer's current size instead of freezing a window.
|
|
||||||
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA8, buffer);
|
|
||||||
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_OFFSET), 0);
|
|
||||||
EXPECT_EQ(levelQuery(GL_TEXTURE_BUFFER_SIZE), static_cast<GLint>(bufferBytes));
|
|
||||||
EXPECT_EQ(levelQuery(GL_TEXTURE_WIDTH), static_cast<GLint>(bufferBytes / kTexelBytes));
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
// Both buffer pnames belong to buffer textures alone; anything else is INVALID_OPERATION,
|
|
||||||
// the same shape GL_TEXTURE_COMPRESSED_IMAGE_SIZE uses for an uncompressed image.
|
|
||||||
GLuint plainTexture = 0;
|
|
||||||
glGenTextures(1, &plainTexture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, plainTexture);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
GLint unused = -1;
|
|
||||||
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_BUFFER_SIZE, &unused);
|
|
||||||
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION));
|
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
|
||||||
glBindBuffer(GL_TEXTURE_BUFFER, 0);
|
|
||||||
glDeleteTextures(1, &plainTexture);
|
|
||||||
glDeleteTextures(1, &texture);
|
|
||||||
glDeleteBuffers(1, &buffer);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -151,18 +151,6 @@ void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
|||||||
glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
|
glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GL_MAX_CLIP_DISTANCES is a real backend answer, not a constant: DirectGLES reports
|
|
||||||
// 0 on a driver without GL_EXT_clip_cull_distance, and DirectVulkan reports 0 without
|
|
||||||
// the shaderClipDistance device feature. On such a stack the shader above cannot
|
|
||||||
// compile - and MUST not, because declaring a clip distance the backend cannot host
|
|
||||||
// is exactly what used to link cleanly and then render nothing. Skip rather than
|
|
||||||
// fail: there is no clipping to assert about.
|
|
||||||
static bool BackendHostsTwoClipDistances() {
|
|
||||||
GLint maxClipDistances = 0;
|
|
||||||
glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
|
|
||||||
return maxClipDistances >= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Never assume the eight start disabled - see the header note about
|
// Never assume the eight start disabled - see the header note about
|
||||||
// XfbAfterClipDistanceScenario leaving one on for the rest of the process.
|
// XfbAfterClipDistanceScenario leaving one on for the rest of the process.
|
||||||
static void DisableEveryClipDistance() {
|
static void DisableEveryClipDistance() {
|
||||||
@@ -241,9 +229,6 @@ void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
|||||||
// The claim: an enabled clip distance removes the fragments where it is negative.
|
// The claim: an enabled clip distance removes the fragments where it is negative.
|
||||||
TEST_F(ClipDistanceScenario, AnEnabledClipDistanceRemovesTheNegativeHalf) {
|
TEST_F(ClipDistanceScenario, AnEnabledClipDistanceRemovesTheNegativeHalf) {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
if (!BackendHostsTwoClipDistances()) {
|
|
||||||
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
|
|
||||||
}
|
|
||||||
HeadlessGL& gl = Gl();
|
HeadlessGL& gl = Gl();
|
||||||
const int width = gl.Width();
|
const int width = gl.Width();
|
||||||
const int height = gl.Height();
|
const int height = gl.Height();
|
||||||
@@ -295,9 +280,6 @@ void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
|||||||
// draw simply failed - would pass the case above.
|
// draw simply failed - would pass the case above.
|
||||||
TEST_F(ClipDistanceScenario, ADisabledClipDistanceRemovesNothing) {
|
TEST_F(ClipDistanceScenario, ADisabledClipDistanceRemovesNothing) {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
if (!BackendHostsTwoClipDistances()) {
|
|
||||||
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
|
|
||||||
}
|
|
||||||
HeadlessGL& gl = Gl();
|
HeadlessGL& gl = Gl();
|
||||||
const int width = gl.Width();
|
const int width = gl.Width();
|
||||||
const int height = gl.Height();
|
const int height = gl.Height();
|
||||||
@@ -347,9 +329,6 @@ void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
|||||||
// passes both cases above and fails this one.
|
// passes both cases above and fails this one.
|
||||||
TEST_F(ClipDistanceScenario, TheEnablesAreIndependentPerDistance) {
|
TEST_F(ClipDistanceScenario, TheEnablesAreIndependentPerDistance) {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
if (!BackendHostsTwoClipDistances()) {
|
|
||||||
GTEST_SKIP() << "this backend advertises no clip distances, so there is nothing to clip with";
|
|
||||||
}
|
|
||||||
HeadlessGL& gl = Gl();
|
HeadlessGL& gl = Gl();
|
||||||
const int width = gl.Width();
|
const int width = gl.Width();
|
||||||
const int height = gl.Height();
|
const int height = gl.Height();
|
||||||
|
|||||||
@@ -6,32 +6,27 @@
|
|||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
//
|
//
|
||||||
// Scenario - GLSL DOUBLES, AT WHATEVER PRECISION THE BACKEND CAN GIVE.
|
// Scenario - GLSL DOUBLES, RUN AT SINGLE PRECISION.
|
||||||
//
|
//
|
||||||
// No mobile GPU has 64-bit floats. Adreno and Mali both report shaderFloat64 == VK_FALSE, so
|
// No mobile GPU has 64-bit floats. Adreno and Mali both report shaderFloat64 == VK_FALSE, so
|
||||||
// Magma cannot build a module that declares the Float64 capability there, and ESSL has no fp64
|
// Magma cannot build a module that declares the Float64 capability, and ESSL has no fp64 type
|
||||||
// type at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES
|
// at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES
|
||||||
// profile") and the program never reaches the driver. On every such backend MobileGL narrows
|
// profile") and the program never reaches the driver. MobileGL therefore narrows every 64-bit
|
||||||
// every 64-bit float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than
|
// float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than declining the
|
||||||
// declining the shader: `double` compiles and runs everywhere, at float precision. Where the
|
// shader: `double` compiles and runs everywhere, at float precision.
|
||||||
// backend DOES consume 64-bit floats - lavapipe is the one that does - the narrowing is skipped
|
|
||||||
// and the doubles reach the driver whole.
|
|
||||||
//
|
//
|
||||||
// Either way it is only half a contract. The other half is the API side: the global UBO is laid
|
// The narrowing is only half a contract. The other half is the API side: the global UBO is
|
||||||
// out by reflecting whichever module was produced, so glUniform*d has to store the width the
|
// laid out by reflecting the DEMOTED module, so glUniform*d has to store a float where the
|
||||||
// shader reads, glGetUniform*v has to read that width back, and a matrix's columns are
|
// shader reads a float, glGetUniform*v has to read one back, and a dmat4's columns are now
|
||||||
// std140-padded to a vec4 or a dvec4 to match. Every one of those is a byte offset that fails
|
// std140-padded like any other matrix's. Every one of those is a byte offset that fails
|
||||||
// silently - the uniform simply reads as something else - so the cases below set values through
|
// silently - the uniform simply reads as something else - so the cases below set values
|
||||||
// the API and have the SHADER report what it saw.
|
// through the API and have the SHADER report what it saw.
|
||||||
//
|
//
|
||||||
// WHY ALMOST EVERY EXPECTATION HERE IS A FLOAT VALUE, and why that is not an accident of the
|
// What is deliberately NOT asserted: that the values are exact to double precision. They are
|
||||||
// demotion: the shader reports through a `float` SSBO, and every value chosen is exact in
|
// not, and cannot be. Every expectation here is the float value of the double that was set,
|
||||||
// float32, so the same number is correct in both regimes and the assertions test the LAYOUT
|
// which is the whole point.
|
||||||
// rather than the precision. Exactly one case (GetUniformdvReadsBackWhatWasStored) uses a value
|
|
||||||
// that is not - 0.1 - and it names both answers explicitly.
|
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstring>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -158,151 +153,6 @@ void main() {
|
|||||||
std::string m_buildLog;
|
std::string m_buildLog;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A SHADER STORAGE BLOCK that holds doubles is the one place the narrowing is NOT free:
|
|
||||||
// demoting `double` to `float` also repacks the block, and the bytes the application
|
|
||||||
// wrote into the buffer do not move with it. Every member past the first double then
|
|
||||||
// reads and writes at the wrong offset, and the block is simply shorter than the one
|
|
||||||
// that was bound - the tail of it is never touched at all
|
|
||||||
// (KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3, whose output matched its
|
|
||||||
// input up to the first double's slot and was zero from there on).
|
|
||||||
//
|
|
||||||
// The block layout is fixed by GL 4.6 core 7.6.2.2 and is asserted here as literal byte
|
|
||||||
// offsets rather than queried, so this says what the SPEC requires and not what MobileGL
|
|
||||||
// happens to report. Both packings are covered because they differ in exactly the places
|
|
||||||
// that matter: std140 rounds an array's stride and a matrix's column stride up to 16,
|
|
||||||
// std430 does not, and only std430 packs the scalars tightly.
|
|
||||||
//
|
|
||||||
// Every value is exactly representable in binary32, so a correct implementation copies
|
|
||||||
// the block BYTE FOR BYTE even though it narrows each double on the way through.
|
|
||||||
constexpr const char* kBlockCopySource = R"(#version 430 core
|
|
||||||
layout(local_size_x = 1) in;
|
|
||||||
layout(std140, binding = 0) buffer In140 {
|
|
||||||
int data0;
|
|
||||||
float data1[3];
|
|
||||||
mat3x2 data2;
|
|
||||||
double data3;
|
|
||||||
double data4[2];
|
|
||||||
int data5;
|
|
||||||
dvec3 data6;
|
|
||||||
} g_in140;
|
|
||||||
layout(std430, binding = 1) buffer In430 {
|
|
||||||
int data0;
|
|
||||||
float data1[3];
|
|
||||||
mat3x2 data2;
|
|
||||||
double data3;
|
|
||||||
double data4[2];
|
|
||||||
int data5;
|
|
||||||
dvec3 data6;
|
|
||||||
} g_in430;
|
|
||||||
layout(std140, binding = 2) buffer Out140 {
|
|
||||||
int data0;
|
|
||||||
float data1[3];
|
|
||||||
mat3x2 data2;
|
|
||||||
double data3;
|
|
||||||
double data4[2];
|
|
||||||
int data5;
|
|
||||||
dvec3 data6;
|
|
||||||
} g_out140;
|
|
||||||
layout(std430, binding = 3) buffer Out430 {
|
|
||||||
int data0;
|
|
||||||
float data1[3];
|
|
||||||
mat3x2 data2;
|
|
||||||
double data3;
|
|
||||||
double data4[2];
|
|
||||||
int data5;
|
|
||||||
dvec3 data6;
|
|
||||||
} g_out430;
|
|
||||||
void main() {
|
|
||||||
g_out140.data0 = g_in140.data0;
|
|
||||||
for (int i = 0; i < 3; ++i) g_out140.data1[i] = g_in140.data1[i];
|
|
||||||
g_out140.data2 = g_in140.data2;
|
|
||||||
g_out140.data3 = g_in140.data3;
|
|
||||||
for (int i = 0; i < 2; ++i) g_out140.data4[i] = g_in140.data4[i];
|
|
||||||
g_out140.data5 = g_in140.data5;
|
|
||||||
g_out140.data6 = g_in140.data6;
|
|
||||||
|
|
||||||
g_out430.data0 = g_in430.data0;
|
|
||||||
for (int i = 0; i < 3; ++i) g_out430.data1[i] = g_in430.data1[i];
|
|
||||||
g_out430.data2 = g_in430.data2;
|
|
||||||
g_out430.data3 = g_in430.data3;
|
|
||||||
for (int i = 0; i < 2; ++i) g_out430.data4[i] = g_in430.data4[i];
|
|
||||||
g_out430.data5 = g_in430.data5;
|
|
||||||
g_out430.data6 = g_in430.data6;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// GL 4.6 core 7.6.2.2 rule by rule, for the block above.
|
|
||||||
// std140: an array's element stride and a matrix's column stride round up to 16, a
|
|
||||||
// double aligns to 8 and a dvec3 to 32.
|
|
||||||
// std430: the same without the rounding - so the scalars pack tightly and only the
|
|
||||||
// dvec3's 32-byte alignment leaves a hole.
|
|
||||||
struct BlockLayout {
|
|
||||||
int data0;
|
|
||||||
int data1;
|
|
||||||
int data1Stride;
|
|
||||||
int data2;
|
|
||||||
int data2ColumnStride;
|
|
||||||
int data3;
|
|
||||||
int data4;
|
|
||||||
int data4Stride;
|
|
||||||
int data5;
|
|
||||||
int data6;
|
|
||||||
int size;
|
|
||||||
};
|
|
||||||
constexpr BlockLayout kStd140{0, 16, 16, 64, 16, 112, 128, 16, 160, 192, 216};
|
|
||||||
constexpr BlockLayout kStd430{0, 4, 4, 16, 8, 40, 48, 8, 64, 96, 120};
|
|
||||||
|
|
||||||
void PokeInt(std::vector<unsigned char>& bytes, int offset, int value) {
|
|
||||||
std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
|
|
||||||
}
|
|
||||||
void PokeFloat(std::vector<unsigned char>& bytes, int offset, float value) {
|
|
||||||
std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
|
|
||||||
}
|
|
||||||
void PokeDouble(std::vector<unsigned char>& bytes, int offset, double value) {
|
|
||||||
std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The block's contents, at the offsets the standard puts them. Padding stays zero, which
|
|
||||||
// is what makes a byte-for-byte comparison against the (zero-initialised) output buffer
|
|
||||||
// catch a member that landed somewhere it should not have.
|
|
||||||
std::vector<unsigned char> MakeBlockContents(const BlockLayout& layout) {
|
|
||||||
std::vector<unsigned char> bytes(static_cast<std::size_t>(layout.size), 0);
|
|
||||||
PokeInt(bytes, layout.data0, 1);
|
|
||||||
for (int i = 0; i < 3; ++i) {
|
|
||||||
PokeFloat(bytes, layout.data1 + i * layout.data1Stride, 2.0f + static_cast<float>(i));
|
|
||||||
}
|
|
||||||
// Column-major, two rows per column.
|
|
||||||
for (int column = 0; column < 3; ++column) {
|
|
||||||
for (int row = 0; row < 2; ++row) {
|
|
||||||
PokeFloat(bytes, layout.data2 + column * layout.data2ColumnStride + row * 4,
|
|
||||||
5.0f + static_cast<float>(column * 2 + row));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PokeDouble(bytes, layout.data3, 11.0);
|
|
||||||
for (int i = 0; i < 2; ++i) {
|
|
||||||
PokeDouble(bytes, layout.data4 + i * layout.data4Stride, 12.0 + static_cast<double>(i));
|
|
||||||
}
|
|
||||||
PokeInt(bytes, layout.data5, 14);
|
|
||||||
for (int i = 0; i < 3; ++i) {
|
|
||||||
PokeDouble(bytes, layout.data6 + i * 8, 15.0 + static_cast<double>(i));
|
|
||||||
}
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Names the first byte that differs, and which member owns it, so a failure is a
|
|
||||||
// diagnosis rather than "the buffer is wrong".
|
|
||||||
std::string DescribeOffset(const BlockLayout& layout, int offset) {
|
|
||||||
const std::pair<int, const char*> members[] = {
|
|
||||||
{layout.data0, "data0"}, {layout.data1, "data1"}, {layout.data2, "data2"},
|
|
||||||
{layout.data3, "data3"}, {layout.data4, "data4"}, {layout.data5, "data5"},
|
|
||||||
{layout.data6, "data6"}};
|
|
||||||
const char* owner = "(padding before data0)";
|
|
||||||
for (const auto& [start, name] : members) {
|
|
||||||
if (offset >= start) owner = name;
|
|
||||||
}
|
|
||||||
return std::string(owner);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every double-typed uniform shape GLSL has, all thirteen of them, in one program - the
|
// Every double-typed uniform shape GLSL has, all thirteen of them, in one program - the
|
||||||
// shape of KHR-GL43.compute_shader.fp64-case2. The scalar and the square matrices are
|
// shape of KHR-GL43.compute_shader.fp64-case2. The scalar and the square matrices are
|
||||||
// covered by the cases above; what only a set like this reaches is the NON-SQUARE
|
// covered by the cases above; what only a set like this reaches is the NON-SQUARE
|
||||||
@@ -578,24 +428,12 @@ void main() {
|
|||||||
glUseProgram(0);
|
glUseProgram(0);
|
||||||
|
|
||||||
// The readback has to undo exactly what the write did - the same std140 column
|
// The readback has to undo exactly what the write did - the same std140 column
|
||||||
// padding, the same component width - or a dmat4 comes back with its columns
|
// padding, the same 4-byte components - or a dmat4 comes back with its columns
|
||||||
// shifted and nothing else in the API would say so. Every value below except the
|
// shifted and nothing else in the API would say so.
|
||||||
// scalar is exact in float32, so those expectations pin the LAYOUT and hold in
|
|
||||||
// either regime; the scalar is the one that also pins the PRECISION.
|
|
||||||
GLdouble readScalar = 0.0;
|
GLdouble readScalar = 0.0;
|
||||||
glGetUniformdv(m_program, scalar, &readScalar);
|
glGetUniformdv(m_program, scalar, &readScalar);
|
||||||
// 0.1 is not representable in float32, so what comes back names the regime: a
|
EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1)))
|
||||||
// backend without native fp64 narrowed it at the glUniform1d above (the module's own
|
<< "the value is what a float can hold, not the double that was passed in";
|
||||||
// doubles were demoted, so its storage is 4 bytes per component), and one with it
|
|
||||||
// stored the double whole. Both are correct; asserting only the narrow answer would
|
|
||||||
// fail the moment fp64 stops being emulated, and asserting only the wide one would
|
|
||||||
// fail on every mobile device there is.
|
|
||||||
if (readScalar == 0.1) {
|
|
||||||
SUCCEED() << "this backend consumes 64-bit floats natively; the double survived whole";
|
|
||||||
} else {
|
|
||||||
EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1)))
|
|
||||||
<< "the value is what a float can hold, not the double that was passed in";
|
|
||||||
}
|
|
||||||
|
|
||||||
GLdouble readVector[3] = {};
|
GLdouble readVector[3] = {};
|
||||||
glGetUniformdv(m_program, vector, readVector);
|
glGetUniformdv(m_program, vector, readVector);
|
||||||
@@ -609,8 +447,7 @@ void main() {
|
|||||||
EXPECT_DOUBLE_EQ(readMatrix[i], 100.0 + i) << "dmat4 component " << i;
|
EXPECT_DOUBLE_EQ(readMatrix[i], 100.0 + i) << "dmat4 component " << i;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The float query sees the same storage through a narrower type, and answers the
|
// The float query sees the same storage through the type it is actually stored as.
|
||||||
// same float either way: GL 4.6 core 7.6 converts on the way out.
|
|
||||||
GLfloat readFloat = 0.0f;
|
GLfloat readFloat = 0.0f;
|
||||||
glGetUniformfv(m_program, scalar, &readFloat);
|
glGetUniformfv(m_program, scalar, &readFloat);
|
||||||
EXPECT_FLOAT_EQ(readFloat, static_cast<float>(0.1));
|
EXPECT_FLOAT_EQ(readFloat, static_cast<float>(0.1));
|
||||||
@@ -860,188 +697,24 @@ void main() {
|
|||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsRecordedAndItsArrayIsDroppedAtDraw) {
|
TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsDeclinedOnEveryBackend) {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
// The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit
|
// The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit
|
||||||
// vertex FETCH could be fetched into - on either backend, and no longer only on the
|
// vertex FETCH could be fetched into - on either backend, and no longer only on the
|
||||||
// ones whose device lacks shaderFloat64.
|
// ones whose device lacks shaderFloat64. Declined loudly rather than accepted and
|
||||||
//
|
// drawn as garbage; the matching POST row says the same thing at startup.
|
||||||
// What that costs is the ARRAY, not the CALL. GL 4.6 core 10.3.2 defines no error for
|
|
||||||
// a well-formed glVertexAttribLFormat and 64-bit attributes are core in the GL 4.3
|
|
||||||
// context MobileGL advertises, so refusing the call would be non-conformant and would
|
|
||||||
// leave four pure state queries unanswerable
|
|
||||||
// (KHR-GL43.vertex_attrib_binding.basic-state1/3). The format is therefore recorded and
|
|
||||||
// queryable; the enabled array is what gets dropped, and the attribute then reads its
|
|
||||||
// generic current value. The matching POST row says exactly that at startup.
|
|
||||||
GLuint vao = 0;
|
GLuint vao = 0;
|
||||||
glGenVertexArrays(1, &vao);
|
glGenVertexArrays(1, &vao);
|
||||||
glBindVertexArray(vao);
|
glBindVertexArray(vao);
|
||||||
while (glGetError() != GL_NO_ERROR) {}
|
while (glGetError() != GL_NO_ERROR) {}
|
||||||
|
|
||||||
glVertexAttribLFormat(1, 3, GL_DOUBLE, 8);
|
glVertexAttribLFormat(0, 3, GL_DOUBLE, 0);
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
|
||||||
<< "glVertexAttribLFormat is a legal call in a GL 4.3 context";
|
|
||||||
|
|
||||||
GLint attribSize = 0;
|
|
||||||
GLint attribType = 0;
|
|
||||||
GLint attribIsLong = 0;
|
|
||||||
GLint attribRelativeOffset = 0;
|
|
||||||
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_SIZE, &attribSize);
|
|
||||||
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_TYPE, &attribType);
|
|
||||||
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_LONG, &attribIsLong);
|
|
||||||
glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &attribRelativeOffset);
|
|
||||||
EXPECT_EQ(attribSize, 3);
|
|
||||||
EXPECT_EQ(attribType, static_cast<GLint>(GL_DOUBLE));
|
|
||||||
EXPECT_EQ(attribIsLong, GL_TRUE) << "GL_VERTEX_ATTRIB_ARRAY_LONG is what makes this the "
|
|
||||||
"unconverted form; without it the state is a lie";
|
|
||||||
EXPECT_EQ(attribRelativeOffset, 8);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
|
||||||
|
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
glDeleteVertexArrays(1, &vao);
|
glDeleteVertexArrays(1, &vao);
|
||||||
while (glGetError() != GL_NO_ERROR) {}
|
while (glGetError() != GL_NO_ERROR) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The consequence of recording the state rather than refusing the call: a 64-bit array can
|
|
||||||
// now be ENABLED in a VAO that a draw uses, which it never could before. That must not
|
|
||||||
// take the draw down. Leaving such an array enabled with no pointer behind it is exactly
|
|
||||||
// the documented Adreno null-deref (SIGSEGV inside the next glDraw*), so DirectGLES
|
|
||||||
// disables it before glVertexAttribPointer can ever see GL_DOUBLE, and DirectVulkan maps
|
|
||||||
// the format to VK_FORMAT_UNDEFINED so it never enters the pipeline's vertex input state.
|
|
||||||
//
|
|
||||||
// The shader deliberately does NOT read location 1: that keeps the two backends on the
|
|
||||||
// same path (DirectVulkan declines a draw whose SHADER reads an unsupported enabled array,
|
|
||||||
// by design and loudly, which is a different assertion from this one) and it is the shape
|
|
||||||
// the crash needed - an enabled array nothing set a pointer for.
|
|
||||||
TEST_F(DoublePrecisionScenario, AnEnabledLongArrayDoesNotBreakADrawThatIgnoresIt) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
constexpr const char* kVs = R"(#version 430 core
|
|
||||||
layout(location = 0) in vec2 aPos;
|
|
||||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
|
||||||
)";
|
|
||||||
constexpr const char* kFs = R"(#version 430 core
|
|
||||||
out vec4 o_color;
|
|
||||||
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
|
||||||
)";
|
|
||||||
std::string error;
|
|
||||||
const unsigned int program = CompileProgram(kVs, kFs, &error);
|
|
||||||
ASSERT_NE(program, 0u) << error;
|
|
||||||
|
|
||||||
ColorFbo target = MakeColorFbo(32, 32);
|
|
||||||
ASSERT_NE(target.fbo, 0u) << "could not create the render target";
|
|
||||||
BindFbo(target);
|
|
||||||
|
|
||||||
const float positions[8] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
|
||||||
const double doubles[4] = {1.0, 2.0, 3.0, 4.0};
|
|
||||||
|
|
||||||
GLuint vao = 0;
|
|
||||||
GLuint positionBuffer = 0;
|
|
||||||
GLuint doubleBuffer = 0;
|
|
||||||
glGenVertexArrays(1, &vao);
|
|
||||||
glBindVertexArray(vao);
|
|
||||||
glGenBuffers(1, &positionBuffer);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, positionBuffer);
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);
|
|
||||||
glGenBuffers(1, &doubleBuffer);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, doubleBuffer);
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, sizeof(doubles), doubles, GL_STATIC_DRAW);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
|
||||||
|
|
||||||
glVertexAttribFormat(0, 2, GL_FLOAT, GL_FALSE, 0);
|
|
||||||
glVertexAttribBinding(0, 0);
|
|
||||||
glBindVertexBuffer(0, positionBuffer, 0, static_cast<GLsizei>(2 * sizeof(float)));
|
|
||||||
glEnableVertexAttribArray(0);
|
|
||||||
|
|
||||||
glVertexAttribLFormat(1, 1, GL_DOUBLE, 0);
|
|
||||||
glVertexAttribBinding(1, 1);
|
|
||||||
glBindVertexBuffer(1, doubleBuffer, 0, static_cast<GLsizei>(sizeof(double)));
|
|
||||||
glEnableVertexAttribArray(1);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << "setting up the 64-bit array was refused";
|
|
||||||
|
|
||||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
glUseProgram(program);
|
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << "a draw with an enabled 64-bit array must not raise an error";
|
|
||||||
|
|
||||||
const Image image = ReadPixels(target.width, target.height);
|
|
||||||
ASSERT_FALSE(image.Empty());
|
|
||||||
EXPECT_GT(image.At(target.width / 2, target.height / 2).g, 200)
|
|
||||||
<< "the draw did not happen; the enabled 64-bit array must be dropped, not fatal";
|
|
||||||
|
|
||||||
glDisableVertexAttribArray(0);
|
|
||||||
glDisableVertexAttribArray(1);
|
|
||||||
glBindVertexArray(0);
|
|
||||||
glDeleteVertexArrays(1, &vao);
|
|
||||||
glDeleteBuffers(1, &positionBuffer);
|
|
||||||
glDeleteBuffers(1, &doubleBuffer);
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
DestroyColorFbo(target);
|
|
||||||
glUseProgram(0);
|
|
||||||
glDeleteProgram(program);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(DoublePrecisionScenario, AStorageBlockWithDoublesKeepsTheLayoutItWasBoundWith) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
GLint blocks = 0;
|
|
||||||
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &blocks);
|
|
||||||
if (blocks < 4) {
|
|
||||||
GTEST_SKIP() << "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS is " << blocks << "; this needs 4";
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsigned int program = CompileComputeProgram(kBlockCopySource);
|
|
||||||
ASSERT_NE(program, 0u) << m_buildLog;
|
|
||||||
|
|
||||||
const std::vector<unsigned char> in140 = MakeBlockContents(kStd140);
|
|
||||||
const std::vector<unsigned char> in430 = MakeBlockContents(kStd430);
|
|
||||||
const std::vector<unsigned char> zero140(in140.size(), 0);
|
|
||||||
const std::vector<unsigned char> zero430(in430.size(), 0);
|
|
||||||
|
|
||||||
GLuint buffers[4] = {};
|
|
||||||
glGenBuffers(4, buffers);
|
|
||||||
const std::vector<unsigned char>* contents[4] = {&in140, &in430, &zero140, &zero430};
|
|
||||||
for (int i = 0; i < 4; ++i) {
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(i), buffers[i]);
|
|
||||||
glBufferData(GL_SHADER_STORAGE_BUFFER, static_cast<GLsizeiptr>(contents[i]->size()),
|
|
||||||
contents[i]->data(), GL_DYNAMIC_COPY);
|
|
||||||
}
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
glUseProgram(program);
|
|
||||||
glDispatchCompute(1, 1, 1);
|
|
||||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
for (int pass = 0; pass < 2; ++pass) {
|
|
||||||
const BlockLayout& layout = pass == 0 ? kStd140 : kStd430;
|
|
||||||
const std::vector<unsigned char>& expected = pass == 0 ? in140 : in430;
|
|
||||||
const char* packing = pass == 0 ? "std140" : "std430";
|
|
||||||
std::vector<unsigned char> observed(expected.size(), 0xEE);
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffers[2 + pass]);
|
|
||||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
|
|
||||||
static_cast<GLsizeiptr>(observed.size()), observed.data());
|
|
||||||
int mismatches = 0;
|
|
||||||
int firstMismatch = -1;
|
|
||||||
for (std::size_t i = 0; i < expected.size(); ++i) {
|
|
||||||
if (expected[i] == observed[i]) continue;
|
|
||||||
++mismatches;
|
|
||||||
if (firstMismatch < 0) firstMismatch = static_cast<int>(i);
|
|
||||||
}
|
|
||||||
EXPECT_EQ(mismatches, 0)
|
|
||||||
<< packing << " block: " << mismatches << " of " << expected.size()
|
|
||||||
<< " bytes differ, first at byte " << firstMismatch << " (in "
|
|
||||||
<< DescribeOffset(layout, firstMismatch < 0 ? 0 : firstMismatch)
|
|
||||||
<< "); a block that was repacked around its doubles reads and writes every "
|
|
||||||
"member after the first one at the wrong offset";
|
|
||||||
}
|
|
||||||
|
|
||||||
glUseProgram(0);
|
|
||||||
glDeleteProgram(program);
|
|
||||||
glDeleteBuffers(4, buffers);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -1,211 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/FormatlessImageBakeScenario.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
|
|
||||||
//
|
|
||||||
// Scenario - A FORMAT-LESS IMAGE UNIFORM WHOSE UNIT HOLDS A NON-CORE FORMAT.
|
|
||||||
//
|
|
||||||
// GLSL 4.20 lets a write-only image uniform omit its layout format; GLSL ES demands one, so
|
|
||||||
// DirectGLES BAKES the format of whatever glBindImageTexture put on the unit into the
|
|
||||||
// declaration. When that format is outside the GLSL ES core thirteen, the bake alone is not
|
|
||||||
// enough - the baked declaration then has to go through the same channel-widening
|
|
||||||
// WidenImageFormatsForEssl gives a DECLARED non-core format (see NonCoreImageFormatScenario for
|
|
||||||
// the widening itself).
|
|
||||||
//
|
|
||||||
// The two routes had different arming. The declared route armed the widening on the format
|
|
||||||
// alone; the baked route armed it only when the driver lacked GL_NV_image_formats. That reads
|
|
||||||
// like an optimisation and is not one: SPIRV-Cross throws for its is_desktop_only_format set the
|
|
||||||
// moment it targets ESSL, whatever the driver would have accepted, so on a driver that HAS the
|
|
||||||
// extension the shader half of the widening stayed switched off while TextureImpl's storage/bind
|
|
||||||
// half - which keys on SpirvCrossCanPrintEsslImageFormat, not on the driver bit - still ran. The
|
|
||||||
// stage threw, the program linked without it, and every dispatch silently did nothing.
|
|
||||||
//
|
|
||||||
// KHR-GL43.stencil_texturing.functional is where it surfaced: its compute half writes through a
|
|
||||||
// format-less `uimage2D` bound to an R8UI texture, and returned zeros for every texel.
|
|
||||||
//
|
|
||||||
// DISCRIMINATING ONLY WHERE THE DRIVER ADVERTISES GL_NV_image_formats - Mesa does, which is what
|
|
||||||
// the software lanes run and where this was found. On Adreno 830 and both Malis the extension is
|
|
||||||
// absent, the old code already armed the widening, and these cases pass before and after; they
|
|
||||||
// are kept running there as a guard against the opposite mistake.
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstring>
|
|
||||||
#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 kExtent = 8;
|
|
||||||
|
|
||||||
// No layout format on uni_image on purpose: that is the whole subject. uni_source is a
|
|
||||||
// plain integer texture so nothing but the image declaration is in play.
|
|
||||||
const char* const kComputeSource = R"(#version 430 core
|
|
||||||
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
|
||||||
writeonly uniform uimage2D uni_image;
|
|
||||||
uniform usampler2D uni_source;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
ivec2 at = ivec2(gl_GlobalInvocationID.xy);
|
|
||||||
imageStore(uni_image, at, uvec4(texelFetch(uni_source, at, 0).r, 0u, 0u, 0u));
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
class FormatlessImageBakeScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
if (!BackendHostsCompute()) {
|
|
||||||
GTEST_SKIP() << "no compute stage on " << Gl().BackendName() << " ("
|
|
||||||
<< Gl().RendererString() << ")";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool BackendHostsCompute() {
|
|
||||||
GLint maxImageUnits = 0;
|
|
||||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
|
||||||
DrainErrors();
|
|
||||||
return maxImageUnits >= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void DrainErrors() {
|
|
||||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static GLuint BuildCompute(const char* source, std::string& log) {
|
|
||||||
const GLuint cs = glCreateShader(GL_COMPUTE_SHADER);
|
|
||||||
glShaderSource(cs, 1, &source, nullptr);
|
|
||||||
glCompileShader(cs);
|
|
||||||
GLint ok = 0;
|
|
||||||
glGetShaderiv(cs, GL_COMPILE_STATUS, &ok);
|
|
||||||
if (!ok) {
|
|
||||||
char buffer[2048] = "";
|
|
||||||
glGetShaderInfoLog(cs, sizeof(buffer), nullptr, buffer);
|
|
||||||
log = buffer;
|
|
||||||
glDeleteShader(cs);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
glAttachShader(program, cs);
|
|
||||||
glLinkProgram(program);
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &ok);
|
|
||||||
glDeleteShader(cs);
|
|
||||||
if (!ok) {
|
|
||||||
char buffer[2048] = "";
|
|
||||||
glGetProgramInfoLog(program, sizeof(buffer), nullptr, buffer);
|
|
||||||
log = buffer;
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
// internalFormat is the NON-CORE image format under test; the destination texture and
|
|
||||||
// the glBindImageTexture argument both use it, and the shader declares nothing.
|
|
||||||
void RunCopy(GLenum internalFormat, GLenum uploadFormat, GLenum uploadType) {
|
|
||||||
std::vector<GLuint> expected(kExtent * kExtent);
|
|
||||||
for (int i = 0; i < kExtent * kExtent; ++i) {
|
|
||||||
expected[i] = static_cast<GLuint>(1 + i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Source: a core-format integer texture holding 1..64.
|
|
||||||
std::vector<GLubyte> sourceBytes(kExtent * kExtent);
|
|
||||||
for (int i = 0; i < kExtent * kExtent; ++i) {
|
|
||||||
sourceBytes[i] = static_cast<GLubyte>(expected[i]);
|
|
||||||
}
|
|
||||||
GLuint sourceTexture = 0;
|
|
||||||
glGenTextures(1, &sourceTexture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, sourceTexture);
|
|
||||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8UI, kExtent, kExtent);
|
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent, GL_RED_INTEGER, GL_UNSIGNED_BYTE,
|
|
||||||
sourceBytes.data());
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
|
|
||||||
// Destination: the format under test, zero-filled so "the dispatch did nothing"
|
|
||||||
// and "the dispatch wrote zeros" are the same observation the CTS made.
|
|
||||||
GLuint destTexture = 0;
|
|
||||||
glGenTextures(1, &destTexture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, destTexture);
|
|
||||||
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kExtent, kExtent);
|
|
||||||
const std::vector<GLubyte> zeros(static_cast<std::size_t>(kExtent) * kExtent * 8, 0);
|
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent, uploadFormat, uploadType, zeros.data());
|
|
||||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "destination storage";
|
|
||||||
|
|
||||||
std::string log;
|
|
||||||
const GLuint program = BuildCompute(kComputeSource, log);
|
|
||||||
ASSERT_NE(program, 0u) << "the format-less image program did not build: " << log;
|
|
||||||
|
|
||||||
glUseProgram(program);
|
|
||||||
glBindImageTexture(1, destTexture, 0, GL_FALSE, 0, GL_WRITE_ONLY, internalFormat);
|
|
||||||
glUniform1i(glGetUniformLocation(program, "uni_image"), 1);
|
|
||||||
glActiveTexture(GL_TEXTURE1);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, sourceTexture);
|
|
||||||
glUniform1i(glGetUniformLocation(program, "uni_source"), 1);
|
|
||||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "binding";
|
|
||||||
|
|
||||||
glDispatchCompute(kExtent, kExtent, 1);
|
|
||||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "dispatch";
|
|
||||||
|
|
||||||
std::vector<GLuint> readback(kExtent * kExtent, 0xFFFFFFFFu);
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, destTexture);
|
|
||||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, readback.data());
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "readback";
|
|
||||||
|
|
||||||
int offenders = 0;
|
|
||||||
for (int i = 0; i < kExtent * kExtent; ++i) {
|
|
||||||
if (readback[i] != expected[i]) ++offenders;
|
|
||||||
}
|
|
||||||
EXPECT_EQ(offenders, 0) << "the dispatch wrote " << offenders << " of "
|
|
||||||
<< (kExtent * kExtent) << " texels wrongly; texel 0 was "
|
|
||||||
<< readback[0] << ", expected " << expected[0]
|
|
||||||
<< ". A whole stage lost to the ESSL emitter looks exactly like this.";
|
|
||||||
|
|
||||||
glUseProgram(0);
|
|
||||||
glDeleteProgram(program);
|
|
||||||
glDeleteTextures(1, &sourceTexture);
|
|
||||||
glDeleteTextures(1, &destTexture);
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// R8UI: one of the seven formats GLSL ES reaches only through GL_NV_image_formats AND one
|
|
||||||
// SPIRV-Cross refuses to print for ESSL, so it needs the widening in both driver modes.
|
|
||||||
TEST_F(FormatlessImageBakeScenario, R8uiBakedFromTheBoundUnitStillReachesTheDriver) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
RunCopy(GL_R8UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// R16UI, from the same set, carried in RGBA16UI: the fix must not be R8UI-shaped.
|
|
||||||
TEST_F(FormatlessImageBakeScenario, R16uiBakedFromTheBoundUnitStillReachesTheDriver) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
RunCopy(GL_R16UI, GL_RED_INTEGER, GL_UNSIGNED_SHORT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The control: R32UI is in the GLSL ES core thirteen, so it is baked and never widened.
|
|
||||||
// It passed before the fix and has to keep passing.
|
|
||||||
TEST_F(FormatlessImageBakeScenario, CoreFormatBakedFromTheBoundUnitIsUnaffected) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
RunCopy(GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -1,413 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GeometryDrawModeScenario.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
|
|
||||||
//
|
|
||||||
// Scenario - A GEOMETRY SHADER'S INPUT PRIMITIVE CONSTRAINS THE DRAW MODE, AND
|
|
||||||
// GL_NONE IS NOT A USABLE "NO GEOMETRY SHADER" SENTINEL.
|
|
||||||
//
|
|
||||||
// GL 4.6 core 11.3.1: mode must be one of the primitive types that decomposes into the
|
|
||||||
// geometry shader's declared input primitive, or the draw is GL_INVALID_OPERATION. The
|
|
||||||
// validator asked "is there a geometry stage?" by comparing the REFLECTED INPUT PRIMITIVE
|
|
||||||
// against GL_NONE - and GL_NONE and GL_POINTS are both 0, so a `layout(points) in` geometry
|
|
||||||
// shader answered "no geometry stage" and every mode sailed through. The rule was therefore
|
|
||||||
// dead for exactly the geometry shaders whose input primitive rejects the most modes.
|
|
||||||
//
|
|
||||||
// KHR-GL43.transform_feedback.api_errors_test is where it showed: it draws a points-in
|
|
||||||
// geometry program with GL_LINES through glDrawTransformFeedbackInstanced and requires
|
|
||||||
// INVALID_OPERATION. The bug is not specific to that entry point - every draw shares this
|
|
||||||
// validator - so the ordinary glDrawArrays spelling is pinned here too, and the lines-in
|
|
||||||
// program is the control that proves the rule was not simply widened.
|
|
||||||
//
|
|
||||||
// Needs a real context: the validator returns before this rule when no backend object is
|
|
||||||
// active, so the GPU-free negative-API suite cannot reach it.
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "../Harness/HeadlessGL.h"
|
|
||||||
#include "../Harness/ScenarioFixture.h"
|
|
||||||
|
|
||||||
#ifdef GLAPI
|
|
||||||
#undef GLAPI
|
|
||||||
#endif
|
|
||||||
#define GL_GLEXT_PROTOTYPES
|
|
||||||
#include <GL/gl.h>
|
|
||||||
#include <GL/glcorearb.h>
|
|
||||||
#undef GL_GLEXT_PROTOTYPES
|
|
||||||
|
|
||||||
namespace MGITest {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
const char* const kVertexSource = R"(#version 420 core
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// The input primitive the CTS case uses, and the one the GL_NONE sentinel erased.
|
|
||||||
// `result` is here so the same program can be captured with transform feedback.
|
|
||||||
const char* const kPointsInGeometrySource = R"(#version 420 core
|
|
||||||
layout(points) in;
|
|
||||||
layout(points, max_vertices = 1) out;
|
|
||||||
out float result;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
gl_Position = gl_in[0].gl_Position;
|
|
||||||
result = 1.0;
|
|
||||||
EmitVertex();
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* const kLinesInGeometrySource = R"(#version 420 core
|
|
||||||
layout(lines) in;
|
|
||||||
layout(points, max_vertices = 1) out;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
gl_Position = gl_in[0].gl_Position;
|
|
||||||
EmitVertex();
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* const kFragmentSource = R"(#version 420 core
|
|
||||||
out vec4 fragColor;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
class GeometryDrawModeScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
glGenVertexArrays(1, &m_vao);
|
|
||||||
glBindVertexArray(m_vao);
|
|
||||||
if (!BackendHostsGeometry()) {
|
|
||||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " ("
|
|
||||||
<< Gl().RendererString() << "); there is no input primitive to validate";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
glUseProgram(0);
|
|
||||||
for (const GLuint program : m_programs) {
|
|
||||||
glDeleteProgram(program);
|
|
||||||
}
|
|
||||||
m_programs.clear();
|
|
||||||
glBindVertexArray(0);
|
|
||||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
||||||
m_vao = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The same real-backend probe IoBlockNameCollisionScenario uses: 0 on a DirectGLES
|
|
||||||
// driver without GL_EXT_geometry_shader and on a DirectVulkan device without the
|
|
||||||
// geometryShader feature.
|
|
||||||
static bool BackendHostsGeometry() {
|
|
||||||
GLint maxGeometryOutputVertices = 0;
|
|
||||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
|
||||||
DrainErrors();
|
|
||||||
return maxGeometryOutputVertices >= 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void DrainErrors() {
|
|
||||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint BuildProgram(const char* geometrySource, const char* capturedVarying = nullptr) {
|
|
||||||
const std::vector<std::pair<GLenum, const char*>> stages = {
|
|
||||||
{GL_VERTEX_SHADER, kVertexSource},
|
|
||||||
{GL_GEOMETRY_SHADER, geometrySource},
|
|
||||||
{GL_FRAGMENT_SHADER, kFragmentSource}};
|
|
||||||
|
|
||||||
std::vector<GLuint> shaders;
|
|
||||||
bool ok = true;
|
|
||||||
for (const auto& [stage, source] : stages) {
|
|
||||||
const GLuint shader = glCreateShader(stage);
|
|
||||||
glShaderSource(shader, 1, &source, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
GLint compiled = 0;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
||||||
shaders.push_back(shader);
|
|
||||||
if (!compiled) {
|
|
||||||
m_buildLog = InfoLog(shader, true);
|
|
||||||
ok = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!ok) {
|
|
||||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
for (const GLuint shader : shaders) glAttachShader(program, shader);
|
|
||||||
if (capturedVarying != nullptr) {
|
|
||||||
glTransformFeedbackVaryings(program, 1, &capturedVarying, GL_INTERLEAVED_ATTRIBS);
|
|
||||||
}
|
|
||||||
glLinkProgram(program);
|
|
||||||
GLint linked = 0;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
|
||||||
if (!linked) {
|
|
||||||
m_buildLog = InfoLog(program, false);
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
m_programs.push_back(program);
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string InfoLog(GLuint object, bool isShader) {
|
|
||||||
GLint length = 0;
|
|
||||||
if (isShader) {
|
|
||||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
|
||||||
} else {
|
|
||||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
|
||||||
}
|
|
||||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
|
||||||
if (isShader) {
|
|
||||||
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
|
||||||
} else {
|
|
||||||
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
|
||||||
}
|
|
||||||
return buffer.data();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& BuildLog() const { return m_buildLog; }
|
|
||||||
|
|
||||||
GLuint m_vao = 0;
|
|
||||||
std::vector<GLuint> m_programs;
|
|
||||||
std::string m_buildLog;
|
|
||||||
};
|
|
||||||
|
|
||||||
// GL_POINTS is the only mode that decomposes into a points input primitive.
|
|
||||||
TEST_F(GeometryDrawModeScenario, PointsInGeometryProgramRejectsEveryOtherMode) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
|
|
||||||
const GLuint program = BuildProgram(kPointsInGeometrySource);
|
|
||||||
ASSERT_NE(program, 0u) << "the points-in geometry program did not build: " << BuildLog();
|
|
||||||
|
|
||||||
glUseProgram(program);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
for (const GLenum mode :
|
|
||||||
{static_cast<GLenum>(GL_LINES), static_cast<GLenum>(GL_LINE_STRIP),
|
|
||||||
static_cast<GLenum>(GL_TRIANGLES), static_cast<GLenum>(GL_TRIANGLE_STRIP)}) {
|
|
||||||
glDrawArrays(mode, 0, 3);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
|
||||||
<< "mode " << mode << " does not decompose into the geometry shader's points input";
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The one mode that IS compatible still draws.
|
|
||||||
glDrawArrays(GL_POINTS, 0, 1);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The same rule reached through glDrawTransformFeedback*, which is the spelling the CTS
|
|
||||||
// case asks about. The capture span is really completed first, so GL_POINTS comes back
|
|
||||||
// GL_NO_ERROR: without that the draw would report INVALID_OPERATION for the
|
|
||||||
// never-ended-a-span reason instead and the case could not tell the two apart.
|
|
||||||
TEST_F(GeometryDrawModeScenario, PointsInGeometryProgramRejectsNonPointModesOnFeedbackDraws) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
|
|
||||||
const GLuint program = BuildProgram(kPointsInGeometrySource, "result");
|
|
||||||
ASSERT_NE(program, 0u) << "the points-in geometry program did not build: " << BuildLog();
|
|
||||||
|
|
||||||
GLuint feedback = 0;
|
|
||||||
glGenTransformFeedbacks(1, &feedback);
|
|
||||||
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, feedback);
|
|
||||||
GLuint captureBuffer = 0;
|
|
||||||
glGenBuffers(1, &captureBuffer);
|
|
||||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, captureBuffer);
|
|
||||||
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, 64, nullptr, GL_STATIC_DRAW);
|
|
||||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer);
|
|
||||||
glUseProgram(program);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
glBeginTransformFeedback(GL_POINTS);
|
|
||||||
glDrawArrays(GL_POINTS, 0, 1);
|
|
||||||
glEndTransformFeedback();
|
|
||||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the capture span did not complete";
|
|
||||||
|
|
||||||
glDrawTransformFeedbackInstanced(GL_LINES, feedback, 1);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
|
||||||
<< "glDrawTransformFeedbackInstanced must honour the geometry input primitive";
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
glDrawTransformFeedbackStreamInstanced(GL_LINES, feedback, 0, 1);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
|
||||||
<< "glDrawTransformFeedbackStreamInstanced must honour the geometry input primitive";
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
// The compatible mode replays the captured span with no error at all, which is what
|
|
||||||
// makes the two assertions above about the MODE and not about the span.
|
|
||||||
glDrawTransformFeedbackInstanced(GL_POINTS, feedback, 1);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
|
||||||
<< "a compatible mode must still replay the captured span";
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
glUseProgram(0);
|
|
||||||
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
|
||||||
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
|
||||||
glDeleteBuffers(1, &captureBuffer);
|
|
||||||
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0);
|
|
||||||
glDeleteTransformFeedbacks(1, &feedback);
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The control: a lines-in geometry shader is a NON-zero input primitive, so it exercised
|
|
||||||
// the rule even before the fix. It must still accept the line modes and still reject the
|
|
||||||
// others - a fix that widened the rule instead of repairing its guard breaks this.
|
|
||||||
TEST_F(GeometryDrawModeScenario, LinesInGeometryProgramStillAcceptsLineModesOnly) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
|
|
||||||
const GLuint program = BuildProgram(kLinesInGeometrySource);
|
|
||||||
ASSERT_NE(program, 0u) << "the lines-in geometry program did not build: " << BuildLog();
|
|
||||||
|
|
||||||
glUseProgram(program);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
for (const GLenum mode : {static_cast<GLenum>(GL_LINES), static_cast<GLenum>(GL_LINE_STRIP),
|
|
||||||
static_cast<GLenum>(GL_LINE_LOOP)}) {
|
|
||||||
glDrawArrays(mode, 0, 2);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
|
||||||
<< "mode " << mode << " decomposes into lines and must be accepted";
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const GLenum mode : {static_cast<GLenum>(GL_POINTS), static_cast<GLenum>(GL_TRIANGLES)}) {
|
|
||||||
glDrawArrays(mode, 0, 3);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
|
||||||
<< "mode " << mode << " does not decompose into lines";
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The other half of "ask the stage": WHICH stage list is asked. gsInputPrimitive is a
|
|
||||||
// LINK artifact, so pairing it with the live attach list re-points the GL_NONE/GL_POINTS
|
|
||||||
// aliasing instead of removing it - inside the window between glAttachShader and the
|
|
||||||
// next link, the live list says "geometry present" while the artifact still reads
|
|
||||||
// GL_NONE, which is 0, which is GL_POINTS, so every mode but GL_POINTS is rejected.
|
|
||||||
//
|
|
||||||
// GL 4.6 core 7.3 makes that window legal and ordinary: an attach affects the program's
|
|
||||||
// executable only at the next link, and leaves LINK_STATUS alone. The attached shader
|
|
||||||
// need not even compile. Worse, it does not heal - glDetachShader defers the removal to
|
|
||||||
// the next Link() too, so the program would keep failing every non-POINTS draw until the
|
|
||||||
// application happened to relink for some unrelated reason.
|
|
||||||
TEST_F(GeometryDrawModeScenario, AttachingAGeometryStageAfterTheLinkDoesNotConstrainTheDrawMode) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
|
|
||||||
// Deliberately NOT BuildProgram: the executable under test has no geometry stage.
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
m_programs.push_back(program);
|
|
||||||
for (const auto& [stage, source] :
|
|
||||||
std::vector<std::pair<GLenum, const char*>>{{GL_VERTEX_SHADER, kVertexSource},
|
|
||||||
{GL_FRAGMENT_SHADER, kFragmentSource}}) {
|
|
||||||
const GLuint shader = glCreateShader(stage);
|
|
||||||
glShaderSource(shader, 1, &source, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
glAttachShader(program, shader);
|
|
||||||
glDeleteShader(shader);
|
|
||||||
}
|
|
||||||
glLinkProgram(program);
|
|
||||||
GLint linked = GL_FALSE;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
ASSERT_EQ(linked, GL_TRUE) << "the vertex+fragment program did not link";
|
|
||||||
|
|
||||||
glUseProgram(program);
|
|
||||||
DrainErrors();
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
|
||||||
<< "a program with no geometry stage must draw triangles";
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
const GLuint geometry = glCreateShader(GL_GEOMETRY_SHADER);
|
|
||||||
glShaderSource(geometry, 1, &kPointsInGeometrySource, nullptr);
|
|
||||||
glCompileShader(geometry);
|
|
||||||
glAttachShader(program, geometry);
|
|
||||||
glDeleteShader(geometry);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
// Same executable as three lines ago - no relink has happened.
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
|
||||||
<< "the attach does not reach the executable until the next link, so the geometry "
|
|
||||||
"shader's points input must not constrain this draw";
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
// And once it IS linked in, the rule applies - the fix must not have simply disabled it.
|
|
||||||
glLinkProgram(program);
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
ASSERT_EQ(linked, GL_TRUE) << "the relink with the geometry stage failed";
|
|
||||||
glUseProgram(program);
|
|
||||||
DrainErrors();
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
|
||||||
<< "now that the points-in geometry shader is in the executable, triangles must be rejected";
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The tessellation guard above the geometry one had the identical defect, and it does not
|
|
||||||
// even need the GL_NONE aliasing to misfire: it drives BOTH directions unconditionally, so
|
|
||||||
// reading the live attach list rejects every non-GL_PATCHES draw the moment an evaluation
|
|
||||||
// shader is attached, whether or not it was ever linked in.
|
|
||||||
TEST_F(GeometryDrawModeScenario, AttachingATessEvalStageAfterTheLinkDoesNotForceGlPatches) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
|
|
||||||
GLint maxPatchVertices = 0;
|
|
||||||
glGetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);
|
|
||||||
DrainErrors();
|
|
||||||
if (maxPatchVertices < 3) GTEST_SKIP() << "no tessellation stage on this backend";
|
|
||||||
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
m_programs.push_back(program);
|
|
||||||
for (const auto& [stage, source] :
|
|
||||||
std::vector<std::pair<GLenum, const char*>>{{GL_VERTEX_SHADER, kVertexSource},
|
|
||||||
{GL_FRAGMENT_SHADER, kFragmentSource}}) {
|
|
||||||
const GLuint shader = glCreateShader(stage);
|
|
||||||
glShaderSource(shader, 1, &source, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
glAttachShader(program, shader);
|
|
||||||
glDeleteShader(shader);
|
|
||||||
}
|
|
||||||
glLinkProgram(program);
|
|
||||||
GLint linked = GL_FALSE;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
ASSERT_EQ(linked, GL_TRUE) << "the vertex+fragment program did not link";
|
|
||||||
|
|
||||||
glUseProgram(program);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
static const char* const kTessEvalSource = R"(#version 420 core
|
|
||||||
layout(triangles, equal_spacing, ccw) in;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
gl_Position = gl_in[0].gl_Position;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
const GLuint tessEval = glCreateShader(GL_TESS_EVALUATION_SHADER);
|
|
||||||
glShaderSource(tessEval, 1, &kTessEvalSource, nullptr);
|
|
||||||
glCompileShader(tessEval);
|
|
||||||
glAttachShader(program, tessEval);
|
|
||||||
glDeleteShader(tessEval);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
|
|
||||||
<< "the executable still has no tessellation stage, so GL_PATCHES must not be required";
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -179,13 +179,6 @@ void main()
|
|||||||
in flat uint v_index;
|
in flat uint v_index;
|
||||||
out vec4 o_color;
|
out vec4 o_color;
|
||||||
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||||
)";
|
|
||||||
|
|
||||||
// The colour index spelled out at its default value. Says nothing that
|
|
||||||
// `layout(location = 0)` alone does not, and must therefore cost nothing.
|
|
||||||
constexpr const char* kExplicitColorIndexFS = R"(#version 420 core
|
|
||||||
layout(location = 0, index = 0) out vec4 o_color;
|
|
||||||
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
|
||||||
)";
|
)";
|
||||||
|
|
||||||
class Glsl420DeclarationScenario : public ScenarioTest {
|
class Glsl420DeclarationScenario : public ScenarioTest {
|
||||||
@@ -480,24 +473,4 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
|||||||
EXPECT_EQ(centre.g, 255) << "the atomic-counter shader linked but painted nothing";
|
EXPECT_EQ(centre.g, 255) << "the atomic-counter shader linked but painted nothing";
|
||||||
}
|
}
|
||||||
|
|
||||||
// `layout(location = 0, index = 0)` is the GL default written out loud, and an application
|
|
||||||
// is entitled to write it - KHR-GL43.shader_atomic_counters.basic-program-query does. It has
|
|
||||||
// to reach the driver as an ORDINARY single-source output: GLSL ES has no `index` qualifier
|
|
||||||
// in core, so a transpiler that prints the decoration back gets "index layout qualifier
|
|
||||||
// requires EXT_blend_func_extended", the stage never compiles, the program runs with a stage
|
|
||||||
// missing and the draw paints nothing at all. Black, not red - which is why the conformance
|
|
||||||
// case looked like the atomic counters had stopped counting.
|
|
||||||
TEST_F(Glsl420DeclarationScenario, AnExplicitDefaultColorIndexStillDraws) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
const GLuint program = Build(kQuadVS, kExplicitColorIndexFS);
|
|
||||||
if (program == 0) return;
|
|
||||||
|
|
||||||
const Rgba8 centre = DrawAndRead(program);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
EXPECT_EQ(centre.g, 255) << "a fragment output declared layout(location = 0, index = 0) painted "
|
|
||||||
"nothing; its stage was almost certainly refused by the driver";
|
|
||||||
EXPECT_EQ(centre.r, 0u);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -127,23 +127,14 @@ void main()
|
|||||||
// One qualifier is all an ARRAY declaration can carry, and ESSL then gives the
|
// One qualifier is all an ARRAY declaration can carry, and ESSL then gives the
|
||||||
// array's elements the CONSECUTIVE units N, N+1, N+2, ... - so a per-element
|
// array's elements the CONSECUTIVE units N, N+1, N+2, ... - so a per-element
|
||||||
// assignment that is not consecutive (the conformance case uses 0, 2, 4, 6) has no
|
// assignment that is not consecutive (the conformance case uses 0, 2, 4, 6) has no
|
||||||
// spelling in a single declaration.
|
// spelling in a single declaration and cannot be expressed at all without splitting
|
||||||
|
// the array into one declaration per element and rewriting every use of it.
|
||||||
//
|
//
|
||||||
// RemapImageArrayElementUnits repairs it by SPLITTING the array into one scalar
|
// Scoped rather than disabled, exactly as ProgramPipelineScenario scopes its
|
||||||
// image uniform per element, each carrying its own binding, which costs exactly the
|
// storage-block rebinding cases: the defect is per-backend and the frontend
|
||||||
// four image uniforms the application declared. (It used to WIDEN the array to cover
|
// mechanism these cases exist for - per-element units surviving the trip to the
|
||||||
// the whole span instead, which cost seven for those four elements and had to be
|
// pipeline composite - is fully exercised on Magma.
|
||||||
// declined on a stage that could not afford them - hence the budget gate that used
|
bool PerElementImageUnitsAreHonoured() const { return Gl().BackendName() == "DirectVulkan"; }
|
||||||
// to be here.) DirectVulkan needs no rewrite at all.
|
|
||||||
bool PerElementImageUnitsAreHonoured() const {
|
|
||||||
if (Gl().BackendName() == "DirectVulkan") return true;
|
|
||||||
GLint maxFragmentImageUniforms = 0;
|
|
||||||
glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
|
|
||||||
while (glGetError() != GL_NO_ERROR) {
|
|
||||||
}
|
|
||||||
// One per element of the four-element array either fragment program declares.
|
|
||||||
return maxFragmentImageUniforms >= 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The scenarios below need image load/store at all; a driver without it should skip
|
// The scenarios below need image load/store at all; a driver without it should skip
|
||||||
// rather than fail.
|
// rather than fail.
|
||||||
@@ -173,7 +164,7 @@ void main()
|
|||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
||||||
if (!PerElementImageUnitsAreHonoured()) {
|
if (!PerElementImageUnitsAreHonoured()) {
|
||||||
GTEST_SKIP() << "fewer than 4 fragment image uniforms: the array under test does not fit";
|
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL";
|
||||||
}
|
}
|
||||||
HeadlessGL& gl = Gl();
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
@@ -292,12 +283,8 @@ void main()
|
|||||||
TEST_F(ImageLoadStoreSsoScenario, AnImageArrayAlongsideAnotherDescriptorKeepsBothBindings) {
|
TEST_F(ImageLoadStoreSsoScenario, AnImageArrayAlongsideAnotherDescriptorKeepsBothBindings) {
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
if (!ImagesAreUsable()) GTEST_SKIP() << "fewer than 8 image units";
|
||||||
// The defect this guards is the SPIR-V descriptor remap, which only Magma has; the units
|
if (!PerElementImageUnitsAreHonoured()) {
|
||||||
// here are consecutive on purpose, so on Espryt this would exercise nothing the case
|
GTEST_SKIP() << "non-consecutive per-element image units cannot be baked into ESSL";
|
||||||
// above does not. Scoped by what it TESTS rather than by the image-array widening, which
|
|
||||||
// it deliberately never triggers.
|
|
||||||
if (Gl().BackendName() != "DirectVulkan") {
|
|
||||||
GTEST_SKIP() << "the descriptor binding remap under test is DirectVulkan's";
|
|
||||||
}
|
}
|
||||||
HeadlessGL& gl = Gl();
|
HeadlessGL& gl = Gl();
|
||||||
|
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageSizeAfterRespecScenario.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
|
|
||||||
//
|
|
||||||
// Scenario - A DRAW READS imageSize() AFTER THE IMAGE TEXTURE IS RE-SPECIFIED.
|
|
||||||
//
|
|
||||||
// KHR-GL43.shader_image_size.advanced-changeSize reduced to its mechanism. The application binds
|
|
||||||
// a texture to an image unit ONCE, draws, then re-specifies that same texture with a new size
|
|
||||||
// through glTexImage2D and draws again - without touching the image unit. GL says the unit
|
|
||||||
// references the texture OBJECT, so the second draw must see the new dimensions.
|
|
||||||
//
|
|
||||||
// On Espryt it did not, and the reason is two facts meeting:
|
|
||||||
//
|
|
||||||
// 1. ES 3.1 only allows IMMUTABLE storage on an image unit, so the backend forces glTexStorage
|
|
||||||
// backing on any texture that reaches one (SyncTextureObjectToBackend's
|
|
||||||
// imageBindableStorageRequired). Immutable storage cannot be redefined, so a glTexImage2D
|
|
||||||
// that changes size or format has to MINT A NEW ES TEXTURE NAME.
|
|
||||||
// 2. The draw path never re-issued glBindImageTexture. Image units were established eagerly,
|
|
||||||
// once, when the application called glBindImageTexture, and PrepareForDraw only ever
|
|
||||||
// re-synced SAMPLED textures - so the unit kept pointing at the deleted name and
|
|
||||||
// imageSize() reported whatever that stale binding still meant.
|
|
||||||
//
|
|
||||||
// A dispatch was never affected: PrepareForCompute has always swept the image units. This is a
|
|
||||||
// draw-path scenario for exactly that reason - a compute-shaped case cannot see the defect.
|
|
||||||
//
|
|
||||||
// Both backends run it. Magma re-derives its image descriptors per draw and so was never wrong
|
|
||||||
// here, which makes it the control: the two backends have to agree on what the second draw sees.
|
|
||||||
|
|
||||||
#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 kTargetSize = 8;
|
|
||||||
|
|
||||||
constexpr const char* kVS = R"(#version 430 core
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
// A single triangle that covers the whole target, with no vertex buffer at all: the
|
|
||||||
// scenario is about the image unit, so nothing else may be able to make it fail.
|
|
||||||
switch (gl_VertexID)
|
|
||||||
{
|
|
||||||
case 0: gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); break;
|
|
||||||
case 1: gl_Position = vec4( 3.0, -1.0, 0.0, 1.0); break;
|
|
||||||
case 2: gl_Position = vec4(-1.0, 3.0, 0.0, 1.0); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// Green when the image the unit currently holds has the size the application last gave
|
|
||||||
// it, red otherwise - the conformance case's own comparison, and its own colours.
|
|
||||||
constexpr const char* kFS = R"(#version 430 core
|
|
||||||
layout(rgba8) readonly uniform image2D g_image;
|
|
||||||
uniform ivec2 g_expected_size;
|
|
||||||
layout(location = 0) out vec4 o_color;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
o_color = (imageSize(g_image) == g_expected_size) ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
class ImageSizeAfterRespecScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
glUseProgram(0);
|
|
||||||
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
if (m_program != 0) glDeleteProgram(m_program);
|
|
||||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
|
||||||
if (m_color != 0) glDeleteTextures(1, &m_color);
|
|
||||||
if (m_image != 0) glDeleteTextures(1, &m_image);
|
|
||||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
||||||
m_program = m_fbo = m_color = m_image = m_vao = 0;
|
|
||||||
while (glGetError() != GL_NO_ERROR) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// imageSize() needs a fragment-stage image uniform; a driver that serves none should
|
|
||||||
// skip rather than fail.
|
|
||||||
bool FragmentImagesAreUsable() const {
|
|
||||||
GLint maxImageUnits = 0;
|
|
||||||
GLint maxFragmentImageUniforms = 0;
|
|
||||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
|
||||||
glGetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImageUniforms);
|
|
||||||
while (glGetError() != GL_NO_ERROR) {
|
|
||||||
}
|
|
||||||
return maxImageUnits >= 1 && maxFragmentImageUniforms >= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint MakeProgram() {
|
|
||||||
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
|
||||||
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
|
||||||
glShaderSource(vs, 1, &kVS, nullptr);
|
|
||||||
glShaderSource(fs, 1, &kFS, nullptr);
|
|
||||||
glCompileShader(vs);
|
|
||||||
glCompileShader(fs);
|
|
||||||
for (const GLuint shader : {vs, fs}) {
|
|
||||||
GLint compiled = GL_FALSE;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
||||||
if (compiled == GL_FALSE) {
|
|
||||||
char log[4096] = {};
|
|
||||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
|
||||||
ADD_FAILURE() << "a shader did not compile: " << log;
|
|
||||||
glDeleteShader(vs);
|
|
||||||
glDeleteShader(fs);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
glAttachShader(program, vs);
|
|
||||||
glAttachShader(program, fs);
|
|
||||||
glLinkProgram(program);
|
|
||||||
glDeleteShader(vs);
|
|
||||||
glDeleteShader(fs);
|
|
||||||
GLint linked = GL_FALSE;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
if (linked == GL_FALSE) {
|
|
||||||
char log[4096] = {};
|
|
||||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
|
||||||
ADD_FAILURE() << "the program did not link: " << log;
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MakeRenderTarget() {
|
|
||||||
glGenTextures(1, &m_color);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_color);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTargetSize, kTargetSize, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
|
||||||
nullptr);
|
|
||||||
glGenFramebuffers(1, &m_fbo);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_color, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw once with `expected` pushed to the shader and report the centre pixel.
|
|
||||||
void DrawAndReadCentre(int expectedWidth, int expectedHeight, unsigned char (¢re)[4]) {
|
|
||||||
const GLint location = glGetUniformLocation(m_program, "g_expected_size");
|
|
||||||
ASSERT_NE(location, -1) << "the program has no g_expected_size uniform";
|
|
||||||
glUseProgram(m_program);
|
|
||||||
glUniform2i(location, expectedWidth, expectedHeight);
|
|
||||||
glViewport(0, 0, kTargetSize, kTargetSize);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
|
||||||
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "the draw left a GL error";
|
|
||||||
|
|
||||||
std::vector<unsigned char> pixels(static_cast<std::size_t>(kTargetSize) * kTargetSize * 4, 0);
|
|
||||||
glReadPixels(0, 0, kTargetSize, kTargetSize, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "reading the target back errored";
|
|
||||||
const std::size_t offset =
|
|
||||||
(static_cast<std::size_t>(kTargetSize / 2) * kTargetSize + kTargetSize / 2) * 4;
|
|
||||||
for (int i = 0; i < 4; ++i) {
|
|
||||||
centre[i] = pixels[offset + static_cast<std::size_t>(i)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint m_program = 0;
|
|
||||||
GLuint m_fbo = 0;
|
|
||||||
GLuint m_color = 0;
|
|
||||||
GLuint m_image = 0;
|
|
||||||
GLuint m_vao = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// The whole conformance shape: bind once, draw, re-specify the SAME texture smaller, draw
|
|
||||||
// again. The first draw is the control - it proves the binding and the shader work at all -
|
|
||||||
// and the second is the regression pin. Blue would mean the draw never ran; red means the
|
|
||||||
// image unit answered with the size the texture had BEFORE the re-spec.
|
|
||||||
TEST_F(ImageSizeAfterRespecScenario, ADrawSeesTheNewSizeOfARespecifiedImageTexture) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
if (!FragmentImagesAreUsable()) GTEST_SKIP() << "no fragment-stage image uniform available";
|
|
||||||
|
|
||||||
m_program = MakeProgram();
|
|
||||||
if (m_program == 0) return;
|
|
||||||
glGenVertexArrays(1, &m_vao);
|
|
||||||
glBindVertexArray(m_vao);
|
|
||||||
MakeRenderTarget();
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "setting the render target up errored";
|
|
||||||
|
|
||||||
glGenTextures(1, &m_image);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_image);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 32, 32, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
|
||||||
glBindImageTexture(0, m_image, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "binding the image texture errored";
|
|
||||||
|
|
||||||
unsigned char centre[4] = {0, 0, 0, 0};
|
|
||||||
DrawAndReadCentre(32, 32, centre);
|
|
||||||
EXPECT_EQ(static_cast<int>(centre[0]), 0) << "the FIRST draw already disagrees about imageSize(): got ("
|
|
||||||
<< static_cast<int>(centre[0]) << ", "
|
|
||||||
<< static_cast<int>(centre[1]) << ", "
|
|
||||||
<< static_cast<int>(centre[2]) << ")";
|
|
||||||
EXPECT_EQ(static_cast<int>(centre[1]), 255);
|
|
||||||
|
|
||||||
// The re-spec. The image unit is deliberately NOT re-bound: GL 4.6 core 8.26 says the
|
|
||||||
// unit references the texture object, so this alone has to be visible to the next draw.
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_image);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "re-specifying the image texture errored";
|
|
||||||
|
|
||||||
DrawAndReadCentre(16, 16, centre);
|
|
||||||
EXPECT_EQ(static_cast<int>(centre[0]), 0)
|
|
||||||
<< "after the re-spec the draw still sees the OLD image size; centre pixel was ("
|
|
||||||
<< static_cast<int>(centre[0]) << ", " << static_cast<int>(centre[1]) << ", "
|
|
||||||
<< static_cast<int>(centre[2]) << ")";
|
|
||||||
EXPECT_EQ(static_cast<int>(centre[1]), 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -66,9 +66,6 @@ namespace MGITest {
|
|||||||
constexpr int kExtent = 6;
|
constexpr int kExtent = 6;
|
||||||
constexpr GLuint kFilledValue = 7u;
|
constexpr GLuint kFilledValue = 7u;
|
||||||
constexpr GLuint kStoredValue = 13u;
|
constexpr GLuint kStoredValue = 13u;
|
||||||
// What the atomic cases add to a filled texel. Distinct from both values above, so a
|
|
||||||
// wrong answer cannot be read as either the untouched fill or a plain store.
|
|
||||||
constexpr GLuint kAtomicAddend = 5u;
|
|
||||||
|
|
||||||
// Everything that differs between the eleven kinds, in one row.
|
// Everything that differs between the eleven kinds, in one row.
|
||||||
struct TargetKind {
|
struct TargetKind {
|
||||||
@@ -132,25 +129,6 @@ namespace MGITest {
|
|||||||
kind.imageType + " i0;\n\nvoid main()\n{\n " + StoreStatement(kind, "i0", "13u") + "\n}\n";
|
kind.imageType + " i0;\n\nvoid main()\n{\n " + StoreStatement(kind, "i0", "13u") + "\n}\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// The third direction, and the one neither of the two above can stand in for: an
|
|
||||||
// imageAtomic* reaches its texel through a SPIR-V operand path of its own
|
|
||||||
// (OpImageTexelPointer), not through OpImageRead or OpImageWrite. SPIRV-Cross's "ES has
|
|
||||||
// no 1D image, address it as 2D" coordinate widening is applied on the read and write
|
|
||||||
// paths and NOT on that one, so a 1D image whose loads and stores are both correct could
|
|
||||||
// still lose its entire stage to a single imageAtomicAdd - which is what
|
|
||||||
// KHR-GL4x.shader_image_load_store.basic-allTargets-atomic measured, with the driver
|
|
||||||
// answering "'imageAtomicAdd' : no matching overloaded function found".
|
|
||||||
//
|
|
||||||
// No readonly/writeonly here: an atomic needs both directions, and r32ui is one of the
|
|
||||||
// three formats GLSL ES exempts from the qualifier rule, so the bare declaration is legal.
|
|
||||||
// Returns the value the texel held BEFORE the add, so one dispatch checks the atomic's
|
|
||||||
// return value and the load case that follows checks its memory effect.
|
|
||||||
std::string SingleAtomicSource(const TargetKind& kind) {
|
|
||||||
return std::string(kComputePrologue) + "layout (location = 0, r32ui) coherent uniform " +
|
|
||||||
kind.imageType + " i0;\n" + kResultBlock + "void main()\n{\n ssb.sum = imageAtomicAdd(i0, " +
|
|
||||||
kind.coord + (kind.multisample ? ", 0, " : ", ") + std::to_string(kAtomicAddend) + "u);\n}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
class ImageTargetKindScenario : public ScenarioTest {
|
class ImageTargetKindScenario : public ScenarioTest {
|
||||||
protected:
|
protected:
|
||||||
void TearDown() override {
|
void TearDown() override {
|
||||||
@@ -396,119 +374,6 @@ namespace MGITest {
|
|||||||
glUseProgram(0);
|
glUseProgram(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill a texture of `kind`, add to texel (0,0,0) atomically, and require BOTH the
|
|
||||||
// value the atomic returned and the value it left behind. The read-back runs as a
|
|
||||||
// second program, for the same reason the store case does: a backend that gets the
|
|
||||||
// atomic's return right and its memory effect wrong cannot cancel itself out.
|
|
||||||
void RunAtomicCase(const TargetKind& kind) {
|
|
||||||
const GLuint atomicProgram = MakeComputeProgram(SingleAtomicSource(kind));
|
|
||||||
const GLuint loadProgram = MakeComputeProgram(SingleLoadSource(kind));
|
|
||||||
if (atomicProgram == 0 || loadProgram == 0) return;
|
|
||||||
const GLuint texture = MakeTexture(kind, true);
|
|
||||||
if (texture == 0) return;
|
|
||||||
const GLuint ssbo = MakeResultBuffer();
|
|
||||||
|
|
||||||
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_WRITE, GL_R32UI);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";
|
|
||||||
|
|
||||||
glUseProgram(atomicProgram);
|
|
||||||
glUniform1i(0, 0);
|
|
||||||
glDispatchCompute(1, 1, 1);
|
|
||||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the atomic dispatch leaked a GL error";
|
|
||||||
EXPECT_EQ(ReadResult(ssbo), kFilledValue)
|
|
||||||
<< kind.name << ": imageAtomicAdd did not return the value the texel held before it";
|
|
||||||
|
|
||||||
glUseProgram(loadProgram);
|
|
||||||
glUniform1i(0, 0);
|
|
||||||
glDispatchCompute(1, 1, 1);
|
|
||||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the loading dispatch leaked a GL error";
|
|
||||||
EXPECT_EQ(ReadResult(ssbo), kFilledValue + kAtomicAddend)
|
|
||||||
<< kind.name << ": imageAtomicAdd did not leave the sum in the texel";
|
|
||||||
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;
|
||||||
@@ -570,54 +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 atomic direction, on the two kinds ES has to emulate -------
|
|
||||||
//
|
|
||||||
// Deliberately NOT every kind. imageAtomic* takes its own SPIR-V operand path
|
|
||||||
// (OpImageTexelPointer), and the only kinds whose coordinate that path has to RESHAPE are the
|
|
||||||
// two 1D ones - everything else addresses its ES texture with the coordinate the application
|
|
||||||
// wrote. GL_TEXTURE_1D_ARRAY is the control (its reshape has been in
|
|
||||||
// Lower1DArrayImagesForEssl from the start, and basic-allTargets-atomic passes on it);
|
|
||||||
// GL_TEXTURE_1D is the one that had none, so `imageAtomicAdd(g_image_1d, coord.x, 2)` reached
|
|
||||||
// the driver as a scalar against an iimage2D and took the whole fragment stage - and its six
|
|
||||||
// other images - with it.
|
|
||||||
|
|
||||||
#define MGL_DEFINE_ATOMIC_CASE(CaseName, Kind) \
|
|
||||||
TEST_F(ImageTargetKindScenario, AtomicallyAddsTo##CaseName) { \
|
|
||||||
if (!Ready()) return; \
|
|
||||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
|
|
||||||
RunAtomicCase(Kind); \
|
|
||||||
}
|
|
||||||
|
|
||||||
MGL_DEFINE_ATOMIC_CASE(Texture1D, kKind1D)
|
|
||||||
MGL_DEFINE_ATOMIC_CASE(Texture1DArray, kKind1DArray)
|
|
||||||
|
|
||||||
#undef MGL_DEFINE_ATOMIC_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,389 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IoBlockNameCollisionScenario.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
|
|
||||||
//
|
|
||||||
// Scenario - ONE BLOCK NAME USED IN BOTH DIRECTIONS BY ONE STAGE STILL CARRIES ITS PAYLOAD.
|
|
||||||
//
|
|
||||||
// Desktop GLSL keeps SEPARATE name namespaces for input and output interface blocks, so a
|
|
||||||
// single stage may legally write
|
|
||||||
//
|
|
||||||
// in TcsData { ... } tes_in[];
|
|
||||||
// out TcsData { ... } tes_out;
|
|
||||||
//
|
|
||||||
// The tessellation evaluation stage of both interface-block tests in
|
|
||||||
// KHR-GL42/43.shading_language_420pack does exactly that, and MobileGL's backend used to
|
|
||||||
// hand the shape straight through: SPIRV-Cross splits the namespace the same way glslang
|
|
||||||
// does (block_input_names vs block_output_names) and re-emits BOTH blocks under the name
|
|
||||||
// TcsData, so the generated ESSL declares two different blocks of one name in one shader.
|
|
||||||
// Adreno's ES compiler keeps them apart. Mali's does not - the stage compiles, the program
|
|
||||||
// links, and the evaluation stage's writes never reach the geometry stage, which is all 22
|
|
||||||
// of that group's Mali failures and none of Adreno's or DirectVulkan's.
|
|
||||||
//
|
|
||||||
// Both cases below drive the SAME five-stage pipeline (vertex -> tessellation control ->
|
|
||||||
// tessellation evaluation -> geometry -> fragment) and differ only in whether the
|
|
||||||
// evaluation stage reuses one name. The distinct-name case is the negative control: it is
|
|
||||||
// what says a red pixel in the colliding case is about the name and not about this machine's
|
|
||||||
// tessellation, its geometry stage, or the block mechanism in general.
|
|
||||||
//
|
|
||||||
// Colour code, so a failure names its own cause:
|
|
||||||
// green - the payload crossed all four stage boundaries, which is the pass.
|
|
||||||
// blue - the clear colour: nothing was drawn at all (the program did not link, or the
|
|
||||||
// backend program was rejected and every draw became a no-op).
|
|
||||||
// red - the pipeline ran but the plain (non-block) varying did not arrive, i.e. the
|
|
||||||
// failure is not about interface blocks.
|
|
||||||
// black - the pipeline ran, the plain varying arrived, and the BLOCK payload came back
|
|
||||||
// zeroed or garbage. That is the defect this scenario exists for.
|
|
||||||
//
|
|
||||||
// llvmpipe and lavapipe run this faithfully but do NOT reproduce the original defect - the
|
|
||||||
// aliasing is a Mali ES compiler behaviour. Read a green run here as "the rename did not
|
|
||||||
// break the ordinary path"; the claim it pins on the device is the CTS group above.
|
|
||||||
|
|
||||||
#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 {
|
|
||||||
|
|
||||||
// The payload starts here and is copied, unmodified, through every block below.
|
|
||||||
const char* const kVertexSource = R"(#version 420 core
|
|
||||||
out VsData {
|
|
||||||
vec4 payload;
|
|
||||||
} vs_out;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
vs_out.payload = vec4(0.0, 1.0, 0.0, 1.0);
|
|
||||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* const kTessControlSource = R"(#version 420 core
|
|
||||||
layout(vertices = 1) out;
|
|
||||||
in VsData {
|
|
||||||
vec4 payload;
|
|
||||||
} tcs_in[];
|
|
||||||
out TcsData {
|
|
||||||
vec4 payload;
|
|
||||||
} tcs_out[];
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
tcs_out[gl_InvocationID].payload = tcs_in[gl_InvocationID].payload;
|
|
||||||
gl_TessLevelOuter[0] = 1.0;
|
|
||||||
gl_TessLevelOuter[1] = 1.0;
|
|
||||||
gl_TessLevelOuter[2] = 1.0;
|
|
||||||
gl_TessLevelOuter[3] = 1.0;
|
|
||||||
gl_TessLevelInner[0] = 1.0;
|
|
||||||
gl_TessLevelInner[1] = 1.0;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// THE CASE UNDER TEST: one name, both directions, in one stage.
|
|
||||||
const char* const kCollidingTessEvalSource = R"(#version 420 core
|
|
||||||
layout(isolines, point_mode) in;
|
|
||||||
in TcsData {
|
|
||||||
vec4 payload;
|
|
||||||
} tes_in[];
|
|
||||||
out TcsData {
|
|
||||||
vec4 payload;
|
|
||||||
} tes_out;
|
|
||||||
out float tes_gs_alive;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
tes_out.payload = tes_in[0].payload;
|
|
||||||
tes_gs_alive = 1.0;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// The negative control: byte-identical but for the output block's name.
|
|
||||||
const char* const kDistinctTessEvalSource = R"(#version 420 core
|
|
||||||
layout(isolines, point_mode) in;
|
|
||||||
in TcsData {
|
|
||||||
vec4 payload;
|
|
||||||
} tes_in[];
|
|
||||||
out TesData {
|
|
||||||
vec4 payload;
|
|
||||||
} tes_out;
|
|
||||||
out float tes_gs_alive;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
tes_out.payload = tes_in[0].payload;
|
|
||||||
tes_gs_alive = 1.0;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// One geometry source per evaluation stage, because the block it consumes is named
|
|
||||||
// after the block the evaluation stage produced.
|
|
||||||
const char* const kCollidingGeometrySource = R"(#version 420 core
|
|
||||||
layout(points) in;
|
|
||||||
layout(triangle_strip, max_vertices = 4) out;
|
|
||||||
in TcsData {
|
|
||||||
vec4 payload;
|
|
||||||
} gs_in[];
|
|
||||||
in float tes_gs_alive[];
|
|
||||||
out GsData {
|
|
||||||
vec4 payload;
|
|
||||||
} gs_out;
|
|
||||||
out float gs_fs_alive;
|
|
||||||
void EmitCorner(vec2 corner)
|
|
||||||
{
|
|
||||||
gs_out.payload = gs_in[0].payload;
|
|
||||||
gs_fs_alive = tes_gs_alive[0];
|
|
||||||
gl_Position = vec4(corner, 0.0, 1.0);
|
|
||||||
EmitVertex();
|
|
||||||
}
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
EmitCorner(vec2(-1.0, -1.0));
|
|
||||||
EmitCorner(vec2(-1.0, 1.0));
|
|
||||||
EmitCorner(vec2( 1.0, -1.0));
|
|
||||||
EmitCorner(vec2( 1.0, 1.0));
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* const kDistinctGeometrySource = R"(#version 420 core
|
|
||||||
layout(points) in;
|
|
||||||
layout(triangle_strip, max_vertices = 4) out;
|
|
||||||
in TesData {
|
|
||||||
vec4 payload;
|
|
||||||
} gs_in[];
|
|
||||||
in float tes_gs_alive[];
|
|
||||||
out GsData {
|
|
||||||
vec4 payload;
|
|
||||||
} gs_out;
|
|
||||||
out float gs_fs_alive;
|
|
||||||
void EmitCorner(vec2 corner)
|
|
||||||
{
|
|
||||||
gs_out.payload = gs_in[0].payload;
|
|
||||||
gs_fs_alive = tes_gs_alive[0];
|
|
||||||
gl_Position = vec4(corner, 0.0, 1.0);
|
|
||||||
EmitVertex();
|
|
||||||
}
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
EmitCorner(vec2(-1.0, -1.0));
|
|
||||||
EmitCorner(vec2(-1.0, 1.0));
|
|
||||||
EmitCorner(vec2( 1.0, -1.0));
|
|
||||||
EmitCorner(vec2( 1.0, 1.0));
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// Red when the PLAIN varying did not arrive, so "the pipeline is broken" and "the
|
|
||||||
// block payload is broken" cannot be confused for one another.
|
|
||||||
const char* const kFragmentSource = R"(#version 420 core
|
|
||||||
in GsData {
|
|
||||||
vec4 payload;
|
|
||||||
} fs_in;
|
|
||||||
in float gs_fs_alive;
|
|
||||||
out vec4 fragColor;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
fragColor = gs_fs_alive > 0.5 ? fs_in.payload : vec4(1.0, 0.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
class IoBlockNameCollisionScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
glGenVertexArrays(1, &m_vao);
|
|
||||||
glBindVertexArray(m_vao);
|
|
||||||
if (!BackendHostsTessellationAndGeometry()) {
|
|
||||||
GTEST_SKIP() << "no tessellation/geometry stages on " << Gl().BackendName() << " ("
|
|
||||||
<< Gl().RendererString() << "); there is no five-stage pipeline to "
|
|
||||||
<< "carry a block through";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
glUseProgram(0);
|
|
||||||
for (const GLuint program : m_programs) {
|
|
||||||
glDeleteProgram(program);
|
|
||||||
}
|
|
||||||
m_programs.clear();
|
|
||||||
glBindVertexArray(0);
|
|
||||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
||||||
m_vao = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GL_MAX_TESS_GEN_LEVEL is a real backend answer, not a frontend constant: it
|
|
||||||
// reads 0 on a DirectGLES driver without GL_EXT_tessellation_shader and on a
|
|
||||||
// DirectVulkan device without the tessellationShader feature. There is no
|
|
||||||
// five-stage pipeline to assert about on such a stack.
|
|
||||||
static bool BackendHostsTessellationAndGeometry() {
|
|
||||||
GLint maxTessGenLevel = 0;
|
|
||||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
|
||||||
GLint maxGeometryOutputVertices = 0;
|
|
||||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
|
||||||
while (glGetError() != GL_NO_ERROR) {
|
|
||||||
}
|
|
||||||
return maxTessGenLevel >= 1 && maxGeometryOutputVertices >= 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint BuildPipeline(const char* tessEvalSource, const char* geometrySource) {
|
|
||||||
const GLenum stages[] = {GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER,
|
|
||||||
GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER,
|
|
||||||
GL_FRAGMENT_SHADER};
|
|
||||||
const char* const sources[] = {kVertexSource, kTessControlSource, tessEvalSource,
|
|
||||||
geometrySource, kFragmentSource};
|
|
||||||
|
|
||||||
GLuint shaders[5] = {0, 0, 0, 0, 0};
|
|
||||||
bool ok = true;
|
|
||||||
for (int i = 0; i < 5; ++i) {
|
|
||||||
shaders[i] = glCreateShader(stages[i]);
|
|
||||||
glShaderSource(shaders[i], 1, &sources[i], nullptr);
|
|
||||||
glCompileShader(shaders[i]);
|
|
||||||
GLint compiled = 0;
|
|
||||||
glGetShaderiv(shaders[i], GL_COMPILE_STATUS, &compiled);
|
|
||||||
if (!compiled) {
|
|
||||||
m_buildLog = InfoLog(shaders[i], true);
|
|
||||||
ok = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!ok) {
|
|
||||||
for (const GLuint shader : shaders) {
|
|
||||||
if (shader != 0) glDeleteShader(shader);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
for (const GLuint shader : shaders) {
|
|
||||||
glAttachShader(program, shader);
|
|
||||||
}
|
|
||||||
glLinkProgram(program);
|
|
||||||
GLint linked = 0;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
for (const GLuint shader : shaders) {
|
|
||||||
glDeleteShader(shader);
|
|
||||||
}
|
|
||||||
if (!linked) {
|
|
||||||
m_buildLog = InfoLog(program, false);
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
m_programs.push_back(program);
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clears to BLUE, so "the draw painted nothing" is a colour of its own rather
|
|
||||||
// than something that could be mistaken for a zeroed payload.
|
|
||||||
Rgba8 DrawAndReadCentre(GLuint program) const {
|
|
||||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
|
||||||
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
|
||||||
glUseProgram(program);
|
|
||||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
|
||||||
glDrawArrays(GL_PATCHES, 0, 1);
|
|
||||||
|
|
||||||
Rgba8 pixel{};
|
|
||||||
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
|
|
||||||
return pixel;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool IsGreen(const Rgba8& pixel) {
|
|
||||||
return pixel.r < 64 && pixel.g > 192 && pixel.b < 64;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& BuildLog() const { return m_buildLog; }
|
|
||||||
|
|
||||||
static GLenum FirstGLError() {
|
|
||||||
const GLenum first = glGetError();
|
|
||||||
while (glGetError() != GL_NO_ERROR) {
|
|
||||||
}
|
|
||||||
return first;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
static std::string InfoLog(GLuint object, bool isShader) {
|
|
||||||
GLint length = 0;
|
|
||||||
if (isShader) {
|
|
||||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
|
||||||
} else {
|
|
||||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
|
||||||
}
|
|
||||||
std::vector<char> log(static_cast<std::size_t>(length > 1 ? length : 1), '\0');
|
|
||||||
if (isShader) {
|
|
||||||
glGetShaderInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
|
||||||
} else {
|
|
||||||
glGetProgramInfoLog(object, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
|
||||||
}
|
|
||||||
return std::string(log.data());
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint m_vao = 0;
|
|
||||||
std::vector<GLuint> m_programs;
|
|
||||||
std::string m_buildLog;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The negative control, and it runs first on purpose: if this one is not green there
|
|
||||||
// is nothing to conclude from the case below it.
|
|
||||||
//
|
|
||||||
// It is also the CALIBRATION. GL_MAX_TESS_GEN_LEVEL answers for the tessellation
|
|
||||||
// stages honestly, but nothing MobileGL reports answers for the geometry stage the
|
|
||||||
// same way (GL_MAX_GEOMETRY_* are frontend constants and an ES driver may legitimately
|
|
||||||
// report zero geometry storage blocks while having geometry shaders), so a stack that
|
|
||||||
// cannot build a five-stage program at all is recognised here, by trying.
|
|
||||||
TEST_F(IoBlockNameCollisionScenario, DistinctlyNamedBlocksCarryThePayloadThroughFiveStages) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
const GLuint program = BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource);
|
|
||||||
if (program == 0) {
|
|
||||||
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
|
||||||
<< Gl().BackendName() << ", so there is no block to carry through: "
|
|
||||||
<< BuildLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
const Rgba8 centre = DrawAndReadCentre(program);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
EXPECT_TRUE(IsGreen(centre)) << "the control pipeline did not deliver its payload: " << centre;
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(IoBlockNameCollisionScenario, OneBlockNameInBothDirectionsStillCarriesThePayload) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
// Same calibration as the case above, and for the same reason: a five-stage program
|
|
||||||
// this stack cannot build at all is not evidence about block names. Only once the
|
|
||||||
// DISTINCT-name build succeeds does a failure of the colliding one mean something.
|
|
||||||
if (BuildPipeline(kDistinctTessEvalSource, kDistinctGeometrySource) == 0) {
|
|
||||||
GTEST_SKIP() << "this stack cannot build a five-stage tessellation+geometry program on "
|
|
||||||
<< Gl().BackendName() << ", so there is no block to carry through: "
|
|
||||||
<< BuildLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legal desktop GLSL: input and output block names live in separate namespaces, so
|
|
||||||
// the evaluation stage below declares TcsData twice and must still compile. The
|
|
||||||
// control above having built is what makes this assertion about the NAME.
|
|
||||||
const GLuint program = BuildPipeline(kCollidingTessEvalSource, kCollidingGeometrySource);
|
|
||||||
ASSERT_NE(program, 0u)
|
|
||||||
<< "an interface block name reused across the two directions of one stage is legal "
|
|
||||||
"desktop GLSL, but the program did not build: "
|
|
||||||
<< BuildLog();
|
|
||||||
|
|
||||||
const Rgba8 centre = DrawAndReadCentre(program);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
EXPECT_TRUE(IsGreen(centre))
|
|
||||||
<< "the payload did not survive the stage that names its input and output block "
|
|
||||||
"the same: "
|
|
||||||
<< centre << " (blue: nothing drew; red: the plain varying was lost too; black: "
|
|
||||||
"the block arrived empty)";
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -1,898 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IterationRPFirstReductionScenario.cpp
|
|
||||||
// Copyright (c) 2026 MobileGL-Dev
|
|
||||||
// Licensed under the GNU Lesser General Public License v3.0:
|
|
||||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
||||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// End of Source File Header
|
|
||||||
//
|
|
||||||
// Scenario - ITERATIONRP'S FIRST SUBGROUP REDUCTION.
|
|
||||||
//
|
|
||||||
// iterationRP reduces a 32 x 16 exposure tile with a vector subgroup inclusive add,
|
|
||||||
// then a shared-memory scan of subgroup totals. The source assumes that every
|
|
||||||
// subgroup has a last lane, that there are 2..32 subgroups, and that local index
|
|
||||||
// 511 belongs to the last subgroup and its last lane. Those are source assumptions,
|
|
||||||
// not API contracts. This probe intentionally does not repair them: it records the
|
|
||||||
// observed topology and makes each handoff independently observable.
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
|
||||||
#include <bit>
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
#include <iomanip>
|
|
||||||
#include <iostream>
|
|
||||||
#include <limits>
|
|
||||||
#include <sstream>
|
|
||||||
#include <string>
|
|
||||||
#include <type_traits>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "../Harness/HeadlessGL.h"
|
|
||||||
#include "../Harness/ScenarioFixture.h"
|
|
||||||
|
|
||||||
#ifdef GLAPI
|
|
||||||
#undef GLAPI
|
|
||||||
#endif
|
|
||||||
#define GL_GLEXT_PROTOTYPES
|
|
||||||
#include <GL/gl.h>
|
|
||||||
#include <GL/glcorearb.h>
|
|
||||||
#undef GL_GLEXT_PROTOTYPES
|
|
||||||
|
|
||||||
namespace MGITest {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
constexpr std::size_t kInvocationCount = 512;
|
|
||||||
constexpr std::size_t kScanStageCount = 6;
|
|
||||||
constexpr std::uint32_t kQuietNanBits = 0x7fc00000u;
|
|
||||||
constexpr std::size_t kNoSlot = std::numeric_limits<std::size_t>::max();
|
|
||||||
|
|
||||||
struct UVec4 {
|
|
||||||
std::uint32_t x;
|
|
||||||
std::uint32_t y;
|
|
||||||
std::uint32_t z;
|
|
||||||
std::uint32_t w;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Vec4 {
|
|
||||||
float x;
|
|
||||||
float y;
|
|
||||||
float z;
|
|
||||||
float w;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Matches the std430 block exactly. uvec4/vec4 arrays have a 16-byte
|
|
||||||
// stride, floats are a dense scalar array, and the outer scan array is
|
|
||||||
// stage-major in both GLSL and C++.
|
|
||||||
struct ProbeOutput {
|
|
||||||
std::array<UVec4, kInvocationCount> invocation;
|
|
||||||
std::array<UVec4, kInvocationCount> subgroup;
|
|
||||||
std::array<Vec4, kInvocationCount> reduction;
|
|
||||||
std::array<float, kInvocationCount> finalAverage;
|
|
||||||
std::array<std::array<float, kInvocationCount>, kScanStageCount> scanAfter;
|
|
||||||
};
|
|
||||||
|
|
||||||
static_assert(sizeof(UVec4) == 16);
|
|
||||||
static_assert(sizeof(Vec4) == 16);
|
|
||||||
static_assert(std::is_standard_layout_v<ProbeOutput>);
|
|
||||||
static_assert(offsetof(ProbeOutput, invocation) == 0);
|
|
||||||
static_assert(offsetof(ProbeOutput, subgroup) == 8192);
|
|
||||||
static_assert(offsetof(ProbeOutput, reduction) == 16384);
|
|
||||||
static_assert(offsetof(ProbeOutput, finalAverage) == 24576);
|
|
||||||
static_assert(offsetof(ProbeOutput, scanAfter) == 26624);
|
|
||||||
static_assert(sizeof(ProbeOutput) == 38912);
|
|
||||||
|
|
||||||
enum class InputMode {
|
|
||||||
SampledRgba32f,
|
|
||||||
IndexedSsbo,
|
|
||||||
};
|
|
||||||
|
|
||||||
const char* InputModeName(InputMode mode) {
|
|
||||||
return mode == InputMode::SampledRgba32f ? "sampled RGBA32F" : "indexed SSBO";
|
|
||||||
}
|
|
||||||
|
|
||||||
std::uint32_t FloatBits(float value) {
|
|
||||||
return std::bit_cast<std::uint32_t>(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SameBits(float lhs, float rhs) {
|
|
||||||
return FloatBits(lhs) == FloatBits(rhs);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsQuietNanSentinel(float value) {
|
|
||||||
return FloatBits(value) == kQuietNanBits;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DrainGlErrors() {
|
|
||||||
bool hadError = false;
|
|
||||||
while (glGetError() != GL_NO_ERROR) hadError = true;
|
|
||||||
return hadError;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HasExtension(const char* wanted) {
|
|
||||||
GLint extensionCount = 0;
|
|
||||||
glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);
|
|
||||||
for (GLint i = 0; i < extensionCount; ++i) {
|
|
||||||
const auto* extension = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
|
|
||||||
if (extension != nullptr && std::string(extension) == wanted) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CapabilityInfo {
|
|
||||||
bool subgroupExtension = false;
|
|
||||||
GLint subgroupSize = 0;
|
|
||||||
GLint supportedStages = 0;
|
|
||||||
GLint supportedFeatures = 0;
|
|
||||||
GLint maxComputeStorageBlocks = 0;
|
|
||||||
GLint maxStorageBindings = 0;
|
|
||||||
GLint maxWorkGroupInvocations = 0;
|
|
||||||
std::array<GLint, 3> maxWorkGroupSize{};
|
|
||||||
bool queryHadError = false;
|
|
||||||
|
|
||||||
// iterationRP's source contract needs gl_NumSubgroups in [2, 32] for its 512
|
|
||||||
// invocations, i.e. an advertised subgroup width in [16, 256]. A device
|
|
||||||
// outside that window (lavapipe's 8-lane subgroups give 64 subgroups) cannot
|
|
||||||
// run the fixture's verbatim reduction at all, so the scenario SKIPS there -
|
|
||||||
// the pack itself replays through the FixIterationRPSubgroupScratch patch, which
|
|
||||||
// this probe deliberately does not model. The width only gates the domain;
|
|
||||||
// lane placement and group counts still come from observed values alone.
|
|
||||||
bool SubgroupWidthInSourceDomain() const {
|
|
||||||
return subgroupSize >= 16 && subgroupSize <= 256;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SupportsProbe() const {
|
|
||||||
const auto stages = static_cast<GLbitfield>(supportedStages);
|
|
||||||
const auto features = static_cast<GLbitfield>(supportedFeatures);
|
|
||||||
return !queryHadError && subgroupExtension &&
|
|
||||||
(stages & GL_COMPUTE_SHADER_BIT) != 0 &&
|
|
||||||
(features & (GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR)) ==
|
|
||||||
(GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR) &&
|
|
||||||
SubgroupWidthInSourceDomain() &&
|
|
||||||
maxComputeStorageBlocks >= 2 && maxStorageBindings >= 2 &&
|
|
||||||
maxWorkGroupInvocations >= static_cast<GLint>(kInvocationCount) && maxWorkGroupSize[0] >= 32 &&
|
|
||||||
maxWorkGroupSize[1] >= 16 && maxWorkGroupSize[2] >= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string MissingRequirements() const {
|
|
||||||
std::vector<std::string> missing;
|
|
||||||
const auto stages = static_cast<GLbitfield>(supportedStages);
|
|
||||||
const auto features = static_cast<GLbitfield>(supportedFeatures);
|
|
||||||
if (queryHadError) missing.emplace_back("a subgroup/compute capability query generated GL error");
|
|
||||||
if (!subgroupExtension) missing.emplace_back("GL_KHR_shader_subgroup");
|
|
||||||
if ((stages & GL_COMPUTE_SHADER_BIT) == 0) {
|
|
||||||
missing.emplace_back("GL_COMPUTE_SHADER_BIT in GL_SUBGROUP_SUPPORTED_STAGES_KHR");
|
|
||||||
}
|
|
||||||
const auto requiredFeatures =
|
|
||||||
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
|
|
||||||
if ((features & requiredFeatures) != requiredFeatures) {
|
|
||||||
missing.emplace_back("basic|arithmetic in GL_SUBGROUP_SUPPORTED_FEATURES_KHR");
|
|
||||||
}
|
|
||||||
if (!SubgroupWidthInSourceDomain()) {
|
|
||||||
missing.emplace_back(
|
|
||||||
"GL_SUBGROUP_SIZE_KHR in [16, 256] (iterationRP's source contract needs "
|
|
||||||
"gl_NumSubgroups in [2, 32] for 512 invocations; width " +
|
|
||||||
std::to_string(subgroupSize) + " is outside the fixture's domain)");
|
|
||||||
}
|
|
||||||
if (maxComputeStorageBlocks < 2 || maxStorageBindings < 2) {
|
|
||||||
missing.emplace_back("two compute SSBO bindings");
|
|
||||||
}
|
|
||||||
if (maxWorkGroupInvocations < static_cast<GLint>(kInvocationCount) || maxWorkGroupSize[0] < 32 ||
|
|
||||||
maxWorkGroupSize[1] < 16 || maxWorkGroupSize[2] < 1) {
|
|
||||||
missing.emplace_back("a 32x16x1 / 512-invocation compute workgroup");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ostringstream message;
|
|
||||||
for (std::size_t i = 0; i < missing.size(); ++i) {
|
|
||||||
if (i != 0) message << ", ";
|
|
||||||
message << missing[i];
|
|
||||||
}
|
|
||||||
return message.str();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
CapabilityInfo QueryCapabilities() {
|
|
||||||
CapabilityInfo info;
|
|
||||||
DrainGlErrors();
|
|
||||||
info.subgroupExtension = HasExtension("GL_KHR_shader_subgroup");
|
|
||||||
glGetIntegerv(GL_SUBGROUP_SIZE_KHR, &info.subgroupSize);
|
|
||||||
glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &info.supportedStages);
|
|
||||||
glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &info.supportedFeatures);
|
|
||||||
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &info.maxComputeStorageBlocks);
|
|
||||||
glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &info.maxStorageBindings);
|
|
||||||
glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &info.maxWorkGroupInvocations);
|
|
||||||
for (GLuint axis = 0; axis < info.maxWorkGroupSize.size(); ++axis) {
|
|
||||||
glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, axis, &info.maxWorkGroupSize[axis]);
|
|
||||||
}
|
|
||||||
info.queryHadError = DrainGlErrors();
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PrintMetadata(const CapabilityInfo& info, std::ostream& output) {
|
|
||||||
output << "IterationRPFirstReductionScenario metadata: "
|
|
||||||
<< "GL_SUBGROUP_SIZE_KHR=" << info.subgroupSize
|
|
||||||
<< ", GL_SUBGROUP_SUPPORTED_STAGES_KHR=0x" << std::hex
|
|
||||||
<< static_cast<GLbitfield>(info.supportedStages)
|
|
||||||
<< ", GL_SUBGROUP_SUPPORTED_FEATURES_KHR=0x"
|
|
||||||
<< static_cast<GLbitfield>(info.supportedFeatures) << std::dec
|
|
||||||
<< ", subgroupExtension=" << info.subgroupExtension
|
|
||||||
<< ", GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS=" << info.maxComputeStorageBlocks
|
|
||||||
<< ", GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS=" << info.maxStorageBindings
|
|
||||||
<< ", GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS=" << info.maxWorkGroupInvocations
|
|
||||||
<< ", GL_MAX_COMPUTE_WORK_GROUP_SIZE=" << info.maxWorkGroupSize[0] << 'x'
|
|
||||||
<< info.maxWorkGroupSize[1] << 'x' << info.maxWorkGroupSize[2]
|
|
||||||
<< ", queryHadError=" << info.queryHadError << '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DumpRequested() {
|
|
||||||
const char* value = std::getenv("MOBILEGL_ITEST_SUBGROUP_PROBE_DUMP");
|
|
||||||
return value != nullptr && std::string(value) == "1";
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr const char* kShaderPreamble = R"(#version 430 core
|
|
||||||
#extension GL_KHR_shader_subgroup_basic : require
|
|
||||||
#extension GL_KHR_shader_subgroup_arithmetic : require
|
|
||||||
|
|
||||||
layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in;
|
|
||||||
|
|
||||||
layout(std430, binding = 1) buffer SubgroupProbeOutput {
|
|
||||||
uvec4 invocation[512];
|
|
||||||
uvec4 subgroup[512];
|
|
||||||
vec4 reduction[512];
|
|
||||||
float finalAverage[512];
|
|
||||||
float scanAfter[6][512];
|
|
||||||
} outProbe;
|
|
||||||
|
|
||||||
shared vec2 prefixSumCache[32];
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr const char* kSampledInput = R"(
|
|
||||||
uniform sampler2D colortex2;
|
|
||||||
uniform vec2 pixelSize;
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr const char* kIndexedInput = R"(
|
|
||||||
layout(std430, binding = 0) readonly buffer Input {
|
|
||||||
float value[512];
|
|
||||||
} inputData;
|
|
||||||
)";
|
|
||||||
|
|
||||||
// Only the expression producing tileExposure differs between the two
|
|
||||||
// tests. The remainder is the iterationRP first reduction, with stores
|
|
||||||
// placed after its existing barriers to expose each handoff.
|
|
||||||
constexpr const char* kSampledTileExposure = R"(
|
|
||||||
vec2 texCoord = (vec2(gl_GlobalInvocationID.xy) + 0.5) *
|
|
||||||
vec2(1.0 / 32.0, 1.0 / 16.0);
|
|
||||||
vec2 sampleCoord = texCoord * (1.0 / 64.0);
|
|
||||||
sampleCoord.x += (15.0 / 32.0) + pixelSize.x * 12.0;
|
|
||||||
|
|
||||||
float tileExposure = dot(
|
|
||||||
textureLod(colortex2, sampleCoord, 0.0).rgb,
|
|
||||||
vec3(0.2125, 0.7154, 0.0721));
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr const char* kIndexedTileExposure = R"(
|
|
||||||
float tileExposure = inputData.value[gl_LocalInvocationIndex];
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr const char* kReductionBody = R"(
|
|
||||||
vec2 sampleLuminance = vec2(tileExposure, 0.0);
|
|
||||||
sampleLuminance = subgroupInclusiveAdd(sampleLuminance);
|
|
||||||
float nativeInclusive = sampleLuminance.x;
|
|
||||||
|
|
||||||
// This is a uniform, safety-only branch: it leaves an invalid source
|
|
||||||
// contract visible without indexing past the 32-entry cache or underflowing
|
|
||||||
// loopLength - 1. It is deliberately a failure on the CPU, not a skip.
|
|
||||||
bool sourceDomain = gl_NumSubgroups >= 2u && gl_NumSubgroups <= 32u;
|
|
||||||
if (!sourceDomain) {
|
|
||||||
float qNaN = uintBitsToFloat(0x7fc00000u);
|
|
||||||
uint localIndex = gl_LocalInvocationIndex;
|
|
||||||
outProbe.invocation[localIndex] = uvec4(localIndex, gl_LocalInvocationID);
|
|
||||||
outProbe.subgroup[localIndex] = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID,
|
|
||||||
gl_SubgroupInvocationID);
|
|
||||||
outProbe.reduction[localIndex] = vec4(tileExposure, nativeInclusive, qNaN, qNaN);
|
|
||||||
outProbe.finalAverage[localIndex] = qNaN;
|
|
||||||
for (uint stage = 0u; stage < 6u; ++stage)
|
|
||||||
outProbe.scanAfter[stage][localIndex] = qNaN;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
|
||||||
barrier();
|
|
||||||
|
|
||||||
float sourceRawSubtotal = prefixSumCache[gl_SubgroupID].x;
|
|
||||||
|
|
||||||
uint loopLength = uint(findMSB(gl_NumSubgroups));
|
|
||||||
loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u);
|
|
||||||
|
|
||||||
for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) {
|
|
||||||
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
|
|
||||||
sampleLuminance += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
|
||||||
}
|
|
||||||
barrier();
|
|
||||||
outProbe.scanAfter[scanStage][gl_LocalInvocationIndex] = sampleLuminance.x;
|
|
||||||
}
|
|
||||||
|
|
||||||
float sourceMergedPrefix = sampleLuminance.x;
|
|
||||||
|
|
||||||
if (gl_LocalInvocationIndex == 511u)
|
|
||||||
prefixSumCache[0] = sampleLuminance / 512.0;
|
|
||||||
barrier();
|
|
||||||
|
|
||||||
float avg = prefixSumCache[0].x;
|
|
||||||
|
|
||||||
uint localIndex = gl_LocalInvocationIndex;
|
|
||||||
outProbe.invocation[localIndex] = uvec4(localIndex, gl_LocalInvocationID);
|
|
||||||
outProbe.subgroup[localIndex] = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID,
|
|
||||||
gl_SubgroupInvocationID);
|
|
||||||
outProbe.reduction[localIndex] = vec4(tileExposure, nativeInclusive, sourceRawSubtotal, sourceMergedPrefix);
|
|
||||||
outProbe.finalAverage[localIndex] = avg;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
std::string BuildProbeShader(InputMode mode) {
|
|
||||||
std::string source = kShaderPreamble;
|
|
||||||
source += mode == InputMode::SampledRgba32f ? kSampledInput : kIndexedInput;
|
|
||||||
source += "\nvoid main() {\n";
|
|
||||||
source += mode == InputMode::SampledRgba32f ? kSampledTileExposure : kIndexedTileExposure;
|
|
||||||
source += kReductionBody;
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string FormatFloat(float value) {
|
|
||||||
std::ostringstream text;
|
|
||||||
text << std::hexfloat << value;
|
|
||||||
return text.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ValidationResult {
|
|
||||||
bool ok = true;
|
|
||||||
std::string phase;
|
|
||||||
std::string message;
|
|
||||||
bool scanStageMismatch = false;
|
|
||||||
int scanStage = -1;
|
|
||||||
bool ownerEvaluated = false;
|
|
||||||
bool index511IsSourceLastLaneWriter = false;
|
|
||||||
bool index511IsHighestSubgroupMember = false;
|
|
||||||
std::uint32_t highestObservedSubgroup = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
ValidationResult Failure(std::string phase, std::string message) {
|
|
||||||
ValidationResult result;
|
|
||||||
result.ok = false;
|
|
||||||
result.phase = std::move(phase);
|
|
||||||
result.message = std::move(message);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr float kSampledLuminance = 0.2125f + 0.7154f + 0.0721f;
|
|
||||||
|
|
||||||
float ExpectedInput(InputMode mode, std::uint32_t localIndex) {
|
|
||||||
return mode == InputMode::SampledRgba32f ? kSampledLuminance : static_cast<float>(localIndex + 1u);
|
|
||||||
}
|
|
||||||
|
|
||||||
ValidationResult ValidateProbe(const ProbeOutput& output, InputMode mode) {
|
|
||||||
std::array<std::size_t, kInvocationCount> slotForLocal{};
|
|
||||||
slotForLocal.fill(kNoSlot);
|
|
||||||
|
|
||||||
// 1. Record identity. Slots are only used to locate each reported
|
|
||||||
// local index; all subgroup behavior below groups recorded IDs/lanes.
|
|
||||||
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
|
|
||||||
const std::uint32_t localIndex = output.invocation[slot].x;
|
|
||||||
if (localIndex >= kInvocationCount) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "output slot " << slot << " reports localIndex " << localIndex << " outside [0, 511]";
|
|
||||||
return Failure("record identity", message.str());
|
|
||||||
}
|
|
||||||
if (slotForLocal[localIndex] != kNoSlot) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "localIndex " << localIndex << " appears in output slots " << slotForLocal[localIndex]
|
|
||||||
<< " and " << slot;
|
|
||||||
return Failure("record identity", message.str());
|
|
||||||
}
|
|
||||||
slotForLocal[localIndex] = slot;
|
|
||||||
}
|
|
||||||
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
|
|
||||||
if (slotForLocal[localIndex] == kNoSlot) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "localIndex " << localIndex << " is missing from all 512 records";
|
|
||||||
return Failure("record identity", message.str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
|
|
||||||
const std::size_t slot = slotForLocal[localIndex];
|
|
||||||
const UVec4& invocation = output.invocation[slot];
|
|
||||||
const std::uint32_t expectedX = static_cast<std::uint32_t>(localIndex % 32u);
|
|
||||||
const std::uint32_t expectedY = static_cast<std::uint32_t>(localIndex / 32u);
|
|
||||||
if (invocation.y != expectedX || invocation.z != expectedY || invocation.w != 0u) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "localIndex " << localIndex << " reports local invocation (" << invocation.y << ','
|
|
||||||
<< invocation.z << ',' << invocation.w << "), expected (" << expectedX << ',' << expectedY
|
|
||||||
<< ",0)";
|
|
||||||
return Failure("record identity", message.str());
|
|
||||||
}
|
|
||||||
const float expectedInput = ExpectedInput(mode, static_cast<std::uint32_t>(localIndex));
|
|
||||||
const float actualInput = output.reduction[slot].x;
|
|
||||||
if (!SameBits(actualInput, expectedInput)) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "localIndex " << localIndex << " input was " << FormatFloat(actualInput) << ", expected "
|
|
||||||
<< FormatFloat(expectedInput);
|
|
||||||
return Failure("input", message.str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Observed topology. Do not derive lanes or subgroup membership
|
|
||||||
// from local invocation indices: only the values the shader recorded
|
|
||||||
// participate in grouping.
|
|
||||||
const std::uint32_t reportedNumSubgroups = output.subgroup[slotForLocal[0]].y;
|
|
||||||
if (reportedNumSubgroups == 0u) {
|
|
||||||
return Failure("observed topology", "localIndex 0 reported gl_NumSubgroups == 0");
|
|
||||||
}
|
|
||||||
if (reportedNumSubgroups > kInvocationCount) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "reported gl_NumSubgroups=" << reportedNumSubgroups
|
|
||||||
<< " exceeds the 512 recorded invocations, so at least one subgroup ID is missing";
|
|
||||||
return Failure("observed topology", message.str());
|
|
||||||
}
|
|
||||||
std::vector<std::vector<std::size_t>> subgroupSlots(reportedNumSubgroups);
|
|
||||||
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
|
|
||||||
const std::size_t slot = slotForLocal[localIndex];
|
|
||||||
const UVec4& subgroup = output.subgroup[slot];
|
|
||||||
if (subgroup.x == 0u || subgroup.y == 0u) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "localIndex " << localIndex << " reported subgroupSize=" << subgroup.x
|
|
||||||
<< ", numSubgroups=" << subgroup.y;
|
|
||||||
return Failure("observed topology", message.str());
|
|
||||||
}
|
|
||||||
if (subgroup.y != reportedNumSubgroups) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "localIndex " << localIndex << " reported numSubgroups=" << subgroup.y
|
|
||||||
<< ", while localIndex 0 reported " << reportedNumSubgroups;
|
|
||||||
return Failure("observed topology", message.str());
|
|
||||||
}
|
|
||||||
if (subgroup.z >= reportedNumSubgroups) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "localIndex " << localIndex << " reported subgroupID=" << subgroup.z
|
|
||||||
<< " outside [0, " << (reportedNumSubgroups - 1u) << ']';
|
|
||||||
return Failure("observed topology", message.str());
|
|
||||||
}
|
|
||||||
if (subgroup.w >= subgroup.x) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "localIndex " << localIndex << " reported laneID=" << subgroup.w
|
|
||||||
<< " outside its subgroupSize=" << subgroup.x;
|
|
||||||
return Failure("observed topology", message.str());
|
|
||||||
}
|
|
||||||
subgroupSlots[subgroup.z].push_back(slot);
|
|
||||||
}
|
|
||||||
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
|
|
||||||
if (subgroupSlots[subgroupID].empty()) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "reported gl_NumSubgroups=" << reportedNumSubgroups
|
|
||||||
<< " but subgroupID " << subgroupID << " has no recorded members";
|
|
||||||
return Failure("observed topology", message.str());
|
|
||||||
}
|
|
||||||
auto& members = subgroupSlots[subgroupID];
|
|
||||||
std::sort(members.begin(), members.end(), [&output](std::size_t lhs, std::size_t rhs) {
|
|
||||||
return output.subgroup[lhs].w < output.subgroup[rhs].w;
|
|
||||||
});
|
|
||||||
for (std::size_t i = 1; i < members.size(); ++i) {
|
|
||||||
if (output.subgroup[members[i - 1]].w == output.subgroup[members[i]].w) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "subgroupID " << subgroupID << " contains duplicate laneID "
|
|
||||||
<< output.subgroup[members[i]].w;
|
|
||||||
return Failure("observed topology", message.str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Native subgroup arithmetic, in the actual lane ordering emitted
|
|
||||||
// by the driver. The fixture values and all partial sums are exactly
|
|
||||||
// representable binary32 values, so compare representation, not epsilon.
|
|
||||||
std::array<float, kInvocationCount> nativePrefix{};
|
|
||||||
std::vector<float> nativeSubtotal(reportedNumSubgroups, 0.0f);
|
|
||||||
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
|
|
||||||
float inclusive = 0.0f;
|
|
||||||
for (const std::size_t slot : subgroupSlots[subgroupID]) {
|
|
||||||
const std::uint32_t localIndex = output.invocation[slot].x;
|
|
||||||
inclusive += ExpectedInput(mode, localIndex);
|
|
||||||
nativePrefix[slot] = inclusive;
|
|
||||||
const float actualNative = output.reduction[slot].y;
|
|
||||||
if (!SameBits(actualNative, inclusive)) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "subgroupID " << subgroupID << ", laneID " << output.subgroup[slot].w
|
|
||||||
<< ", localIndex " << localIndex << " nativeInclusive was " << FormatFloat(actualNative)
|
|
||||||
<< ", expected " << FormatFloat(inclusive);
|
|
||||||
return Failure("native subgroup arithmetic", message.str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nativeSubtotal[subgroupID] = inclusive;
|
|
||||||
}
|
|
||||||
|
|
||||||
// sourceDomain is the narrow source-side safety branch. It is checked
|
|
||||||
// after native arithmetic so an unsupported source topology still
|
|
||||||
// reports native subgroup behavior before failing explicitly.
|
|
||||||
if (reportedNumSubgroups < 2u || reportedNumSubgroups > 32u) {
|
|
||||||
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
|
|
||||||
const std::size_t slot = slotForLocal[localIndex];
|
|
||||||
const Vec4& reduction = output.reduction[slot];
|
|
||||||
if (!IsQuietNanSentinel(reduction.z) || !IsQuietNanSentinel(reduction.w) ||
|
|
||||||
!IsQuietNanSentinel(output.finalAverage[slot])) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "iterationRP source reduction has no valid contract for gl_NumSubgroups="
|
|
||||||
<< reportedNumSubgroups << "; localIndex " << localIndex
|
|
||||||
<< " did not preserve its qNaN source-reduction sentinel";
|
|
||||||
return Failure("source domain", message.str());
|
|
||||||
}
|
|
||||||
for (std::size_t stage = 0; stage < kScanStageCount; ++stage) {
|
|
||||||
if (!IsQuietNanSentinel(output.scanAfter[stage][slot])) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "iterationRP source reduction has no valid contract for gl_NumSubgroups="
|
|
||||||
<< reportedNumSubgroups << "; localIndex " << localIndex << ", scan stage " << stage
|
|
||||||
<< " did not preserve its qNaN source-reduction sentinel";
|
|
||||||
return Failure("source domain", message.str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "iterationRP source reduction has no valid contract for observed gl_NumSubgroups="
|
|
||||||
<< reportedNumSubgroups << " (requires 2..32); native subgroup results were recorded";
|
|
||||||
return Failure("source domain", message.str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. iterationRP source writer and first shared-memory handoff.
|
|
||||||
std::vector<std::size_t> sourceWriter(reportedNumSubgroups, kNoSlot);
|
|
||||||
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
|
|
||||||
std::size_t writerCount = 0;
|
|
||||||
for (const std::size_t slot : subgroupSlots[subgroupID]) {
|
|
||||||
const UVec4& subgroup = output.subgroup[slot];
|
|
||||||
if (subgroup.w == subgroup.x - 1u) {
|
|
||||||
sourceWriter[subgroupID] = slot;
|
|
||||||
++writerCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (writerCount != 1u) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "subgroupID " << subgroupID << " has " << writerCount
|
|
||||||
<< " recorded lane(s) where laneID == subgroupSize - 1; iterationRP leaves that "
|
|
||||||
"shared-cache entry unwritten";
|
|
||||||
return Failure("source writer", message.str());
|
|
||||||
}
|
|
||||||
for (const std::size_t slot : subgroupSlots[subgroupID]) {
|
|
||||||
const float actualRawSubtotal = output.reduction[slot].z;
|
|
||||||
if (!SameBits(actualRawSubtotal, nativeSubtotal[subgroupID])) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "subgroupID " << subgroupID << ", localIndex " << output.invocation[slot].x
|
|
||||||
<< " sourceRawSubtotal was " << FormatFloat(actualRawSubtotal) << ", expected "
|
|
||||||
<< FormatFloat(nativeSubtotal[subgroupID]);
|
|
||||||
return Failure("source raw subtotal", message.str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Reproduce the source loop exactly, including the redundant final
|
|
||||||
// scan iteration on power-of-two subgroup counts. Reads and writes in
|
|
||||||
// one iteration target disjoint cache entries, so update the cache at
|
|
||||||
// the CPU equivalent of the source barrier.
|
|
||||||
std::array<float, kInvocationCount> mergedPrefix = nativePrefix;
|
|
||||||
std::vector<float> cache = nativeSubtotal;
|
|
||||||
std::uint32_t loopLength = std::bit_width(reportedNumSubgroups) - 1u;
|
|
||||||
loopLength +=
|
|
||||||
static_cast<std::uint32_t>(reportedNumSubgroups - (1u << (loopLength - 1u)) > 0u);
|
|
||||||
for (std::uint32_t scanStage = 0u; scanStage < loopLength; ++scanStage) {
|
|
||||||
std::vector<float> cacheAfterStage = cache;
|
|
||||||
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
|
|
||||||
if ((subgroupID & (1u << scanStage)) == 0u) continue;
|
|
||||||
const std::uint32_t sourceCacheIndex = (subgroupID >> scanStage << scanStage) - 1u;
|
|
||||||
const float sourcePrefix = cache[sourceCacheIndex];
|
|
||||||
for (const std::size_t slot : subgroupSlots[subgroupID]) {
|
|
||||||
mergedPrefix[slot] += sourcePrefix;
|
|
||||||
}
|
|
||||||
cacheAfterStage[subgroupID] = mergedPrefix[sourceWriter[subgroupID]];
|
|
||||||
}
|
|
||||||
cache.swap(cacheAfterStage);
|
|
||||||
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
|
|
||||||
const std::size_t slot = slotForLocal[localIndex];
|
|
||||||
const float actualAfterStage = output.scanAfter[scanStage][slot];
|
|
||||||
if (!SameBits(actualAfterStage, mergedPrefix[slot])) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "scanStage " << scanStage << ", subgroupID " << output.subgroup[slot].z
|
|
||||||
<< ", laneID " << output.subgroup[slot].w << ", localIndex " << localIndex
|
|
||||||
<< " scanAfter was " << FormatFloat(actualAfterStage) << ", expected "
|
|
||||||
<< FormatFloat(mergedPrefix[slot]);
|
|
||||||
ValidationResult result = Failure("source scan", message.str());
|
|
||||||
result.scanStageMismatch = true;
|
|
||||||
result.scanStage = static_cast<int>(scanStage);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) {
|
|
||||||
const std::size_t slot = slotForLocal[localIndex];
|
|
||||||
const float actualMergedPrefix = output.reduction[slot].w;
|
|
||||||
if (!SameBits(actualMergedPrefix, mergedPrefix[slot])) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "localIndex " << localIndex << " sourceMergedPrefix was "
|
|
||||||
<< FormatFloat(actualMergedPrefix) << ", expected " << FormatFloat(mergedPrefix[slot]);
|
|
||||||
return Failure("source scan", message.str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Final owner and average. The uniformity check is intentionally
|
|
||||||
// separate from the source's topology contract at local index 511.
|
|
||||||
const float firstAverage = output.finalAverage[slotForLocal[0]];
|
|
||||||
for (std::size_t localIndex = 1; localIndex < kInvocationCount; ++localIndex) {
|
|
||||||
const float actualAverage = output.finalAverage[slotForLocal[localIndex]];
|
|
||||||
if (!SameBits(actualAverage, firstAverage)) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "finalAverage differs: localIndex 0 has " << FormatFloat(firstAverage)
|
|
||||||
<< ", localIndex " << localIndex << " has " << FormatFloat(actualAverage);
|
|
||||||
return Failure("final average", message.str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ValidationResult ownerResult;
|
|
||||||
ownerResult.ownerEvaluated = true;
|
|
||||||
for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) {
|
|
||||||
if (!subgroupSlots[subgroupID].empty()) {
|
|
||||||
ownerResult.highestObservedSubgroup = std::max(ownerResult.highestObservedSubgroup, subgroupID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const std::size_t index511Slot = slotForLocal[kInvocationCount - 1u];
|
|
||||||
const UVec4& index511Subgroup = output.subgroup[index511Slot];
|
|
||||||
ownerResult.index511IsSourceLastLaneWriter =
|
|
||||||
index511Subgroup.w == index511Subgroup.x - 1u;
|
|
||||||
ownerResult.index511IsHighestSubgroupMember =
|
|
||||||
index511Subgroup.z == ownerResult.highestObservedSubgroup;
|
|
||||||
if (!ownerResult.index511IsSourceLastLaneWriter || !ownerResult.index511IsHighestSubgroupMember) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "iterationRP topology incompatibility: localIndex 511 is sourceLastLaneWriter="
|
|
||||||
<< ownerResult.index511IsSourceLastLaneWriter << ", highestSubgroupMember="
|
|
||||||
<< ownerResult.index511IsHighestSubgroupMember << " (subgroupID=" << index511Subgroup.z
|
|
||||||
<< ", highest observed subgroupID=" << ownerResult.highestObservedSubgroup << ')';
|
|
||||||
ownerResult.ok = false;
|
|
||||||
ownerResult.phase = "final average";
|
|
||||||
ownerResult.message = message.str();
|
|
||||||
return ownerResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
float total = 0.0f;
|
|
||||||
for (const float subtotal : nativeSubtotal) total += subtotal;
|
|
||||||
float sampledExpectedTotal = 0.0f;
|
|
||||||
for (std::size_t i = 0; i < kInvocationCount; ++i) sampledExpectedTotal += kSampledLuminance;
|
|
||||||
const float expectedTotal = mode == InputMode::IndexedSsbo ? 131328.0f : sampledExpectedTotal;
|
|
||||||
if (!SameBits(total, expectedTotal) || !SameBits(mergedPrefix[index511Slot], expectedTotal)) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "iterationRP source total was " << FormatFloat(mergedPrefix[index511Slot])
|
|
||||||
<< " (native total " << FormatFloat(total) << "), expected " << FormatFloat(expectedTotal);
|
|
||||||
ownerResult.ok = false;
|
|
||||||
ownerResult.phase = "final average";
|
|
||||||
ownerResult.message = message.str();
|
|
||||||
return ownerResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
const float expectedAverage = mode == InputMode::IndexedSsbo ? 256.5f : sampledExpectedTotal / 512.0f;
|
|
||||||
if (!SameBits(firstAverage, expectedAverage)) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "finalAverage was " << FormatFloat(firstAverage) << ", expected "
|
|
||||||
<< FormatFloat(expectedAverage);
|
|
||||||
ownerResult.ok = false;
|
|
||||||
ownerResult.phase = "final average";
|
|
||||||
ownerResult.message = message.str();
|
|
||||||
return ownerResult;
|
|
||||||
}
|
|
||||||
return ownerResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DumpProbe(const ProbeOutput& output, const CapabilityInfo& capabilities, const ValidationResult& validation,
|
|
||||||
bool includeScanStages) {
|
|
||||||
PrintMetadata(capabilities, std::cout);
|
|
||||||
if (validation.ok) {
|
|
||||||
std::cout << "IterationRPFirstReductionScenario firstFailure=none\n";
|
|
||||||
} else {
|
|
||||||
std::cout << "IterationRPFirstReductionScenario firstFailure=" << validation.phase << ": "
|
|
||||||
<< validation.message << '\n';
|
|
||||||
}
|
|
||||||
std::cout << "localIndex,localX,localY,localZ,subgroupSize,numSubgroups,subgroupID,laneID,input,"
|
|
||||||
"nativeInclusive,subgroupSubtotal,mergedPrefix,finalAverage\n";
|
|
||||||
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
|
|
||||||
const UVec4& invocation = output.invocation[slot];
|
|
||||||
const UVec4& subgroup = output.subgroup[slot];
|
|
||||||
const Vec4& reduction = output.reduction[slot];
|
|
||||||
std::cout << invocation.x << ',' << invocation.y << ',' << invocation.z << ',' << invocation.w << ','
|
|
||||||
<< subgroup.x << ',' << subgroup.y << ',' << subgroup.z << ',' << subgroup.w << ','
|
|
||||||
<< std::hexfloat << reduction.x << ',' << reduction.y << ',' << reduction.z << ','
|
|
||||||
<< reduction.w << ',' << output.finalAverage[slot] << std::defaultfloat << '\n';
|
|
||||||
}
|
|
||||||
if (includeScanStages) {
|
|
||||||
std::cout << "scanStage,localIndex,scanAfter\n";
|
|
||||||
for (std::size_t scanStage = 0; scanStage < kScanStageCount; ++scanStage) {
|
|
||||||
for (std::size_t slot = 0; slot < kInvocationCount; ++slot) {
|
|
||||||
std::cout << scanStage << ',' << output.invocation[slot].x << ',' << std::hexfloat
|
|
||||||
<< output.scanAfter[scanStage][slot] << std::defaultfloat << '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class IterationRPFirstReductionScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
m_capabilities = QueryCapabilities();
|
|
||||||
// GL_SUBGROUP_SIZE_KHR gates only whether the fixture's source contract
|
|
||||||
// can hold on this device (SubgroupWidthInSourceDomain); it is
|
|
||||||
// deliberately never used to infer lane placement or an expected group
|
|
||||||
// count - those come from observed values alone.
|
|
||||||
PrintMetadata(m_capabilities, std::cout);
|
|
||||||
RecordProperty("iterationrp_gl_subgroup_size_khr", std::to_string(m_capabilities.subgroupSize));
|
|
||||||
if (!m_capabilities.SupportsProbe()) {
|
|
||||||
GTEST_SKIP() << "subgroup probe requires " << m_capabilities.MissingRequirements();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
glUseProgram(0);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
|
||||||
glActiveTexture(GL_TEXTURE3);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
|
||||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
|
||||||
if (m_inputBuffer != 0) glDeleteBuffers(1, &m_inputBuffer);
|
|
||||||
if (m_outputBuffer != 0) glDeleteBuffers(1, &m_outputBuffer);
|
|
||||||
if (m_program != 0) glDeleteProgram(m_program);
|
|
||||||
m_texture = 0;
|
|
||||||
m_inputBuffer = 0;
|
|
||||||
m_outputBuffer = 0;
|
|
||||||
m_program = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint CompileComputeProgram(const std::string& source, std::string* outError) {
|
|
||||||
const char* text = source.c_str();
|
|
||||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
|
||||||
if (shader == 0) {
|
|
||||||
*outError = "glCreateShader(GL_COMPUTE_SHADER) returned 0";
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
glShaderSource(shader, 1, &text, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
GLint compiled = GL_FALSE;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
||||||
if (compiled == GL_FALSE) {
|
|
||||||
char log[8192] = {};
|
|
||||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
|
||||||
*outError = std::string("the subgroup probe compute shader did not compile: ") + log;
|
|
||||||
glDeleteShader(shader);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
glAttachShader(program, shader);
|
|
||||||
glLinkProgram(program);
|
|
||||||
glDeleteShader(shader);
|
|
||||||
GLint linked = GL_FALSE;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
if (linked == GL_FALSE) {
|
|
||||||
char log[8192] = {};
|
|
||||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
|
||||||
*outError = std::string("the subgroup probe compute program did not link: ") + log;
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RunProbe(InputMode mode, ProbeOutput* output, std::string* outError) {
|
|
||||||
m_program = CompileComputeProgram(BuildProbeShader(mode), outError);
|
|
||||||
if (m_program == 0) return false;
|
|
||||||
|
|
||||||
ProbeOutput poison{};
|
|
||||||
std::memset(&poison, 0xa5, sizeof(poison));
|
|
||||||
glGenBuffers(1, &m_outputBuffer);
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_outputBuffer);
|
|
||||||
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(ProbeOutput), &poison, GL_DYNAMIC_COPY);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_outputBuffer);
|
|
||||||
|
|
||||||
if (mode == InputMode::IndexedSsbo) {
|
|
||||||
std::array<float, kInvocationCount> values{};
|
|
||||||
for (std::size_t i = 0; i < values.size(); ++i) values[i] = static_cast<float>(i + 1u);
|
|
||||||
glGenBuffers(1, &m_inputBuffer);
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_inputBuffer);
|
|
||||||
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(values), values.data(), GL_STATIC_DRAW);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_inputBuffer);
|
|
||||||
} else {
|
|
||||||
constexpr std::array<float, 4> kOneTexel = {1.0f, 1.0f, 1.0f, 1.0f};
|
|
||||||
glGenTextures(1, &m_texture);
|
|
||||||
glActiveTexture(GL_TEXTURE3);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 1, 1, 0, GL_RGBA, GL_FLOAT, kOneTexel.data());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (const GLenum error = FirstGLError(); error != GL_NO_ERROR) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "subgroup probe resource setup left " << GLErrorName(error);
|
|
||||||
*outError = message.str();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
glUseProgram(m_program);
|
|
||||||
if (mode == InputMode::SampledRgba32f) {
|
|
||||||
const GLint sampler = glGetUniformLocation(m_program, "colortex2");
|
|
||||||
const GLint pixelSize = glGetUniformLocation(m_program, "pixelSize");
|
|
||||||
if (sampler == -1 || pixelSize == -1) {
|
|
||||||
*outError = "the sampled probe uniforms were optimized away or not reflected";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
glUniform1i(sampler, 3);
|
|
||||||
glUniform2f(pixelSize, 1.0f / 854.0f, 1.0f / 480.0f);
|
|
||||||
}
|
|
||||||
glDispatchCompute(1, 1, 1);
|
|
||||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_outputBuffer);
|
|
||||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(ProbeOutput), output);
|
|
||||||
if (const GLenum error = FirstGLError(); error != GL_NO_ERROR) {
|
|
||||||
std::ostringstream message;
|
|
||||||
message << "subgroup probe dispatch/readback left " << GLErrorName(error);
|
|
||||||
*outError = message.str();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RunAndValidate(InputMode mode) {
|
|
||||||
ProbeOutput output{};
|
|
||||||
std::string error;
|
|
||||||
ASSERT_TRUE(RunProbe(mode, &output, &error)) << InputModeName(mode) << ": " << error;
|
|
||||||
|
|
||||||
const ValidationResult validation = ValidateProbe(output, mode);
|
|
||||||
if (validation.ownerEvaluated) {
|
|
||||||
RecordProperty("iterationrp_index511_source_last_lane_writer",
|
|
||||||
validation.index511IsSourceLastLaneWriter ? "true" : "false");
|
|
||||||
RecordProperty("iterationrp_index511_highest_subgroup_member",
|
|
||||||
validation.index511IsHighestSubgroupMember ? "true" : "false");
|
|
||||||
RecordProperty("iterationrp_highest_observed_subgroup",
|
|
||||||
std::to_string(validation.highestObservedSubgroup));
|
|
||||||
std::cout << "IterationRPFirstReductionScenario owner: localIndex511 sourceLastLaneWriter="
|
|
||||||
<< validation.index511IsSourceLastLaneWriter << ", highestSubgroupMember="
|
|
||||||
<< validation.index511IsHighestSubgroupMember << ", highestObservedSubgroup="
|
|
||||||
<< validation.highestObservedSubgroup << '\n';
|
|
||||||
}
|
|
||||||
if (!validation.ok || DumpRequested()) {
|
|
||||||
DumpProbe(output, m_capabilities, validation, validation.scanStageMismatch || DumpRequested());
|
|
||||||
}
|
|
||||||
EXPECT_TRUE(validation.ok) << validation.phase << ": " << validation.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
CapabilityInfo m_capabilities;
|
|
||||||
GLuint m_program = 0;
|
|
||||||
GLuint m_inputBuffer = 0;
|
|
||||||
GLuint m_outputBuffer = 0;
|
|
||||||
GLuint m_texture = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_F(IterationRPFirstReductionScenario, SampledRgba32fFirstAverage) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
RunAndValidate(InputMode::SampledRgba32f);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(IterationRPFirstReductionScenario, IndexedInputTopologyAndReduction) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
RunAndValidate(InputMode::IndexedSsbo);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -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
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IterationRPScratchFixScenario.cpp
|
|
||||||
// Copyright (c) 2026 MobileGL-Dev
|
|
||||||
// Licensed under the GNU Lesser General Public License v3.0:
|
|
||||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
||||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// End of Source File Header
|
|
||||||
//
|
|
||||||
// Scenario - THE FIXTURE-SHAPED SUBGROUP REDUCTION, ON WHATEVER WIDTH THE DEVICE HAS.
|
|
||||||
//
|
|
||||||
// iterationRP hard-sizes the scratch its subgroup prefix scans write through
|
|
||||||
// prefixSumCache[gl_SubgroupID], and ships that idiom twice: the auto-exposure pass
|
|
||||||
// declares `shared vec2 prefixSumCache[32]` for a 512-invocation workgroup, and the
|
|
||||||
// RTW importance warp declares `shared float prefixSumCache[64]` for a 1024-invocation
|
|
||||||
// one. Both algorithms are width-agnostic; only the static lengths bake in "at most 32
|
|
||||||
// (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
|
|
||||||
// width) on the modules that match the pack's reduction fingerprint.
|
|
||||||
//
|
|
||||||
// This scenario replays the fixture's reduction shape verbatim - the same 32-entry
|
|
||||||
// declaration, the same last-lane handoff, the same findMSB combine loop, and NO
|
|
||||||
// domain guard - and asserts only the width-independent result: the workgroup total.
|
|
||||||
// The inputs are small integers, so the fp32 sum is exact under any lane order and any
|
|
||||||
// association; a correct run produces the exact constant on a 4-lane device and a
|
|
||||||
// 128-lane device alike. Without the patch, a sub-16-lane device indexes the
|
|
||||||
// 32-entry array out of bounds - on lavapipe that is literal heap corruption - and
|
|
||||||
// this scenario is the regression test that keeps the patch working, and it runs on every device that
|
|
||||||
// has basic+arithmetic compute subgroups (unlike IterationRPFirstReductionScenario,
|
|
||||||
// which probes the UNREPAIRED source contract and must skip outside [16, 256]).
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstring>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include "../Harness/HeadlessGL.h"
|
|
||||||
#include "../Harness/ScenarioFixture.h"
|
|
||||||
|
|
||||||
#ifdef GLAPI
|
|
||||||
#undef GLAPI
|
|
||||||
#endif
|
|
||||||
#define GL_GLEXT_PROTOTYPES
|
|
||||||
#include <GL/gl.h>
|
|
||||||
#include <GL/glcorearb.h>
|
|
||||||
#undef GL_GLEXT_PROTOTYPES
|
|
||||||
|
|
||||||
namespace MGITest {
|
|
||||||
namespace {
|
|
||||||
constexpr std::uint32_t kInvocationCount = 512u;
|
|
||||||
// sum of 0..511, exactly representable and associativity-proof in fp32.
|
|
||||||
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
|
|
||||||
#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 total;
|
|
||||||
uint numSubgroups;
|
|
||||||
uint maxSubgroupId;
|
|
||||||
} outputData;
|
|
||||||
|
|
||||||
shared vec2 prefixSumCache[32];
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec2 sampleLuminance = vec2(float(gl_LocalInvocationIndex), 0.0);
|
|
||||||
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 scanStage = 0u; scanStage < loopLength; ++scanStage) {
|
|
||||||
if ((gl_SubgroupID & (1u << scanStage)) > 0u) {
|
|
||||||
sampleLuminance += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u];
|
|
||||||
if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u)
|
|
||||||
prefixSumCache[gl_SubgroupID] = sampleLuminance;
|
|
||||||
}
|
|
||||||
barrier();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gl_LocalInvocationIndex == 511u) {
|
|
||||||
outputData.total = sampleLuminance.x;
|
|
||||||
outputData.numSubgroups = gl_NumSubgroups;
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
float total = -1.0f;
|
|
||||||
std::uint32_t numSubgroups = 0;
|
|
||||||
std::uint32_t maxSubgroupId = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool HasExtension(const char* wanted) {
|
|
||||||
GLint extensionCount = 0;
|
|
||||||
glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);
|
|
||||||
for (GLint i = 0; i < extensionCount; ++i) {
|
|
||||||
const auto* extension =
|
|
||||||
reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
|
|
||||||
if (extension != nullptr && std::string(extension) == wanted) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
class IterationRPScratchFixScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
GLint stages = 0;
|
|
||||||
GLint features = 0;
|
|
||||||
GLint invocations = 0;
|
|
||||||
const bool subgroupExtension = HasExtension("GL_KHR_shader_subgroup");
|
|
||||||
if (subgroupExtension) {
|
|
||||||
glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &stages);
|
|
||||||
glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &features);
|
|
||||||
}
|
|
||||||
glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &invocations);
|
|
||||||
const GLbitfield requiredFeatures =
|
|
||||||
GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR;
|
|
||||||
if (!subgroupExtension || (static_cast<GLbitfield>(stages) & GL_COMPUTE_SHADER_BIT) == 0 ||
|
|
||||||
(static_cast<GLbitfield>(features) & requiredFeatures) != requiredFeatures ||
|
|
||||||
invocations < static_cast<GLint>(kInvocationCount)) {
|
|
||||||
GTEST_SKIP() << "needs GL_KHR_shader_subgroup basic+arithmetic in compute and a "
|
|
||||||
"512-invocation workgroup";
|
|
||||||
}
|
|
||||||
|
|
||||||
m_maxInvocations = static_cast<std::uint32_t>(invocations);
|
|
||||||
|
|
||||||
glGenBuffers(1, &m_output);
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
|
||||||
// maxSubgroupId starts at zero HOST-side: the word is touched only by
|
|
||||||
// atomicMax during the dispatch, since a plain shader-side zeroing store
|
|
||||||
// would race the other invocations' atomics (barrier() orders shared
|
|
||||||
// memory, not SSBO stores).
|
|
||||||
const OutputBlock poison{-1.0f, 0xa5a5a5a5u, 0u};
|
|
||||||
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(OutputBlock), &poison, GL_DYNAMIC_READ);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_output);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
|
|
||||||
if (m_output != 0) glDeleteBuffers(1, &m_output);
|
|
||||||
if (m_program != 0) glDeleteProgram(m_program);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned int CompileComputeProgram(const char* source) {
|
|
||||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
|
||||||
glShaderSource(shader, 1, &source, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
GLint compiled = 0;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
||||||
if (compiled == GL_FALSE) {
|
|
||||||
char log[2048] = {};
|
|
||||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
|
||||||
m_buildLog = std::string("compute shader did not compile: ") + log;
|
|
||||||
glDeleteShader(shader);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
glAttachShader(program, shader);
|
|
||||||
glLinkProgram(program);
|
|
||||||
glDeleteShader(shader);
|
|
||||||
GLint linked = 0;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
if (linked == GL_FALSE) {
|
|
||||||
char log[2048] = {};
|
|
||||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
|
||||||
m_buildLog = std::string("compute program did not link: ") + log;
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-poisons the block, compiles the shape under test and runs it once.
|
|
||||||
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);
|
|
||||||
glDispatchCompute(1, 1, 1);
|
|
||||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
|
||||||
OutputBlock block{};
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
|
|
||||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(OutputBlock), &block);
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint m_program = 0;
|
|
||||||
GLuint m_output = 0;
|
|
||||||
std::uint32_t m_maxInvocations = 0;
|
|
||||||
std::string m_buildLog;
|
|
||||||
};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_F(IterationRPScratchFixScenario, FixtureShapedReductionSumsEveryInvocation) {
|
|
||||||
const OutputBlock block = Dispatch(kComputeSource);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
|
||||||
|
|
||||||
// The topology diagnostics catch the failure modes by name before the sum does:
|
|
||||||
// an out-of-bounds handoff corrupts the total, a wrong gl_NumSubgroups breaks
|
|
||||||
// the combine loop's length.
|
|
||||||
ASSERT_NE(block.numSubgroups, 0xa5a5a5a5u) << "invocation 511 never reached its store";
|
|
||||||
EXPECT_GE(block.numSubgroups, 1u);
|
|
||||||
EXPECT_LE(block.numSubgroups, kInvocationCount);
|
|
||||||
EXPECT_LT(block.maxSubgroupId, block.numSubgroups)
|
|
||||||
<< "gl_SubgroupID exceeds gl_NumSubgroups - the inconsistency "
|
|
||||||
"DeriveNumSubgroupsPass exists to repair";
|
|
||||||
|
|
||||||
// Integer-valued fp32 inputs: the workgroup total is exact under any subgroup
|
|
||||||
// width, lane order, and association. This is the value iterationRP's exposure
|
|
||||||
// average is built from; without FixIterationRPSubgroupScratchPass an 8-lane
|
|
||||||
// device writes prefixSumCache[32..63] out of bounds and this comparison fails.
|
|
||||||
EXPECT_EQ(block.total, kExpectedTotal)
|
|
||||||
<< "workgroup reduction produced " << block.total << " with gl_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
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LayeredTextureReadbackScenario.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
|
|
||||||
//
|
|
||||||
// Scenario - READING EVERY LAYER OF A 1D-ARRAY / CUBE-MAP-ARRAY LEVEL BACK.
|
|
||||||
//
|
|
||||||
// glGetTexImage has no ES equivalent, so Espryt serves it by attaching the level to a scratch
|
|
||||||
// READ framebuffer and reading it with glReadPixels. Two of the targets it has to answer for do
|
|
||||||
// not fit that shape the way the others do, and both came back as zeroes in
|
|
||||||
// KHR-GL4x.shader_image_load_store.basic-allTargets-* and .non-layered_binding:
|
|
||||||
//
|
|
||||||
// * GL_TEXTURE_1D_ARRAY carries its LAYERS in the state-side height - that is what
|
|
||||||
// glTexImage2D(GL_TEXTURE_1D_ARRAY, w, layers) means - while the ES texture behind it is a 2D
|
|
||||||
// array of height 1 with the layers in depth. The readback used the state-side shape, so it
|
|
||||||
// asked layer 0 for a `layers`-row rectangle that layer does not have: row 0 was the only one
|
|
||||||
// that could be right, and everything past it was whatever reading outside an attachment
|
|
||||||
// produces.
|
|
||||||
// * GL_TEXTURE_CUBE_MAP_ARRAY has no glFramebufferTexture2D target token at all, so the 2D
|
|
||||||
// attach it used to take errored, the scratch FBO stayed incomplete, and every read fell
|
|
||||||
// through to the CPU shadow - which holds what was UPLOADED, i.e. the seed, not what the
|
|
||||||
// shader stored.
|
|
||||||
//
|
|
||||||
// Both cases store from a compute dispatch (so the only copy of the data is the GPU one and a
|
|
||||||
// stale shadow cannot pass) and then read the whole level back in one glGetTexImage, checking
|
|
||||||
// every layer separately so a failure names which one. r32ui throughout: it is a core GLSL ES
|
|
||||||
// image format, so nothing here can be confused with the missing-format story that
|
|
||||||
// ImageFormatQualifierScenario covers.
|
|
||||||
//
|
|
||||||
// Magma reads these back through its own path and is unaffected by the ES attachment rules, so
|
|
||||||
// both cases run on both backends and must agree.
|
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#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 kExtent = 4;
|
|
||||||
constexpr int kArrayLayers = 3; // enough that "layer 0 only" is visibly wrong
|
|
||||||
constexpr int kCubeLayerFaces = 12; // two cubes, which is what the conformance case uses
|
|
||||||
// A value no store writes, so "the store never landed" and "the store wrote the wrong
|
|
||||||
// thing" cannot be confused - and so a readback served from the stale CPU shadow is
|
|
||||||
// recognisable on sight.
|
|
||||||
constexpr GLuint kSeed = 0xFEEDBEEFu;
|
|
||||||
// Deliberately not 0: the unit has to travel through glUniform1i and be baked into the
|
|
||||||
// generated ESSL, so a defect there cannot hide behind the default.
|
|
||||||
constexpr GLint kImageUnit = 1;
|
|
||||||
|
|
||||||
GLuint Expected1DArrayTexel(int x, int layer) {
|
|
||||||
return 1000u + static_cast<GLuint>(layer) * 100u + static_cast<GLuint>(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint ExpectedCubeArrayTexel(int x, int y, int layerFace) {
|
|
||||||
return 1000u + static_cast<GLuint>(layerFace) * 100u + static_cast<GLuint>(y) * 10u +
|
|
||||||
static_cast<GLuint>(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
// One invocation per texel, and the value it writes is a function of its coordinate - so
|
|
||||||
// a layer read from the wrong slice does not merely differ, it says which slice it came
|
|
||||||
// from.
|
|
||||||
const char* k1DArrayStoreSource = R"(#version 430 core
|
|
||||||
|
|
||||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
|
||||||
|
|
||||||
layout (r32ui) writeonly uniform uimage1DArray uni_image;
|
|
||||||
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
uint x = gl_GlobalInvocationID.x;
|
|
||||||
uint layer = gl_GlobalInvocationID.z;
|
|
||||||
imageStore(uni_image, ivec2(int(x), int(layer)), uvec4(1000u + layer * 100u + x, 0u, 0u, 0u));
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* kCubeArrayStoreSource = R"(#version 430 core
|
|
||||||
|
|
||||||
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
|
||||||
|
|
||||||
layout (r32ui) writeonly uniform uimageCubeArray uni_image;
|
|
||||||
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
uint x = gl_GlobalInvocationID.x;
|
|
||||||
uint y = gl_GlobalInvocationID.y;
|
|
||||||
uint layerFace = gl_GlobalInvocationID.z;
|
|
||||||
imageStore(uni_image, ivec3(int(x), int(y), int(layerFace)),
|
|
||||||
uvec4(1000u + layerFace * 100u + y * 10u + x, 0u, 0u, 0u));
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
class LayeredTextureReadbackScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
glUseProgram(0);
|
|
||||||
for (GLuint p : m_programs) glDeleteProgram(p);
|
|
||||||
for (GLuint t : m_textures) glDeleteTextures(1, &t);
|
|
||||||
m_programs.clear();
|
|
||||||
m_textures.clear();
|
|
||||||
GLint maxImageUnits = 0;
|
|
||||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
|
||||||
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
|
|
||||||
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
|
||||||
}
|
|
||||||
while (glGetError() != GL_NO_ERROR) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ImagesAreUsable() const {
|
|
||||||
GLint maxImageUnits = 0;
|
|
||||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
|
||||||
GLint maxComputeImageUniforms = 0;
|
|
||||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
|
||||||
while (glGetError() != GL_NO_ERROR) {
|
|
||||||
}
|
|
||||||
return maxImageUnits > kImageUnit && maxComputeImageUniforms >= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint MakeComputeProgram(const char* source) {
|
|
||||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
|
||||||
glShaderSource(shader, 1, &source, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
GLint compiled = GL_FALSE;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
||||||
if (compiled == GL_FALSE) {
|
|
||||||
char log[4096] = {};
|
|
||||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
|
||||||
ADD_FAILURE() << "the compute shader did not compile: " << log;
|
|
||||||
glDeleteShader(shader);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
m_programs.push_back(program);
|
|
||||||
glAttachShader(program, shader);
|
|
||||||
glLinkProgram(program);
|
|
||||||
glDeleteShader(shader);
|
|
||||||
GLint linked = GL_FALSE;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
if (linked == GL_FALSE) {
|
|
||||||
char log[4096] = {};
|
|
||||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
|
||||||
ADD_FAILURE() << "the compute program did not link: " << log;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint TrackTexture() {
|
|
||||||
GLuint texture = 0;
|
|
||||||
glGenTextures(1, &texture);
|
|
||||||
m_textures.push_back(texture);
|
|
||||||
return texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
// layered = GL_TRUE, i.e. the whole level: that is what makes every layer reachable
|
|
||||||
// from one dispatch, and it is what glBindImageTextures is specified to pass.
|
|
||||||
bool DispatchStore(GLuint program, GLuint texture, GLsizei groupsX, GLsizei groupsY, GLsizei groupsZ) {
|
|
||||||
glBindImageTexture(static_cast<GLuint>(kImageUnit), texture, 0, GL_TRUE, 0, GL_WRITE_ONLY, GL_R32UI);
|
|
||||||
if (const GLenum error = FirstGLError()) {
|
|
||||||
ADD_FAILURE() << "glBindImageTexture errored with " << GLErrorName(error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
glUseProgram(program);
|
|
||||||
const GLint location = glGetUniformLocation(program, "uni_image");
|
|
||||||
if (location < 0) {
|
|
||||||
ADD_FAILURE() << "the image uniform was not reflected";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
glUniform1i(location, kImageUnit);
|
|
||||||
if (const GLenum error = FirstGLError()) {
|
|
||||||
ADD_FAILURE() << "assigning the image unit errored with " << GLErrorName(error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
glDispatchCompute(groupsX, groupsY, groupsZ);
|
|
||||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
|
||||||
glUseProgram(0);
|
|
||||||
if (const GLenum error = FirstGLError()) {
|
|
||||||
ADD_FAILURE() << "the dispatch errored with " << GLErrorName(error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<GLuint> m_programs;
|
|
||||||
std::vector<GLuint> m_textures;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The 1D-array half. A layer past the first is the whole test: layer 0 lines up with the
|
|
||||||
// ES image's only row whichever way the axes are read, so a readback that never swapped
|
|
||||||
// them still got it right and only the deeper layers came back wrong.
|
|
||||||
TEST_F(LayeredTextureReadbackScenario, GetTexImageReturnsEveryLayerOfA1DArray) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";
|
|
||||||
|
|
||||||
const GLuint program = MakeComputeProgram(k1DArrayStoreSource);
|
|
||||||
if (program == 0) return;
|
|
||||||
|
|
||||||
const GLuint texture = TrackTexture();
|
|
||||||
glBindTexture(GL_TEXTURE_1D_ARRAY, texture);
|
|
||||||
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
const std::vector<GLuint> seed(static_cast<std::size_t>(kExtent) * kArrayLayers, kSeed);
|
|
||||||
glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_R32UI, kExtent, kArrayLayers, 0, GL_RED_INTEGER, GL_UNSIGNED_INT,
|
|
||||||
seed.data());
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "creating the R32UI 1D-array texture errored";
|
|
||||||
|
|
||||||
if (!DispatchStore(program, texture, kExtent, 1, kArrayLayers)) return;
|
|
||||||
|
|
||||||
std::vector<GLuint> texels(seed.size(), 0u);
|
|
||||||
glBindTexture(GL_TEXTURE_1D_ARRAY, texture);
|
|
||||||
glGetTexImage(GL_TEXTURE_1D_ARRAY, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "reading the 1D-array level back errored";
|
|
||||||
|
|
||||||
// GL hands a 1D array back as a plain two-dimensional image whose ROWS are the
|
|
||||||
// layers, so the destination index is layer * width + x.
|
|
||||||
for (int layer = 0; layer < kArrayLayers; ++layer) {
|
|
||||||
for (int x = 0; x < kExtent; ++x) {
|
|
||||||
const std::size_t index = static_cast<std::size_t>(layer) * kExtent + x;
|
|
||||||
EXPECT_EQ(texels[index], Expected1DArrayTexel(x, layer))
|
|
||||||
<< "layer " << layer << " texel " << x << " read back "
|
|
||||||
<< (texels[index] == kSeed ? "the seed (the store never reached it, or the readback came "
|
|
||||||
"from the stale CPU shadow)"
|
|
||||||
: "an unexpected value");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The cube-map-array half. glFramebufferTexture2D has no token for the target, so the
|
|
||||||
// scratch FBO used to stay incomplete and every read - including layer 0 - was answered
|
|
||||||
// from the CPU shadow; the seed is what makes that visible rather than merely wrong.
|
|
||||||
TEST_F(LayeredTextureReadbackScenario, GetTexImageReturnsEveryLayerFaceOfACubeMapArray) {
|
|
||||||
if (!Ready()) return;
|
|
||||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";
|
|
||||||
|
|
||||||
const GLuint program = MakeComputeProgram(kCubeArrayStoreSource);
|
|
||||||
if (program == 0) return;
|
|
||||||
|
|
||||||
const GLuint texture = TrackTexture();
|
|
||||||
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
|
|
||||||
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
const std::vector<GLuint> seed(static_cast<std::size_t>(kExtent) * kExtent * kCubeLayerFaces, kSeed);
|
|
||||||
glTexImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, GL_R32UI, kExtent, kExtent, kCubeLayerFaces, 0, GL_RED_INTEGER,
|
|
||||||
GL_UNSIGNED_INT, seed.data());
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "creating the R32UI cube-map-array texture errored";
|
|
||||||
|
|
||||||
if (!DispatchStore(program, texture, kExtent, kExtent, kCubeLayerFaces)) return;
|
|
||||||
|
|
||||||
std::vector<GLuint> texels(seed.size(), 0u);
|
|
||||||
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
|
|
||||||
glGetTexImage(GL_TEXTURE_CUBE_MAP_ARRAY, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored";
|
|
||||||
|
|
||||||
for (int layerFace = 0; layerFace < kCubeLayerFaces; ++layerFace) {
|
|
||||||
for (int y = 0; y < kExtent; ++y) {
|
|
||||||
for (int x = 0; x < kExtent; ++x) {
|
|
||||||
const std::size_t index =
|
|
||||||
(static_cast<std::size_t>(layerFace) * kExtent + y) * kExtent + x;
|
|
||||||
EXPECT_EQ(texels[index], ExpectedCubeArrayTexel(x, y, layerFace))
|
|
||||||
<< "layer-face " << layerFace << " texel (" << x << ", " << y << ") read back "
|
|
||||||
<< (texels[index] == kSeed ? "the seed (the store never reached it, or the readback "
|
|
||||||
"came from the stale CPU shadow)"
|
|
||||||
: "an unexpected value");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace MGITest
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,220 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PackedWordReadbackScenario.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
|
|
||||||
//
|
|
||||||
// glGetTexImage of a 32-bit packed format read with its OWN client type owes the application the
|
|
||||||
// words the image HOLDS, and KHR-GL43.copy_image compares exactly those words. Two routes used to
|
|
||||||
// answer, and both are wrong for a level glCopyImageSubData wrote:
|
|
||||||
//
|
|
||||||
// * the colour-attachment route reads GL_RGBA/GL_FLOAT and re-encodes, which canonicalizes an
|
|
||||||
// RGB9_E5 shared exponent and collapses an R11F_G11F_B10F NaN payload to 1;
|
|
||||||
// * the CPU shadow only holds what was UPLOADED, and the mirror that replays a copy into it
|
|
||||||
// declines - silently - for a renderbuffer source, which has no shadow to mirror from.
|
|
||||||
//
|
|
||||||
// Both are pinned here with words the CTS itself uses, because both failures are invisible to a
|
|
||||||
// value comparison: every assertion below is on BITS that decode to the very value the wrong
|
|
||||||
// answer also decodes to.
|
|
||||||
//
|
|
||||||
// The fix is a raw-word route (DirectGLES::ReadPackedLevelWordsViaScratch: copy the level into a
|
|
||||||
// scratch GL_R32UI image, read that back as unsigned integers), and DirectVulkan reaches the same
|
|
||||||
// place through PackReadbackToClientOrPbo's raw-word branch over the staging bytes - so these
|
|
||||||
// scenarios are backend-agnostic on purpose.
|
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#include <ios>
|
|
||||||
#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 GLsizei kExtent = 4;
|
|
||||||
|
|
||||||
// The non-canonical RGB9_E5 word KHR-GL43.copy_image writes: R=0, G=0, B mantissa 63,
|
|
||||||
// shared exponent 31, i.e. the value 8064, which the spec's own encoder would emit as
|
|
||||||
// 0xe7e00000 instead. Anything that decodes and re-encodes hands back the canonical word.
|
|
||||||
//
|
|
||||||
// Reinterpreted in the destination of an RGB9_E5 -> R11F_G11F_B10F copy it is R=0,
|
|
||||||
// G=1920, B=995 - and B's 5-bit exponent is all ones with a nonzero mantissa, i.e. a NaN
|
|
||||||
// whose payload 3 does not survive a float32 round trip (it comes back as the canonical
|
|
||||||
// payload 1, B=993, word 0xf87c0000). The two defects therefore land on the same word.
|
|
||||||
constexpr GLuint kRgb9E5Word = 0xf8fc0000u;
|
|
||||||
|
|
||||||
// The R11F_G11F_B10F word the same test pairs with it: R=0, G=0, B = exponent 12,
|
|
||||||
// mantissa 0 = 0.125. As an RGB9_E5 word it is all-zero channels with a shared exponent of
|
|
||||||
// 12, which the canonical encoder would write as 0x00000000 - so a decode/re-encode of THIS
|
|
||||||
// one loses every bit that distinguishes it.
|
|
||||||
constexpr GLuint kR11fG11fB10fWord = 0x60000000u;
|
|
||||||
|
|
||||||
class PackedWordReadbackScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
DeleteObjects();
|
|
||||||
DrainErrors();
|
|
||||||
ScenarioTest::TearDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void DrainErrors() {
|
|
||||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DeleteObjects() {
|
|
||||||
if (m_src != 0) glDeleteTextures(1, &m_src);
|
|
||||||
if (m_dst != 0) glDeleteTextures(1, &m_dst);
|
|
||||||
if (m_rbo != 0) glDeleteRenderbuffers(1, &m_rbo);
|
|
||||||
m_src = 0;
|
|
||||||
m_dst = 0;
|
|
||||||
m_rbo = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A complete single-level texture whose every texel holds `word`, uploaded through the
|
|
||||||
// packed client type so the stored bits are the client's bits and nothing has had a
|
|
||||||
// chance to re-encode them.
|
|
||||||
GLuint MakePackedTexture(GLenum internalFormat, GLenum type, GLuint word) {
|
|
||||||
const std::vector<GLuint> words(static_cast<std::size_t>(kExtent) * kExtent, word);
|
|
||||||
GLuint texture = 0;
|
|
||||||
glGenTextures(1, &texture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, texture);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(internalFormat), kExtent, kExtent, 0, GL_RGB, type,
|
|
||||||
words.data());
|
|
||||||
// What Utils::makeTextureComplete does in the conformance cases, and what
|
|
||||||
// glCopyImageSubData requires of both endpoints.
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
return texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every texel of level 0, as raw client words.
|
|
||||||
std::vector<GLuint> ReadPackedWords(GLuint texture, GLenum type) {
|
|
||||||
std::vector<GLuint> words(static_cast<std::size_t>(kExtent) * kExtent, 0xDEADBEEFu);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, texture);
|
|
||||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, type, words.data());
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
return words;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The copy under test. Returns the error it raised so a driver that cannot perform the
|
|
||||||
// move at all can skip rather than fail: the point of these cases is which BITS come
|
|
||||||
// back, and there are none to compare if the copy never happened.
|
|
||||||
GLenum CopyWholeImage(GLuint srcName, GLenum srcTarget, GLuint dstName, GLenum dstTarget) {
|
|
||||||
DrainErrors();
|
|
||||||
glCopyImageSubData(srcName, srcTarget, 0, 0, 0, 0, dstName, dstTarget, 0, 0, 0, 0, kExtent, kExtent,
|
|
||||||
1);
|
|
||||||
const GLenum error = glGetError();
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the copy recorded more than one error";
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ExpectEveryTexel(const std::vector<GLuint>& words, GLuint expected, const char* what) {
|
|
||||||
for (std::size_t i = 0; i < words.size(); ++i) {
|
|
||||||
ASSERT_EQ(words[i], expected)
|
|
||||||
<< what << ": texel " << i << " read 0x" << std::hex << words[i] << ", expected 0x"
|
|
||||||
<< expected;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint m_src = 0;
|
|
||||||
GLuint m_dst = 0;
|
|
||||||
GLuint m_rbo = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The control that has to hold before either regression means anything: a packed word
|
|
||||||
// uploaded and read straight back must be the SAME word, not merely the same colour.
|
|
||||||
TEST_F(PackedWordReadbackScenario, AnUploadedPackedWordReadsBackVerbatim) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
|
|
||||||
m_src = MakePackedTexture(GL_RGB9_E5, GL_UNSIGNED_INT_5_9_9_9_REV, kRgb9E5Word);
|
|
||||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "RGB9_E5 upload";
|
|
||||||
ExpectEveryTexel(ReadPackedWords(m_src, GL_UNSIGNED_INT_5_9_9_9_REV), kRgb9E5Word, "RGB9_E5 round trip");
|
|
||||||
|
|
||||||
m_dst = MakePackedTexture(GL_R11F_G11F_B10F, GL_UNSIGNED_INT_10F_11F_11F_REV, kR11fG11fB10fWord);
|
|
||||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "R11F_G11F_B10F upload";
|
|
||||||
ExpectEveryTexel(ReadPackedWords(m_dst, GL_UNSIGNED_INT_10F_11F_11F_REV), kR11fG11fB10fWord,
|
|
||||||
"R11F_G11F_B10F round trip");
|
|
||||||
}
|
|
||||||
|
|
||||||
// KHR-GL43.copy_image.functional rgb9_e5 -> r11f_g11f_b10f, all nine target combinations of
|
|
||||||
// which failed on both GPUs. glCopyImageSubData is a raw block move, so the destination
|
|
||||||
// physically holds the source's word - but the readback decoded it to float and re-encoded,
|
|
||||||
// and the destination's blue field is a NaN whose payload float32 does not carry. Every
|
|
||||||
// texel came back 0xf87c0000 (payload 1) instead of 0xf8fc0000 (payload 3): the same
|
|
||||||
// "colour", two bits apart.
|
|
||||||
TEST_F(PackedWordReadbackScenario, ACopiedRgb9E5WordSurvivesInAnR11fG11fB10fDestination) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
|
|
||||||
m_src = MakePackedTexture(GL_RGB9_E5, GL_UNSIGNED_INT_5_9_9_9_REV, kRgb9E5Word);
|
|
||||||
m_dst = MakePackedTexture(GL_R11F_G11F_B10F, GL_UNSIGNED_INT_10F_11F_11F_REV, 0u);
|
|
||||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture setup";
|
|
||||||
|
|
||||||
const GLenum copyError = CopyWholeImage(m_src, GL_TEXTURE_2D, m_dst, GL_TEXTURE_2D);
|
|
||||||
if (copyError != static_cast<GLenum>(GL_NO_ERROR)) {
|
|
||||||
GTEST_SKIP() << "this driver declined the RGB9_E5 -> R11F_G11F_B10F copy (" << copyError << ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
ExpectEveryTexel(ReadPackedWords(m_dst, GL_UNSIGNED_INT_10F_11F_11F_REV), kRgb9E5Word,
|
|
||||||
"copied word in the R11F_G11F_B10F destination");
|
|
||||||
// ...and the source is still the source. This is verify()'s FIRST check in the
|
|
||||||
// conformance case, and the half that a canonicalizing readback fails on its own.
|
|
||||||
ExpectEveryTexel(ReadPackedWords(m_src, GL_UNSIGNED_INT_5_9_9_9_REV), kRgb9E5Word,
|
|
||||||
"the RGB9_E5 source after the copy");
|
|
||||||
}
|
|
||||||
|
|
||||||
// KHR-GL43.copy_image.functional *->rgb9_e5 with a GL_RENDERBUFFER source: exactly the three
|
|
||||||
// renderbuffer combinations of each such family failed, and no texture one did. The
|
|
||||||
// destination's CPU shadow is what the readback answered from, the mirror that replays a
|
|
||||||
// copy into it declines when an endpoint is a renderbuffer (there is no shadow to mirror
|
|
||||||
// FROM), and the decline is silent - so glGetTexImage handed back the destination's
|
|
||||||
// pre-copy contents. The word chosen here makes that unmissable: it decodes to the same
|
|
||||||
// all-zero channels the canonical encoder would write as 0x00000000.
|
|
||||||
TEST_F(PackedWordReadbackScenario, ACopyThroughARenderbufferReachesAnRgb9E5Destination) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
|
|
||||||
m_src = MakePackedTexture(GL_R11F_G11F_B10F, GL_UNSIGNED_INT_10F_11F_11F_REV, kR11fG11fB10fWord);
|
|
||||||
m_dst = MakePackedTexture(GL_RGB9_E5, GL_UNSIGNED_INT_5_9_9_9_REV, 0xFFFFFFFFu);
|
|
||||||
glGenRenderbuffers(1, &m_rbo);
|
|
||||||
glBindRenderbuffer(GL_RENDERBUFFER, m_rbo);
|
|
||||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_R11F_G11F_B10F, kExtent, kExtent);
|
|
||||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
|
||||||
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "renderbuffer setup";
|
|
||||||
|
|
||||||
// The conformance case's own shape: texture -> renderbuffer -> texture.
|
|
||||||
const GLenum toRenderbuffer = CopyWholeImage(m_src, GL_TEXTURE_2D, m_rbo, GL_RENDERBUFFER);
|
|
||||||
if (toRenderbuffer != static_cast<GLenum>(GL_NO_ERROR)) {
|
|
||||||
GTEST_SKIP() << "this driver declined a renderbuffer copy destination (" << toRenderbuffer << ")";
|
|
||||||
}
|
|
||||||
const GLenum fromRenderbuffer = CopyWholeImage(m_rbo, GL_RENDERBUFFER, m_dst, GL_TEXTURE_2D);
|
|
||||||
if (fromRenderbuffer != static_cast<GLenum>(GL_NO_ERROR)) {
|
|
||||||
GTEST_SKIP() << "this driver declined a renderbuffer copy source (" << fromRenderbuffer << ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
ExpectEveryTexel(ReadPackedWords(m_dst, GL_UNSIGNED_INT_5_9_9_9_REV), kR11fG11fB10fWord,
|
|
||||||
"copied word in the RGB9_E5 destination");
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PostLinkAttachScenario.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
|
|
||||||
//
|
|
||||||
// Scenario - A PROGRAM'S LIVE ATTACH LIST IS NOT ITS EXECUTABLE, AND THE BACKENDS MAY NOT
|
|
||||||
// INDEX ONE BY THE OTHER.
|
|
||||||
//
|
|
||||||
// GL 4.6 core 7.3: glAttachShader adds to the program's attach list immediately and affects
|
|
||||||
// what the program RUNS only at the next link (glDetachShader defers its removal the same
|
|
||||||
// way). So between an attach and the relink the two lists differ - the attach list is
|
|
||||||
// strictly longer - and the program stays perfectly drawable throughout, with the executable
|
|
||||||
// its last link produced.
|
|
||||||
//
|
|
||||||
// Both backends walked the attach list while indexing the LAST LINK's generated SPIR-V by
|
|
||||||
// the same running index:
|
|
||||||
//
|
|
||||||
// DirectGLES BackendProgramObjectImpl::SyncToBackend - `shaderSpirvs[index]` over
|
|
||||||
// `attachedShaders.size()`
|
|
||||||
// DirectVulkan ProgramFactory::GetOrCreateProgram - `spirv[i]` and `moduleSpirvs[i]`
|
|
||||||
// over `shaders.size()`
|
|
||||||
//
|
|
||||||
// One post-link attach therefore read one Vector past the end of the module array and
|
|
||||||
// copied it, which is the SIGSEGV this scenario is the regression test for (the source
|
|
||||||
// vector reported a capacity of 35177040171136). DirectGLES additionally derived
|
|
||||||
// "does this program tessellate" from the same wrong list, which would synthesize a
|
|
||||||
// pass-through tessellation control stage for an executable that does not tessellate.
|
|
||||||
//
|
|
||||||
// The repro needs the attach to land BEFORE the program's first backend build: the ES
|
|
||||||
// twin's rebuild is gated on the link version (which an attach does not move), so a program
|
|
||||||
// that was already drawn once keeps its built driver program and never re-reads the list.
|
|
||||||
// Every case below therefore attaches first and draws second.
|
|
||||||
//
|
|
||||||
// Deliberately pinned with a PIXEL and not just with glGetError. "Reject the draw earlier"
|
|
||||||
// would silence the crash while breaking the spec - GL requires this draw to execute - so
|
|
||||||
// the assertion has to be that the frame really came out, not merely that nothing complained.
|
|
||||||
//
|
|
||||||
// Needs a real context: the crash is in a backend program build, which the GPU-free suites
|
|
||||||
// never reach.
|
|
||||||
|
|
||||||
#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 kFboWidth = 64;
|
|
||||||
constexpr int kFboHeight = 64;
|
|
||||||
|
|
||||||
// A full-viewport triangle from gl_VertexID alone, so the scenario needs no vertex
|
|
||||||
// buffer and every pixel of the target is covered by the one draw.
|
|
||||||
const char* const kVertexSource = R"(#version 330 core
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
||||||
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* const kFragmentSource = R"(#version 330 core
|
|
||||||
out vec4 fragColor;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// The replacement fragment stage of the last case. A different colour, so "which
|
|
||||||
// executable did this draw run" is answerable from the frame alone.
|
|
||||||
const char* const kBlueFragmentSource = R"(#version 330 core
|
|
||||||
out vec4 fragColor;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
fragColor = vec4(0.0, 0.0, 1.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr Rgba8 kGreen{0, 255, 0, 255};
|
|
||||||
constexpr Rgba8 kBlue{0, 0, 255, 255};
|
|
||||||
|
|
||||||
// The extra attaches. Each declares a stage the executable ALREADY has and no main(),
|
|
||||||
// which is what a real shader library looks like and what makes the relink at the end
|
|
||||||
// of the second case legal. Their whole job here is to make the attach list longer
|
|
||||||
// than the module array.
|
|
||||||
const char* const kVertexHelperSource = R"(#version 330 core
|
|
||||||
vec4 mgPostLinkAttachVertexHelper()
|
|
||||||
{
|
|
||||||
return vec4(0.0, 0.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* const kFragmentHelperSource = R"(#version 330 core
|
|
||||||
vec4 mgPostLinkAttachFragmentHelper()
|
|
||||||
{
|
|
||||||
return vec4(1.0, 0.0, 1.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// A pass-through, so that once it IS linked in the same full-viewport triangle still
|
|
||||||
// reaches the rasterizer and the final frame is still comparable to the first one.
|
|
||||||
const char* const kGeometrySource = R"(#version 330 core
|
|
||||||
layout(triangles) in;
|
|
||||||
layout(triangle_strip, max_vertices = 3) out;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
for (int i = 0; i < 3; ++i) {
|
|
||||||
gl_Position = gl_in[i].gl_Position;
|
|
||||||
EmitVertex();
|
|
||||||
}
|
|
||||||
EndPrimitive();
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
class PostLinkAttachScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
glGenVertexArrays(1, &m_vao);
|
|
||||||
glBindVertexArray(m_vao);
|
|
||||||
m_target = MakeColorFbo(kFboWidth, kFboHeight);
|
|
||||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the scenario's colour target";
|
|
||||||
BindFbo(m_target);
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
glUseProgram(0);
|
|
||||||
for (const GLuint program : m_programs) glDeleteProgram(program);
|
|
||||||
m_programs.clear();
|
|
||||||
for (const GLuint shader : m_shaders) glDeleteShader(shader);
|
|
||||||
m_shaders.clear();
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
DestroyColorFbo(m_target);
|
|
||||||
glBindVertexArray(0);
|
|
||||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
||||||
m_vao = 0;
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void DrainErrors() {
|
|
||||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool BackendHostsGeometry() {
|
|
||||||
GLint maxGeometryOutputVertices = 0;
|
|
||||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
|
||||||
DrainErrors();
|
|
||||||
return maxGeometryOutputVertices >= 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kept alive until TearDown rather than flagged for deletion at attach time: a
|
|
||||||
// deleted-but-attached shader is a second, unrelated lifetime rule, and this
|
|
||||||
// scenario is about which LIST the backend reads.
|
|
||||||
GLuint MakeShader(GLenum stage, const char* source) {
|
|
||||||
const GLuint shader = glCreateShader(stage);
|
|
||||||
if (shader == 0) return 0;
|
|
||||||
m_shaders.push_back(shader);
|
|
||||||
glShaderSource(shader, 1, &source, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
return shader;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vertex + fragment, linked. This is the executable every case draws with.
|
|
||||||
// `outFragmentShader` is the stage that paints green, which the last case needs a
|
|
||||||
// name for in order to detach it.
|
|
||||||
GLuint LinkBaseProgram(GLuint* outFragmentShader = nullptr) {
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
m_programs.push_back(program);
|
|
||||||
const GLuint fragment = MakeShader(GL_FRAGMENT_SHADER, kFragmentSource);
|
|
||||||
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
|
|
||||||
glAttachShader(program, fragment);
|
|
||||||
glLinkProgram(program);
|
|
||||||
GLint linked = GL_FALSE;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
if (!linked) return 0;
|
|
||||||
if (outFragmentShader != nullptr) *outFragmentShader = fragment;
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clears to red, draws the full-viewport triangle, and hands back the frame. Red
|
|
||||||
// is deliberately the clear colour: a draw that silently did not execute leaves a
|
|
||||||
// red target, which is a different failure message from a draw that executed and
|
|
||||||
// painted the wrong thing.
|
|
||||||
//
|
|
||||||
// `outDrawError` is sampled between the draw and the readback, so a rejected draw
|
|
||||||
// is never confused with a readback that went wrong afterwards.
|
|
||||||
Image DrawFullViewportTriangle(GLuint program, GLenum mode, GLenum* outDrawError = nullptr) {
|
|
||||||
glUseProgram(program);
|
|
||||||
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
DrainErrors();
|
|
||||||
glDrawArrays(mode, 0, 3);
|
|
||||||
if (outDrawError != nullptr) *outDrawError = glGetError();
|
|
||||||
return ReadPixels(kFboWidth, kFboHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The clear colour is red and no shader here ever writes red, so "still red" reads
|
|
||||||
// as "the draw did not execute" and any other wrong colour as "it executed against
|
|
||||||
// the wrong modules" - two failures worth telling apart.
|
|
||||||
static void ExpectFullyColored(const Image& frame, const Rgba8& expected, const char* what) {
|
|
||||||
ASSERT_FALSE(frame.Empty()) << what << ": nothing was read back";
|
|
||||||
for (const int y : {0, kFboHeight / 2, kFboHeight - 1}) {
|
|
||||||
for (const int x : {0, kFboWidth / 2, kFboWidth - 1}) {
|
|
||||||
EXPECT_EQ(frame.At(x, y), expected)
|
|
||||||
<< what << ": pixel (" << x << ", " << y << ") is " << frame.ColorName(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint m_vao = 0;
|
|
||||||
ColorFbo m_target{};
|
|
||||||
std::vector<GLuint> m_programs;
|
|
||||||
std::vector<GLuint> m_shaders;
|
|
||||||
};
|
|
||||||
|
|
||||||
// THE REGRESSION. Up to four shaders attached after the link (the geometry one only
|
|
||||||
// where the backend has that stage), two of them duplicating a stage the executable
|
|
||||||
// already carries - so the attach list runs to five or six while the last link produced
|
|
||||||
// two modules, and the old loops read indices 2..5 of a 2-element array.
|
|
||||||
//
|
|
||||||
// Duplicating a stage is the sharp case on purpose: it is the one shape under which a
|
|
||||||
// "look the stage up in the attach list instead" repair still returns a valid-looking
|
|
||||||
// index for a module that does not exist.
|
|
||||||
TEST_F(PostLinkAttachScenario, DrawingAfterPostLinkAttachesStaysInsideTheGeneratedModules) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
|
|
||||||
const GLuint program = LinkBaseProgram();
|
|
||||||
ASSERT_NE(program, 0u) << "the vertex+fragment program did not link";
|
|
||||||
|
|
||||||
// Not drawn yet: the ES backend's rebuild is gated on the link version, so a draw
|
|
||||||
// here would build the driver program from the 2-module executable and the attaches
|
|
||||||
// below would never be re-read. The repro is the FIRST build seeing the long list.
|
|
||||||
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexHelperSource));
|
|
||||||
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentHelperSource));
|
|
||||||
if (BackendHostsGeometry()) {
|
|
||||||
glAttachShader(program, MakeShader(GL_GEOMETRY_SHADER, kGeometrySource));
|
|
||||||
}
|
|
||||||
// The stage that made DirectGLES synthesize a pass-through control stage for a
|
|
||||||
// program whose executable does not tessellate. Attached whether or not this
|
|
||||||
// backend can tessellate - an attach needs no support and no successful compile.
|
|
||||||
const GLuint tessEval = MakeShader(GL_TESS_EVALUATION_SHADER, R"(#version 420 core
|
|
||||||
layout(triangles, equal_spacing, ccw) in;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
gl_Position = gl_in[0].gl_Position;
|
|
||||||
}
|
|
||||||
)");
|
|
||||||
if (tessEval != 0) glAttachShader(program, tessEval);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
GLint attachedCount = 0;
|
|
||||||
glGetProgramiv(program, GL_ATTACHED_SHADERS, &attachedCount);
|
|
||||||
DrainErrors();
|
|
||||||
ASSERT_GT(attachedCount, 2) << "the attaches did not land, so this case is not testing anything";
|
|
||||||
|
|
||||||
// Still the two-stage executable of three lines ago, and GL says it draws.
|
|
||||||
GLenum drawError = GL_NO_ERROR;
|
|
||||||
const Image frame = DrawFullViewportTriangle(program, GL_TRIANGLES, &drawError);
|
|
||||||
EXPECT_EQ(drawError, static_cast<GLenum>(GL_NO_ERROR))
|
|
||||||
<< "the attaches have not been linked in, so nothing about them may reject this draw";
|
|
||||||
ExpectFullyColored(frame, kGreen, "the post-attach draw");
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The same window, asked to prove something stronger than "it did not crash": WHICH
|
|
||||||
// modules the draw in that window ran. Between the detach+attach and the relink the
|
|
||||||
// program has three attached shaders and two modules, and GL 4.6 core 7.3 says the
|
|
||||||
// executable is still the one the last link produced - so the frame must come out in
|
|
||||||
// the OLD fragment shader's colour, not the newly attached one's and not garbage.
|
|
||||||
//
|
|
||||||
// This is also the other direction of the fix, so it cannot be "freeze the backend on
|
|
||||||
// the first link": the relink really does swap the executable, and the very next draw
|
|
||||||
// has to be rebuilt from it.
|
|
||||||
TEST_F(PostLinkAttachScenario, TheWindowKeepsTheOldExecutableAndTheRelinkSwapsIt) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
|
|
||||||
GLuint greenFragment = 0;
|
|
||||||
const GLuint program = LinkBaseProgram(&greenFragment);
|
|
||||||
ASSERT_NE(program, 0u) << "the vertex+fragment program did not link";
|
|
||||||
|
|
||||||
// Both of these are deferred to the next link, in opposite directions: the green
|
|
||||||
// stage stays in the executable until then, and the blue one stays out of it.
|
|
||||||
const GLuint blueFragment = MakeShader(GL_FRAGMENT_SHADER, kBlueFragmentSource);
|
|
||||||
glDetachShader(program, greenFragment);
|
|
||||||
glAttachShader(program, blueFragment);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
GLenum windowDrawError = GL_NO_ERROR;
|
|
||||||
const Image inTheWindow = DrawFullViewportTriangle(program, GL_TRIANGLES, &windowDrawError);
|
|
||||||
EXPECT_EQ(windowDrawError, static_cast<GLenum>(GL_NO_ERROR))
|
|
||||||
<< "neither the detach nor the attach has been linked in, so the draw must execute";
|
|
||||||
ExpectFullyColored(inTheWindow, kGreen, "the draw inside the attach window");
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
glLinkProgram(program);
|
|
||||||
GLint linked = GL_FALSE;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
ASSERT_EQ(linked, GL_TRUE) << "the relink onto the blue fragment stage failed";
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
GLenum relinkedDrawError = GL_NO_ERROR;
|
|
||||||
const Image afterRelink = DrawFullViewportTriangle(program, GL_TRIANGLES, &relinkedDrawError);
|
|
||||||
EXPECT_EQ(relinkedDrawError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked program must draw";
|
|
||||||
ExpectFullyColored(afterRelink, kBlue, "the draw after the relink");
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -1,366 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/RelinkStageSetScenario.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
|
|
||||||
//
|
|
||||||
// Scenario - A RELINK MAY CHANGE WHICH STAGES A PROGRAM HAS, AND EVERY DRAW AFTER IT RUNS
|
|
||||||
// THE NEW STAGE SET.
|
|
||||||
//
|
|
||||||
// GL 4.6 core 7.3: glLinkProgram builds an executable out of whatever is attached at that
|
|
||||||
// moment, so the stage set is a property of a LINK and not of a program. A program that
|
|
||||||
// linked vertex+fragment, drew, then had a geometry shader attached and was relinked runs
|
|
||||||
// three stages from that point on.
|
|
||||||
//
|
|
||||||
// DirectGLES rebuilds its driver program in place - same GL name, new executable - and the
|
|
||||||
// per-draw bind dedupes on that name, so a relink that changed the stage set installed
|
|
||||||
// nothing and the following draws rendered NOTHING at all: no GL error, LINK_STATUS true,
|
|
||||||
// and a framebuffer that kept its clear colour. See the note at the glLinkProgram in
|
|
||||||
// BackendProgramObjectImpl::SyncToBackend for what the driver does with such a relink.
|
|
||||||
//
|
|
||||||
// PostLinkAttachScenario pins the other half of the same rule - that the executable does
|
|
||||||
// NOT move until the relink. This one pins what happens when it does, in all three
|
|
||||||
// directions: a stage added, a stage removed, and a stage added that the ES backend has to
|
|
||||||
// synthesize a partner for.
|
|
||||||
//
|
|
||||||
// Every case asserts on a SHAPE and not merely on "something came out". The geometry and
|
|
||||||
// tessellation stages here halve the triangle, so a full-viewport green frame and a
|
|
||||||
// half-size one say which executable ran - "still drew" and "drew the right stages" are
|
|
||||||
// different claims and only the second one is worth pinning.
|
|
||||||
//
|
|
||||||
// Needs a real context: what is asserted is a rendered pixel out of a backend program build.
|
|
||||||
|
|
||||||
#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 kFboWidth = 64;
|
|
||||||
constexpr int kFboHeight = 64;
|
|
||||||
|
|
||||||
// A full-viewport triangle out of gl_VertexID alone, so no case here needs a vertex
|
|
||||||
// buffer and one draw covers every pixel of the target.
|
|
||||||
const char* const kVertexSource = R"(#version 420 core
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
||||||
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* const kFragmentSource = R"(#version 420 core
|
|
||||||
out vec4 fragColor;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// Halves the triangle instead of passing it through: the centre pixel stays covered
|
|
||||||
// and all four corners fall outside, so the frame alone says whether this stage ran.
|
|
||||||
const char* const kGeometrySource = R"(#version 420 core
|
|
||||||
layout(triangles) in;
|
|
||||||
layout(triangle_strip, max_vertices = 3) out;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
for (int i = 0; i < 3; ++i) {
|
|
||||||
gl_Position = vec4(gl_in[i].gl_Position.xy * 0.5, gl_in[i].gl_Position.zw);
|
|
||||||
EmitVertex();
|
|
||||||
}
|
|
||||||
EndPrimitive();
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// No control stage on purpose: OpenGL ES rejects that shape outright, so DirectGLES
|
|
||||||
// synthesizes a pass-through one (AttachPassthroughTessControlStage) and DirectVulkan
|
|
||||||
// does the same. Reading only gl_in[].gl_Position keeps this inside what such a
|
|
||||||
// pass-through may forward. At the tessellation levels it sets (all 1.0) the patch
|
|
||||||
// comes back out as one triangle whose gl_TessCoord values are the three corners, so
|
|
||||||
// the barycentric sum reproduces the vertex stage's triangle - halved, for the same
|
|
||||||
// reason the geometry stage above halves it.
|
|
||||||
const char* const kTessEvalSource = R"(#version 420 core
|
|
||||||
layout(triangles, equal_spacing, ccw) in;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
vec4 p = gl_TessCoord.x * gl_in[0].gl_Position +
|
|
||||||
gl_TessCoord.y * gl_in[1].gl_Position +
|
|
||||||
gl_TessCoord.z * gl_in[2].gl_Position;
|
|
||||||
gl_Position = vec4(p.xy * 0.5, p.zw);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr Rgba8 kGreen{0, 255, 0, 255};
|
|
||||||
constexpr Rgba8 kRed{255, 0, 0, 255};
|
|
||||||
|
|
||||||
class RelinkStageSetScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
glGenVertexArrays(1, &m_vao);
|
|
||||||
glBindVertexArray(m_vao);
|
|
||||||
m_target = MakeColorFbo(kFboWidth, kFboHeight);
|
|
||||||
ASSERT_NE(m_target.fbo, 0u) << "could not create the scenario's colour target";
|
|
||||||
BindFbo(m_target);
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
glUseProgram(0);
|
|
||||||
for (const GLuint program : m_programs) glDeleteProgram(program);
|
|
||||||
m_programs.clear();
|
|
||||||
for (const GLuint shader : m_shaders) glDeleteShader(shader);
|
|
||||||
m_shaders.clear();
|
|
||||||
BindDefaultFramebuffer();
|
|
||||||
DestroyColorFbo(m_target);
|
|
||||||
glBindVertexArray(0);
|
|
||||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
||||||
m_vao = 0;
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void DrainErrors() {
|
|
||||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The same real-backend probes the other stage-gated scenarios use: 0 on a
|
|
||||||
// DirectGLES driver without the extension and on a DirectVulkan device without
|
|
||||||
// the feature.
|
|
||||||
static bool BackendHostsGeometry() {
|
|
||||||
GLint maxGeometryOutputVertices = 0;
|
|
||||||
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
|
||||||
DrainErrors();
|
|
||||||
return maxGeometryOutputVertices >= 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool BackendHostsTessellation() {
|
|
||||||
GLint maxTessGenLevel = 0;
|
|
||||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
|
||||||
DrainErrors();
|
|
||||||
return maxTessGenLevel >= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string InfoLog(GLuint object, bool isShader) {
|
|
||||||
GLint length = 0;
|
|
||||||
if (isShader) {
|
|
||||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
|
||||||
} else {
|
|
||||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
|
||||||
}
|
|
||||||
if (length <= 0) return {};
|
|
||||||
std::string log(static_cast<size_t>(length), '\0');
|
|
||||||
if (isShader) {
|
|
||||||
glGetShaderInfoLog(object, length, nullptr, log.data());
|
|
||||||
} else {
|
|
||||||
glGetProgramInfoLog(object, length, nullptr, log.data());
|
|
||||||
}
|
|
||||||
log.resize(std::char_traits<char>::length(log.c_str()));
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint MakeShader(GLenum stage, const char* source) {
|
|
||||||
const GLuint shader = glCreateShader(stage);
|
|
||||||
if (shader == 0) return 0;
|
|
||||||
m_shaders.push_back(shader);
|
|
||||||
glShaderSource(shader, 1, &source, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
GLint compiled = GL_FALSE;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
||||||
EXPECT_EQ(compiled, GL_TRUE) << "a scenario shader did not compile: " << InfoLog(shader, true);
|
|
||||||
return shader;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint MakeProgram() {
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
m_programs.push_back(program);
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Link(GLuint program) {
|
|
||||||
glLinkProgram(program);
|
|
||||||
GLint linked = GL_FALSE;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
if (linked != GL_TRUE) {
|
|
||||||
ADD_FAILURE() << "the link failed: " << InfoLog(program, false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clears to red and draws. Red is the clear colour deliberately: nothing here ever
|
|
||||||
// paints red inside the triangle, so a frame that is red where it should be green
|
|
||||||
// says "this draw did not execute" while a frame that is green where it should be
|
|
||||||
// red says "it executed against the wrong executable" - two failures worth telling
|
|
||||||
// apart. The error is sampled between the draw and the readback so a rejected draw
|
|
||||||
// is never confused with a readback that went wrong afterwards.
|
|
||||||
Image DrawTriangle(GLuint program, GLenum mode, GLenum* outDrawError = nullptr) {
|
|
||||||
glUseProgram(program);
|
|
||||||
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
|
|
||||||
DrainErrors();
|
|
||||||
glDrawArrays(mode, 0, 3);
|
|
||||||
if (outDrawError != nullptr) *outDrawError = glGetError();
|
|
||||||
return ReadPixels(kFboWidth, kFboHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The vertex stage's triangle covers the whole target, corners included.
|
|
||||||
static void ExpectFullTriangle(const Image& frame, const char* what) {
|
|
||||||
ASSERT_FALSE(frame.Empty()) << what << ": nothing was read back";
|
|
||||||
ExpectPixel(frame, kFboWidth / 2, kFboHeight / 2, kGreen, what, "centre");
|
|
||||||
for (const int y : {0, kFboHeight - 1}) {
|
|
||||||
for (const int x : {0, kFboWidth - 1}) {
|
|
||||||
ExpectPixel(frame, x, y, kGreen, what, "corner");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...and halved by a geometry or tessellation stage it no longer reaches any of
|
|
||||||
// them, which is what makes the shape readable as "that stage ran".
|
|
||||||
static void ExpectHalvedTriangle(const Image& frame, const char* what) {
|
|
||||||
ASSERT_FALSE(frame.Empty()) << what << ": nothing was read back";
|
|
||||||
ExpectPixel(frame, kFboWidth / 2, kFboHeight / 2, kGreen, what, "centre");
|
|
||||||
for (const int y : {0, kFboHeight - 1}) {
|
|
||||||
for (const int x : {0, kFboWidth - 1}) {
|
|
||||||
ExpectPixel(frame, x, y, kRed, what, "corner");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ExpectPixel(const Image& frame, int x, int y, const Rgba8& expected, const char* what,
|
|
||||||
const char* where) {
|
|
||||||
EXPECT_EQ(frame.At(x, y), expected)
|
|
||||||
<< what << ": " << where << " pixel (" << x << ", " << y << ") is " << frame.ColorName(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint m_vao = 0;
|
|
||||||
ColorFbo m_target{};
|
|
||||||
std::vector<GLuint> m_programs;
|
|
||||||
std::vector<GLuint> m_shaders;
|
|
||||||
};
|
|
||||||
|
|
||||||
// THE REGRESSION. Vertex+fragment, linked and DRAWN - which is what puts a built driver
|
|
||||||
// program on the backend twin - then a geometry shader attached and the program
|
|
||||||
// relinked. The halved frame is the assertion: the three-stage executable really is
|
|
||||||
// what the next draw ran.
|
|
||||||
TEST_F(RelinkStageSetScenario, RelinkingToAddAGeometryStageRunsTheNewExecutable) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
if (!BackendHostsGeometry()) {
|
|
||||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString()
|
|
||||||
<< "); there is no stage to add";
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint program = MakeProgram();
|
|
||||||
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
|
|
||||||
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentSource));
|
|
||||||
ASSERT_TRUE(Link(program));
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
GLenum beforeError = GL_NO_ERROR;
|
|
||||||
const Image before = DrawTriangle(program, GL_TRIANGLES, &beforeError);
|
|
||||||
EXPECT_EQ(beforeError, static_cast<GLenum>(GL_NO_ERROR)) << "the vertex+fragment draw must execute";
|
|
||||||
ExpectFullTriangle(before, "the draw before the relink");
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
glAttachShader(program, MakeShader(GL_GEOMETRY_SHADER, kGeometrySource));
|
|
||||||
ASSERT_TRUE(Link(program));
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
GLenum afterError = GL_NO_ERROR;
|
|
||||||
const Image after = DrawTriangle(program, GL_TRIANGLES, &afterError);
|
|
||||||
EXPECT_EQ(afterError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked three-stage program must draw";
|
|
||||||
ExpectHalvedTriangle(after, "the draw after the geometry stage was linked in");
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The same move in the other direction, which no repair may confuse with "the stage
|
|
||||||
// set did not change": the geometry stage leaves the executable, so the halving has to
|
|
||||||
// stop with it.
|
|
||||||
TEST_F(RelinkStageSetScenario, RelinkingToRemoveAGeometryStageRunsTheNewExecutable) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
if (!BackendHostsGeometry()) {
|
|
||||||
GTEST_SKIP() << "no geometry stage on " << Gl().BackendName() << " (" << Gl().RendererString()
|
|
||||||
<< "); there is no stage to remove";
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint program = MakeProgram();
|
|
||||||
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
|
|
||||||
const GLuint geometry = MakeShader(GL_GEOMETRY_SHADER, kGeometrySource);
|
|
||||||
glAttachShader(program, geometry);
|
|
||||||
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentSource));
|
|
||||||
ASSERT_TRUE(Link(program));
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
// Also the control for the case above: a three-stage program linked in ONE go and
|
|
||||||
// never relinked draws its halved triangle.
|
|
||||||
GLenum beforeError = GL_NO_ERROR;
|
|
||||||
const Image before = DrawTriangle(program, GL_TRIANGLES, &beforeError);
|
|
||||||
EXPECT_EQ(beforeError, static_cast<GLenum>(GL_NO_ERROR)) << "the three-stage draw must execute";
|
|
||||||
ExpectHalvedTriangle(before, "the draw before the geometry stage was dropped");
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
glDetachShader(program, geometry);
|
|
||||||
ASSERT_TRUE(Link(program));
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
GLenum afterError = GL_NO_ERROR;
|
|
||||||
const Image after = DrawTriangle(program, GL_TRIANGLES, &afterError);
|
|
||||||
EXPECT_EQ(afterError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked vertex+fragment program must draw";
|
|
||||||
ExpectFullTriangle(after, "the draw after the geometry stage was dropped");
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The third direction, and the one that asks the most of the rebuild: the added stage
|
|
||||||
// is a tessellation evaluation shader with no control stage, so the ES backend has to
|
|
||||||
// synthesize a pass-through control stage for an executable that had neither a moment
|
|
||||||
// ago. GL_PATCHES becomes the only legal mode with it, which is also the only draw-mode
|
|
||||||
// change any case here makes.
|
|
||||||
TEST_F(RelinkStageSetScenario, RelinkingToAddATessEvalStageRunsTheNewExecutable) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
if (!BackendHostsTessellation()) {
|
|
||||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " (" << Gl().RendererString()
|
|
||||||
<< "); there is no stage to add";
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint program = MakeProgram();
|
|
||||||
glAttachShader(program, MakeShader(GL_VERTEX_SHADER, kVertexSource));
|
|
||||||
glAttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFragmentSource));
|
|
||||||
ASSERT_TRUE(Link(program));
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
GLenum beforeError = GL_NO_ERROR;
|
|
||||||
const Image before = DrawTriangle(program, GL_TRIANGLES, &beforeError);
|
|
||||||
EXPECT_EQ(beforeError, static_cast<GLenum>(GL_NO_ERROR)) << "the vertex+fragment draw must execute";
|
|
||||||
ExpectFullTriangle(before, "the draw before the relink");
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
glAttachShader(program, MakeShader(GL_TESS_EVALUATION_SHADER, kTessEvalSource));
|
|
||||||
ASSERT_TRUE(Link(program));
|
|
||||||
// Three, which is already the default; spelled out because the synthesized control
|
|
||||||
// stage's output patch size is compiled from it.
|
|
||||||
glPatchParameteri(GL_PATCH_VERTICES, 3);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
GLenum afterError = GL_NO_ERROR;
|
|
||||||
const Image after = DrawTriangle(program, GL_PATCHES, &afterError);
|
|
||||||
EXPECT_EQ(afterError, static_cast<GLenum>(GL_NO_ERROR)) << "the relinked tessellating program must draw";
|
|
||||||
ExpectHalvedTriangle(after, "the draw after the tessellation stage was linked in");
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SnormAttachmentScenario.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
|
|
||||||
//
|
|
||||||
// Scenario - SIGNED-NORMALIZED COLOUR ATTACHMENTS, on a live driver.
|
|
||||||
//
|
|
||||||
// The bug: a GLES driver without GL_EXT_render_snorm treats every signed-normalized format as
|
|
||||||
// texture-only. DirectGLES had a colour-renderable substitute for exactly one of the eight
|
|
||||||
// (GL_RGB16_SNORM, through the three-channel widening), so an R8_SNORM or R16_SNORM attachment got
|
|
||||||
// no storage the driver would render into: the ES framebuffer was incomplete, the draw landed
|
|
||||||
// nowhere, and glGetTexImage fell through to the CPU shadow - all zeroes for a texture created with
|
|
||||||
// no data. KHR-GL4x.texture_swizzle renders into a SINGLE-CHANNEL SNORM output for every one of its
|
|
||||||
// SNORM source formats, which is why all 46 of its GL43 SNORM cases failed on Mali.
|
|
||||||
//
|
|
||||||
// THE OTHER HALF, and the reason this scenario asserts VALUES rather than only completeness: the
|
|
||||||
// substitute has to be exact. A half float's 11-bit mantissa cannot represent a 16-bit SNORM
|
|
||||||
// channel - 23451/32767 quantizes about six SNORM steps away, against a conformance window of one -
|
|
||||||
// so the 16-bit formats must land on a 32-bit float even though the 8-bit ones are fine in a half.
|
|
||||||
// Trading 46 visible failures for silent precision loss in Iris' SNORM normal buffers would be the
|
|
||||||
// worse outcome, so the round trip below is pinned tightly enough to fail on a half-float substitute
|
|
||||||
// (tolerance two SNORM steps, half-float error six).
|
|
||||||
//
|
|
||||||
// WHAT THIS GATE CAN AND CANNOT SEE. Both CI drivers (Mesa llvmpipe) and Adreno expose
|
|
||||||
// GL_EXT_render_snorm, so they take the NATIVE path here and the substitution stays dead. That is
|
|
||||||
// precisely why the assertions are written as invariants of the format rather than of the fallback:
|
|
||||||
// "a signed-normalized colour attachment is complete and round-trips its channel values" has to
|
|
||||||
// hold whichever path answers it, so the scenario fails if anyone ever routes these formats to a
|
|
||||||
// lossy storage on a driver where it IS live. The substitution itself can only be observed on a
|
|
||||||
// device without EXT_render_snorm (Mali Immortalis-G925).
|
|
||||||
//
|
|
||||||
// DirectGLES only, like the three-channel scenario next door: DirectVulkan resolves SNORM formats
|
|
||||||
// on its own terms and asserting Espryt's answers there would pin a coincidence.
|
|
||||||
|
|
||||||
#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 const char* kVS = R"(#version 330 core
|
|
||||||
in vec2 aPos;
|
|
||||||
void main() {
|
|
||||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// A uniform rather than a literal so nothing can constant-fold the value into a different
|
|
||||||
// precision than the one the attachment stores.
|
|
||||||
constexpr const char* kFS = R"(#version 330 core
|
|
||||||
out vec4 oColor;
|
|
||||||
uniform float uValue;
|
|
||||||
void main() { oColor = vec4(uValue, 0.0, 0.0, 1.0); }
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr int kSize = 8;
|
|
||||||
|
|
||||||
// The two channel values the round trip is pinned on. Both are positive on purpose:
|
|
||||||
// glReadPixels applies GL_CLAMP_READ_COLOR (GL_FIXED_ONLY by default) to a fixed-point
|
|
||||||
// colour buffer, so the negative half of a SNORM attachment reads back as 0 and would
|
|
||||||
// measure the clamp instead of the storage.
|
|
||||||
constexpr int kSnorm8Value = 99;
|
|
||||||
constexpr int kSnorm16Value = 23451;
|
|
||||||
|
|
||||||
class SnormAttachmentScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
if (Gl().BackendName() != "DirectGLES") {
|
|
||||||
GTEST_SKIP() << "the signed-normalized substitution is a DirectGLES fallback; backend is "
|
|
||||||
<< Gl().BackendName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A single-level 2D texture in `internalFormat`, or 0 when the driver rejects the
|
|
||||||
// storage outright (which is a different failure from rejecting the ATTACHMENT).
|
|
||||||
static GLuint MakeTexture(GLenum internalFormat) {
|
|
||||||
GLuint texture = 0;
|
|
||||||
glGenTextures(1, &texture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, texture);
|
|
||||||
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kSize, kSize);
|
|
||||||
if (glGetError() != GL_NO_ERROR) {
|
|
||||||
glDeleteTextures(1, &texture);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
|
||||||
return texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
static GLenum SingleAttachmentStatus(GLenum internalFormat) {
|
|
||||||
const GLuint texture = MakeTexture(internalFormat);
|
|
||||||
if (texture == 0) return GL_NONE;
|
|
||||||
GLuint fbo = 0;
|
|
||||||
glGenFramebuffers(1, &fbo);
|
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
|
||||||
const GLenum status = glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
|
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
|
||||||
glDeleteFramebuffers(1, &fbo);
|
|
||||||
glDeleteTextures(1, &texture);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Renders `value` into the red channel of a fresh `internalFormat` attachment and hands
|
|
||||||
// back what glReadPixels sees. Returns false when the framebuffer never came up, which
|
|
||||||
// is the failure mode this scenario exists for - a draw into an incomplete framebuffer
|
|
||||||
// is dropped by the driver and leaves the caller reading the cleared texture.
|
|
||||||
bool RenderAndReadRed(GLenum internalFormat, float value, float* outRed) {
|
|
||||||
std::string error;
|
|
||||||
const GLuint program = CompileProgram(kVS, kFS, &error);
|
|
||||||
EXPECT_NE(program, 0u) << error;
|
|
||||||
if (program == 0) return false;
|
|
||||||
const GLint valueLocation = glGetUniformLocation(program, "uValue");
|
|
||||||
EXPECT_GE(valueLocation, 0);
|
|
||||||
|
|
||||||
const GLuint texture = MakeTexture(internalFormat);
|
|
||||||
EXPECT_NE(texture, 0u) << "the driver refused the texture storage itself";
|
|
||||||
if (texture == 0) {
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint fbo = 0;
|
|
||||||
glGenFramebuffers(1, &fbo);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
|
||||||
const bool complete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
|
||||||
|
|
||||||
if (complete) {
|
|
||||||
const float quad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
|
||||||
GLuint vao = 0;
|
|
||||||
GLuint vbo = 0;
|
|
||||||
glGenVertexArrays(1, &vao);
|
|
||||||
glBindVertexArray(vao);
|
|
||||||
glGenBuffers(1, &vbo);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
|
|
||||||
glEnableVertexAttribArray(0);
|
|
||||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
|
||||||
glUseProgram(program);
|
|
||||||
glUniform1f(valueLocation, value);
|
|
||||||
glViewport(0, 0, kSize, kSize);
|
|
||||||
// Cleared to zero so a dropped draw cannot be mistaken for a correct one.
|
|
||||||
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
||||||
|
|
||||||
std::vector<float> pixels(static_cast<std::size_t>(kSize) * kSize * 4, -1.0f);
|
|
||||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
|
||||||
glReadPixels(0, 0, kSize, kSize, GL_RGBA, GL_FLOAT, pixels.data());
|
|
||||||
if (outRed) *outRed = pixels[0];
|
|
||||||
|
|
||||||
glDeleteBuffers(1, &vbo);
|
|
||||||
glDeleteVertexArrays(1, &vao);
|
|
||||||
}
|
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
glDeleteFramebuffers(1, &fbo);
|
|
||||||
glDeleteTextures(1, &texture);
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return complete;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// THE regression gate for the frontend's answer. Every one of these used to be
|
|
||||||
// GL_FRAMEBUFFER_UNSUPPORTED on a driver without EXT_render_snorm, and nothing in the CTS
|
|
||||||
// (or in Iris) checks the status before drawing, so the failure was silent all the way to a
|
|
||||||
// readback of zeroes.
|
|
||||||
TEST_F(SnormAttachmentScenario, SignedNormalizedColorAttachmentsReportComplete) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
|
|
||||||
// GL_R8 is the control: colour-renderable in ES core, so it must pass with or without
|
|
||||||
// any substitution. If it ever fails, nothing below means anything.
|
|
||||||
EXPECT_EQ(SingleAttachmentStatus(GL_R8), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
|
||||||
<< "GL_R8 is ES-core colour-renderable";
|
|
||||||
|
|
||||||
// The single-channel pair KHR-GL4x.texture_swizzle renders into for every SNORM source
|
|
||||||
// format - the whole 46-case failure.
|
|
||||||
EXPECT_EQ(SingleAttachmentStatus(GL_R8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
|
||||||
EXPECT_EQ(SingleAttachmentStatus(GL_R16_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
|
||||||
// ...and the two- and four-channel siblings, which are what a shaderpack actually
|
|
||||||
// declares (Iris colortex buffers in RGBA16_SNORM).
|
|
||||||
EXPECT_EQ(SingleAttachmentStatus(GL_RG8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
|
||||||
EXPECT_EQ(SingleAttachmentStatus(GL_RG16_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
|
||||||
EXPECT_EQ(SingleAttachmentStatus(GL_RGBA8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
|
||||||
EXPECT_EQ(SingleAttachmentStatus(GL_RGBA16_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
|
||||||
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
|
||||||
}
|
|
||||||
|
|
||||||
// The other half: whatever storage answers for the attachment has to hold the channel value
|
|
||||||
// to the format's own precision. This is the assertion that fails if the 16-bit formats are
|
|
||||||
// ever routed to a half float - the substitute an implementer naturally reaches for, because
|
|
||||||
// it is what the 8-bit ones correctly use.
|
|
||||||
TEST_F(SnormAttachmentScenario, SignedNormalizedAttachmentsRoundTripTheirChannelValues) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
|
|
||||||
const float snorm8Expected = static_cast<float>(kSnorm8Value) / 127.0f;
|
|
||||||
float red8 = -1.0f;
|
|
||||||
ASSERT_TRUE(RenderAndReadRed(GL_R8_SNORM, snorm8Expected, &red8))
|
|
||||||
<< "an R8_SNORM colour attachment must be complete before any value can be asserted";
|
|
||||||
// Two 8-bit SNORM steps. A half float is exact here (worst case 0.03 of a step), so this
|
|
||||||
// only has to catch a storage that quantizes harder than the format itself.
|
|
||||||
EXPECT_NEAR(red8, snorm8Expected, 2.0f / 127.0f)
|
|
||||||
<< "R8_SNORM attachment lost its channel value";
|
|
||||||
EXPECT_GT(red8, 0.5f) << "the draw never landed - this is the cleared texture, not the rendered one";
|
|
||||||
|
|
||||||
const float snorm16Expected = static_cast<float>(kSnorm16Value) / 32767.0f;
|
|
||||||
float red16 = -1.0f;
|
|
||||||
ASSERT_TRUE(RenderAndReadRed(GL_R16_SNORM, snorm16Expected, &red16))
|
|
||||||
<< "an R16_SNORM colour attachment must be complete before any value can be asserted";
|
|
||||||
// Two 16-bit SNORM steps (6.1e-5). A half float would land 1.9e-4 away - three times
|
|
||||||
// this window - which is exactly the failure this bound exists to catch.
|
|
||||||
EXPECT_NEAR(red16, snorm16Expected, 2.0f / 32767.0f)
|
|
||||||
<< "R16_SNORM attachment was stored in something that cannot hold 16 signed bits";
|
|
||||||
EXPECT_GT(red16, 0.5f) << "the draw never landed - this is the cleared texture, not the rendered one";
|
|
||||||
|
|
||||||
float red16x4 = -1.0f;
|
|
||||||
ASSERT_TRUE(RenderAndReadRed(GL_RGBA16_SNORM, snorm16Expected, &red16x4))
|
|
||||||
<< "an RGBA16_SNORM colour attachment must be complete before any value can be asserted";
|
|
||||||
EXPECT_NEAR(red16x4, snorm16Expected, 2.0f / 32767.0f)
|
|
||||||
<< "RGBA16_SNORM attachment was stored in something that cannot hold 16 signed bits";
|
|
||||||
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SsboArrayDynamicIndexScenario.cpp
|
|
||||||
// Copyright (c) 2026 MobileGL-Dev
|
|
||||||
// Licensed under the GNU Lesser General Public License v3.0:
|
|
||||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
||||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// End of Source File Header
|
|
||||||
//
|
|
||||||
// Scenario - A NON-CONSTANT INDEX INTO AN ARRAY OF SHADER STORAGE BLOCKS.
|
|
||||||
//
|
|
||||||
// GL 4.3 allows any dynamically-uniform expression there; GLSL ES keeps the ES 3.1 rule that the
|
|
||||||
// index must be a constant integral expression, and the Qualcomm compiler enforces it:
|
|
||||||
//
|
|
||||||
// '[' : indexing into an SSBO array using a non-constant expression is not permitted
|
|
||||||
//
|
|
||||||
// The stage then never compiles, the backend program links nothing, and every dispatch is a
|
|
||||||
// silent no-op - while glGetProgramiv(GL_LINK_STATUS) keeps reporting the successful link the
|
|
||||||
// frontend already published. That is why the conformance failures
|
|
||||||
// (KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case1/case4,
|
|
||||||
// advanced-indirectAddressing-case2, compute_shader.resources-max, 7 cases in all) read back as
|
|
||||||
// "the buffer was never written" rather than as an error, and why this scenario asserts on
|
|
||||||
// contents rather than on link status.
|
|
||||||
//
|
|
||||||
// Both index shapes the legalization has to cover are exercised in one dispatch: a loop induction
|
|
||||||
// variable (which folds when the loop unrolls) and a `uniform int` (which nothing can fold, so the
|
|
||||||
// switch/select lowering is what carries it), for a read AND for a write.
|
|
||||||
|
|
||||||
#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 {
|
|
||||||
|
|
||||||
// Bindings 0..3 are the block array, 4 is the output.
|
|
||||||
constexpr const char* kComputeSource = R"(#version 430 core
|
|
||||||
layout(local_size_x = 1) in;
|
|
||||||
layout(std430, binding = 0) buffer Slot {
|
|
||||||
uint value;
|
|
||||||
} g_slots[4];
|
|
||||||
layout(std430, binding = 4) buffer Output {
|
|
||||||
uint g_result[];
|
|
||||||
};
|
|
||||||
uniform int g_index;
|
|
||||||
void main() {
|
|
||||||
// Loop-derived index: foldable by unrolling.
|
|
||||||
for (int i = 0; i < 4; ++i) {
|
|
||||||
g_result[i] = g_slots[i].value;
|
|
||||||
}
|
|
||||||
// Uniform-derived index: not foldable, read and write both.
|
|
||||||
g_result[4] = g_slots[g_index].value;
|
|
||||||
g_slots[g_index].value = 99u;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr int kSlotCount = 4;
|
|
||||||
constexpr int kResultCount = 5;
|
|
||||||
|
|
||||||
class SsboArrayDynamicIndexScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
GLint blocks = 0;
|
|
||||||
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &blocks);
|
|
||||||
if (blocks < kSlotCount + 1) {
|
|
||||||
GTEST_SKIP() << "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS is " << blocks << "; this needs "
|
|
||||||
<< kSlotCount + 1;
|
|
||||||
}
|
|
||||||
m_program = CompileComputeProgram(kComputeSource);
|
|
||||||
ASSERT_NE(m_program, 0u) << m_buildLog;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
if (!m_buffers.empty()) glDeleteBuffers(static_cast<GLsizei>(m_buffers.size()), m_buffers.data());
|
|
||||||
if (m_program != 0) glDeleteProgram(m_program);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned int CompileComputeProgram(const char* source) {
|
|
||||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
|
||||||
glShaderSource(shader, 1, &source, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
GLint compiled = 0;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
||||||
if (compiled == GL_FALSE) {
|
|
||||||
char log[2048] = {};
|
|
||||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
|
||||||
m_buildLog = std::string("compute shader did not compile: ") + log;
|
|
||||||
glDeleteShader(shader);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
glAttachShader(program, shader);
|
|
||||||
glLinkProgram(program);
|
|
||||||
glDeleteShader(shader);
|
|
||||||
GLint linked = 0;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
if (linked == GL_FALSE) {
|
|
||||||
char log[2048] = {};
|
|
||||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
|
||||||
m_buildLog = std::string("compute program did not link: ") + log;
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint MakeStorageBuffer(const std::vector<unsigned int>& contents) {
|
|
||||||
GLuint buffer = 0;
|
|
||||||
glGenBuffers(1, &buffer);
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
|
||||||
glBufferData(GL_SHADER_STORAGE_BUFFER,
|
|
||||||
static_cast<GLsizeiptr>(contents.size() * sizeof(unsigned int)), contents.data(),
|
|
||||||
GL_DYNAMIC_COPY);
|
|
||||||
m_buffers.push_back(buffer);
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::vector<unsigned int> ReadBuffer(GLuint buffer, int count) {
|
|
||||||
std::vector<unsigned int> values(static_cast<std::size_t>(count), 0xDEADBEEFu);
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer);
|
|
||||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
|
|
||||||
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned int m_program = 0;
|
|
||||||
std::string m_buildLog;
|
|
||||||
std::vector<GLuint> m_buffers;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_F(SsboArrayDynamicIndexScenario, ReadsAndWritesTheBlockTheIndexNames) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
|
|
||||||
GLuint slots[kSlotCount] = {};
|
|
||||||
for (int i = 0; i < kSlotCount; ++i) {
|
|
||||||
slots[i] = MakeStorageBuffer({static_cast<unsigned int>(10 + i)});
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(i), slots[i]);
|
|
||||||
}
|
|
||||||
const GLuint output = MakeStorageBuffer(std::vector<unsigned int>(kResultCount, 0u));
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, kSlotCount, output);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
glUseProgram(m_program);
|
|
||||||
const GLint indexLocation = glGetUniformLocation(m_program, "g_index");
|
|
||||||
ASSERT_NE(indexLocation, -1);
|
|
||||||
glUniform1i(indexLocation, 2);
|
|
||||||
glDispatchCompute(1, 1, 1);
|
|
||||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
const std::vector<unsigned int> result = ReadBuffer(output, kResultCount);
|
|
||||||
for (int i = 0; i < kSlotCount; ++i) {
|
|
||||||
EXPECT_EQ(result[static_cast<std::size_t>(i)], static_cast<unsigned int>(10 + i))
|
|
||||||
<< "g_slots[" << i << "] read through the loop index came back as "
|
|
||||||
<< result[static_cast<std::size_t>(i)]
|
|
||||||
<< "; 0 means the stage never compiled and the dispatch was a silent no-op";
|
|
||||||
}
|
|
||||||
EXPECT_EQ(result[4], 12u) << "g_slots[g_index] with g_index = 2 read back as " << result[4];
|
|
||||||
|
|
||||||
const std::vector<unsigned int> written = ReadBuffer(slots[2], 1);
|
|
||||||
EXPECT_EQ(written[0], 99u) << "the uniform-indexed WRITE landed as " << written[0]
|
|
||||||
<< " instead of 99 in g_slots[2]";
|
|
||||||
// The write must have gone to element 2 and nowhere else.
|
|
||||||
for (int i = 0; i < kSlotCount; ++i) {
|
|
||||||
if (i == 2) continue;
|
|
||||||
const std::vector<unsigned int> untouched = ReadBuffer(slots[i], 1);
|
|
||||||
EXPECT_EQ(untouched[0], static_cast<unsigned int>(10 + i))
|
|
||||||
<< "g_slots[" << i << "] was overwritten by a write that named element 2";
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i <= kSlotCount; ++i) {
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(i), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -64,25 +64,6 @@ void main() {
|
|||||||
g_length[2] = g_input23[0].data.length();
|
g_length[2] = g_input23[0].data.length();
|
||||||
g_length[3] = g_input23[1].data.length();
|
g_length[3] = g_input23[1].data.length();
|
||||||
}
|
}
|
||||||
)";
|
|
||||||
|
|
||||||
// GL 4.6 core 4.10 lets a buffer variable be declared readonly AND writeonly at once:
|
|
||||||
// it can then be neither read nor written, and `.length()` is the only thing left that
|
|
||||||
// may be asked of it. The pair is inert - and printing it into ESSL is not, because
|
|
||||||
// SPIRV-Cross hoists the qualifiers every member shares onto the BLOCK and Mesa's ES
|
|
||||||
// compiler refuses that spelling ("Interface block sets both readonly and writeonly").
|
|
||||||
// Lifted from KHR-GL43.shader_storage_buffer_object.basic-readonly-writeonly.
|
|
||||||
constexpr const char* kReadonlyWriteonlyComputeSource = R"(#version 430 core
|
|
||||||
layout(local_size_x = 1) in;
|
|
||||||
layout(std430, binding = 0) buffer Input {
|
|
||||||
readonly writeonly int g_in[];
|
|
||||||
};
|
|
||||||
layout(std430, binding = 4) buffer Output {
|
|
||||||
int g_length[];
|
|
||||||
};
|
|
||||||
void main() {
|
|
||||||
g_length[0] = g_in.length();
|
|
||||||
}
|
|
||||||
)";
|
)";
|
||||||
|
|
||||||
constexpr int kElementBytes = 16; // ivec4, std430
|
constexpr int kElementBytes = 16; // ivec4, std430
|
||||||
@@ -231,33 +212,4 @@ void main() {
|
|||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, input0);
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, input0);
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, input3);
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, input3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A buffer variable qualified readonly AND writeonly can only be asked its length, and that
|
|
||||||
// question still has to be answered. A stage the driver refused answers 0 - and refuses
|
|
||||||
// silently, because the program links without it and the dispatch is then a no-op.
|
|
||||||
TEST_F(SsboArrayLengthScenario, AReadonlyWriteonlyArrayStillReportsItsLength) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
|
|
||||||
const GLuint program = CompileComputeProgram(kReadonlyWriteonlyComputeSource);
|
|
||||||
ASSERT_NE(program, 0u) << m_buildLog;
|
|
||||||
|
|
||||||
const GLuint input = MakeStorageBuffer(6); // 6 ivec4 = 24 ints
|
|
||||||
const GLuint output = MakeStorageBuffer(1);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, input);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, output);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
glUseProgram(program);
|
|
||||||
glDispatchCompute(1, 1, 1);
|
|
||||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
|
||||||
int length = -1;
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, output);
|
|
||||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(length), &length);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
EXPECT_EQ(length, 24) << "a readonly+writeonly runtime array reported length " << length
|
|
||||||
<< "; 0 means the stage never reached the program";
|
|
||||||
|
|
||||||
glUseProgram(m_program);
|
|
||||||
glDeleteProgram(program);
|
|
||||||
}
|
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -42,10 +42,10 @@
|
|||||||
namespace MGITest {
|
namespace MGITest {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// The eight vertex shaders of the conformance sweep, verbatim in shape, plus a ninth that
|
// The eight vertex shaders of the conformance sweep, verbatim in shape. Each reads three
|
||||||
// is not from the sweep (see form 8). Each reads three vec4 positions out of a storage
|
// vec4 positions out of a storage block on binding 0 and emits them as a triangle that
|
||||||
// block on binding 0 and emits them as a triangle that covers the whole viewport.
|
// covers the whole viewport.
|
||||||
constexpr const char* kFormVS[9] = {
|
constexpr const char* kFormVS[8] = {
|
||||||
// 0 - instance name, no binding qualifier, sized array member
|
// 0 - instance name, no binding qualifier, sized array member
|
||||||
R"(#version 430 core
|
R"(#version 430 core
|
||||||
layout(std430) buffer Buffer {
|
layout(std430) buffer Buffer {
|
||||||
@@ -127,38 +127,6 @@ void main() {
|
|||||||
case 2: gl_Position = g_buffer.position2[gl_VertexID - 2]; break;
|
case 2: gl_Position = g_buffer.position2[gl_VertexID - 2]; break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)",
|
|
||||||
// 8 - NOT from the conformance sweep. An unqualified storage block with a UNIFORM
|
|
||||||
// BLOCK beside it, which is what makes the block's DEFAULT binding observable at all.
|
|
||||||
//
|
|
||||||
// GL 4.3 core 7.8 gives a storage block with no layout(binding = N) a buffer binding
|
|
||||||
// of zero. Forms 0, 1, 3, 4 and 5 above are all unqualified and all pass, but they
|
|
||||||
// cannot prove that rule holds: they are the only resource in their shader, so the
|
|
||||||
// binding glslang's IO mapper invents for them happens to BE zero and the right answer
|
|
||||||
// arrives for the wrong reason.
|
|
||||||
//
|
|
||||||
// Every shader here is parsed as a Vulkan client, so that mapper allocates out of ONE
|
|
||||||
// flat space shared by samplers, images, uniform blocks, storage blocks and the
|
|
||||||
// synthesized global-uniform block (iomapper.cpp resolveBinding takes the `ent.newSet`
|
|
||||||
// branch, and every resource resolves to set 0), and then writes the result back into
|
|
||||||
// the type's qualifier - so the reflection cannot tell an invented binding from a
|
|
||||||
// declared one. Put anything live next to the block and it is pushed off zero, the
|
|
||||||
// draw reads a binding point nothing was ever bound to, and the triangle collapses
|
|
||||||
// with no GL error anywhere. That is
|
|
||||||
// KHR-GL43.compute_shader.resource-ubo's whole failure, in a vertex stage.
|
|
||||||
//
|
|
||||||
// The uniform block is REBOUND explicitly with glUniformBlockBinding, exactly as that
|
|
||||||
// conformance case does. That keeps this case about the storage block's default and
|
|
||||||
// not about the uniform block's - the rebinding path has always worked, and the
|
|
||||||
// uniform-block default is a separate (still open) question.
|
|
||||||
R"(#version 430 core
|
|
||||||
layout(std140) uniform ScaleBlock {
|
|
||||||
vec4 factor;
|
|
||||||
} g_scale;
|
|
||||||
layout(std430) buffer Buffer {
|
|
||||||
vec4 position[3];
|
|
||||||
} g_input_buffer;
|
|
||||||
void main() { gl_Position = g_input_buffer.position[gl_VertexID] * g_scale.factor; }
|
|
||||||
)",
|
)",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -229,26 +197,6 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
|||||||
const unsigned int program = CompileProgram(kFormVS[form], kFormFS, &error);
|
const unsigned int program = CompileProgram(kFormVS[form], kFormFS, &error);
|
||||||
ASSERT_NE(program, 0u) << "form " << form << " did not build: " << error;
|
ASSERT_NE(program, 0u) << "form " << form << " did not build: " << error;
|
||||||
|
|
||||||
// Form 8 alone declares a uniform block, and it exists only to occupy a slot the
|
|
||||||
// storage block must not be pushed onto. Bound to a buffer of ones so it scales
|
|
||||||
// the positions by exactly 1 - the block's contribution to the IMAGE is nothing,
|
|
||||||
// and its contribution to the TEST is that it is there at all.
|
|
||||||
GLuint uniformBuffer = 0;
|
|
||||||
if (form == 8) {
|
|
||||||
const float ones[4] = {1.0f, 1.0f, 1.0f, 1.0f};
|
|
||||||
glGenBuffers(1, &uniformBuffer);
|
|
||||||
glBindBuffer(GL_UNIFORM_BUFFER, uniformBuffer);
|
|
||||||
glBufferData(GL_UNIFORM_BUFFER, sizeof(ones), ones, GL_STATIC_DRAW);
|
|
||||||
glBindBufferBase(GL_UNIFORM_BUFFER, 0, uniformBuffer);
|
|
||||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
|
||||||
const GLuint blockIndex = glGetUniformBlockIndex(program, "ScaleBlock");
|
|
||||||
ASSERT_NE(blockIndex, GL_INVALID_INDEX) << "form 8: the uniform block is not active";
|
|
||||||
// Explicit, so this case cannot fail on the uniform block's own default
|
|
||||||
// binding - which is a separate question from the storage block's.
|
|
||||||
glUniformBlockBinding(program, blockIndex, 0);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u) << "form 8: uniform block setup errored";
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint vao = 0;
|
GLuint vao = 0;
|
||||||
glGenVertexArrays(1, &vao);
|
glGenVertexArrays(1, &vao);
|
||||||
glBindVertexArray(vao);
|
glBindVertexArray(vao);
|
||||||
@@ -273,7 +221,6 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
|||||||
glDeleteVertexArrays(1, &vao);
|
glDeleteVertexArrays(1, &vao);
|
||||||
glDeleteProgram(program);
|
glDeleteProgram(program);
|
||||||
glDeleteBuffers(1, &buffer);
|
glDeleteBuffers(1, &buffer);
|
||||||
if (uniformBuffer != 0) glDeleteBuffers(1, &uniformBuffer);
|
|
||||||
gl.EndFrame();
|
gl.EndFrame();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -294,10 +241,6 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
|||||||
MGL_SSBO_FORM_CASE(3, GlobalLayoutDefaultsThenAnInstanceNamedBlock)
|
MGL_SSBO_FORM_CASE(3, GlobalLayoutDefaultsThenAnInstanceNamedBlock)
|
||||||
MGL_SSBO_FORM_CASE(4, BlockInstanceArrayOfOne)
|
MGL_SSBO_FORM_CASE(4, BlockInstanceArrayOfOne)
|
||||||
MGL_SSBO_FORM_CASE(5, BlockInstanceArrayOfOneWithSharedLayout)
|
MGL_SSBO_FORM_CASE(5, BlockInstanceArrayOfOneWithSharedLayout)
|
||||||
// The form that makes the DEFAULT binding observable rather than accidental: forms 0/1/3/4/5
|
|
||||||
// are unqualified too, but nothing competes with them for glslang's flat slot 0, so they
|
|
||||||
// would keep passing even with the default wrong. See the comment on kFormVS[8].
|
|
||||||
MGL_SSBO_FORM_CASE(8, NoBindingQualifierBesideAUniformBlock)
|
|
||||||
// ---- the two forms that do not work yet ----
|
// ---- the two forms that do not work yet ----
|
||||||
//
|
//
|
||||||
// Both carry an UNSIZED array that is not the block's sole trailing member, and both fail
|
// Both carry an UNSIZED array that is not the block's sole trailing member, and both fail
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/StorageBufferRegrowScenario.cpp
|
|
||||||
// Copyright (c) 2026 MobileGL-Dev
|
|
||||||
// Licensed under the GNU Lesser General Public License v3.0:
|
|
||||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
||||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// End of Source File Header
|
|
||||||
//
|
|
||||||
// Scenario - glBufferData GROWS A BUFFER THAT IS ALREADY BOUND AT AN INDEXED POINT.
|
|
||||||
//
|
|
||||||
// GL says the indexed binding follows the buffer object, so after the store is re-specified the
|
|
||||||
// shader sees the NEW extent. DirectGLES shadows the indexed bindings so a redundant
|
|
||||||
// glBindBufferBase can be skipped, and nothing used to invalidate that shadow when the store was
|
|
||||||
// re-specified underneath it - so on a driver that resolves a whole-buffer indexed binding's
|
|
||||||
// extent at BIND time (Adreno does; Mali does not) the shader kept seeing the OLD, smaller range.
|
|
||||||
// Stores past it are dropped and loads return zero, which is exactly what
|
|
||||||
// KHR-GL43.compute_shader.dispatch-indirect reported: the first iteration's 6 elements correct and
|
|
||||||
// everything past byte 24 zero, after the same buffer was re-specified from 24 to 96 bytes.
|
|
||||||
//
|
|
||||||
// The assertion is deliberately on the WHOLE grown range, so a partial write names the byte the
|
|
||||||
// stale extent stopped at.
|
|
||||||
|
|
||||||
#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 const char* kComputeSource = R"(#version 430 core
|
|
||||||
layout(local_size_x = 1) in;
|
|
||||||
layout(std430, binding = 0) buffer Output {
|
|
||||||
uint g_data[];
|
|
||||||
};
|
|
||||||
void main() {
|
|
||||||
g_data[gl_GlobalInvocationID.x] = gl_GlobalInvocationID.x + 1u;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
constexpr int kSmallElements = 6; // 24 bytes - the first iteration's size
|
|
||||||
constexpr int kLargeElements = 24; // 96 bytes - what the second iteration grows to
|
|
||||||
|
|
||||||
class StorageBufferRegrowScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
m_program = CompileComputeProgram(kComputeSource);
|
|
||||||
ASSERT_NE(m_program, 0u) << m_buildLog;
|
|
||||||
glGenBuffers(1, &m_buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
if (m_buffer != 0) glDeleteBuffers(1, &m_buffer);
|
|
||||||
if (m_program != 0) glDeleteProgram(m_program);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned int CompileComputeProgram(const char* source) {
|
|
||||||
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
|
||||||
glShaderSource(shader, 1, &source, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
GLint compiled = 0;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
||||||
if (compiled == GL_FALSE) {
|
|
||||||
char log[2048] = {};
|
|
||||||
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
|
||||||
m_buildLog = std::string("compute shader did not compile: ") + log;
|
|
||||||
glDeleteShader(shader);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
glAttachShader(program, shader);
|
|
||||||
glLinkProgram(program);
|
|
||||||
glDeleteShader(shader);
|
|
||||||
GLint linked = 0;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
if (linked == GL_FALSE) {
|
|
||||||
char log[2048] = {};
|
|
||||||
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
|
||||||
m_buildLog = std::string("compute program did not link: ") + log;
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RespecifyTo(int elements) {
|
|
||||||
const std::vector<unsigned int> zeros(static_cast<std::size_t>(elements), 0u);
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
|
|
||||||
glBufferData(GL_SHADER_STORAGE_BUFFER,
|
|
||||||
static_cast<GLsizeiptr>(zeros.size() * sizeof(unsigned int)), zeros.data(),
|
|
||||||
GL_DYNAMIC_COPY);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<unsigned int> DispatchAndRead(int elements) {
|
|
||||||
glUseProgram(m_program);
|
|
||||||
glDispatchCompute(static_cast<GLuint>(elements), 1, 1);
|
|
||||||
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
|
||||||
std::vector<unsigned int> values(static_cast<std::size_t>(elements), 0xDEADBEEFu);
|
|
||||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_buffer);
|
|
||||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
|
|
||||||
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned int m_program = 0;
|
|
||||||
GLuint m_buffer = 0;
|
|
||||||
std::string m_buildLog;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_F(StorageBufferRegrowScenario, AGrownStoreIsVisibleThroughItsExistingIndexedBinding) {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
|
|
||||||
// Iteration one: 24 bytes, bound once, six groups.
|
|
||||||
RespecifyTo(kSmallElements);
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_buffer);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
const std::vector<unsigned int> small = DispatchAndRead(kSmallElements);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
for (int i = 0; i < kSmallElements; ++i) {
|
|
||||||
ASSERT_EQ(small[static_cast<std::size_t>(i)], static_cast<unsigned int>(i + 1))
|
|
||||||
<< "the 24-byte iteration itself did not write element " << i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iteration two: the SAME buffer grows to 96 bytes with NO new glBindBufferBase, which is
|
|
||||||
// what the application is entitled to do and what the shadow used to swallow.
|
|
||||||
RespecifyTo(kLargeElements);
|
|
||||||
ASSERT_EQ(FirstGLError(), 0u);
|
|
||||||
|
|
||||||
const std::vector<unsigned int> large = DispatchAndRead(kLargeElements);
|
|
||||||
EXPECT_EQ(FirstGLError(), 0u);
|
|
||||||
for (int i = 0; i < kLargeElements; ++i) {
|
|
||||||
EXPECT_EQ(large[static_cast<std::size_t>(i)], static_cast<unsigned int>(i + 1))
|
|
||||||
<< "element " << i << " (byte " << i * 4 << ") of the grown store came back as "
|
|
||||||
<< large[static_cast<std::size_t>(i)]
|
|
||||||
<< "; zero from element " << kSmallElements
|
|
||||||
<< " on means the shader still saw the pre-growth extent";
|
|
||||||
}
|
|
||||||
|
|
||||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
|
||||||
}
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TessellationDrawModeScenario.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
|
|
||||||
//
|
|
||||||
// Scenario - GL_PATCHES AND THE TESSELLATION PIPELINE ARE EACH OTHER'S ONLY PARTNER.
|
|
||||||
//
|
|
||||||
// GL 4.6 core 10.1 states the rule in both directions, and both are GL_INVALID_OPERATION:
|
|
||||||
// a program with a tessellation evaluation shader may only be drawn with GL_PATCHES, and
|
|
||||||
// GL_PATCHES may only be drawn with such a program. MobileGL's draw-mode validator
|
|
||||||
// implemented the geometry-shader input-primitive rule and NOTHING for tessellation, which
|
|
||||||
// is two of the four sites KHR-GL43.transform_feedback.api_errors_test checks (all four
|
|
||||||
// share one copy-pasted message string, so the trace cannot say which one it stopped at).
|
|
||||||
//
|
|
||||||
// Needs a real context: the validator returns before either rule when no backend object is
|
|
||||||
// active, so the GPU-free negative-API suite cannot reach them.
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "../Harness/HeadlessGL.h"
|
|
||||||
#include "../Harness/ScenarioFixture.h"
|
|
||||||
|
|
||||||
#ifdef GLAPI
|
|
||||||
#undef GLAPI
|
|
||||||
#endif
|
|
||||||
#define GL_GLEXT_PROTOTYPES
|
|
||||||
#include <GL/gl.h>
|
|
||||||
#include <GL/glcorearb.h>
|
|
||||||
#undef GL_GLEXT_PROTOTYPES
|
|
||||||
|
|
||||||
namespace MGITest {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
const char* const kVertexSource = R"(#version 420 core
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* const kTessControlSource = R"(#version 420 core
|
|
||||||
layout(vertices = 1) out;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
gl_TessLevelOuter[0] = 1.0;
|
|
||||||
gl_TessLevelOuter[1] = 1.0;
|
|
||||||
gl_TessLevelOuter[2] = 1.0;
|
|
||||||
gl_TessLevelInner[0] = 1.0;
|
|
||||||
gl_out[gl_InvocationID].gl_Position = gl_in[0].gl_Position;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* const kTessEvalSource = R"(#version 420 core
|
|
||||||
layout(triangles, equal_spacing, cw) in;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
gl_Position = gl_in[0].gl_Position;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* const kFragmentSource = R"(#version 420 core
|
|
||||||
out vec4 fragColor;
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
fragColor = vec4(0.0, 1.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
class TessellationDrawModeScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
glGenVertexArrays(1, &m_vao);
|
|
||||||
glBindVertexArray(m_vao);
|
|
||||||
if (!BackendHostsTessellation()) {
|
|
||||||
GTEST_SKIP() << "no tessellation stages on " << Gl().BackendName() << " ("
|
|
||||||
<< Gl().RendererString() << "); there is no patch draw to validate";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready()) return;
|
|
||||||
glUseProgram(0);
|
|
||||||
for (const GLuint program : m_programs) {
|
|
||||||
glDeleteProgram(program);
|
|
||||||
}
|
|
||||||
m_programs.clear();
|
|
||||||
glBindVertexArray(0);
|
|
||||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
||||||
m_vao = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The same real-backend probe IoBlockNameCollisionScenario uses: 0 on a DirectGLES
|
|
||||||
// driver without GL_EXT_tessellation_shader and on a DirectVulkan device without
|
|
||||||
// the tessellationShader feature.
|
|
||||||
static bool BackendHostsTessellation() {
|
|
||||||
GLint maxTessGenLevel = 0;
|
|
||||||
glGetIntegerv(GL_MAX_TESS_GEN_LEVEL, &maxTessGenLevel);
|
|
||||||
DrainErrors();
|
|
||||||
return maxTessGenLevel >= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void DrainErrors() {
|
|
||||||
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint BuildProgram(const std::vector<std::pair<GLenum, const char*>>& stages) {
|
|
||||||
std::vector<GLuint> shaders;
|
|
||||||
bool ok = true;
|
|
||||||
for (const auto& [stage, source] : stages) {
|
|
||||||
const GLuint shader = glCreateShader(stage);
|
|
||||||
glShaderSource(shader, 1, &source, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
GLint compiled = 0;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
||||||
shaders.push_back(shader);
|
|
||||||
if (!compiled) {
|
|
||||||
m_buildLog = InfoLog(shader, true);
|
|
||||||
ok = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!ok) {
|
|
||||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
for (const GLuint shader : shaders) glAttachShader(program, shader);
|
|
||||||
glLinkProgram(program);
|
|
||||||
GLint linked = 0;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
for (const GLuint shader : shaders) glDeleteShader(shader);
|
|
||||||
if (!linked) {
|
|
||||||
m_buildLog = InfoLog(program, false);
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
m_programs.push_back(program);
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string InfoLog(GLuint object, bool isShader) {
|
|
||||||
GLint length = 0;
|
|
||||||
if (isShader) {
|
|
||||||
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
|
||||||
} else {
|
|
||||||
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
|
||||||
}
|
|
||||||
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
|
||||||
if (isShader) {
|
|
||||||
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
|
||||||
} else {
|
|
||||||
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
|
||||||
}
|
|
||||||
return buffer.data();
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& BuildLog() const { return m_buildLog; }
|
|
||||||
|
|
||||||
GLuint m_vao = 0;
|
|
||||||
std::vector<GLuint> m_programs;
|
|
||||||
std::string m_buildLog;
|
|
||||||
};
|
|
||||||
|
|
||||||
// A tessellation program drawn with anything but GL_PATCHES.
|
|
||||||
TEST_F(TessellationDrawModeScenario, TessellationProgramRejectsNonPatchModes) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
|
|
||||||
const GLuint program = BuildProgram({{GL_VERTEX_SHADER, kVertexSource},
|
|
||||||
{GL_TESS_CONTROL_SHADER, kTessControlSource},
|
|
||||||
{GL_TESS_EVALUATION_SHADER, kTessEvalSource},
|
|
||||||
{GL_FRAGMENT_SHADER, kFragmentSource}});
|
|
||||||
ASSERT_NE(program, 0u) << "the tessellation program did not build: " << BuildLog();
|
|
||||||
|
|
||||||
glUseProgram(program);
|
|
||||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
for (const GLenum mode : {static_cast<GLenum>(GL_POINTS), static_cast<GLenum>(GL_LINES),
|
|
||||||
static_cast<GLenum>(GL_TRIANGLES)}) {
|
|
||||||
glDrawArrays(mode, 0, 1);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
|
||||||
<< "mode " << mode << " must not be accepted while tessellation is active";
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The one mode that IS accepted still is - a rule keyed any wider would break every
|
|
||||||
// patch draw in the suite.
|
|
||||||
glDrawArrays(GL_PATCHES, 0, 1);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... and the other direction: GL_PATCHES without a tessellation evaluation stage.
|
|
||||||
TEST_F(TessellationDrawModeScenario, PatchesRejectedWithoutATessellationEvaluationStage) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
|
|
||||||
const GLuint program =
|
|
||||||
BuildProgram({{GL_VERTEX_SHADER, kVertexSource}, {GL_FRAGMENT_SHADER, kFragmentSource}});
|
|
||||||
ASSERT_NE(program, 0u) << "the vertex/fragment program did not build: " << BuildLog();
|
|
||||||
|
|
||||||
glUseProgram(program);
|
|
||||||
glPatchParameteri(GL_PATCH_VERTICES, 1);
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
glDrawArrays(GL_PATCHES, 0, 1);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION))
|
|
||||||
<< "GL_PATCHES has no meaning without a tessellation evaluation stage";
|
|
||||||
DrainErrors();
|
|
||||||
|
|
||||||
// The same program with an ordinary mode is untouched.
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
|
|
||||||
DrainErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
} // namespace MGITest
|
|
||||||
@@ -673,86 +673,4 @@ void main() {
|
|||||||
glDeleteProgram(program);
|
glDeleteProgram(program);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A GL_DOUBLE array is NARROWED to float32 and fetched, not dropped. No backend here has a
|
|
||||||
// 64-bit vertex format, but glVertexAttribFormat(GL_DOUBLE) is defined as "doubles in memory,
|
|
||||||
// converted to float" and the shader input is a plain vec4 either way, so nothing about fp64
|
|
||||||
// is needed - only the fetch conversion (KHR-GL43.vertex_attrib_binding.basic-input-case4).
|
|
||||||
// Every value here is exact in float32, so the capture is an equality test.
|
|
||||||
TEST_F(VertexAttribBindingScenario, DoubleArrayIsFetchedAtFloat32Precision) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
ResetCurrentAttribs();
|
|
||||||
|
|
||||||
const double vertices[] = {100.0, 200.0, 300.0, 400.0};
|
|
||||||
GLuint vbo = 0;
|
|
||||||
glGenBuffers(1, &vbo);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
|
||||||
|
|
||||||
glBindVertexBuffer(0, vbo, 0, 2 * static_cast<GLsizei>(sizeof(double)));
|
|
||||||
glVertexAttribFormat(1, 2, GL_DOUBLE, GL_FALSE, 0);
|
|
||||||
glVertexAttribBinding(1, 0);
|
|
||||||
glEnableVertexAttribArray(1);
|
|
||||||
|
|
||||||
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
|
|
||||||
EXPECT_TRUE(Vec4Is(data, 0, 1, 100.0f, 200.0f, 0.0f, 1.0f));
|
|
||||||
EXPECT_TRUE(Vec4Is(data, 1, 1, 300.0f, 400.0f, 0.0f, 1.0f));
|
|
||||||
|
|
||||||
glDisableVertexAttribArray(1);
|
|
||||||
glDeleteBuffers(1, &vbo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GL ignores `normalized` for floating-point array types, GL_DOUBLE included: the fetched
|
|
||||||
// values are the raw ones, not scaled into [0,1]. A conversion that forwarded the flag would
|
|
||||||
// return zeros here (KHR-GL43.vertex_attrib_binding.basic-input-case5).
|
|
||||||
TEST_F(VertexAttribBindingScenario, NormalizedIsIgnoredForDoubleArrays) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
ResetCurrentAttribs();
|
|
||||||
|
|
||||||
const double vertices[] = {0.0, 10.0, 20.0, 0.0};
|
|
||||||
GLuint vbo = 0;
|
|
||||||
glGenBuffers(1, &vbo);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
|
||||||
|
|
||||||
glBindVertexBuffer(0, vbo, 0, 4 * static_cast<GLsizei>(sizeof(double)));
|
|
||||||
glVertexAttribFormat(2, 4, GL_DOUBLE, GL_TRUE, 0);
|
|
||||||
glVertexAttribBinding(2, 0);
|
|
||||||
glEnableVertexAttribArray(2);
|
|
||||||
|
|
||||||
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 1, 1);
|
|
||||||
EXPECT_TRUE(Vec4Is(data, 0, 2, 0.0f, 10.0f, 20.0f, 0.0f));
|
|
||||||
|
|
||||||
glDisableVertexAttribArray(2);
|
|
||||||
glDeleteBuffers(1, &vbo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The LONG form asks for more precision than any backend here can give and gets the same
|
|
||||||
// float32 stream. IsLong must not gate the narrowing off
|
|
||||||
// (KHR-GL43.vertex_attrib_binding.advanced-bindingUpdate feeds its dvec3 this way).
|
|
||||||
TEST_F(VertexAttribBindingScenario, LongDoubleArrayIsFetchedAtFloat32Precision) {
|
|
||||||
if (!Ready()) GTEST_SKIP();
|
|
||||||
ResetCurrentAttribs();
|
|
||||||
|
|
||||||
const double vertices[] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
|
|
||||||
GLuint vbo = 0;
|
|
||||||
glGenBuffers(1, &vbo);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
|
||||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
|
||||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
|
||||||
|
|
||||||
glBindVertexBuffer(0, vbo, 0, 3 * static_cast<GLsizei>(sizeof(double)));
|
|
||||||
glVertexAttribLFormat(3, 3, GL_DOUBLE, 0);
|
|
||||||
glVertexAttribBinding(3, 0);
|
|
||||||
glEnableVertexAttribArray(3);
|
|
||||||
|
|
||||||
const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
|
|
||||||
EXPECT_TRUE(Vec4Is(data, 0, 3, 1.0f, 2.0f, 3.0f, 1.0f));
|
|
||||||
EXPECT_TRUE(Vec4Is(data, 1, 3, 4.0f, 5.0f, 6.0f, 1.0f));
|
|
||||||
|
|
||||||
glDisableVertexAttribArray(3);
|
|
||||||
glDeleteBuffers(1, &vbo);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -30,23 +30,15 @@
|
|||||||
// applies the flip to viewport 0 and forgets the other fifteen renders a correct-looking FBO and
|
// applies the flip to viewport 0 and forgets the other fifteen renders a correct-looking FBO and
|
||||||
// an upside-down window - the classic multi-viewport bug, and invisible to every FBO-only case.
|
// an upside-down window - the classic multi-viewport bug, and invisible to every FBO-only case.
|
||||||
//
|
//
|
||||||
// BOTH BACKENDS RUN EVERY CASE, by two completely different routes, which is the point of
|
// HONEST LIMIT OF THIS FILE. DirectGLES SKIPS every case: GLES has one viewport, one scissor
|
||||||
// keeping them in one file. DirectVulkan declares sixteen viewports on the pipeline and lets the
|
// rectangle and no gl_ViewportIndex, so routing to index > 0 is an emulation feature that has
|
||||||
// hardware route. DirectGLES has one viewport, one scissor rectangle and one depth range and no
|
// not been built (the Espryt half of KHR-GL43.viewport_array's rendering group is deliberately
|
||||||
// gl_ViewportIndex at all, so it EMULATES: the builtin becomes a flat varying, the fragment stage
|
// still red). The skip is explicit rather than silent so a future emulation lands here as a
|
||||||
// gets a gate, and the draw is replayed once per distinct viewport state (Managers.h,
|
// failing test and not as a test that was quietly never running. DirectVulkan additionally
|
||||||
// ForEachViewportRoutingPass). Every assertion below is about pixels, so it cannot tell the two
|
// skips when the device lacks the multiViewport feature - Vulkan then forbids a pipeline from
|
||||||
// apart - which is exactly what has to be true.
|
// declaring more than one viewport at all, which is a device limit and not a MobileGL bug;
|
||||||
//
|
// lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do run where
|
||||||
// DirectVulkan skips when the device lacks the multiViewport feature - Vulkan then forbids a
|
// it matters.
|
||||||
// pipeline from declaring more than one viewport at all, which is a device limit and not a
|
|
||||||
// MobileGL bug; lavapipe (every CI lane) and both Mali/Adreno devices support it, so the cases do
|
|
||||||
// run where it matters.
|
|
||||||
//
|
|
||||||
// The last case is the negative control for the emulation and runs on DirectGLES only: it builds
|
|
||||||
// the SAME program with the emulation switched off and requires the routing to collapse onto
|
|
||||||
// viewport 0. Without it every assertion above could be satisfied by a backend that happened to
|
|
||||||
// be right for some other reason, and the emulation's own switch would be untested.
|
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -55,10 +47,6 @@
|
|||||||
#include "../Harness/HeadlessGL.h"
|
#include "../Harness/HeadlessGL.h"
|
||||||
#include "../Harness/ScenarioFixture.h"
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
// For the emulation switch the negative-control case below flips. Nothing else in this file needs
|
|
||||||
// to know which backend it is running on.
|
|
||||||
#include <Config.h>
|
|
||||||
|
|
||||||
#ifdef GLAPI
|
#ifdef GLAPI
|
||||||
#undef GLAPI
|
#undef GLAPI
|
||||||
#endif
|
#endif
|
||||||
@@ -154,6 +142,13 @@ void main() { fragColor = gl_FragCoord.z; }
|
|||||||
ScenarioTest::SetUp();
|
ScenarioTest::SetUp();
|
||||||
if (!Ready()) return;
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
if (Gl().BackendName() == "DirectGLES") {
|
||||||
|
GTEST_SKIP() << "gl_ViewportIndex routing is not emulated on DirectGLES: GLES has one viewport "
|
||||||
|
"and one scissor rectangle, so every index rasterizes as index 0. The indexed "
|
||||||
|
"STATE is still asserted (MG_Test RenderStateTest); this is the deferred "
|
||||||
|
"rendering half of KHR-GL43.viewport_array.";
|
||||||
|
}
|
||||||
|
|
||||||
GLint maxViewports = 0;
|
GLint maxViewports = 0;
|
||||||
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||||
ASSERT_GE(maxViewports, kViewportCount) << "GL 4.3 core requires GL_MAX_VIEWPORTS >= 16";
|
ASSERT_GE(maxViewports, kViewportCount) << "GL 4.3 core requires GL_MAX_VIEWPORTS >= 16";
|
||||||
@@ -433,15 +428,15 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
|||||||
std::vector<GLfloat> pixels(static_cast<size_t>(kWidth) * kHeight, 0.0f);
|
std::vector<GLfloat> pixels(static_cast<size_t>(kWidth) * kHeight, 0.0f);
|
||||||
glReadPixels(0, 0, kWidth, kHeight, GL_RED, GL_FLOAT, pixels.data());
|
glReadPixels(0, 0, kWidth, kHeight, GL_RED, GL_FLOAT, pixels.data());
|
||||||
for (int i = 0; i < kViewportCount; ++i) {
|
for (int i = 0; i < kViewportCount; ++i) {
|
||||||
const float nearDepth = static_cast<float>(i) / 16.0f;
|
const float near = static_cast<float>(i) / 16.0f;
|
||||||
const float farDepth = 1.0f - static_cast<float>(i) / 16.0f;
|
const float far = 1.0f - static_cast<float>(i) / 16.0f;
|
||||||
// The tolerance covers depth-buffer-free rasterization of gl_FragCoord.z on a
|
// The tolerance covers depth-buffer-free rasterization of gl_FragCoord.z on a
|
||||||
// software rasterizer; the per-index values are 1/16 apart, so it cannot let a
|
// software rasterizer; the per-index values are 1/16 apart, so it cannot let a
|
||||||
// neighbouring viewport's range through, and viewport 0's range (0, 1) differs
|
// neighbouring viewport's range through, and viewport 0's range (0, 1) differs
|
||||||
// from every other index by at least 1/16.
|
// from every other index by at least 1/16.
|
||||||
EXPECT_NEAR(pixels[i], nearDepth, 1.0e-3f)
|
EXPECT_NEAR(pixels[i], near, 1.0e-3f)
|
||||||
<< "viewport " << i << " near-plane depth; got viewport 0's range if this is 0";
|
<< "viewport " << i << " near-plane depth; got viewport 0's range if this is 0";
|
||||||
EXPECT_NEAR(pixels[static_cast<size_t>(kWidth) + i], farDepth, 1.0e-3f)
|
EXPECT_NEAR(pixels[static_cast<size_t>(kWidth) + i], far, 1.0e-3f)
|
||||||
<< "viewport " << i << " far-plane depth; got viewport 0's range if this is 1";
|
<< "viewport " << i << " far-plane depth; got viewport 0's range if this is 1";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,282 +520,5 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); }
|
|||||||
DestroyIntTarget(target);
|
DestroyIntTarget(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 4. the negative control for the DirectGLES emulation -----------------------------
|
|
||||||
//
|
|
||||||
// Everything above is a claim about pixels, and a claim about pixels cannot tell an
|
|
||||||
// emulation that works from a backend that was going to be right anyway. This case builds
|
|
||||||
// the SAME program with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION off and requires case 1's
|
|
||||||
// result to COLLAPSE: with no routing, every geometry invocation rasterizes against
|
|
||||||
// viewport 0's rectangle, so the last invocation paints the whole surface and every cell
|
|
||||||
// reads 15 instead of its own index. That is the pre-emulation behaviour this backend had
|
|
||||||
// (and the failure signature KHR-GL43.viewport_array reported on it), pinned here so that
|
|
||||||
// (a) the three cases above are known to be testing the emulation and not the weather,
|
|
||||||
// and (b) the switch itself has a test.
|
|
||||||
//
|
|
||||||
// DirectGLES only: the flag steers nothing on DirectVulkan, which routes natively.
|
|
||||||
TEST_F(ViewportArrayScenario, WithoutTheEmulationEveryIndexCollapsesOntoViewportZero) {
|
|
||||||
if (Gl().BackendName() != "DirectGLES") {
|
|
||||||
GTEST_SKIP() << "the emulation switch is a DirectGLES concern; DirectVulkan routes "
|
|
||||||
"gl_ViewportIndex natively and ignores it";
|
|
||||||
}
|
|
||||||
|
|
||||||
// The feature table is a process-global and this fixture shares its context with every
|
|
||||||
// other scenario in the process, so the restore is not optional.
|
|
||||||
struct ScopedEmulationOff {
|
|
||||||
ScopedEmulationOff(): saved(MobileGL::MG_Config::Features.ViewportArrayEmulation) {
|
|
||||||
MobileGL::MG_Config::Features.ViewportArrayEmulation =
|
|
||||||
MobileGL::MG_Config::QuirkOverride::ForceOff;
|
|
||||||
}
|
|
||||||
~ScopedEmulationOff() { MobileGL::MG_Config::Features.ViewportArrayEmulation = saved; }
|
|
||||||
MobileGL::MG_Config::QuirkOverride saved;
|
|
||||||
};
|
|
||||||
|
|
||||||
IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide);
|
|
||||||
SetupGridViewports(kCellSize, kCellSize);
|
|
||||||
|
|
||||||
GLuint unroutedProgram = 0;
|
|
||||||
{
|
|
||||||
const ScopedEmulationOff scopedEmulationOff;
|
|
||||||
// A FRESH program: the emitted ESSL is decided at link time and memoized on a key
|
|
||||||
// that carries this flag, so reusing m_program would just replay the routed build.
|
|
||||||
unroutedProgram = BuildProgram(kGridGeometrySource, kIntFragmentSource);
|
|
||||||
ASSERT_NE(unroutedProgram, 0u) << "unrouted program failed to build: " << m_buildLog;
|
|
||||||
glUseProgram(unroutedProgram);
|
|
||||||
glBindVertexArray(m_vao);
|
|
||||||
glDrawArrays(GL_POINTS, 0, 1);
|
|
||||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::vector<GLint> pixels = ReadInts(kSurfaceSide, kSurfaceSide);
|
|
||||||
// Cell (0, 0) IS viewport 0's rectangle, so it is the one cell an unrouted draw paints
|
|
||||||
// with something. Everything it holds comes from the last geometry invocation.
|
|
||||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, 0, 0), kViewportCount - 1)
|
|
||||||
<< "with the emulation off, viewport 0's rectangle must hold the LAST invocation's "
|
|
||||||
"index - if it holds 0 the routing is still happening and this control proves "
|
|
||||||
"nothing";
|
|
||||||
for (int y = 0; y < kGridSide; ++y) {
|
|
||||||
for (int x = 0; x < kGridSide; ++x) {
|
|
||||||
if (x == 0 && y == 0) continue;
|
|
||||||
EXPECT_EQ(CellCentre(pixels, kSurfaceSide, x, y), kUnwritten)
|
|
||||||
<< "cell (" << x << ", " << y << ") is outside viewport 0's rectangle and an "
|
|
||||||
<< "unrouted draw cannot reach it";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
glUseProgram(0);
|
|
||||||
glDeleteProgram(unroutedProgram);
|
|
||||||
DestroyIntTarget(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 5. an explicitly EMPTY scissor box clips, it does not mean "never written" --------
|
|
||||||
//
|
|
||||||
// Deliberately NOT a ViewportArrayScenario case, because that fixture's geometry stage
|
|
||||||
// routes and this claim needs none of it: one viewport, one scissor rectangle, no
|
|
||||||
// geometry stage - and it has to hold identically whether or not anything routes.
|
|
||||||
//
|
|
||||||
// glScissor(0, 0, 0, 0) is legal GL meaning "the scissor test rejects every fragment",
|
|
||||||
// but it is byte-identical to the all-zero rectangle a context starts with, whose meaning
|
|
||||||
// is the OPPOSITE ("the whole window", which the frontend cannot spell before a surface
|
|
||||||
// exists). DirectGLES resolved the collision from the EXTENT, so it substituted the whole
|
|
||||||
// surface for a deliberately empty box and inverted the request into "clip nothing" -
|
|
||||||
// and did so on every draw, at any origin, no matter how many times the application had
|
|
||||||
// already called glScissor. KHR-GL43.viewport_array.scissor_zero_dimension is the
|
|
||||||
// conformance shape of exactly this, and it is what the written-flag now separates.
|
|
||||||
|
|
||||||
const char* const kFullScreenVertexSource = R"(#version 330 core
|
|
||||||
void main() {
|
|
||||||
// One clip-space-covering triangle straight from gl_VertexID: no buffers, no attributes,
|
|
||||||
// and nothing that could clip the draw except the scissor rectangle under test.
|
|
||||||
const vec2 corners[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
|
||||||
gl_Position = vec4(corners[gl_VertexID], 0.0, 1.0);
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const char* const kConstantIntFragmentSource = R"(#version 330 core
|
|
||||||
layout(location = 0) out int fragColor;
|
|
||||||
void main() { fragColor = 7; }
|
|
||||||
)";
|
|
||||||
constexpr GLint kPainted = 7;
|
|
||||||
|
|
||||||
class EmptyScissorScenario : public ScenarioTest {
|
|
||||||
protected:
|
|
||||||
void SetUp() override {
|
|
||||||
ScenarioTest::SetUp();
|
|
||||||
if (!Ready()) return;
|
|
||||||
|
|
||||||
m_program = BuildQuadProgram();
|
|
||||||
ASSERT_NE(m_program, 0u) << "full-screen program failed to build: " << m_buildLog;
|
|
||||||
glGenVertexArrays(1, &m_vao);
|
|
||||||
glBindVertexArray(m_vao);
|
|
||||||
|
|
||||||
glGenTextures(1, &m_texture);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32I, kSurfaceSide, kSurfaceSide, 0, GL_RED_INTEGER, GL_INT,
|
|
||||||
nullptr);
|
|
||||||
glGenFramebuffers(1, &m_fbo);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
|
|
||||||
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GL_FRAMEBUFFER_COMPLETE)
|
|
||||||
<< "R32I is required to be colour-renderable; an incomplete target would make every "
|
|
||||||
"assertion below vacuous";
|
|
||||||
|
|
||||||
glViewport(0, 0, kSurfaceSide, kSurfaceSide);
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
|
||||||
ResetScissorState();
|
|
||||||
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "setup left a GL error behind";
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override {
|
|
||||||
if (!Ready() || IsSkipped()) return;
|
|
||||||
// The context is shared with every other scenario in the process, and a leftover
|
|
||||||
// 0x0 scissor box with the test enabled would silently blank whatever runs next.
|
|
||||||
ResetScissorState();
|
|
||||||
glScissor(0, 0, kSurfaceSide, kSurfaceSide);
|
|
||||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
||||||
if (m_program != 0) glDeleteProgram(m_program);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
|
|
||||||
if (m_texture != 0) glDeleteTextures(1, &m_texture);
|
|
||||||
while (glGetError() != GL_NO_ERROR) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ResetScissorState() {
|
|
||||||
for (int i = 0; i < kViewportCount; ++i) {
|
|
||||||
glDisablei(GL_SCISSOR_TEST, static_cast<GLuint>(i));
|
|
||||||
}
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uploaded, not cleared, for the reason FillIntTarget gives - and here for a second
|
|
||||||
// one that is decisive: glClear is ITSELF scissored, so a clear issued under the very
|
|
||||||
// state this case is testing would be clipped away and prove nothing.
|
|
||||||
void FillTarget() const {
|
|
||||||
const std::vector<GLint> unwritten(static_cast<size_t>(kSurfaceSide) * kSurfaceSide, kUnwritten);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, m_texture);
|
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSurfaceSide, kSurfaceSide, GL_RED_INTEGER, GL_INT,
|
|
||||||
unwritten.data());
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::vector<GLint> ReadTarget() {
|
|
||||||
std::vector<GLint> pixels(static_cast<size_t>(kSurfaceSide) * kSurfaceSide, 0);
|
|
||||||
glReadPixels(0, 0, kSurfaceSide, kSurfaceSide, GL_RED_INTEGER, GL_INT, pixels.data());
|
|
||||||
return pixels;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint BuildQuadProgram() {
|
|
||||||
const GLuint vs = CompileOne(GL_VERTEX_SHADER, kFullScreenVertexSource);
|
|
||||||
if (vs == 0) return 0;
|
|
||||||
const GLuint fs = CompileOne(GL_FRAGMENT_SHADER, kConstantIntFragmentSource);
|
|
||||||
if (fs == 0) {
|
|
||||||
glDeleteShader(vs);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const GLuint program = glCreateProgram();
|
|
||||||
glAttachShader(program, vs);
|
|
||||||
glAttachShader(program, fs);
|
|
||||||
glLinkProgram(program);
|
|
||||||
GLint linked = 0;
|
|
||||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
||||||
glDeleteShader(vs);
|
|
||||||
glDeleteShader(fs);
|
|
||||||
if (linked) return program;
|
|
||||||
GLint length = 0;
|
|
||||||
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
|
||||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
|
||||||
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
|
||||||
m_buildLog = log.data();
|
|
||||||
glDeleteProgram(program);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint CompileOne(GLenum stage, const char* source) {
|
|
||||||
const GLuint shader = glCreateShader(stage);
|
|
||||||
glShaderSource(shader, 1, &source, nullptr);
|
|
||||||
glCompileShader(shader);
|
|
||||||
GLint compiled = 0;
|
|
||||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
||||||
if (compiled) return shader;
|
|
||||||
GLint length = 0;
|
|
||||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
|
||||||
std::vector<char> log(static_cast<size_t>(length > 1 ? length : 1), '\0');
|
|
||||||
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
|
|
||||||
m_buildLog = log.data();
|
|
||||||
glDeleteShader(shader);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string m_buildLog;
|
|
||||||
GLuint m_program = 0;
|
|
||||||
GLuint m_vao = 0;
|
|
||||||
GLuint m_fbo = 0;
|
|
||||||
GLuint m_texture = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
TEST_F(EmptyScissorScenario, AnExplicitlyEmptyScissorBoxClipsEveryFragment) {
|
|
||||||
// Positive control FIRST. Without it a regression that simply lost the draw entirely
|
|
||||||
// would sail through the half below, which only asserts that nothing was painted.
|
|
||||||
FillTarget();
|
|
||||||
glEnable(GL_SCISSOR_TEST);
|
|
||||||
glScissor(0, 0, kSurfaceSide, kSurfaceSide);
|
|
||||||
glUseProgram(m_program);
|
|
||||||
glBindVertexArray(m_vao);
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
|
||||||
{
|
|
||||||
const std::vector<GLint> pixels = ReadTarget();
|
|
||||||
ASSERT_EQ(pixels.front(), kPainted) << "control: a full-surface scissor box must not clip";
|
|
||||||
ASSERT_EQ(pixels.back(), kPainted) << "control: a full-surface scissor box must not clip";
|
|
||||||
}
|
|
||||||
|
|
||||||
// The case itself, and note it runs AFTER an explicit glScissor - the old
|
|
||||||
// extent-based sentinel misfired here too, which is what made this a live rendering
|
|
||||||
// bug and not just a first-frame startup quirk.
|
|
||||||
FillTarget();
|
|
||||||
glScissor(0, 0, 0, 0);
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
|
||||||
{
|
|
||||||
const std::vector<GLint> pixels = ReadTarget();
|
|
||||||
for (size_t i = 0; i < pixels.size(); ++i) {
|
|
||||||
ASSERT_EQ(pixels[i], kUnwritten)
|
|
||||||
<< "texel " << i << " was painted through a 0x0 scissor box: the empty rectangle was "
|
|
||||||
"substituted with the whole surface, inverting 'clip everything' into 'clip nothing'";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(EmptyScissorScenario, IndexedZeroDimensionScissorBoxesClipEveryFragment) {
|
|
||||||
// The conformance shape: setup4x4Scissor(..., set_zeros=true) writes all 16 boxes
|
|
||||||
// through glScissorArrayv with zero extents at a 4x4 grid of origins and enables the
|
|
||||||
// test on every index. Index 0's box is (0, 0, 0, 0) - byte-identical to the
|
|
||||||
// never-written default - which is precisely the collision the written flag breaks.
|
|
||||||
// Backends that collapse every index to 0 (DirectGLES today) still pass: index 0's
|
|
||||||
// box is empty, so the draw is clipped away, which is what the case requires.
|
|
||||||
FillTarget();
|
|
||||||
std::vector<GLint> boxes(static_cast<size_t>(kViewportCount) * 4, 0);
|
|
||||||
for (int i = 0; i < kViewportCount; ++i) {
|
|
||||||
boxes[static_cast<size_t>(i) * 4 + 0] = (i % kGridSide) * kCellSize;
|
|
||||||
boxes[static_cast<size_t>(i) * 4 + 1] = (i / kGridSide) * kCellSize;
|
|
||||||
// width and height stay 0 - that IS the case.
|
|
||||||
}
|
|
||||||
glScissorArrayv(0, kViewportCount, boxes.data());
|
|
||||||
for (int i = 0; i < kViewportCount; ++i) {
|
|
||||||
glEnablei(GL_SCISSOR_TEST, static_cast<GLuint>(i));
|
|
||||||
}
|
|
||||||
glUseProgram(m_program);
|
|
||||||
glBindVertexArray(m_vao);
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
||||||
ASSERT_EQ(glGetError(), GL_NO_ERROR);
|
|
||||||
|
|
||||||
const std::vector<GLint> pixels = ReadTarget();
|
|
||||||
for (size_t i = 0; i < pixels.size(); ++i) {
|
|
||||||
ASSERT_EQ(pixels[i], kUnwritten) << "texel " << i << " was painted through a zero-extent indexed "
|
|
||||||
"scissor box";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // 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
|
|
||||||
@@ -163,7 +163,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
if (!m_resource.IsGpuResident() &&
|
if (!m_resource.IsGpuResident() &&
|
||||||
!(m_mappingAccess & BufferMappingAccessBit::FlushExplicit)) { // if we didn't flush explicitly
|
!(m_mappingAccess & BufferMappingAccessBit::FlushExplicit)) { // if we didn't flush explicitly
|
||||||
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
|
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
|
||||||
Memcpy(m_resource.Bytes() + m_mappedRange.start, m_stagingData.data() + m_stagingBias,
|
Memcpy(m_resource.Bytes() + m_mappedRange.start, m_stagingData.data(),
|
||||||
m_mappedRange.end - m_mappedRange.start);
|
m_mappedRange.end - m_mappedRange.start);
|
||||||
}
|
}
|
||||||
NotifyFlushMappedRange(m_mappedRange, m_mappingAccess);
|
NotifyFlushMappedRange(m_mappedRange, m_mappingAccess);
|
||||||
@@ -175,7 +175,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
m_isMapped = false;
|
m_isMapped = false;
|
||||||
m_mappingAccess = BufferMappingAccessBit::Null;
|
m_mappingAccess = BufferMappingAccessBit::Null;
|
||||||
m_mappedRange = {0, 0};
|
m_mappedRange = {0, 0};
|
||||||
m_stagingBias = 0;
|
|
||||||
m_ownsStagingData = false;
|
m_ownsStagingData = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +193,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// FLUSH_EXPLICIT maps are never GPU-resident (only coherent maps are adopted), so
|
// FLUSH_EXPLICIT maps are never GPU-resident (only coherent maps are adopted), so
|
||||||
// the staged bytes must be copied into the shadow before the backend reads them.
|
// the staged bytes must be copied into the shadow before the backend reads them.
|
||||||
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
|
if (!(m_mappingAccess & BufferMappingAccessBit::Persistent)) {
|
||||||
Memcpy(m_resource.Bytes() + start, m_stagingData.data() + m_stagingBias + offset, length);
|
Memcpy(m_resource.Bytes() + start, m_stagingData.data() + offset, length);
|
||||||
}
|
}
|
||||||
NotifyFlushMappedRange({start, end}, m_mappingAccess);
|
NotifyFlushMappedRange({start, end}, m_mappingAccess);
|
||||||
}
|
}
|
||||||
@@ -312,9 +311,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
m_mappedRange = {0, m_size};
|
m_mappedRange = {0, m_size};
|
||||||
|
|
||||||
if (m_mappingAccess & BufferMappingAccessBit::Write) {
|
if (m_mappingAccess & BufferMappingAccessBit::Write) {
|
||||||
// glMapBuffer maps from offset 0, so no bias: the allocation's own
|
|
||||||
// GL_MIN_MAP_BUFFER_ALIGNMENT-aligned base is what the application must get.
|
|
||||||
m_stagingBias = 0;
|
|
||||||
m_stagingData.resize(m_size);
|
m_stagingData.resize(m_size);
|
||||||
m_ownsStagingData = true;
|
m_ownsStagingData = true;
|
||||||
|
|
||||||
@@ -376,21 +372,14 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (access & BufferMappingAccessBit::Write) {
|
if (access & BufferMappingAccessBit::Write) {
|
||||||
// ARB_map_buffer_alignment constrains (returned pointer - offset), not the pointer:
|
m_stagingData.resize(range.end - range.start);
|
||||||
// a map at offset 63 must hand back a pointer 63 bytes past the alignment grid, which
|
|
||||||
// is exactly what the read path below gets for free from shadowBase + offset. The
|
|
||||||
// staging store has to be biased by the same phase to match, so it over-allocates by
|
|
||||||
// it and the mapped bytes start at data() + m_stagingBias.
|
|
||||||
m_stagingBias = range.start % MIN_MAP_BUFFER_ALIGNMENT;
|
|
||||||
const SizeT mappedLength = range.end - range.start;
|
|
||||||
m_stagingData.resize(m_stagingBias + mappedLength);
|
|
||||||
m_ownsStagingData = true;
|
m_ownsStagingData = true;
|
||||||
|
|
||||||
if (!(access & (BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer))) {
|
if (!(access & (BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer))) {
|
||||||
Memcpy(m_stagingData.data() + m_stagingBias, m_resource.Bytes() + range.start, mappedLength);
|
Memcpy(m_stagingData.data(), m_resource.Bytes() + range.start, m_stagingData.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
return m_stagingData.data() + m_stagingBias;
|
return m_stagingData.data();
|
||||||
} else {
|
} else {
|
||||||
m_ownsStagingData = false;
|
m_ownsStagingData = false;
|
||||||
return m_resource.Bytes() + range.start;
|
return m_resource.Bytes() + range.start;
|
||||||
@@ -449,7 +438,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return const_cast<Uint8*>(m_resource.Bytes()) + m_mappedRange.start;
|
return const_cast<Uint8*>(m_resource.Bytes()) + m_mappedRange.start;
|
||||||
}
|
}
|
||||||
if (m_ownsStagingData) {
|
if (m_ownsStagingData) {
|
||||||
return const_cast<Uint8*>(m_stagingData.data()) + m_stagingBias;
|
return const_cast<Uint8*>(m_stagingData.data());
|
||||||
}
|
}
|
||||||
return const_cast<Uint8*>(m_resource.Bytes()) + m_mappedRange.start;
|
return const_cast<Uint8*>(m_resource.Bytes()) + m_mappedRange.start;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,14 +239,7 @@ namespace MobileGL {
|
|||||||
// Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed.
|
// Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed.
|
||||||
Bool m_gpuWritePending = false;
|
Bool m_gpuWritePending = false;
|
||||||
Range1D m_mappedRange;
|
Range1D m_mappedRange;
|
||||||
// The write-map staging store. MapAlignedData because the application is handed a
|
Vector<Uint8> m_stagingData;
|
||||||
// pointer into it, and biased by m_stagingBias because ARB_map_buffer_alignment
|
|
||||||
// requires (returned pointer - offset) to be aligned, not the pointer itself: a range
|
|
||||||
// map at offset 63 must hand back a pointer sitting 63 bytes past the alignment grid.
|
|
||||||
// The bias is the offset's phase, so the mapped bytes still start at
|
|
||||||
// m_stagingData.data() + m_stagingBias and the allocation is that much longer.
|
|
||||||
MapAlignedData m_stagingData;
|
|
||||||
SizeT m_stagingBias = 0;
|
|
||||||
Bool m_ownsStagingData;
|
Bool m_ownsStagingData;
|
||||||
};
|
};
|
||||||
} // namespace MG_State::GLState
|
} // namespace MG_State::GLState
|
||||||
|
|||||||
@@ -10,56 +10,8 @@
|
|||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
#include <MG_Util/Types.h>
|
#include <MG_Util/Types.h>
|
||||||
#include <bit>
|
#include <bit>
|
||||||
#include <new>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
// GL_MIN_MAP_BUFFER_ALIGNMENT. GL 4.2 / ARB_map_buffer_alignment fix the minimum at 64 and
|
|
||||||
// MobileGL advertises exactly that (MG_Impl/GLImpl/Getter/GL_Getter.cpp reads this constant),
|
|
||||||
// so under-reporting is not available - the implementation has to be brought up to the number
|
|
||||||
// instead. The promise is about POINTERS, not just the query: glMapBuffer must return a
|
|
||||||
// 64-byte-aligned pointer, and glMapBufferRange must return one whose base - the returned
|
|
||||||
// pointer minus the offset the caller asked for - is. Every pointer the frontend hands out
|
|
||||||
// comes from the shadow below or from BufferObject's staging buffer, and std::vector only
|
|
||||||
// promises alignof(std::max_align_t) (16 on aarch64), so both allocations carry the alignment
|
|
||||||
// themselves. One constant for the getter and the allocator, because the two may never
|
|
||||||
// disagree - the same reason the atomic-counter limits are shared through
|
|
||||||
// MG_Util/ShaderTranspiler/Types.h.
|
|
||||||
inline constexpr SizeT MIN_MAP_BUFFER_ALIGNMENT = 64;
|
|
||||||
|
|
||||||
// Allocator that gives every allocation MIN_MAP_BUFFER_ALIGNMENT. Deliberately minimal: the
|
|
||||||
// vectors it backs hold raw bytes and are only ever sized, so allocate/deallocate plus the
|
|
||||||
// rebinding and equality boilerplate std::vector requires is the whole interface.
|
|
||||||
template <typename T>
|
|
||||||
struct MapAlignedAllocator {
|
|
||||||
using value_type = T;
|
|
||||||
|
|
||||||
MapAlignedAllocator() noexcept = default;
|
|
||||||
template <typename U>
|
|
||||||
MapAlignedAllocator(const MapAlignedAllocator<U>&) noexcept {}
|
|
||||||
|
|
||||||
T* allocate(SizeT count) {
|
|
||||||
if (count == 0) return nullptr;
|
|
||||||
return static_cast<T*>(
|
|
||||||
::operator new(count * sizeof(T), std::align_val_t{MIN_MAP_BUFFER_ALIGNMENT}));
|
|
||||||
}
|
|
||||||
void deallocate(T* pointer, SizeT) noexcept {
|
|
||||||
::operator delete(pointer, std::align_val_t{MIN_MAP_BUFFER_ALIGNMENT});
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename U>
|
|
||||||
Bool operator==(const MapAlignedAllocator<U>&) const noexcept {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
template <typename U>
|
|
||||||
Bool operator!=(const MapAlignedAllocator<U>&) const noexcept {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Byte store for anything the application may end up holding a mapped pointer into.
|
|
||||||
using MapAlignedData = std::vector<Uint8, MapAlignedAllocator<Uint8>>;
|
|
||||||
|
|
||||||
// Opaque, refcounted handle to the backend's GPU storage for one buffer
|
// Opaque, refcounted handle to the backend's GPU storage for one buffer
|
||||||
// (the driver-side resource). The active backend derives from it and attaches
|
// (the driver-side resource). The active backend derives from it and attaches
|
||||||
// its own payload (VkBufferResource / GLESBufferResource). Held by PipeResource.
|
// its own payload (VkBufferResource / GLESBufferResource). Held by PipeResource.
|
||||||
@@ -105,8 +57,8 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
// Direct shadow access, used only by the backend's upload-from-shadow path,
|
// Direct shadow access, used only by the backend's upload-from-shadow path,
|
||||||
// which never runs for a GPU-resident (persistent) buffer.
|
// which never runs for a GPU-resident (persistent) buffer.
|
||||||
MapAlignedData& Shadow() { return *m_shadow; }
|
Data& Shadow() { return *m_shadow; }
|
||||||
const MapAlignedData& Shadow() const { return *m_shadow; }
|
const Data& Shadow() const { return *m_shadow; }
|
||||||
|
|
||||||
// Transition to persistent GPU residency: adopt the backend's coherent
|
// Transition to persistent GPU residency: adopt the backend's coherent
|
||||||
// mapped base as the source of truth and drop the CPU shadow. The caller
|
// mapped base as the source of truth and drop the CPU shadow. The caller
|
||||||
@@ -133,10 +85,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
SharedPtr<BackendBufferResource> ReleaseBackend() { return std::move(m_backend); }
|
SharedPtr<BackendBufferResource> ReleaseBackend() { return std::move(m_backend); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// MapAlignedData, not Data: a read-only glMapBuffer hands the application this very
|
SharedPtr<Data> m_shadow = MakeShared<Data>();
|
||||||
// pointer, and a range map hands it base + offset, so the base has to be on the
|
|
||||||
// GL_MIN_MAP_BUFFER_ALIGNMENT grid for either to satisfy ARB_map_buffer_alignment.
|
|
||||||
SharedPtr<MapAlignedData> m_shadow = MakeShared<MapAlignedData>();
|
|
||||||
void* m_gpuMapped = nullptr;
|
void* m_gpuMapped = nullptr;
|
||||||
SharedPtr<BackendBufferResource> m_backend;
|
SharedPtr<BackendBufferResource> m_backend;
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user