mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 21:28:32 +09:00
Compare commits
181
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3025284a6e | ||
|
|
d8d7530011 | ||
|
|
0f394fa46f | ||
|
|
107669b3db | ||
|
|
3e0460e472 | ||
|
|
33ff177bb2 | ||
|
|
2e6fc1ffc0 | ||
|
|
c6299f754f | ||
|
|
dcf918b9ee | ||
|
|
d98f72447d | ||
|
|
f15cb8900f | ||
|
|
bd0def6133 | ||
|
|
6f8b7fbc40 | ||
|
|
e5fb57f7eb | ||
|
|
c93e5fa409 | ||
|
|
8191075133 | ||
|
|
d6caed7822 | ||
|
|
9152e88734 | ||
|
|
2406e2d219 | ||
|
|
b228f813c0 | ||
|
|
0d0527192a | ||
|
|
81bcbd6c14 | ||
|
|
867fe3e0ef | ||
|
|
0ec487c993 | ||
|
|
23b880c8be | ||
|
|
ebc5bff9b1 | ||
|
|
231d5c90e4 | ||
|
|
d5f5e6405b | ||
|
|
ac3a83b207 | ||
|
|
335f2decbd | ||
|
|
fb1ad96c04 | ||
|
|
313b75a7c0 | ||
|
|
d7976326fa | ||
|
|
72ee7c439c | ||
|
|
cdea275227 | ||
|
|
6a02c5fea0 | ||
|
|
7db5b35a3e | ||
|
|
990e518e33 | ||
|
|
25a8f51db5 | ||
|
|
8f2b766b56 | ||
|
|
b9d8ad0421 | ||
|
|
f8069c0624 | ||
|
|
d0aae85da2 | ||
|
|
b904658b10 | ||
|
|
4b3fd11462 | ||
|
|
9be5d95440 | ||
|
|
d49d79a64b | ||
|
|
f5761ea1f3 | ||
|
|
b3f774d2c0 | ||
|
|
d524330032 | ||
|
|
f2d210b12d | ||
|
|
fd40960f70 | ||
|
|
49aab57f03 | ||
|
|
62dea3bea4 | ||
|
|
57aeeec053 | ||
|
|
9c0144d24a | ||
|
|
1e45958e01 | ||
|
|
6e6f5268fb | ||
|
|
08f98ad9ce | ||
|
|
d39a706d57 | ||
|
|
f3d52faad4 | ||
|
|
ba81ee114e | ||
|
|
c8c7b19579 | ||
|
|
0e7692251d | ||
|
|
34f09291da | ||
|
|
3b65e646e1 | ||
|
|
25b9370815 | ||
|
|
4ce808b9f2 | ||
|
|
5545d31c37 | ||
|
|
588ddba722 | ||
|
|
62301b1061 | ||
|
|
f3a846d336 | ||
|
|
9cdc82fbdd | ||
|
|
4a9d20c49f | ||
|
|
394d1ce748 | ||
|
|
300b458132 | ||
|
|
1f1a331a44 | ||
|
|
817091641c | ||
|
|
e64c7c7e65 | ||
|
|
dd60ff39ce | ||
|
|
96ad7ca0cc | ||
|
|
e80a23eae6 | ||
|
|
088f263495 | ||
|
|
3b3b6e5b8b | ||
|
|
9eda2147b1 | ||
|
|
a63699cde6 | ||
|
|
765aaec6dc | ||
|
|
bcd669bd25 | ||
|
|
f3405d1d53 | ||
|
|
81604d5596 | ||
|
|
31ea6aa5a3 | ||
|
|
7d6f6603c1 | ||
|
|
39c17c0b1b | ||
|
|
88138b48ec | ||
|
|
c114ce750b | ||
|
|
1ca2d3c0fe | ||
|
|
58c17f85a5 | ||
|
|
4873da6844 | ||
|
|
efeb24ff9b | ||
|
|
18c1a4d586 | ||
|
|
66ac3486e1 | ||
|
|
764a44f589 | ||
|
|
d96acb7972 | ||
|
|
bd710078fc | ||
|
|
95a7b17d45 | ||
|
|
19932f9e49 | ||
|
|
1011d9fea1 | ||
|
|
b5565ae503 | ||
|
|
b06ad3f877 | ||
|
|
3311e6034a | ||
|
|
027c1bd4ab | ||
|
|
da52cc3906 | ||
|
|
ebe4fe133f | ||
|
|
534ec65dda | ||
|
|
35ad1ae7fc | ||
|
|
6152ee933f | ||
|
|
dac02ca044 | ||
|
|
42fd02d82f | ||
|
|
6359b0002b | ||
|
|
c186f5f255 | ||
|
|
3d97f6fa8f | ||
|
|
bb582203d9 | ||
|
|
f39e6eb82d | ||
|
|
cb2ba71feb | ||
|
|
6ea7ccdf64 | ||
|
|
14605723f0 | ||
|
|
a680611c9f | ||
|
|
93224ca406 | ||
|
|
fbed4485b7 | ||
|
|
90ae0f048c | ||
|
|
cff959b2e8 | ||
|
|
a50b2c422b | ||
|
|
9dcda82d71 | ||
|
|
28d0af6f04 | ||
|
|
44ee6b66b3 | ||
|
|
28c3cfc1d6 | ||
|
|
38e04eefae | ||
|
|
fd29cb914e | ||
|
|
38497174c8 | ||
|
|
b95fcb7bca | ||
|
|
5fce287de5 | ||
|
|
ae6949e459 | ||
|
|
5437947240 | ||
|
|
f38dbf018d | ||
|
|
2dcc15bb0e | ||
|
|
ff76af9df7 | ||
|
|
76f37a18e6 | ||
|
|
8d1a734c22 | ||
|
|
7d215028fb | ||
|
|
00534d8bbc | ||
|
|
d81a6a0998 | ||
|
|
9bf23d7ffd | ||
|
|
41e45f7d48 | ||
|
|
3dc6a1b6db | ||
|
|
598c5497b0 | ||
|
|
86c00bdf18 | ||
|
|
2f2f95498f | ||
|
|
7105c2ebdc | ||
|
|
13bab780f2 | ||
|
|
027310f993 | ||
|
|
c741a938bc | ||
|
|
01d0f01d13 | ||
|
|
e45f7ae5d4 | ||
|
|
a687873d32 | ||
|
|
43a43c1180 | ||
|
|
65dbfa6f26 | ||
|
|
6de38c666c | ||
|
|
9dff24f3e1 | ||
|
|
5a400e0297 | ||
|
|
b6a2bf08d4 | ||
|
|
6b2a2b5e00 | ||
|
|
512c857f18 | ||
|
|
9f3cac6691 | ||
|
|
4c7332d5e6 | ||
|
|
1c5f6c0986 | ||
|
|
8b75628dec | ||
|
|
8269a1786f | ||
|
|
3019c68945 | ||
|
|
800142c104 | ||
|
|
e9382f5329 | ||
|
|
df7d1edeca |
@@ -201,6 +201,11 @@ fetch_file_from_mirror() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Files no mirror could serve, even after retrying every mirror. Only these fall
|
||||||
|
# back to Git LFS, so a mirror that served the rest of the case still spares
|
||||||
|
# GitHub the bandwidth for those files.
|
||||||
|
mirror_failures=()
|
||||||
|
|
||||||
fetch_from_mirror() {
|
fetch_from_mirror() {
|
||||||
mkdir -p "${fixture_dir}"
|
mkdir -p "${fixture_dir}"
|
||||||
for file in "${files[@]}"; do
|
for file in "${files[@]}"; do
|
||||||
@@ -219,17 +224,19 @@ fetch_from_mirror() {
|
|||||||
echo "Mirror did not serve ${name}; trying the next mirror" >&2
|
echo "Mirror did not serve ${name}; trying the next mirror" >&2
|
||||||
done
|
done
|
||||||
if [ "${fetched}" -ne 1 ]; then
|
if [ "${fetched}" -ne 1 ]; then
|
||||||
return 1
|
mirror_failures+=("${file}")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
[ "${#mirror_failures[@]}" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
if fetch_from_mirror; then
|
if fetch_from_mirror; then
|
||||||
echo "Fetched trace fixture files for ${case_name} from mirror: ${include}"
|
echo "Fetched trace fixture files for ${case_name} from mirror: ${include}"
|
||||||
else
|
else
|
||||||
echo "All mirrors failed for ${case_name}; falling back to Git LFS: ${include}"
|
fallback_include="$(IFS=,; echo "${mirror_failures[*]}")"
|
||||||
|
echo "All mirrors failed for ${#mirror_failures[@]} of ${#files[@]} file(s) of ${case_name}; falling back to Git LFS: ${fallback_include}"
|
||||||
git lfs install --local
|
git lfs install --local
|
||||||
git lfs pull --include="${include}" --exclude=""
|
git lfs pull --include="${fallback_include}" --exclude=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for file in "${files[@]}"; do
|
for file in "${files[@]}"; do
|
||||||
|
|||||||
@@ -177,9 +177,13 @@ jobs:
|
|||||||
uses: lukka/get-cmake@v4.3.3
|
uses: lukka/get-cmake@v4.3.3
|
||||||
|
|
||||||
- name: Install runtime dependencies
|
- name: Install runtime dependencies
|
||||||
|
# libegl-mesa0 is the EGL vendor library itself: DriverBench brings up a
|
||||||
|
# real GL context, and libegl1 is only glvnd's dispatch. It normally
|
||||||
|
# arrives as a Recommends of libegl1, which is too quiet a dependency for
|
||||||
|
# the one job that needs a working driver.
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libvulkan1 libegl1 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
|
sudo apt-get install -y libvulkan1 libegl1 libegl-mesa0 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
|
||||||
|
|
||||||
- name: Download Linux runtime
|
- name: Download Linux runtime
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
|
|||||||
@@ -31,3 +31,9 @@
|
|||||||
[submodule "3rdparty/apitrace"]
|
[submodule "3rdparty/apitrace"]
|
||||||
path = 3rdparty/apitrace
|
path = 3rdparty/apitrace
|
||||||
url = https://github.com/MobileGL-Dev/apitrace.git
|
url = https://github.com/MobileGL-Dev/apitrace.git
|
||||||
|
[submodule "3rdparty/asio"]
|
||||||
|
path = 3rdparty/asio
|
||||||
|
url = https://github.com/chriskohlhoff/asio.git
|
||||||
|
[submodule "3rdparty/libfork"]
|
||||||
|
path = 3rdparty/libfork
|
||||||
|
url = https://github.com/ConorWilliams/libfork.git
|
||||||
|
|||||||
+1
Submodule 3rdparty/asio added at 8806a6803c
+1
Submodule 3rdparty/libfork added at 9b2b844a5f
+51
-1
@@ -4,6 +4,11 @@ project("MobileGL")
|
|||||||
|
|
||||||
option(MOBILEGL_BUILD_TEST "Build MobileGL tests" ON )
|
option(MOBILEGL_BUILD_TEST "Build MobileGL tests" ON )
|
||||||
option(MOBILEGL_BUILD_BENCHMARK "Build MobileGL benchmarks" ON )
|
option(MOBILEGL_BUILD_BENCHMARK "Build MobileGL benchmarks" ON )
|
||||||
|
# Headless end-to-end GPU scenarios (MobileGL/MG_IntegrationTest). They need a
|
||||||
|
# real GPU/ICD to do anything, so they are off by default for CI; every scenario
|
||||||
|
# skips cleanly where there is none. Registered under the `integration-gpu`
|
||||||
|
# ctest label so a run can select or exclude them.
|
||||||
|
option(MOBILEGL_BUILD_INTEGRATION_TEST "Build MobileGL headless GPU integration tests" OFF)
|
||||||
option(MOBILEGL_FORCE_RELEASE_OPT "Enable Release optimization flags in Debug build" ON )
|
option(MOBILEGL_FORCE_RELEASE_OPT "Enable Release optimization flags in Debug build" ON )
|
||||||
option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling" OFF)
|
option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling" OFF)
|
||||||
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
|
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
|
||||||
@@ -17,7 +22,9 @@ if (ANDROID)
|
|||||||
set(MOBILEGL_BUILD_BENCHMARK OFF CACHE BOOL "Build MobileGL benchmarks" FORCE)
|
set(MOBILEGL_BUILD_BENCHMARK OFF CACHE BOOL "Build MobileGL benchmarks" FORCE)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT)
|
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF)
|
||||||
|
|
||||||
|
if ((NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) AND MOBILEGL_ENABLE_LTO)
|
||||||
# Check if ThinLTO or LTO is suppported
|
# Check if ThinLTO or LTO is suppported
|
||||||
include(CheckIPOSupported)
|
include(CheckIPOSupported)
|
||||||
include(CheckCCompilerFlag)
|
include(CheckCCompilerFlag)
|
||||||
@@ -147,6 +154,9 @@ set(SOURCE_FILES
|
|||||||
|
|
||||||
MobileGL/MG_Util/Debug/Log.cpp
|
MobileGL/MG_Util/Debug/Log.cpp
|
||||||
|
|
||||||
|
MobileGL/MG_Util/Async/JobNode.cpp
|
||||||
|
MobileGL/MG_Util/Async/ShaderCompilePool.cpp
|
||||||
|
|
||||||
MobileGL/MG_Util/Math/VectorTypes.cpp
|
MobileGL/MG_Util/Math/VectorTypes.cpp
|
||||||
MobileGL/MG_Util/Metrics/TextureMetrics.cpp
|
MobileGL/MG_Util/Metrics/TextureMetrics.cpp
|
||||||
|
|
||||||
@@ -180,6 +190,7 @@ set(SOURCE_FILES
|
|||||||
|
|
||||||
MobileGL/MG_Util/Classifiers/TextureEnumClassifier.cpp
|
MobileGL/MG_Util/Classifiers/TextureEnumClassifier.cpp
|
||||||
|
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp
|
||||||
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
|
||||||
@@ -187,10 +198,13 @@ set(SOURCE_FILES
|
|||||||
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
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
|
||||||
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/LowerDrawParametersPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.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
|
||||||
@@ -204,6 +218,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
||||||
|
|
||||||
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
|
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
|
||||||
|
MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp
|
||||||
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
|
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
|
||||||
|
|
||||||
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
|
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
|
||||||
@@ -217,6 +232,8 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp
|
MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp
|
||||||
MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp
|
MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp
|
||||||
MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp
|
MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp
|
||||||
|
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
|
||||||
|
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
|
||||||
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
|
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
|
||||||
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
|
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
|
||||||
MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp
|
MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp
|
||||||
@@ -239,6 +256,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
|
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
|
||||||
MobileGL/MG_Backend/DirectGLES/Utils.cpp
|
MobileGL/MG_Backend/DirectGLES/Utils.cpp
|
||||||
MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
MobileGL/MG_Backend/DirectGLES/Managers.cpp
|
||||||
|
MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
|
||||||
|
|
||||||
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
|
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
|
||||||
MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
|
MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
|
||||||
@@ -277,7 +295,11 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp
|
MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp
|
||||||
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/ShaderCompileTask.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
||||||
|
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
|
||||||
|
MobileGL/MG_State/GLState/ProgramState/ShaderCompileAdoptionMap.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp
|
||||||
MobileGL/MG_State/GLState/RenderState/RenderState.cpp
|
MobileGL/MG_State/GLState/RenderState/RenderState.cpp
|
||||||
MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp
|
MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp
|
||||||
@@ -300,6 +322,7 @@ endif()
|
|||||||
if (ANDROID)
|
if (ANDROID)
|
||||||
list(APPEND SOURCE_FILES
|
list(APPEND SOURCE_FILES
|
||||||
MobileGL/MG_Util/SelfTest/DriverPostJni.cpp
|
MobileGL/MG_Util/SelfTest/DriverPostJni.cpp
|
||||||
|
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -310,6 +333,11 @@ if (WIN32)
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The shader-compile pool runs standalone Asio on real threads. This host's glibc (>= 2.34)
|
||||||
|
# merged pthread into libc, so it links without asking, but the NDK and musl are not
|
||||||
|
# guaranteed to be as forgiving - ask for it explicitly rather than rely on the accident.
|
||||||
|
find_package(Threads REQUIRED)
|
||||||
|
|
||||||
set(MOBILEGL_LINK_LIBRARIES
|
set(MOBILEGL_LINK_LIBRARIES
|
||||||
glslang::glslang
|
glslang::glslang
|
||||||
spirv-cross-c
|
spirv-cross-c
|
||||||
@@ -319,12 +347,17 @@ set(MOBILEGL_LINK_LIBRARIES
|
|||||||
GPUOpen::VulkanMemoryAllocator
|
GPUOpen::VulkanMemoryAllocator
|
||||||
Vulkan::UtilityHeaders
|
Vulkan::UtilityHeaders
|
||||||
spirv-reflect-static
|
spirv-reflect-static
|
||||||
|
Threads::Threads
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
-DVMA_VULKAN_VERSION=1001000
|
-DVMA_VULKAN_VERSION=1001000
|
||||||
|
# Header-only Asio, no Boost, no deprecated interfaces. Set on the definition list
|
||||||
|
# rather than per-target so the shared library and the _s static target agree.
|
||||||
|
-DASIO_STANDALONE
|
||||||
|
-DASIO_NO_DEPRECATED
|
||||||
)
|
)
|
||||||
|
|
||||||
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
|
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
|
||||||
@@ -336,6 +369,17 @@ set(MOBILEGL_INCLUDE_DIR
|
|||||||
${spirv-tools_SOURCE_DIR}/include
|
${spirv-tools_SOURCE_DIR}/include
|
||||||
${spirv-tools_BINARY_DIR}
|
${spirv-tools_BINARY_DIR}
|
||||||
${SPIRV-Headers_SOURCE_DIR}/include
|
${SPIRV-Headers_SOURCE_DIR}/include
|
||||||
|
# Header-only submodule: no add_subdirectory, no link target. Only
|
||||||
|
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
|
||||||
|
# pimpl so no consumer target needs this path.
|
||||||
|
${CMAKE_SOURCE_DIR}/3rdparty/asio/asio/include
|
||||||
|
# The second shader-compile execution engine (MOBILEGL_ASYNC_POOL=libfork), on the
|
||||||
|
# same terms as Asio above: header-only, no add_subdirectory (its CMakeLists only
|
||||||
|
# declares an INTERFACE target plus install/test scaffolding we do not want), no link
|
||||||
|
# target, and reachable from exactly one translation unit. libfork's own
|
||||||
|
# target_compile_features asks for cxx_std_23, which this project already sets
|
||||||
|
# globally, so its C++20 coroutines need no per-source standard override.
|
||||||
|
${CMAKE_SOURCE_DIR}/3rdparty/libfork/include
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||||
@@ -533,6 +577,12 @@ if (NOT ANDROID)
|
|||||||
add_subdirectory(MobileGL/MG_Test)
|
add_subdirectory(MobileGL/MG_Test)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# After MG_Test so googletest is already available when the unit tests are
|
||||||
|
# built; the module fetches its own copy when they are not.
|
||||||
|
if (MOBILEGL_BUILD_INTEGRATION_TEST)
|
||||||
|
add_subdirectory(MobileGL/MG_IntegrationTest)
|
||||||
|
endif()
|
||||||
|
|
||||||
if (MOBILEGL_BUILD_BENCHMARK)
|
if (MOBILEGL_BUILD_BENCHMARK)
|
||||||
add_subdirectory(MobileGL/MG_Benchmark)
|
add_subdirectory(MobileGL/MG_Benchmark)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -29,6 +29,33 @@ namespace MobileGL::MG_Config {
|
|||||||
ForceOff,
|
ForceOff,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Preferred DirectVulkan dispatch tier for the glMultiDraw* families. A preference,
|
||||||
|
// never a demand: the renderer clamps it to what the device supports at device
|
||||||
|
// creation, falling down the chain ext -> indirect -> unroll with one log line.
|
||||||
|
enum class MultiDrawMode : Uint8 {
|
||||||
|
Auto = 0, // unset: best supported tier
|
||||||
|
Ext, // VK_EXT_multi_draw: one vkCmdDrawMultiEXT / vkCmdDrawMultiIndexedEXT
|
||||||
|
Indirect, // multiDrawIndirect feature: one vkCmdDraw*Indirect over a transient command array
|
||||||
|
Unroll, // one vkCmdDraw* per sub-draw
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preferred DirectGLES emulation tier for glMultiDrawElements(BaseVertex). GLES has no
|
||||||
|
// such entry point in core, so every tier below is an emulation; they differ only in
|
||||||
|
// which driver capability they lean on and how many driver calls a batch costs. Like
|
||||||
|
// the Magma knob this is a preference, clamped at resolution time to what the ES
|
||||||
|
// driver actually supports, with one log line when it falls back.
|
||||||
|
enum class GLESMultiDrawMode : Uint8 {
|
||||||
|
Auto = 0, // unset: best supported tier
|
||||||
|
Ext, // one glMultiDrawElementsBaseVertexEXT
|
||||||
|
MultiIndirect, // one glMultiDrawElementsIndirectEXT over a scratch command buffer
|
||||||
|
Indirect, // one glDrawElementsIndirect per sub-draw over that same buffer
|
||||||
|
BaseVertex, // one glDrawElementsBaseVertex per sub-draw
|
||||||
|
DrawElements, // baseVertex folded into a scratch index buffer on the CPU, then plain
|
||||||
|
// glDrawElements per sub-draw (for drivers with no base-vertex draw at all)
|
||||||
|
Compute, // a compute shader flattens every sub-draw into one rebased index buffer,
|
||||||
|
// drawn by a single glDrawElements
|
||||||
|
};
|
||||||
|
|
||||||
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
|
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
|
||||||
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
|
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
|
||||||
// one truthy rule: the variable is set, non-empty, not "0", and not "false"
|
// one truthy rule: the variable is set, non-empty, not "0", and not "false"
|
||||||
@@ -39,6 +66,12 @@ namespace MobileGL::MG_Config {
|
|||||||
// - DISPLAY: X11 session variable, not MobileGL configuration.
|
// - DISPLAY: X11 session variable, not MobileGL configuration.
|
||||||
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
|
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
|
||||||
// (see MG_Util/Debug/Log.cpp).
|
// (see MG_Util/Debug/Log.cpp).
|
||||||
|
// - MOBILEGL_ASYNC_POOL: a ShaderCompilePool is constructed by binaries that never call
|
||||||
|
// MobileGL::Initialize() and so never run MG_ConfigLoader::Init - MG_Test's
|
||||||
|
// JobNodeTest builds pools directly, and it is the suite that runs the whole async
|
||||||
|
// matrix against both execution engines. Mirroring it here would resolve to the
|
||||||
|
// default in exactly the tests that exist to tell the engines apart (see
|
||||||
|
// MG_Util/Async/ShaderCompilePool.cpp, DetectAsyncPoolEngine).
|
||||||
struct FeaturesTable {
|
struct FeaturesTable {
|
||||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||||
Bool DisableTimerQuery = false;
|
Bool DisableTimerQuery = false;
|
||||||
@@ -91,6 +124,24 @@ namespace MobileGL::MG_Config {
|
|||||||
// feature off. It is enabled by default to match GL's defined out-of-range fetch
|
// feature off. It is enabled by default to match GL's defined out-of-range fetch
|
||||||
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
|
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
|
||||||
Bool DisableRobustBufferAccess = false;
|
Bool DisableRobustBufferAccess = false;
|
||||||
|
// MOBILEGL_MAGMA_MULTIDRAW_MODE: preferred DirectVulkan multi-draw dispatch tier
|
||||||
|
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
|
||||||
|
// unset picks the best supported tier.
|
||||||
|
MultiDrawMode MagmaMultiDrawMode = MultiDrawMode::Auto;
|
||||||
|
// MOBILEGL_ESPRYT_MULTIDRAW_MODE: preferred DirectGLES glMultiDrawElements emulation
|
||||||
|
// tier ("ext" | "multiindirect" | "indirect" | "basevertex" | "drawelements" |
|
||||||
|
// "compute", see GLESMultiDrawMode). Clamped to driver support; unset picks the best
|
||||||
|
// supported tier, which never includes "compute" - see the note on its resolution.
|
||||||
|
GLESMultiDrawMode EsprytMultiDrawMode = GLESMultiDrawMode::Auto;
|
||||||
|
// MOBILEGL_ASYNC_SHADER_COMPILE: overrides asynchronous shader compilation. Unset
|
||||||
|
// keeps the built-in default (MG_Util::Async::kAsyncShaderCompileDefault); falsy
|
||||||
|
// forces every glCompileShader/glLinkProgram to run synchronously on the calling
|
||||||
|
// thread AND withdraws GL_KHR_parallel_shader_compile, so the single switch reverts
|
||||||
|
// both the threading and the application-visible behaviour change.
|
||||||
|
QuirkOverride AsyncShaderCompile = QuirkOverride::Auto;
|
||||||
|
// MOBILEGL_ASYNC_SHADER_COMPILE_THREADS: shader-compile worker count. 0 (unset) means
|
||||||
|
// auto, which is min(4, big cores); an explicit value is honoured as given.
|
||||||
|
Uint32 AsyncShaderCompileThreads = 0;
|
||||||
};
|
};
|
||||||
extern FeaturesTable Features;
|
extern FeaturesTable Features;
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|||||||
@@ -97,6 +97,47 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
: MG_Config::QuirkOverride::ForceOff;
|
: MG_Config::QuirkOverride::ForceOff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multi-draw mode is a named-value preference: unset keeps Auto (best supported tier),
|
||||||
|
// a recognized name selects that tier as the ceiling, anything else warns and keeps Auto.
|
||||||
|
inline MG_Config::MultiDrawMode QueryEnvMultiDrawMode(const String& key) {
|
||||||
|
auto it = acceptedEnvVariablesMap->find(key);
|
||||||
|
if (it == acceptedEnvVariablesMap->end()) {
|
||||||
|
return MG_Config::MultiDrawMode::Auto;
|
||||||
|
}
|
||||||
|
String lowered = it->second;
|
||||||
|
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
if (lowered == "ext") return MG_Config::MultiDrawMode::Ext;
|
||||||
|
if (lowered == "indirect") return MG_Config::MultiDrawMode::Indirect;
|
||||||
|
if (lowered == "unroll") return MG_Config::MultiDrawMode::Unroll;
|
||||||
|
if (lowered.empty() || lowered == "auto") return MG_Config::MultiDrawMode::Auto;
|
||||||
|
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected ext|indirect|unroll|auto, using auto",
|
||||||
|
key.c_str(), it->second.c_str());
|
||||||
|
return MG_Config::MultiDrawMode::Auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same contract as QueryEnvMultiDrawMode, over the DirectGLES tier names.
|
||||||
|
inline MG_Config::GLESMultiDrawMode QueryEnvGLESMultiDrawMode(const String& key) {
|
||||||
|
auto it = acceptedEnvVariablesMap->find(key);
|
||||||
|
if (it == acceptedEnvVariablesMap->end()) {
|
||||||
|
return MG_Config::GLESMultiDrawMode::Auto;
|
||||||
|
}
|
||||||
|
String lowered = it->second;
|
||||||
|
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
if (lowered == "ext") return MG_Config::GLESMultiDrawMode::Ext;
|
||||||
|
if (lowered == "multiindirect") return MG_Config::GLESMultiDrawMode::MultiIndirect;
|
||||||
|
if (lowered == "indirect") return MG_Config::GLESMultiDrawMode::Indirect;
|
||||||
|
if (lowered == "basevertex") return MG_Config::GLESMultiDrawMode::BaseVertex;
|
||||||
|
if (lowered == "drawelements") return MG_Config::GLESMultiDrawMode::DrawElements;
|
||||||
|
if (lowered == "compute") return MG_Config::GLESMultiDrawMode::Compute;
|
||||||
|
if (lowered.empty() || lowered == "auto") return MG_Config::GLESMultiDrawMode::Auto;
|
||||||
|
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected "
|
||||||
|
"ext|multiindirect|indirect|basevertex|drawelements|compute|auto, using auto",
|
||||||
|
key.c_str(), it->second.c_str());
|
||||||
|
return MG_Config::GLESMultiDrawMode::Auto;
|
||||||
|
}
|
||||||
|
|
||||||
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
|
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
|
||||||
auto it = acceptedEnvVariablesMap->find(key);
|
auto it = acceptedEnvVariablesMap->find(key);
|
||||||
if (it == acceptedEnvVariablesMap->end()) {
|
if (it == acceptedEnvVariablesMap->end()) {
|
||||||
@@ -138,6 +179,10 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||||
|
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
|
||||||
|
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||||
|
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||||
|
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void InitBackendType() {
|
inline void InitBackendType() {
|
||||||
|
|||||||
+31
-1
@@ -15,6 +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_Util/Async/ShaderCompilePool.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -37,7 +39,12 @@ namespace MobileGL {
|
|||||||
if (logLifecycle) {
|
if (logLifecycle) {
|
||||||
MGLOG_I("MobileGL closing...");
|
MGLOG_I("MobileGL closing...");
|
||||||
}
|
}
|
||||||
glslang::FinalizeProcess();
|
// First, before anything else is torn down. In-flight compile/link jobs own
|
||||||
|
// their own inputs and are safe against everything below EXCEPT glslang's
|
||||||
|
// process globals and the TShader/TProgram objects hanging off pGLContext,
|
||||||
|
// both of which this function is about to destroy. This is the one
|
||||||
|
// cancellation path in the whole design that waits.
|
||||||
|
MG_Util::Async::ShaderCompilePool::Get().StopAndDrain();
|
||||||
// GL syncs die with their contexts, and every context is gone by the
|
// GL syncs die with their contexts, and every context is gone by the
|
||||||
// time full teardown runs: drain the live-sync registry while the
|
// time full teardown runs: drain the live-sync registry while the
|
||||||
// backend function table can still release the backend handles (and
|
// backend function table can still release the backend handles (and
|
||||||
@@ -49,6 +56,16 @@ namespace MobileGL {
|
|||||||
MG_State::pEGLContext.reset();
|
MG_State::pEGLContext.reset();
|
||||||
MG_Impl::GLImpl::TextureImpl::pProxyTextureManager.reset();
|
MG_Impl::GLImpl::TextureImpl::pProxyTextureManager.reset();
|
||||||
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo.reset();
|
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo.reset();
|
||||||
|
// Must run AFTER pGLContext.reset(). FinalizeProcess -> ShFinalize deletes
|
||||||
|
// glslang's process-wide pool allocator and every cached built-in symbol table,
|
||||||
|
// while the TShader/TProgram objects owned by the shader and program objects
|
||||||
|
// still reference levels adopted from those tables. Finalizing first left live
|
||||||
|
// glslang objects pointing at freed memory for the rest of the teardown.
|
||||||
|
glslang::FinalizeProcess();
|
||||||
|
// Immediately after, and never apart from it: FinalizeProcess just deleted the
|
||||||
|
// built-in symbol tables the prewarm latch stands for, so leaving it set would
|
||||||
|
// make the next Initialize() skip a prewarm it genuinely needs.
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch();
|
||||||
MG_Backend::gBackendFunctionsTable = {};
|
MG_Backend::gBackendFunctionsTable = {};
|
||||||
g_isInitialized = false;
|
g_isInitialized = false;
|
||||||
if (logLifecycle) {
|
if (logLifecycle) {
|
||||||
@@ -76,6 +93,19 @@ namespace MobileGL {
|
|||||||
MG_Impl::Init();
|
MG_Impl::Init();
|
||||||
MGLOG_D("MG_Impl initialized");
|
MGLOG_D("MG_Impl initialized");
|
||||||
glslang::InitializeProcess();
|
glslang::InitializeProcess();
|
||||||
|
// On the GL thread, before any worker can exist. glslang builds its built-in symbol
|
||||||
|
// tables lazily under a process-wide lock held for the whole build, so without this
|
||||||
|
// the first concurrent compiles of a shaderpack all serialize behind the very first
|
||||||
|
// parse and asynchronous compilation looks like it is doing nothing.
|
||||||
|
//
|
||||||
|
// Gated on the flag, because the problem it solves only exists when there are
|
||||||
|
// workers: with compilation synchronous, nothing ever contends for that lock and the
|
||||||
|
// three throwaway parses buy nothing - they just add to every eglInitialize. Read the
|
||||||
|
// flag here rather than inside PrewarmBuiltins so ShaderCompiler keeps no dependency
|
||||||
|
// on the async subsystem (ProgramUtilTest compiles that file without it).
|
||||||
|
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||||
|
MG_Util::ShaderTranspiler::ShaderCompiler::PrewarmBuiltins();
|
||||||
|
}
|
||||||
MGLOG_D("glslang initialized");
|
MGLOG_D("glslang initialized");
|
||||||
g_isInitialized = true;
|
g_isInitialized = true;
|
||||||
MGLOG_I("MobileGL initialized");
|
MGLOG_I("MobileGL initialized");
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ namespace MobileGL {
|
|||||||
GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||||
void (*ClearNamedFramebufferfi)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
void (*ClearNamedFramebufferfi)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||||
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||||
|
void (*ClearNamedFramebufferiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||||
|
GLenum buffer, GLint drawbuffer, const GLint* value);
|
||||||
|
void (*ClearNamedFramebufferuiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||||
|
GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||||
void (*BlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
|
void (*BlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
|
||||||
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
|
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
|
||||||
void (*BlitNamedFramebuffer)(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
void (*BlitNamedFramebuffer)(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
||||||
@@ -177,15 +181,18 @@ namespace MobileGL {
|
|||||||
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
|
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
|
||||||
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
|
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
|
||||||
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
|
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
|
||||||
void (*GetProgramInterfaceiv)(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
|
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT
|
||||||
GLuint (*GetProgramResourceIndex)(GLuint program, GLenum programInterface, const GLchar* name);
|
// a backend query: it describes the program the application wrote, in the
|
||||||
void (*GetProgramResourceName)(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
|
// application's namespace, which neither backend program is in. It is answered
|
||||||
GLsizei* length, GLchar* name);
|
// entirely by MG_Impl/GLImpl/Program/ProgramInterface from the frontend reflection.
|
||||||
void (*GetProgramResourceiv)(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
// Takes the block's GL NAME, not glShaderStorageBlockBinding's index. The index
|
||||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
// the application passes is the frontend interface-query enumeration's, and no
|
||||||
GLint (*GetProgramResourceLocation)(GLuint program, GLenum programInterface, const GLchar* name);
|
// backend shares that index space: DirectVulkan enumerates SPIR-V descriptor
|
||||||
GLint (*GetProgramResourceLocationIndex)(GLuint program, GLenum programInterface, const GLchar* name);
|
// bindings and DirectGLES asks a real driver about SPIRV-Cross-generated ESSL.
|
||||||
void (*ShaderStorageBlockBinding)(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
// The name is the one coordinate all three agree on, so the frontend resolves the
|
||||||
|
// index against its own enumeration and each backend maps the name to its own.
|
||||||
|
void (*ShaderStorageBlockBinding)(GLuint program, const GLchar* storageBlockName,
|
||||||
|
GLuint storageBlockBinding);
|
||||||
// GL fence sync objects. All entries are optional (may be null); the
|
// GL fence sync objects. All entries are optional (may be null); the
|
||||||
// frontend then falls back to always-signaled sync semantics.
|
// frontend then falls back to always-signaled sync semantics.
|
||||||
// FenceSync may itself return null when the backend cannot create a
|
// FenceSync may itself return null when the backend cannot create a
|
||||||
@@ -229,6 +236,22 @@ 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);
|
||||||
|
// Transform feedback capture spans, for backends whose own GL/ES driver
|
||||||
|
// performs the capture (DirectGLES). Both optional; null means the backend
|
||||||
|
// drives capture from its draw recording instead (DirectVulkan). End is
|
||||||
|
// called while the frontend capture state is still active, so the backend
|
||||||
|
// can still see the capture program and buffer bindings.
|
||||||
|
// GL_PATCH_VERTICES; ES 3.2 spells it the same way.
|
||||||
|
void (*PatchParameteri)(GLenum pname, GLint value);
|
||||||
|
void (*BeginTransformFeedback)(GLenum primitiveMode);
|
||||||
|
void (*EndTransformFeedback)();
|
||||||
|
// ARB_transform_feedback2. A backend that leaves these null keeps the single
|
||||||
|
// implicit capture span the frontend has always modelled; the frontend state
|
||||||
|
// (paused flag, per-object bindings) is tracked either way.
|
||||||
|
void (*PauseTransformFeedback)();
|
||||||
|
void (*ResumeTransformFeedback)();
|
||||||
|
void (*BindTransformFeedback)(GLuint name);
|
||||||
|
void (*DeleteTransformFeedback)(GLuint name);
|
||||||
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
|
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
|
||||||
};
|
};
|
||||||
struct GlobalBackendFunctionsTable {
|
struct GlobalBackendFunctionsTable {
|
||||||
@@ -281,6 +304,13 @@ namespace MobileGL {
|
|||||||
Int MaxIntegerSamples = 1;
|
Int MaxIntegerSamples = 1;
|
||||||
Int MaxSamples = 1;
|
Int MaxSamples = 1;
|
||||||
Int MaxSampleMaskWords = 1;
|
Int MaxSampleMaskWords = 1;
|
||||||
|
// Tessellation limits; defaults are the GL 4.0 core minimums.
|
||||||
|
Int MaxPatchVertices = 32;
|
||||||
|
Int MaxTessGenLevel = 64;
|
||||||
|
// GL_MIN/MAX_PROGRAM_TEXTURE_GATHER_OFFSET. Defaults are the GL 4.0 core
|
||||||
|
// minimums, which every ES 3.1 driver also guarantees.
|
||||||
|
Int MinProgramTextureGatherOffset = -8;
|
||||||
|
Int MaxProgramTextureGatherOffset = 7;
|
||||||
Int MaxTextureImageUnits = 32;
|
Int MaxTextureImageUnits = 32;
|
||||||
Int MaxVertexTextureImageUnits = 32;
|
Int MaxVertexTextureImageUnits = 32;
|
||||||
Int MaxComputeTextureImageUnits = 32;
|
Int MaxComputeTextureImageUnits = 32;
|
||||||
@@ -292,6 +322,8 @@ namespace MobileGL {
|
|||||||
Int MaxComputeWorkGroupInvocations = 128;
|
Int MaxComputeWorkGroupInvocations = 128;
|
||||||
Int MaxShaderStorageBufferBindings = 8;
|
Int MaxShaderStorageBufferBindings = 8;
|
||||||
Int MaxTextureBufferSize = 65536;
|
Int MaxTextureBufferSize = 65536;
|
||||||
|
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
|
||||||
|
Int TextureBufferOffsetAlignment = 1;
|
||||||
Int MaxUniformBufferBindings = 24;
|
Int MaxUniformBufferBindings = 24;
|
||||||
Int MaxUniformBlockSize = 16384;
|
Int MaxUniformBlockSize = 16384;
|
||||||
Int MaxImageUnits = 8;
|
Int MaxImageUnits = 8;
|
||||||
@@ -317,6 +349,47 @@ namespace MobileGL {
|
|||||||
Float MaxFragmentInterpolationOffset = 0.4375f;
|
Float MaxFragmentInterpolationOffset = 0.4375f;
|
||||||
Int FragmentInterpolationOffsetBits = 4;
|
Int FragmentInterpolationOffsetBits = 4;
|
||||||
Bool SupportsWideLines = false;
|
Bool SupportsWideLines = false;
|
||||||
|
// Whether a framebuffer whose depth and stencil attachments are distinct
|
||||||
|
// images can be rendered to. GL only requires support when both refer to the
|
||||||
|
// same image and lets an implementation answer GL_FRAMEBUFFER_UNSUPPORTED
|
||||||
|
// otherwise, which is what DirectVulkan (one combined attachment) and the
|
||||||
|
// real ES drivers behind DirectGLES both do. Defaults to true so a backend
|
||||||
|
// that never sets it keeps the permissive behaviour.
|
||||||
|
Bool SupportsDistinctDepthStencilAttachments = true;
|
||||||
|
// Whether attaching a single layer of a 3D or array texture to a framebuffer actually
|
||||||
|
// renders to that layer. DirectGLES hands the layer straight to
|
||||||
|
// glFramebufferTextureLayer, so it does; DirectVulkan maps a GL layer onto a Vulkan
|
||||||
|
// array layer with no notion of a 3D depth slice, so it does not yet. Defaults to false
|
||||||
|
// so a backend that never sets it gets the conservative answer.
|
||||||
|
// Which layered texture targets this backend can attach ONE layer of to a framebuffer
|
||||||
|
// and then really clear, render and read back that layer. Bit (1u << TextureTarget) is
|
||||||
|
// set for each supported target. Deliberately per target rather than one flag: the three
|
||||||
|
// ways a GL layer maps onto Vulkan are independent capabilities. A 2D or 2D multisample
|
||||||
|
// array layer IS a VkImage array layer and needs nothing extra; a 3D texture's layer is
|
||||||
|
// a z slice, which needs a 2D-array-compatible image and a per-slice clear that
|
||||||
|
// vkCmdClearColorImage cannot express; a cube map array needs an image shape and the
|
||||||
|
// imageCubeArray feature before it can be attached at any layer at all. Defaults to 0 so
|
||||||
|
// a backend that never sets it gets the conservative answer.
|
||||||
|
Uint32 PerLayerFramebufferAttachmentTargets = 0;
|
||||||
|
|
||||||
|
static constexpr Uint32 PerLayerFramebufferAttachmentBit(TextureTarget target) {
|
||||||
|
return (static_cast<Int>(target) >= 0 &&
|
||||||
|
static_cast<Int>(target) < static_cast<Int>(TextureTarget::TextureTargetCount))
|
||||||
|
? (1u << static_cast<Uint32>(target))
|
||||||
|
: 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool SupportsPerLayerFramebufferAttachment(TextureTarget target) const {
|
||||||
|
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
|
||||||
|
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
|
||||||
|
}
|
||||||
|
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
|
||||||
|
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
|
||||||
|
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (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
|
||||||
|
// all. Defaults to false so a backend that never sets it gets the conservative answer.
|
||||||
|
Bool SupportsFloat64VertexAttributes = false;
|
||||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||||
Uint32 SubgroupSize = 0;
|
Uint32 SubgroupSize = 0;
|
||||||
Uint32 SubgroupSupportedStages = 0;
|
Uint32 SubgroupSupportedStages = 0;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||||
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -32,8 +33,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
void ClearGLErrors(const MG_External::GLESFunctionsTable& gl) {
|
void ClearGLErrors(const MG_External::GLESFunctionsTable& gl) {
|
||||||
if (!gl.glGetError) return;
|
if (!gl.glGetError) return;
|
||||||
while (gl.glGetError() != GL_NO_ERROR) {
|
while (gl.glGetError() != GL_NO_ERROR) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool CheckNoGLError(const MG_External::GLESFunctionsTable& gl) {
|
Bool CheckNoGLError(const MG_External::GLESFunctionsTable& gl) {
|
||||||
@@ -77,8 +77,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bool IsGLESProbeMultisampleTarget(TextureTarget target) {
|
Bool IsGLESProbeMultisampleTarget(TextureTarget target) {
|
||||||
return target == TextureTarget::Texture2DMultisample ||
|
return target == TextureTarget::Texture2DMultisample || target == TextureTarget::Texture2DMultisampleArray;
|
||||||
target == TextureTarget::Texture2DMultisampleArray;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GLenum GetFramebufferAttachment(TextureInternalFormat format) {
|
GLenum GetFramebufferAttachment(TextureInternalFormat format) {
|
||||||
@@ -115,8 +114,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
GLenum normalizedInternalFormat = glFormat;
|
GLenum normalizedInternalFormat = glFormat;
|
||||||
GLenum imageFormat = GL_RGBA;
|
GLenum imageFormat = GL_RGBA;
|
||||||
GLenum imageType = GL_UNSIGNED_BYTE;
|
GLenum imageType = GL_UNSIGNED_BYTE;
|
||||||
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
|
MG_Util::TextureFormatProcessor::NormalizePixelFormat(glFormat, PixelFormatNormalizeOptionBit::None,
|
||||||
glFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat, &imageFormat, &imageType);
|
&normalizedInternalFormat, &imageFormat, &imageType);
|
||||||
return imageFormat != GL_RED_INTEGER && imageFormat != GL_RG_INTEGER && imageFormat != GL_RGB_INTEGER &&
|
return imageFormat != GL_RED_INTEGER && imageFormat != GL_RG_INTEGER && imageFormat != GL_RGB_INTEGER &&
|
||||||
imageFormat != GL_RGBA_INTEGER && !MG_Util::IsDepthFormatInternalFormat(format) &&
|
imageFormat != GL_RGBA_INTEGER && !MG_Util::IsDepthFormatInternalFormat(format) &&
|
||||||
!MG_Util::IsStencilFormatInternalFormat(format);
|
!MG_Util::IsStencilFormatInternalFormat(format);
|
||||||
@@ -154,9 +153,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
GLESProbeFormatInfo BuildNativeProbeFormatInfo(GLenum requestedInternalFormat) {
|
GLESProbeFormatInfo BuildNativeProbeFormatInfo(GLenum requestedInternalFormat) {
|
||||||
GLESProbeFormatInfo info;
|
GLESProbeFormatInfo info;
|
||||||
info.InternalFormat = requestedInternalFormat;
|
info.InternalFormat = requestedInternalFormat;
|
||||||
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
|
MG_Util::TextureFormatProcessor::NormalizePixelFormat(requestedInternalFormat,
|
||||||
requestedInternalFormat, PixelFormatNormalizeOptionBit::None, nullptr, &info.ImageFormat,
|
PixelFormatNormalizeOptionBit::None, nullptr,
|
||||||
&info.ImageType);
|
&info.ImageFormat, &info.ImageType);
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +209,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) {
|
if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) {
|
||||||
reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES");
|
reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES");
|
||||||
}
|
}
|
||||||
|
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
|
||||||
|
reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
|
||||||
|
}
|
||||||
|
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
|
||||||
|
reasons.push_back("EXT_render_snorm not supported");
|
||||||
|
}
|
||||||
|
|
||||||
String reason;
|
String reason;
|
||||||
for (SizeT i = 0; i < reasons.size(); ++i) {
|
for (SizeT i = 0; i < reasons.size(); ++i) {
|
||||||
@@ -227,20 +232,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return MG_Util::ConvertGLEnumToString(internalFormat);
|
return MG_Util::ConvertGLEnumToString(internalFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LogGLESFormatCaveat(TextureInternalFormat logicalFormat,
|
void LogGLESFormatCaveat(TextureInternalFormat logicalFormat, SizeT targetIndex,
|
||||||
SizeT targetIndex,
|
|
||||||
const GLESProbeFormatInfo& fallbackInfo) {
|
const GLESProbeFormatInfo& fallbackInfo) {
|
||||||
MGLOG_D("Caveat: %s %s not fully supported. Reason: %s. Fallback: %s",
|
MGLOG_D("Caveat: %s %s not fully supported. Reason: %s. Fallback: %s",
|
||||||
GetFormatCapabilityTargetName(targetIndex).c_str(),
|
GetFormatCapabilityTargetName(targetIndex).c_str(),
|
||||||
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
|
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(), fallbackInfo.Reason.c_str(),
|
||||||
fallbackInfo.Reason.c_str(),
|
|
||||||
ConvertFallbackInternalFormatToString(fallbackInfo.InternalFormat).c_str());
|
ConvertFallbackInternalFormatToString(fallbackInfo.InternalFormat).c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool BuildFallbackProbeFormatInfo(GLenum requestedInternalFormat,
|
Bool BuildFallbackProbeFormatInfo(GLenum requestedInternalFormat, Flags<PixelFormatNormalizeOptionBit> options,
|
||||||
Flags<PixelFormatNormalizeOptionBit> options,
|
Bool forced, GLESProbeFormatInfo& outInfo) {
|
||||||
Bool forced,
|
|
||||||
GLESProbeFormatInfo& outInfo) {
|
|
||||||
const Flags<PixelFormatNormalizeOptionBit> applicableOptions =
|
const Flags<PixelFormatNormalizeOptionBit> applicableOptions =
|
||||||
MG_Util::TextureFormatProcessor::GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat,
|
MG_Util::TextureFormatProcessor::GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat,
|
||||||
options);
|
options);
|
||||||
@@ -255,8 +256,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return outInfo.InternalFormat != GL_UNKNOWN_MGL;
|
return outInfo.InternalFormat != GL_UNKNOWN_MGL;
|
||||||
}
|
}
|
||||||
|
|
||||||
FormatCapabilityFlags BuildTextureCapsFromProbe(TextureInternalFormat logicalFormat,
|
FormatCapabilityFlags BuildTextureCapsFromProbe(TextureInternalFormat logicalFormat, TextureTarget target,
|
||||||
TextureTarget target,
|
|
||||||
Bool renderable) {
|
Bool renderable) {
|
||||||
FormatCapabilityFlags caps = GetTextureFeatureCaps(logicalFormat, target);
|
FormatCapabilityFlags caps = GetTextureFeatureCaps(logicalFormat, target);
|
||||||
if (renderable) {
|
if (renderable) {
|
||||||
@@ -270,16 +270,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return caps;
|
return caps;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AddFullFormatCaps(FormatCapabilityCache& cache,
|
void AddFullFormatCaps(FormatCapabilityCache& cache, SizeT targetIndex, SizeT formatIndex,
|
||||||
SizeT targetIndex,
|
|
||||||
SizeT formatIndex,
|
|
||||||
FormatCapabilityFlags caps) {
|
FormatCapabilityFlags caps) {
|
||||||
cache.FullCaps[targetIndex][formatIndex] |= caps;
|
cache.FullCaps[targetIndex][formatIndex] |= caps;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool AddCaveatFormatCaps(FormatCapabilityCache& cache,
|
Bool AddCaveatFormatCaps(FormatCapabilityCache& cache, SizeT targetIndex, SizeT formatIndex,
|
||||||
SizeT targetIndex,
|
|
||||||
SizeT formatIndex,
|
|
||||||
FormatCapabilityFlags caps) {
|
FormatCapabilityFlags caps) {
|
||||||
Bool added = false;
|
Bool added = false;
|
||||||
for (FormatCapability capability : kReportedFormatCapabilities) {
|
for (FormatCapability capability : kReportedFormatCapabilities) {
|
||||||
@@ -293,8 +289,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Int GetGLESFormatMaxSamples(const MG_External::GLESCapabilities& capabilities,
|
Int GetGLESFormatMaxSamples(const MG_External::GLESCapabilities& capabilities,
|
||||||
TextureInternalFormat logicalFormat,
|
TextureInternalFormat logicalFormat, GLenum imageFormat) {
|
||||||
GLenum imageFormat) {
|
|
||||||
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
|
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
|
||||||
const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(logicalFormat);
|
const Bool isStencil = MG_Util::IsStencilFormatInternalFormat(logicalFormat);
|
||||||
const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER ||
|
const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER ||
|
||||||
@@ -308,10 +303,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return capabilities.MaxColorTextureSamples;
|
return capabilities.MaxColorTextureSamples;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl,
|
Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
|
||||||
TextureTarget target,
|
GLuint texture, TextureInternalFormat format) {
|
||||||
GLuint texture,
|
|
||||||
TextureInternalFormat format) {
|
|
||||||
GLuint framebuffer = 0;
|
GLuint framebuffer = 0;
|
||||||
GLint prevFramebuffer = 0;
|
GLint prevFramebuffer = 0;
|
||||||
if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glCheckFramebufferStatus ||
|
if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glCheckFramebufferStatus ||
|
||||||
@@ -357,9 +350,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return complete;
|
return complete;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ProbeFramebufferCompletenessForRenderbuffer(const MG_External::GLESFunctionsTable& gl,
|
// Whether the driver renders to a framebuffer whose depth and stencil come from
|
||||||
GLuint renderbuffer,
|
// two different renderbuffers. GL only requires support when both attachments are
|
||||||
TextureInternalFormat format) {
|
// the same image, and ES drivers commonly answer GL_FRAMEBUFFER_UNSUPPORTED here;
|
||||||
|
// reporting COMPLETE from the frontend and then rendering into a framebuffer the
|
||||||
|
// driver refuses leaves the results silently empty.
|
||||||
|
Bool ProbeDistinctDepthStencilAttachments(const MG_External::GLESFunctionsTable& gl) {
|
||||||
|
if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glFramebufferRenderbuffer ||
|
||||||
|
!gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers || !gl.glGenRenderbuffers ||
|
||||||
|
!gl.glBindRenderbuffer || !gl.glRenderbufferStorage || !gl.glDeleteRenderbuffers) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint prevFramebuffer = 0, prevRenderbuffer = 0;
|
||||||
|
gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer);
|
||||||
|
gl.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer);
|
||||||
|
|
||||||
|
GLuint framebuffer = 0;
|
||||||
|
GLuint renderbuffers[2] = {0, 0};
|
||||||
|
gl.glGenFramebuffers(1, &framebuffer);
|
||||||
|
gl.glGenRenderbuffers(2, renderbuffers);
|
||||||
|
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[0]);
|
||||||
|
gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, 4, 4);
|
||||||
|
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[1]);
|
||||||
|
gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, 4, 4);
|
||||||
|
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||||
|
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderbuffers[0]);
|
||||||
|
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, renderbuffers[1]);
|
||||||
|
const Bool supported = gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||||
|
|
||||||
|
gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prevFramebuffer));
|
||||||
|
gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(prevRenderbuffer));
|
||||||
|
gl.glDeleteFramebuffers(1, &framebuffer);
|
||||||
|
gl.glDeleteRenderbuffers(2, renderbuffers);
|
||||||
|
return supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ProbeFramebufferCompletenessForRenderbuffer(const MG_External::GLESFunctionsTable& gl, GLuint renderbuffer,
|
||||||
|
TextureInternalFormat format) {
|
||||||
GLuint framebuffer = 0;
|
GLuint framebuffer = 0;
|
||||||
GLint prevFramebuffer = 0;
|
GLint prevFramebuffer = 0;
|
||||||
if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glFramebufferRenderbuffer ||
|
if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glFramebufferRenderbuffer ||
|
||||||
@@ -426,16 +454,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TextureTarget::Texture3D:
|
case TextureTarget::Texture3D:
|
||||||
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 2, 0, imageFormat,
|
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 2, 0, imageFormat, imageType,
|
||||||
imageType, nullptr);
|
nullptr);
|
||||||
break;
|
break;
|
||||||
case TextureTarget::Texture2DArray:
|
case TextureTarget::Texture2DArray:
|
||||||
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 1, 0, imageFormat,
|
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 1, 0, imageFormat, imageType,
|
||||||
imageType, nullptr);
|
nullptr);
|
||||||
break;
|
break;
|
||||||
case TextureTarget::TextureCubeMapArray:
|
case TextureTarget::TextureCubeMapArray:
|
||||||
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 6, 0, imageFormat,
|
gl.glTexImage3D(glTarget, 0, static_cast<GLint>(internalFormat), 2, 2, 6, 0, imageFormat, imageType,
|
||||||
imageType, nullptr);
|
nullptr);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -456,11 +484,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ProbeRenderbuffer(const MG_External::GLESFunctionsTable& gl,
|
Bool ProbeRenderbuffer(const MG_External::GLESFunctionsTable& gl, GLenum internalFormat,
|
||||||
GLenum internalFormat,
|
TextureInternalFormat logicalFormat, Bool multisample, Int samples) {
|
||||||
TextureInternalFormat logicalFormat,
|
|
||||||
Bool multisample,
|
|
||||||
Int samples) {
|
|
||||||
if (!gl.glGenRenderbuffers || !gl.glBindRenderbuffer || !gl.glDeleteRenderbuffers) {
|
if (!gl.glGenRenderbuffers || !gl.glBindRenderbuffer || !gl.glDeleteRenderbuffers) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -482,17 +507,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
gl.glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, 1, 1);
|
gl.glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, 1, 1);
|
||||||
}
|
}
|
||||||
const Bool created = CheckNoGLError(gl);
|
const Bool created = CheckNoGLError(gl);
|
||||||
const Bool complete = created && ProbeFramebufferCompletenessForRenderbuffer(gl, renderbuffer, logicalFormat);
|
const Bool complete =
|
||||||
|
created && ProbeFramebufferCompletenessForRenderbuffer(gl, renderbuffer, logicalFormat);
|
||||||
gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(prevRenderbuffer));
|
gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(prevRenderbuffer));
|
||||||
gl.glDeleteRenderbuffers(1, &renderbuffer);
|
gl.glDeleteRenderbuffers(1, &renderbuffer);
|
||||||
ClearGLErrors(gl);
|
ClearGLErrors(gl);
|
||||||
return complete;
|
return complete;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<Int> ProbeRenderbufferSampleCounts(const MG_External::GLESFunctionsTable& gl,
|
Vector<Int> ProbeRenderbufferSampleCounts(const MG_External::GLESFunctionsTable& gl, GLenum internalFormat,
|
||||||
GLenum internalFormat,
|
TextureInternalFormat logicalFormat, Int maxSamples) {
|
||||||
TextureInternalFormat logicalFormat,
|
|
||||||
Int maxSamples) {
|
|
||||||
Vector<Int> sampleCounts;
|
Vector<Int> sampleCounts;
|
||||||
for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) {
|
for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) {
|
||||||
if (ProbeRenderbuffer(gl, internalFormat, logicalFormat, true, samples)) {
|
if (ProbeRenderbuffer(gl, internalFormat, logicalFormat, true, samples)) {
|
||||||
@@ -520,20 +544,84 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GLESProbeFormatInfo nativeInfo = BuildNativeProbeFormatInfo(requestedInternalFormat);
|
const GLESProbeFormatInfo nativeInfo = BuildNativeProbeFormatInfo(requestedInternalFormat);
|
||||||
GLESProbeFormatInfo fallbackInfo;
|
GLESProbeFormatInfo outerFallbackInfo;
|
||||||
const Bool hasForcedFallback =
|
const Bool outerHasForcedFallback =
|
||||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, fallbackInfo);
|
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, outerFallbackInfo);
|
||||||
if (!hasForcedFallback) {
|
if (!outerHasForcedFallback) {
|
||||||
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, fallbackInfo);
|
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, outerFallbackInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
|
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
|
||||||
const auto target = static_cast<TextureTarget>(targetIndex);
|
const auto target = static_cast<TextureTarget>(targetIndex);
|
||||||
|
// Colour-attachable targets need a colour-renderable fallback; the ordinary
|
||||||
|
// fallback for a three-channel format is another three-channel one, which ES
|
||||||
|
// accepts as a texture but never as an attachment. Recompute the fallback per
|
||||||
|
// target so those formats get widened where the target demands it.
|
||||||
|
const Flags<PixelFormatNormalizeOptionBit> renderTargetOptions =
|
||||||
|
TextureImpl::GetRenderTargetNormalizeOptions(capabilities, targetIndex);
|
||||||
|
// Multisample storage has no three-channel form on ES at all, so its widening
|
||||||
|
// is unconditional and skips the native probe (which cannot succeed). Every
|
||||||
|
// other target keeps the widening on the DRIVER branch, behind the native
|
||||||
|
// probe: `shouldProbeFallback = !nativeCreated || !nativeRenderable` below is
|
||||||
|
// what makes the substitution conditional on the driver actually refusing, so
|
||||||
|
// a driver that does render to a three-channel image keeps allocating it byte
|
||||||
|
// for byte. That is a per-format runtime answer, NOT a desktop-vs-device
|
||||||
|
// split: llvmpipe renders to GL_RGB16F but refuses GL_RGB8_SNORM, GL_SRGB8,
|
||||||
|
// GL_RGB32F and the RGB integer formats, so the CI driver widens those eight
|
||||||
|
// too. Re-run the retrace fixtures and the glcts suites on any change here.
|
||||||
|
const Bool widenUnconditionally = IsGLESProbeMultisampleTarget(target);
|
||||||
|
GLESProbeFormatInfo fallbackInfo = outerFallbackInfo;
|
||||||
|
Bool hasForcedFallback = outerHasForcedFallback;
|
||||||
|
if (renderTargetOptions) {
|
||||||
|
// Folded into the forced options only when a forced fallback already
|
||||||
|
// applies, so the render-target bits never *create* one: ANGLE's forced
|
||||||
|
// GL_RGB8_SNORM -> GL_RGB16F is still three-channel and still needs
|
||||||
|
// widening, but a non-ANGLE driver must not lose its native probe.
|
||||||
|
const Flags<PixelFormatNormalizeOptionBit> forcedProbeOptions =
|
||||||
|
(outerHasForcedFallback || widenUnconditionally) ? forcedOptions | renderTargetOptions
|
||||||
|
: forcedOptions;
|
||||||
|
hasForcedFallback =
|
||||||
|
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedProbeOptions, true,
|
||||||
|
fallbackInfo);
|
||||||
|
if (!hasForcedFallback) {
|
||||||
|
BuildFallbackProbeFormatInfo(requestedInternalFormat,
|
||||||
|
driverOptions | renderTargetOptions, false, fallbackInfo);
|
||||||
|
}
|
||||||
|
// HONEST STATUS OF THE FORCED PATH. A forced fallback is only ever built
|
||||||
|
// for ANGLE (GetForcedPixelFormatNormalizeOptions returns nothing for any
|
||||||
|
// other renderer), and it SKIPS the native probe entirely - the widened
|
||||||
|
// format is asserted rather than measured on this device. That assertion
|
||||||
|
// is validated on exactly one configuration, the android-angle retrace
|
||||||
|
// golden; it is NOT covered by the headless llvmpipe suites, which take
|
||||||
|
// the driver branch below and prove nothing about ANGLE's answers. So log
|
||||||
|
// the choice at INFO rather than the usual MGLOG_D caveat: on any other
|
||||||
|
// ANGLE device the device report is the only evidence there is of which
|
||||||
|
// storage format the image really got. Once per format on the ordinary 2D
|
||||||
|
// target - repeating it for all ten targets would bury the report.
|
||||||
|
if (hasForcedFallback && target == TextureTarget::Texture2D &&
|
||||||
|
(MG_Util::TextureFormatProcessor::GetApplicablePixelFormatNormalizeOptions(
|
||||||
|
requestedInternalFormat, renderTargetOptions) &
|
||||||
|
PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget)) {
|
||||||
|
MGLOG_I("Three-channel widening (FORCED path, no native probe): %s stored as %s. "
|
||||||
|
"Reason: %s. Device-validated on the android-angle golden only.",
|
||||||
|
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
|
||||||
|
ConvertFallbackInternalFormatToString(fallbackInfo.InternalFormat).c_str(),
|
||||||
|
fallbackInfo.Reason.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1D, 1D-array and rectangle textures live on an ES target (see
|
||||||
|
// TextureImpl::MapToBackendTextureTarget), so they have to be probed there too -
|
||||||
|
// probing the desktop-only target itself always failed, which left those slots
|
||||||
|
// of the cache empty and stopped any fallback format from being selected for
|
||||||
|
// them (a GL_DEPTH_COMPONENT32 1D texture then got no storage at all).
|
||||||
|
const TextureTarget probeTarget = TextureImpl::MapToBackendTextureTarget(target);
|
||||||
|
|
||||||
Bool shouldProbeFallback = hasForcedFallback;
|
Bool shouldProbeFallback = hasForcedFallback;
|
||||||
if (!hasForcedFallback) {
|
if (!hasForcedFallback) {
|
||||||
Bool nativeRenderable = false;
|
Bool nativeRenderable = false;
|
||||||
const Bool nativeCreated =
|
const Bool nativeCreated =
|
||||||
ProbeTexture(gl, target, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
|
ProbeTexture(gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
|
||||||
nativeInfo.ImageType, logicalFormat, &nativeRenderable);
|
nativeInfo.ImageType, logicalFormat, &nativeRenderable);
|
||||||
if (nativeCreated) {
|
if (nativeCreated) {
|
||||||
AddFullFormatCaps(cache, targetIndex, formatIndex,
|
AddFullFormatCaps(cache, targetIndex, formatIndex,
|
||||||
@@ -548,12 +636,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
if (shouldProbeFallback && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL) {
|
if (shouldProbeFallback && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL) {
|
||||||
Bool fallbackRenderable = false;
|
Bool fallbackRenderable = false;
|
||||||
const Bool fallbackCreated =
|
const Bool fallbackCreated =
|
||||||
ProbeTexture(gl, target, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
|
ProbeTexture(gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
|
||||||
fallbackInfo.ImageType, logicalFormat, &fallbackRenderable);
|
fallbackInfo.ImageType, logicalFormat, &fallbackRenderable);
|
||||||
if (fallbackCreated) {
|
if (fallbackCreated) {
|
||||||
if (AddCaveatFormatCaps(cache, targetIndex, formatIndex,
|
if (AddCaveatFormatCaps(
|
||||||
BuildTextureCapsFromProbe(logicalFormat, target,
|
cache, targetIndex, formatIndex,
|
||||||
fallbackRenderable))) {
|
BuildTextureCapsFromProbe(logicalFormat, target, fallbackRenderable))) {
|
||||||
LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo);
|
LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo);
|
||||||
}
|
}
|
||||||
if (IsGLESProbeMultisampleTarget(target)) {
|
if (IsGLESProbeMultisampleTarget(target)) {
|
||||||
@@ -564,8 +652,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
|
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
|
||||||
Bool shouldProbeFallbackRenderbuffer = hasForcedFallback;
|
// A renderbuffer exists only to be attached, so it needs the same three-channel
|
||||||
if (!hasForcedFallback) {
|
// widening the colour-attachable texture targets get - and on the same terms: the
|
||||||
|
// native storage is probed first, so a driver that renders to it keeps it.
|
||||||
|
const Flags<PixelFormatNormalizeOptionBit> renderbufferOptions =
|
||||||
|
TextureImpl::GetRenderTargetNormalizeOptions(capabilities, renderbufferTargetIndex);
|
||||||
|
GLESProbeFormatInfo renderbufferFallbackInfo = outerFallbackInfo;
|
||||||
|
Bool renderbufferHasForcedFallback = outerHasForcedFallback;
|
||||||
|
if (renderbufferOptions) {
|
||||||
|
const Flags<PixelFormatNormalizeOptionBit> forcedProbeOptions =
|
||||||
|
outerHasForcedFallback ? forcedOptions | renderbufferOptions : forcedOptions;
|
||||||
|
renderbufferHasForcedFallback = BuildFallbackProbeFormatInfo(
|
||||||
|
requestedInternalFormat, forcedProbeOptions, true, renderbufferFallbackInfo);
|
||||||
|
if (!renderbufferHasForcedFallback) {
|
||||||
|
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | renderbufferOptions,
|
||||||
|
false, renderbufferFallbackInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool shouldProbeFallbackRenderbuffer = renderbufferHasForcedFallback;
|
||||||
|
if (!renderbufferHasForcedFallback) {
|
||||||
const Bool nativeRenderbufferComplete =
|
const Bool nativeRenderbufferComplete =
|
||||||
ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1);
|
ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1);
|
||||||
if (nativeRenderbufferComplete) {
|
if (nativeRenderbufferComplete) {
|
||||||
@@ -579,16 +685,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
shouldProbeFallbackRenderbuffer = true;
|
shouldProbeFallbackRenderbuffer = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (shouldProbeFallbackRenderbuffer && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
|
if (shouldProbeFallbackRenderbuffer && renderbufferFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
|
||||||
ProbeRenderbuffer(gl, fallbackInfo.InternalFormat, logicalFormat, false, 1)) {
|
ProbeRenderbuffer(gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, false, 1)) {
|
||||||
if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex,
|
if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex,
|
||||||
GetRenderbufferFeatureCaps(logicalFormat))) {
|
GetRenderbufferFeatureCaps(logicalFormat))) {
|
||||||
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, fallbackInfo);
|
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo);
|
||||||
}
|
}
|
||||||
const Int maxSamples =
|
const Int maxSamples =
|
||||||
GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
|
GetGLESFormatMaxSamples(capabilities, logicalFormat, renderbufferFallbackInfo.ImageFormat);
|
||||||
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
|
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(
|
||||||
ProbeRenderbufferSampleCounts(gl, fallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -604,7 +710,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
.ExtraVendor = Nullopt, // Extra vendor
|
.ExtraVendor = Nullopt, // Extra vendor
|
||||||
.RendererGLInfo =
|
.RendererGLInfo =
|
||||||
{
|
{
|
||||||
.TargetGLVersion = {3, 3, 0}, // GL target version
|
.TargetGLVersion = {4, 0, 0}, // GL target version
|
||||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||||
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
||||||
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||||
@@ -635,8 +741,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void PopulateFormatCapabilities(const MG_External::GLESFunctionsTable& gl,
|
void PopulateFormatCapabilities(const MG_External::GLESFunctionsTable& gl,
|
||||||
const MG_External::GLESCapabilities& capabilities,
|
const MG_External::GLESCapabilities& capabilities, FormatCapabilityCache& cache) {
|
||||||
FormatCapabilityCache& cache) {
|
|
||||||
PopulateFormatCapabilitiesImpl(gl, capabilities, cache);
|
PopulateFormatCapabilitiesImpl(gl, capabilities, cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,10 +805,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((handle.Backend != WindowBackend::Android &&
|
if ((handle.Backend != WindowBackend::Android && handle.Backend != WindowBackend::X11 &&
|
||||||
handle.Backend != WindowBackend::X11 &&
|
handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::Win32) ||
|
||||||
handle.Backend != WindowBackend::MetalLayer &&
|
|
||||||
handle.Backend != WindowBackend::Win32) ||
|
|
||||||
!handle.Handle) {
|
!handle.Handle) {
|
||||||
MGLOG_E("DirectGLES backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
|
MGLOG_E("DirectGLES backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
|
||||||
return false;
|
return false;
|
||||||
@@ -822,17 +925,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
|
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
|
||||||
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
|
Vector<GLExtension> extensions = {
|
||||||
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
|
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||||
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
|
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
|
||||||
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
|
||||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
|
E_GL_ARB_texture_storage_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
||||||
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters, E_GL_ARB_shader_draw_parameters,
|
||||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
|
||||||
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
E_GL_ARB_vertex_attrib_binding,
|
||||||
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
|
// Both are core from GL 3.2/3.3 on and implemented here for
|
||||||
E_GL_ARB_shader_image_size};
|
// every advertised version, but an app targeting 3.0/3.1
|
||||||
|
// only reaches them through the extension string - the CTS
|
||||||
|
// picks a whole different shader for draw_buffers without
|
||||||
|
// explicit_attrib_location. DirectVulkan advertises both.
|
||||||
|
E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample, E_GL_ARB_shader_image_size,
|
||||||
|
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||||
|
// extension explicitly permits. It is also the only thing that
|
||||||
|
// exposes glProgramParameteri before GL 4.1.
|
||||||
|
E_GL_ARB_get_program_binary};
|
||||||
|
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES
|
||||||
|
// driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram
|
||||||
|
// are serviced entirely inside the frontend. Whether the device driver advertises
|
||||||
|
// the string is irrelevant here (the POST reports it separately, for the day the
|
||||||
|
// driver-side link is what gets parallelised).
|
||||||
|
//
|
||||||
|
// Gated on the async flag deliberately, and this is the whole reason the gate
|
||||||
|
// exists. Advertising the string is the one part of asynchronous compilation that a
|
||||||
|
// recorded trace can never cover: Iris and Sodium change their SUBMISSION SCHEDULE
|
||||||
|
// the moment they see it - they enqueue whole pipeline batches and poll
|
||||||
|
// GL_COMPLETION_STATUS_KHR instead of compiling one program at a time - so
|
||||||
|
// MOBILEGL_ASYNC_SHADER_COMPILE=0 has to withdraw the application-visible behaviour
|
||||||
|
// change as well as the threading, or the kill switch would only be half a switch.
|
||||||
|
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||||
|
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
||||||
|
}
|
||||||
// Only advertised when the device driver actually has usable timer queries
|
// Only advertised when the device driver actually has usable timer queries
|
||||||
// (GL_EXT_disjoint_timer_query plus its entry points) and the
|
// (GL_EXT_disjoint_timer_query plus its entry points) and the
|
||||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
||||||
@@ -893,12 +1020,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
||||||
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
||||||
funcsTable.GL.GetProgramiv = GetProgramiv;
|
funcsTable.GL.GetProgramiv = GetProgramiv;
|
||||||
funcsTable.GL.GetProgramInterfaceiv = GetProgramInterfaceiv;
|
|
||||||
funcsTable.GL.GetProgramResourceIndex = GetProgramResourceIndex;
|
|
||||||
funcsTable.GL.GetProgramResourceName = GetProgramResourceName;
|
|
||||||
funcsTable.GL.GetProgramResourceiv = GetProgramResourceiv;
|
|
||||||
funcsTable.GL.GetProgramResourceLocation = GetProgramResourceLocation;
|
|
||||||
funcsTable.GL.GetProgramResourceLocationIndex = GetProgramResourceLocationIndex;
|
|
||||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||||
funcsTable.GL.Clear = Clear;
|
funcsTable.GL.Clear = Clear;
|
||||||
funcsTable.GL.ClearBufferfi = ClearBufferfi;
|
funcsTable.GL.ClearBufferfi = ClearBufferfi;
|
||||||
@@ -906,6 +1027,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
|
funcsTable.GL.ClearBufferuiv = ClearBufferuiv;
|
||||||
funcsTable.GL.ClearBufferiv = ClearBufferiv;
|
funcsTable.GL.ClearBufferiv = ClearBufferiv;
|
||||||
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
|
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
|
||||||
|
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
|
||||||
|
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
|
||||||
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
|
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
|
||||||
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
|
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
|
||||||
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
|
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
|
||||||
@@ -942,9 +1065,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// when the timer-query group above is disabled.
|
// when the timer-query group above is disabled.
|
||||||
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
|
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
|
||||||
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
|
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
|
||||||
|
// Real driver primitive counters: the frontend's CPU accounting cannot see a
|
||||||
|
// geometry shader's amplification.
|
||||||
|
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
|
||||||
|
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
|
||||||
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
|
||||||
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
|
||||||
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
|
||||||
|
// Transform feedback is captured by the real ES driver rather than
|
||||||
|
// reconstructed from the draw recording, so the frontend has to hand the
|
||||||
|
// span boundaries over.
|
||||||
|
funcsTable.GL.PatchParameteri = DirectGLES::PatchParameteri;
|
||||||
|
funcsTable.GL.BeginTransformFeedback = XfbImpl::BeginTransformFeedback;
|
||||||
|
funcsTable.GL.EndTransformFeedback = XfbImpl::EndTransformFeedback;
|
||||||
|
funcsTable.GL.PauseTransformFeedback = XfbImpl::PauseTransformFeedback;
|
||||||
|
funcsTable.GL.ResumeTransformFeedback = XfbImpl::ResumeTransformFeedback;
|
||||||
|
funcsTable.GL.BindTransformFeedback = XfbImpl::BindTransformFeedback;
|
||||||
|
funcsTable.GL.DeleteTransformFeedback = XfbImpl::DeleteTransformFeedback;
|
||||||
funcsTableInitialized = true;
|
funcsTableInitialized = true;
|
||||||
}
|
}
|
||||||
return funcsTable;
|
return funcsTable;
|
||||||
@@ -954,8 +1091,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return m_dynamicParameters;
|
return m_dynamicParameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BackendObject_DirectGLES::ApplyGLESCapabilitiesForTesting(
|
void BackendObject_DirectGLES::ApplyGLESCapabilitiesForTesting(const MG_External::GLESCapabilities& capabilities) {
|
||||||
const MG_External::GLESCapabilities& capabilities) {
|
|
||||||
m_GLESCapabilities = capabilities;
|
m_GLESCapabilities = capabilities;
|
||||||
UpdateDynamicBackendParameters();
|
UpdateDynamicBackendParameters();
|
||||||
}
|
}
|
||||||
@@ -985,6 +1121,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_dynamicParameters.MaxIntegerSamples = m_GLESCapabilities.MaxIntegerSamples;
|
m_dynamicParameters.MaxIntegerSamples = m_GLESCapabilities.MaxIntegerSamples;
|
||||||
m_dynamicParameters.MaxSamples = m_GLESCapabilities.MaxSamples;
|
m_dynamicParameters.MaxSamples = m_GLESCapabilities.MaxSamples;
|
||||||
m_dynamicParameters.MaxSampleMaskWords = m_GLESCapabilities.MaxSampleMaskWords;
|
m_dynamicParameters.MaxSampleMaskWords = m_GLESCapabilities.MaxSampleMaskWords;
|
||||||
|
m_dynamicParameters.MaxPatchVertices = m_GLESCapabilities.MaxPatchVertices;
|
||||||
|
m_dynamicParameters.MaxTessGenLevel = m_GLESCapabilities.MaxTessGenLevel;
|
||||||
|
m_dynamicParameters.MinProgramTextureGatherOffset = m_GLESCapabilities.MinProgramTextureGatherOffset;
|
||||||
|
m_dynamicParameters.MaxProgramTextureGatherOffset = m_GLESCapabilities.MaxProgramTextureGatherOffset;
|
||||||
// Clamp the advertised sampler limits the same way the DirectVulkan backend does: per-stage
|
// Clamp the advertised sampler limits the same way the DirectVulkan backend does: per-stage
|
||||||
// GL_MAX_TEXTURE_IMAGE_UNITS must never exceed host-side fixed arrays sized off it (e.g.
|
// GL_MAX_TEXTURE_IMAGE_UNITS must never exceed host-side fixed arrays sized off it (e.g.
|
||||||
// Minecraft's 128-entry Blaze3D GlStateManager.TEXTURES[], iterated by Iris), and the combined
|
// Minecraft's 128-entry Blaze3D GlStateManager.TEXTURES[], iterated by Iris), and the combined
|
||||||
@@ -1012,10 +1152,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
|
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
|
||||||
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
|
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
|
||||||
m_dynamicParameters.MaxTextureBufferSize = m_GLESCapabilities.MaxTextureBufferSize;
|
m_dynamicParameters.MaxTextureBufferSize = m_GLESCapabilities.MaxTextureBufferSize;
|
||||||
|
m_dynamicParameters.TextureBufferOffsetAlignment = m_GLESCapabilities.TextureBufferOffsetAlignment;
|
||||||
m_dynamicParameters.MaxUniformBufferBindings = m_GLESCapabilities.MaxUniformBufferBindings;
|
m_dynamicParameters.MaxUniformBufferBindings = m_GLESCapabilities.MaxUniformBufferBindings;
|
||||||
m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize;
|
m_dynamicParameters.MaxUniformBlockSize = m_GLESCapabilities.MaxUniformBlockSize;
|
||||||
const Int maxSupportedTextureUnits =
|
const Int maxSupportedTextureUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||||
static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
|
||||||
m_dynamicParameters.MaxImageUnits =
|
m_dynamicParameters.MaxImageUnits =
|
||||||
std::max(std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits), 0);
|
std::max(std::min(m_GLESCapabilities.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||||
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_GLESCapabilities.MaxCombinedImageUniforms, 0);
|
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_GLESCapabilities.MaxCombinedImageUniforms, 0);
|
||||||
@@ -1023,14 +1163,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return std::min({std::max(stageLimit, 0), m_dynamicParameters.MaxImageUnits,
|
return std::min({std::max(stageLimit, 0), m_dynamicParameters.MaxImageUnits,
|
||||||
m_dynamicParameters.MaxCombinedImageUniforms});
|
m_dynamicParameters.MaxCombinedImageUniforms});
|
||||||
};
|
};
|
||||||
m_dynamicParameters.MaxVertexImageUniforms =
|
m_dynamicParameters.MaxVertexImageUniforms = clampStageImageUniforms(m_GLESCapabilities.MaxVertexImageUniforms);
|
||||||
clampStageImageUniforms(m_GLESCapabilities.MaxVertexImageUniforms);
|
|
||||||
m_dynamicParameters.MaxGeometryImageUniforms =
|
m_dynamicParameters.MaxGeometryImageUniforms =
|
||||||
clampStageImageUniforms(m_GLESCapabilities.MaxGeometryImageUniforms);
|
clampStageImageUniforms(m_GLESCapabilities.MaxGeometryImageUniforms);
|
||||||
m_dynamicParameters.MaxFragmentImageUniforms =
|
m_dynamicParameters.MaxFragmentImageUniforms =
|
||||||
clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms);
|
clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms);
|
||||||
m_dynamicParameters.MaxComputeImageUniforms =
|
m_dynamicParameters.MaxComputeImageUniforms =
|
||||||
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
|
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
|
||||||
|
m_dynamicParameters.SupportsDistinctDepthStencilAttachments =
|
||||||
|
ProbeDistinctDepthStencilAttachments(DirectGLES::g_GLESFuncs);
|
||||||
|
// SyncAttachmentObject routes a layered upload target to glFramebufferTextureLayer with the
|
||||||
|
// attachment's layer passed through, so this backend really does render to the layer it was
|
||||||
|
// given - provided the driver resolved the entry point at all.
|
||||||
|
// SyncAttachmentObject (Managers.cpp, the glFramebufferTextureLayer branch) routes exactly
|
||||||
|
// five upload targets to glFramebufferTextureLayer with the attachment's layer passed
|
||||||
|
// through, so this backend really does render to the layer it was given - provided the driver
|
||||||
|
// resolved the entry point at all. The cube map array is the one target that also needs
|
||||||
|
// ES-level support before it has any storage to attach.
|
||||||
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets = 0;
|
||||||
|
if (DirectGLES::g_GLESFuncs.glFramebufferTextureLayer != nullptr) {
|
||||||
|
using DynParams = MG_Backend::DynamicBackendParameters;
|
||||||
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D) |
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture1DArray) |
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
|
||||||
|
if (m_GLESCapabilities.SupportsTextureCubeMapArray) {
|
||||||
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Not a driver question and never will be: OpenGL ES has no double-precision vertex format
|
||||||
|
// and ESSL has no fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to
|
||||||
|
// land on this backend regardless of what the driver underneath happens to support.
|
||||||
|
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;
|
||||||
@@ -1052,8 +1218,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const Float requiredMaxOffset =
|
const Float requiredMaxOffset =
|
||||||
0.5f - std::ldexp(1.0f, -m_GLESCapabilities.FragmentInterpolationOffsetBits);
|
0.5f - std::ldexp(1.0f, -m_GLESCapabilities.FragmentInterpolationOffsetBits);
|
||||||
if (m_GLESCapabilities.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
|
if (m_GLESCapabilities.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
|
||||||
m_dynamicParameters.MaxFragmentInterpolationOffset =
|
m_dynamicParameters.MaxFragmentInterpolationOffset = m_GLESCapabilities.MaxFragmentInterpolationOffset;
|
||||||
m_GLESCapabilities.MaxFragmentInterpolationOffset;
|
|
||||||
m_dynamicParameters.FragmentInterpolationOffsetBits =
|
m_dynamicParameters.FragmentInterpolationOffsetBits =
|
||||||
m_GLESCapabilities.FragmentInterpolationOffsetBits;
|
m_GLESCapabilities.FragmentInterpolationOffsetBits;
|
||||||
}
|
}
|
||||||
@@ -1062,9 +1227,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f;
|
m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f;
|
||||||
|
|
||||||
const auto containsAny = [](const String& haystack, std::initializer_list<const char*> needles) {
|
const auto containsAny = [](const String& haystack, std::initializer_list<const char*> needles) {
|
||||||
return std::any_of(needles.begin(), needles.end(), [&](const char* needle) {
|
return std::any_of(needles.begin(), needles.end(),
|
||||||
return haystack.find(needle) != String::npos;
|
[&](const char* needle) { return haystack.find(needle) != String::npos; });
|
||||||
});
|
|
||||||
};
|
};
|
||||||
const String vendorAndRenderer =
|
const String vendorAndRenderer =
|
||||||
m_GLESCapabilities.GLESVendorString + " " + m_GLESCapabilities.GLESRendererString;
|
m_GLESCapabilities.GLESVendorString + " " + m_GLESCapabilities.GLESRendererString;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -59,6 +59,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||||
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||||
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||||
|
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||||
|
GLenum buffer, GLint drawbuffer, const GLint* value);
|
||||||
|
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||||
|
GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||||
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);
|
||||||
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
|
||||||
@@ -88,15 +92,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||||
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
|
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||||
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
|
||||||
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length,
|
|
||||||
GLchar* name);
|
|
||||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
|
||||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
|
||||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
|
||||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
|
||||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
|
||||||
Bool InitWindowSurface(NativeWindowType window);
|
Bool InitWindowSurface(NativeWindowType window);
|
||||||
Bool InitPbufferSurface(EGLint width, EGLint height);
|
Bool InitPbufferSurface(EGLint width, EGLint height);
|
||||||
Bool MakeCurrent();
|
Bool MakeCurrent();
|
||||||
@@ -135,6 +131,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// flow through GetQueryResult64/DeleteBackendQuery like the timer queries above.
|
// flow through GetQueryResult64/DeleteBackendQuery like the timer queries above.
|
||||||
BackendQueryHandle BeginOcclusionQuery();
|
BackendQueryHandle BeginOcclusionQuery();
|
||||||
void EndOcclusionQuery(BackendQueryHandle query);
|
void EndOcclusionQuery(BackendQueryHandle query);
|
||||||
|
// GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED, also core ES
|
||||||
|
// (GL_PRIMITIVES_GENERATED from ES 3.2 on). Null when the target is unavailable, in
|
||||||
|
// which case the frontend falls back to counting primitives from the draw calls.
|
||||||
|
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
|
||||||
|
void EndXfbPrimitivesQuery(BackendQueryHandle query);
|
||||||
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
Bool IsQueryResultAvailable(BackendQueryHandle query);
|
||||||
// Returns true when a final value landed in *outNanoseconds (a zero for
|
// Returns true when a final value landed in *outNanoseconds (a zero for
|
||||||
// null or stale-generation handles IS final: the frontend may cache it
|
// null or stale-generation handles IS final: the frontend may cache it
|
||||||
@@ -151,6 +152,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// A buffer retired during frame N is safe to recycle once CompletedFrameSerial() >= N.
|
// A buffer retired during frame N is safe to recycle once CompletedFrameSerial() >= N.
|
||||||
Uint64 CurrentFrameSerial();
|
Uint64 CurrentFrameSerial();
|
||||||
Uint64 CompletedFrameSerial();
|
Uint64 CompletedFrameSerial();
|
||||||
|
// Block (up to timeoutNs) until the given frame serial provably retired on the
|
||||||
|
// GPU, using the per-frame fence ring. False when no usable fence covers the
|
||||||
|
// serial (fence-less context, foreign thread, or the slot was recycled);
|
||||||
|
// completion state is untouched in that case.
|
||||||
|
Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs);
|
||||||
// Applies (or defers until the window surface exists) the app-requested
|
// Applies (or defers until the window surface exists) the app-requested
|
||||||
// eglSwapInterval on the native EGL surface.
|
// eglSwapInterval on the native EGL surface.
|
||||||
void SetSwapInterval(Int interval);
|
void SetSwapInterval(Int interval);
|
||||||
@@ -159,6 +165,49 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
|
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
|
||||||
void DestroyEGLContext();
|
void DestroyEGLContext();
|
||||||
|
|
||||||
|
// Transform feedback capture spans, performed by the real ES driver. The
|
||||||
|
// capture set is declared on the backend program at link time; the driver-side
|
||||||
|
// begin is deferred to the first draw of the span (ES needs the capturing
|
||||||
|
// program current and the capture buffers bound), and the end also mirrors the
|
||||||
|
// captured bytes back into the frontend buffer shadows.
|
||||||
|
void PatchParameteri(GLenum pname, GLint value);
|
||||||
|
|
||||||
|
namespace XfbImpl {
|
||||||
|
Bool AreTransformFeedbacksSupported();
|
||||||
|
// True while a capture span is open on the current transform feedback object
|
||||||
|
// (frontend Begin seen and not paused), whether or not the deferred driver-side
|
||||||
|
// Begin has been issued yet. Draw paths that would restructure the primitive
|
||||||
|
// stream, or that need to dispatch compute mid-draw, decline while it is set.
|
||||||
|
Bool IsCaptureSpanOpen();
|
||||||
|
void BeginTransformFeedback(GLenum primitiveMode);
|
||||||
|
void EndTransformFeedback();
|
||||||
|
void PauseTransformFeedback();
|
||||||
|
void ResumeTransformFeedback();
|
||||||
|
void BindTransformFeedback(GLuint name);
|
||||||
|
void DeleteTransformFeedback(GLuint name);
|
||||||
|
void OnBackendContextDestroyed();
|
||||||
|
} // namespace XfbImpl
|
||||||
|
|
||||||
|
namespace RenderStateImpl {
|
||||||
|
// Pushes the frontend's render-state block to the ES driver, diffed against what was
|
||||||
|
// last pushed.
|
||||||
|
//
|
||||||
|
// `forColorClear` names the CALLER, and the only thing it changes is the colour write
|
||||||
|
// mask handed to the driver. A draw into a colour attachment the backend widened from
|
||||||
|
// three channels to four gets that buffer's alpha channel masked OFF, so nothing can
|
||||||
|
// move the stored alpha away from the 1.0 the application's three-channel format
|
||||||
|
// implies (see FramebufferImpl::g_alphaWidenedDrawBufferMask). A CLEAR is how that 1.0
|
||||||
|
// gets there in the first place, so it must be allowed to write alpha - hence the flag
|
||||||
|
// rather than an unconditional doctoring. It is part of the sync memo, so a clear
|
||||||
|
// followed by a draw re-pushes the mask instead of early-outing on an unchanged
|
||||||
|
// frontend version.
|
||||||
|
//
|
||||||
|
// The application's own colour mask is never modified: glGet(GL_COLOR_WRITEMASK)
|
||||||
|
// answers from the frontend state, which this function only reads.
|
||||||
|
void SyncRenderState(Bool forColorClear = false);
|
||||||
|
void InvalidateSyncedRenderState();
|
||||||
|
} // namespace RenderStateImpl
|
||||||
|
|
||||||
extern MG_External::EGLFunctionsTable g_EGLFuncs;
|
extern MG_External::EGLFunctionsTable g_EGLFuncs;
|
||||||
extern MG_External::GLESFunctionsTable g_GLESFuncs;
|
extern MG_External::GLESFunctionsTable g_GLESFuncs;
|
||||||
extern MG_External::GLESCapabilities g_GLESCapabilities;
|
extern MG_External::GLESCapabilities g_GLESCapabilities;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -21,45 +21,126 @@ 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);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// call into g_GLESFuncs (the observed crash is a jump through an unmapped driver
|
||||||
|
// pointer from __run_exit_handlers) nor touch statics in other TUs (cross-TU
|
||||||
|
// destruction order is unspecified). Deliberate leak: the process is exiting and
|
||||||
|
// the driver reclaims GPU objects. The flag is set by a std::atexit handler that
|
||||||
|
// EnsureProcessTeardownSentinel() registers lazily on first registry use - by
|
||||||
|
// then every static everywhere has finished constructing, so this handler is
|
||||||
|
// guaranteed to run BEFORE any static destructor (atexit is LIFO). A destructor
|
||||||
|
// hook on the registry itself was tried first and is WRONG: tests and cache
|
||||||
|
// resets destroy temporary registry instances mid-run, which would latch the
|
||||||
|
// flag while the process is very much alive.
|
||||||
|
Bool InProcessTeardown();
|
||||||
|
void EnsureProcessTeardownSentinel();
|
||||||
|
|
||||||
|
// Which optional pieces of state a draw needs synchronized before it is issued.
|
||||||
|
// Index/indirect buffer syncs and the instancing-related work are skipped for
|
||||||
|
// draws that provably cannot read them.
|
||||||
|
enum class DrawSyncBit : Uint32 {
|
||||||
|
None = 0,
|
||||||
|
IndexBuffer = 1 << 0,
|
||||||
|
IndirectBuffer = 1 << 1,
|
||||||
|
Instancing = 1 << 2
|
||||||
|
};
|
||||||
|
// Deliberately the shared Flags<> rather than hand-written operators for this enum:
|
||||||
|
// a namespace-local operator| here would hide MobileGL::operator|(Bit, Bit) from
|
||||||
|
// every other scoped-enum flag set used inside this namespace.
|
||||||
|
using DrawSyncFlags = Flags<DrawSyncBit>;
|
||||||
|
|
||||||
|
// The GL-defined indirect command layouts, byte-identical to what the driver reads
|
||||||
|
// out of a GL_DRAW_INDIRECT_BUFFER. Also the staging layout the multi-draw emulation
|
||||||
|
// synthesizes commands into.
|
||||||
|
struct DrawElementsIndirectCommand {
|
||||||
|
Uint32 count = 0;
|
||||||
|
Uint32 instanceCount = 0;
|
||||||
|
Uint32 firstIndex = 0;
|
||||||
|
Int32 baseVertex = 0;
|
||||||
|
Uint32 baseInstance = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DrawArraysIndirectCommand {
|
||||||
|
Uint32 count = 0;
|
||||||
|
Uint32 instanceCount = 0;
|
||||||
|
Uint32 first = 0;
|
||||||
|
Uint32 baseInstance = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Brings the whole draw-relevant frontend state onto the native ES context and binds
|
||||||
|
// the program; every GL draw entry point calls it exactly once before issuing draws.
|
||||||
|
void PrepareForDraw(DrawSyncFlags syncBits);
|
||||||
|
// GLES core supports only GL_PRIMITIVE_RESTART_FIXED_INDEX. Throws when the app enabled
|
||||||
|
// the arbitrary GL_PRIMITIVE_RESTART with a non-fixed index for this index type.
|
||||||
|
void CheckPrimitiveRestartSupported(GLenum indexType);
|
||||||
|
// Feed the current program's gl_BaseInstance / gl_DrawID emulation uniforms. Both are
|
||||||
|
// no-ops when the program does not read the corresponding builtin.
|
||||||
|
void SetCurrentBaseInstance(Uint32 baseInstance);
|
||||||
|
void SetCurrentDrawID(Uint32 drawId);
|
||||||
|
// True when the current program actually reads gl_DrawID, i.e. when a batched
|
||||||
|
// (single driver call) multi-draw tier would have to feed it one value for the whole
|
||||||
|
// batch and would therefore be wrong.
|
||||||
|
Bool CurrentProgramReadsDrawID();
|
||||||
|
|
||||||
template <typename StateObject, typename BackendObject>
|
template <typename StateObject, typename BackendObject>
|
||||||
class StateBackendObjectRegistry {
|
class StateBackendObjectRegistry {
|
||||||
public:
|
public:
|
||||||
|
|
||||||
using StatePtr = SharedPtr<StateObject>;
|
using StatePtr = SharedPtr<StateObject>;
|
||||||
using StateWeakPtr = std::weak_ptr<StateObject>;
|
using StateWeakPtr = std::weak_ptr<StateObject>;
|
||||||
using BackendPtr = SharedPtr<BackendObject>;
|
using BackendPtr = SharedPtr<BackendObject>;
|
||||||
using BackendMap = UnorderedMap<StateObject*, BackendPtr>;
|
|
||||||
using StateRefMap = UnorderedMap<StateObject*, StateWeakPtr>;
|
// The backend twin and the weak reference that decides whether the raw key still
|
||||||
|
// names the state object the twin was built for. Both live in one entry: a
|
||||||
|
// separate liveness map answered nothing the backend probe had not already found
|
||||||
|
// and cost a second hash lookup on every Find, which the draw path runs ~10 times.
|
||||||
|
struct Entry {
|
||||||
|
BackendPtr backend;
|
||||||
|
StateWeakPtr stateRef;
|
||||||
|
};
|
||||||
|
using BackendMap = UnorderedMap<StateObject*, Entry>;
|
||||||
using iterator = typename BackendMap::iterator;
|
using iterator = typename BackendMap::iterator;
|
||||||
using const_iterator = typename BackendMap::const_iterator;
|
using const_iterator = typename BackendMap::const_iterator;
|
||||||
|
|
||||||
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
|
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
|
||||||
MOBILEGL_ASSERT(stateObj != nullptr, "State object must not be null");
|
MOBILEGL_ASSERT(stateObj != nullptr, "State object must not be null");
|
||||||
|
|
||||||
auto* key = stateObj.get();
|
// Twin creation is the moment a driver-owned id starts needing a guarded
|
||||||
auto trackedStateIt = m_stateRefs.find(key);
|
// destructor; cold path, so the once-guard costs nothing per draw.
|
||||||
if (trackedStateIt != m_stateRefs.end() && trackedStateIt->second.expired()) {
|
EnsureProcessTeardownSentinel();
|
||||||
EraseByKey(key);
|
auto& entry = m_entries[stateObj.get()];
|
||||||
|
if (entry.stateRef.expired()) {
|
||||||
|
// 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.
|
||||||
|
entry.backend.reset();
|
||||||
}
|
}
|
||||||
m_stateRefs[key] = stateObj;
|
entry.stateRef = stateObj;
|
||||||
return m_backendObjects[key];
|
return entry.backend;
|
||||||
}
|
}
|
||||||
|
|
||||||
iterator find(StateObject* stateObj) {
|
// Null when no live state object owns this key. The result points into the map, so
|
||||||
if (!IsAlive(stateObj)) {
|
// it stays valid only until the next GetOrCreate/Find/CollectGarbage on this registry.
|
||||||
EraseByKey(stateObj);
|
BackendPtr* Find(StateObject* stateObj) {
|
||||||
return m_backendObjects.end();
|
const auto entryIt = m_entries.find(stateObj);
|
||||||
|
if (entryIt == m_entries.end()) {
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
return m_backendObjects.find(stateObj);
|
if (entryIt->second.stateRef.expired()) {
|
||||||
|
m_entries.erase(entryIt);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return &entryIt->second.backend;
|
||||||
}
|
}
|
||||||
|
|
||||||
const_iterator find(StateObject* stateObj) const {
|
const BackendPtr* Find(StateObject* stateObj) const {
|
||||||
return const_cast<StateBackendObjectRegistry*>(this)->find(stateObj);
|
return const_cast<StateBackendObjectRegistry*>(this)->Find(stateObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
iterator begin() { return m_backendObjects.begin(); }
|
iterator begin() { return m_entries.begin(); }
|
||||||
const_iterator begin() const { return m_backendObjects.begin(); }
|
const_iterator begin() const { return m_entries.begin(); }
|
||||||
iterator end() { return m_backendObjects.end(); }
|
iterator end() { return m_entries.end(); }
|
||||||
const_iterator end() const { return m_backendObjects.end(); }
|
const_iterator end() const { return m_entries.end(); }
|
||||||
|
|
||||||
void CollectGarbageIfNeeded() {
|
void CollectGarbageIfNeeded() {
|
||||||
++m_gcTick;
|
++m_gcTick;
|
||||||
@@ -73,19 +154,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void CollectGarbageNow() { CollectGarbage(); }
|
void CollectGarbageNow() { CollectGarbage(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool IsAlive(StateObject* stateObj) const {
|
|
||||||
const auto trackedStateIt = m_stateRefs.find(stateObj);
|
|
||||||
if (trackedStateIt == m_stateRefs.end()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !trackedStateIt->second.expired();
|
|
||||||
}
|
|
||||||
|
|
||||||
void EraseByKey(StateObject* stateObj) {
|
|
||||||
m_stateRefs.erase(stateObj);
|
|
||||||
m_backendObjects.erase(stateObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectGarbage() {
|
void CollectGarbage() {
|
||||||
if (m_isCollecting) {
|
if (m_isCollecting) {
|
||||||
return;
|
return;
|
||||||
@@ -94,16 +162,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
m_isCollecting = true;
|
m_isCollecting = true;
|
||||||
|
|
||||||
Vector<StateObject*> staleKeys;
|
Vector<StateObject*> staleKeys;
|
||||||
staleKeys.reserve(m_stateRefs.size());
|
staleKeys.reserve(m_entries.size());
|
||||||
for (const auto& [stateKey, stateWeakRef] : m_stateRefs) {
|
for (const auto& [stateKey, entry] : m_entries) {
|
||||||
if (stateWeakRef.expired()) {
|
if (entry.stateRef.expired()) {
|
||||||
staleKeys.push_back(stateKey);
|
staleKeys.push_back(stateKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto* stateKey : staleKeys) {
|
for (auto* stateKey : staleKeys) {
|
||||||
m_stateRefs.erase(stateKey);
|
m_entries.erase(stateKey);
|
||||||
m_backendObjects.erase(stateKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_isCollecting = false;
|
m_isCollecting = false;
|
||||||
@@ -111,8 +178,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr Uint32 kGCInterval = 1024;
|
static constexpr Uint32 kGCInterval = 1024;
|
||||||
StateRefMap m_stateRefs;
|
BackendMap m_entries;
|
||||||
BackendMap m_backendObjects;
|
|
||||||
Uint32 m_gcTick = 0;
|
Uint32 m_gcTick = 0;
|
||||||
Bool m_isCollecting = false;
|
Bool m_isCollecting = false;
|
||||||
};
|
};
|
||||||
@@ -120,6 +186,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
namespace BufferImpl {
|
namespace BufferImpl {
|
||||||
const GLenum TempBufferTarget = GL_ARRAY_BUFFER;
|
const GLenum TempBufferTarget = GL_ARRAY_BUFFER;
|
||||||
|
|
||||||
|
// --- Buffer-mutation epoch -------------------------------------------------
|
||||||
|
// Manager-wide monotonic counter: it moves whenever ANY buffer resource may
|
||||||
|
// have gone from draw-clean to dirty. Draw-path memos read it once per pass
|
||||||
|
// (CurrentBufferMutationEpoch, acquire), re-run their IsBufferDrawClean
|
||||||
|
// probes only when it moved, and stamp the PRE-pass value after a pass in
|
||||||
|
// which every probe came up clean - so a concurrent bump lands strictly
|
||||||
|
// after the stamped value and forces a re-probe on the next pass no matter
|
||||||
|
// how the probe interleaved with the mutation. Conservative-correct: a bump
|
||||||
|
// never skips work, it only re-runs the probes once.
|
||||||
|
//
|
||||||
|
// Every clean->dirty transition path bumps it (BumpBufferMutationEpoch,
|
||||||
|
// release, AFTER the mutation lands so an acquire reader that still sees
|
||||||
|
// the old epoch cannot have missed the mutation):
|
||||||
|
// * the frontend BufferBackendOps table - Respecify, SubData,
|
||||||
|
// FlushMappedRange, AcquirePersistentMap, ReadbackFromGpu, OnDestroy -
|
||||||
|
// which every frontend change-serial bump and every pending-range
|
||||||
|
// queueing reaches while ops are registered (upload, orphan/respecify,
|
||||||
|
// map flush/unmap writeback, persistent-map adoption, delete/pooling);
|
||||||
|
// * backend-initiated shadow writebacks that bump the frontend change
|
||||||
|
// serial without an op: transform-feedback capture readback
|
||||||
|
// (XfbImpl::ReadbackCapturedRanges and the scatter path) and every
|
||||||
|
// pack-PBO WritebackFromBackend site (glReadPixels/glGetTexImage);
|
||||||
|
// * RegisterBufferBackendOps/UnregisterBufferBackendOps - while ops are
|
||||||
|
// unregistered, frontend writes advance serials silently, so both edges
|
||||||
|
// of that window re-open every memo;
|
||||||
|
// * OnBackendContextDestroyed - the buffer context generation moved, so
|
||||||
|
// every previously clean resource is invalid.
|
||||||
|
// NOT bumped (cleanliness provably unchanged): MarkGpuWritten (the backend
|
||||||
|
// copy is authoritative; IsBufferDrawClean does not consult it),
|
||||||
|
// NotifyContentWrite on a GPU-resident buffer (persistent-mapped resources
|
||||||
|
// are clean by construction), and EnsureBufferResource itself (it only
|
||||||
|
// repairs toward clean). A non-persistent map (draws on it are GL errors
|
||||||
|
// the frontend rejects) sets IsMapped without an op; persistent maps reach
|
||||||
|
// AcquirePersistentMap or (FLUSH_EXPLICIT) publish only via FlushMappedRange.
|
||||||
|
Uint64 CurrentBufferMutationEpoch();
|
||||||
|
void BumpBufferMutationEpoch();
|
||||||
|
|
||||||
// The DirectGLES storage behind one frontend buffer. Owned (refcounted) by
|
// The DirectGLES storage behind one frontend buffer. Owned (refcounted) by
|
||||||
// the frontend BufferObject; immediate BufferBackendOps keep it current, so
|
// the frontend BufferObject; immediate BufferBackendOps keep it current, so
|
||||||
// draw-time "sync" reduces to ensuring the storage exists.
|
// draw-time "sync" reduces to ensuring the storage exists.
|
||||||
@@ -145,6 +248,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool pendingRespecify = false;
|
Bool pendingRespecify = false;
|
||||||
VecRange1D pendingRanges;
|
VecRange1D pendingRanges;
|
||||||
std::mutex pendingMutex;
|
std::mutex pendingMutex;
|
||||||
|
// Buffer-mutation epoch (see CurrentBufferMutationEpoch) at which this
|
||||||
|
// resource last probed IsBufferDrawClean == true, 0 = never (epochs start
|
||||||
|
// at 1). Written only on the draw thread; per-draw resource consumers
|
||||||
|
// (the UBO binding walk) skip the probe while their pre-pass epoch read
|
||||||
|
// matches, exactly like the per-VAO memo stamps.
|
||||||
|
Uint64 drawCleanEpoch = 0;
|
||||||
// Zero-copy coherent persistent map (EXT_buffer_storage): the GL store is
|
// Zero-copy coherent persistent map (EXT_buffer_storage): the GL store is
|
||||||
// immutable, persistently+coherently mapped, and persistentPtr is what the app
|
// immutable, persistently+coherently mapped, and persistentPtr is what the app
|
||||||
// (and the frontend PipeResource) write into directly. While set, draw-time
|
// (and the frontend PipeResource) write into directly. While set, draw-time
|
||||||
@@ -170,6 +279,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
GLESBufferResource* EnsureBufferResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
||||||
// Existing resource or nullptr; performs no GL calls.
|
// Existing resource or nullptr; performs no GL calls.
|
||||||
GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject);
|
GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject);
|
||||||
|
// True when EnsureBufferResource(frontend) would provably fall straight through
|
||||||
|
// every branch and do no work — i.e. `resource` is still the frontend's own
|
||||||
|
// resource, its id belongs to the live ES context, and either it is the
|
||||||
|
// zero-copy coherent persistent store (draw-time sync is a no-op by design) or
|
||||||
|
// the storage is initialized at the right size with no pending ops and a synced
|
||||||
|
// change serial while the buffer is not mapped (an active map may owe a
|
||||||
|
// per-draw persistent-range push, so it always takes the full path).
|
||||||
|
// `frontend` must be non-null and alive; the caller guarantees that by holding
|
||||||
|
// (or shadowing something that holds) a SharedPtr to it. Enables the per-VAO
|
||||||
|
// resolved-buffers memo to skip EnsureBufferResource on clean static buffers.
|
||||||
|
Bool IsBufferDrawClean(const MG_State::GLState::BufferObject* frontend, const GLESBufferResource* resource);
|
||||||
|
|
||||||
// Deletes GL buffers whose owning frontend objects died (possibly on a
|
// Deletes GL buffers whose owning frontend objects died (possibly on a
|
||||||
// thread without a current ES context). Called from draw-time sync.
|
// thread without a current ES context). Called from draw-time sync.
|
||||||
@@ -255,33 +375,104 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint GetBackendVertexArrayId() const { return m_backendVAOId; }
|
Uint GetBackendVertexArrayId() const { return m_backendVAOId; }
|
||||||
void Bind() const;
|
void Bind() const;
|
||||||
|
|
||||||
|
// Draw-path memo of SyncNeccessaryBuffers' attribute walk for this VAO: the
|
||||||
|
// distinct enabled-attribute buffers (deduped) and the index buffer, resolved
|
||||||
|
// to their backend resources once. Valid while the VAO's config version is
|
||||||
|
// unchanged — every attach/enable/disable/format mutation bumps it (the same
|
||||||
|
// invariant SyncToBackend's gate already leans on), and the VAO's attribute
|
||||||
|
// SharedPtrs pin each memoed frontend buffer for exactly that long, so the raw
|
||||||
|
// pointers cannot dangle on a hit. Per-buffer cleanliness is NOT memoed here:
|
||||||
|
// each hit re-checks IsBufferDrawClean (resource identity, context generation,
|
||||||
|
// pending ops, change serial) and falls back to EnsureBufferResource for just
|
||||||
|
// the dirty entries via their attribute index. The IBO entry is keyed on the
|
||||||
|
// slot's bound-object identity instead (its slot version is a wrapping Uint16
|
||||||
|
// and is not covered by the config version).
|
||||||
|
struct ResolvedDrawBuffers {
|
||||||
|
struct Entry {
|
||||||
|
MG_State::GLState::BufferObject* frontend = nullptr;
|
||||||
|
BufferImpl::GLESBufferResource* resource = nullptr;
|
||||||
|
Uint8 attribIndex = 0;
|
||||||
|
};
|
||||||
|
Bool valid = false;
|
||||||
|
Uint32 configVersion = 0;
|
||||||
|
Uint count = 0;
|
||||||
|
Array<Entry, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> entries;
|
||||||
|
MG_State::GLState::BufferObject* iboFrontend = nullptr;
|
||||||
|
BufferImpl::GLESBufferResource* iboResource = nullptr;
|
||||||
|
// Buffer-mutation epoch (BufferImpl::CurrentBufferMutationEpoch) at which
|
||||||
|
// the LAST probe pass found every entry / the IBO clean; 0 = not stamped
|
||||||
|
// (epochs start at 1). While a stamp matches the pre-pass epoch read, the
|
||||||
|
// probes are skipped outright: any path that can dirty ANY buffer bumps
|
||||||
|
// the epoch (the exhaustive site list lives at the epoch declaration).
|
||||||
|
// The IBO stamp is only trusted together with the bound-object identity
|
||||||
|
// compare - the VAO's index slot can rebind with no epoch or config move.
|
||||||
|
Uint64 vboCleanEpoch = 0;
|
||||||
|
Uint64 iboCleanEpoch = 0;
|
||||||
|
};
|
||||||
|
ResolvedDrawBuffers& GetResolvedDrawBuffersMemo() { return m_resolvedDrawBuffers; }
|
||||||
|
|
||||||
|
// Memo for SyncCurrentVertexAttributeValues: which of a program's ACTIVE
|
||||||
|
// attribute locations lack an enabled array in this VAO (those read the
|
||||||
|
// context's current generic value instead of a buffer). Keyed on the VAO
|
||||||
|
// config version (enable/disable bumps it) and the program's active-location
|
||||||
|
// mask. Hosted per twin — the former function-static single entry missed on
|
||||||
|
// every draw once the app cycled VAOs, re-reading the cold attribute slots.
|
||||||
|
struct PendingAttribValueMask {
|
||||||
|
Bool valid = false;
|
||||||
|
Uint32 configVersion = 0;
|
||||||
|
Uint32 activeMask = 0;
|
||||||
|
Uint32 pendingMask = 0;
|
||||||
|
};
|
||||||
|
PendingAttribValueMask& GetPendingAttribValueMaskMemo() { return m_pendingAttribValueMask; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
ResolvedDrawBuffers m_resolvedDrawBuffers;
|
||||||
|
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;
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
Uint16 m_syncedIndexBufferVersion = 0;
|
Uint16 m_syncedIndexBufferVersion = 0;
|
||||||
|
// 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
|
||||||
|
// its only writers), so an unchanged config version proves every per-attribute
|
||||||
|
// compare in SyncToBackend would come up clean. The index-buffer slot has its own
|
||||||
|
// version and is NOT covered. The Bool (not a sentinel value) marks "never synced".
|
||||||
|
Bool m_hasSyncedConfigVersion = false;
|
||||||
|
Uint32 m_syncedConfigVersion = 0;
|
||||||
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
|
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
|
||||||
m_syncedAttributeVersions;
|
m_syncedAttributeVersions;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
|
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
|
||||||
g_backendVertexArrayObjects;
|
g_backendVertexArrayObjects;
|
||||||
|
|
||||||
|
// Shadowed glBindVertexArray: every backend VAO bind goes through here so a
|
||||||
|
// draw's second bind of the same VAO (SyncToBackend, then PrepareForDraw's
|
||||||
|
// re-bind) reaches the driver once. Invalidate whenever the ES context is
|
||||||
|
// replaced - ids restart and the resting binding is 0 again.
|
||||||
|
void BindBackendVAOId(Uint id);
|
||||||
|
void InvalidateVAOBindingCache();
|
||||||
|
// ES resets the binding to 0 when the currently bound VAO is deleted.
|
||||||
|
void NoteVAOIdDeleted(Uint id);
|
||||||
} // namespace VertexArrayImpl
|
} // namespace VertexArrayImpl
|
||||||
|
|
||||||
namespace TextureImpl {
|
namespace TextureImpl {
|
||||||
inline Bool IsSupportedTextureTarget(TextureTarget target) {
|
inline Bool IsSupportedTextureTarget(TextureTarget target) {
|
||||||
// Rectangle textures need non-normalized sampling ES cannot express; everything else is
|
// Every desktop-only target is stored on an ES one; see MapToBackendTextureTarget.
|
||||||
// either native or emulated (1D -> 2D with height 1, 1D array -> 2D array, see
|
(void)target;
|
||||||
// MapToBackendTextureTarget). SPIRV-Cross already emits the matching ESSL samplers and
|
return true;
|
||||||
// coordinate padding for 1D/1D-array shaders.
|
|
||||||
return target != TextureTarget::TextureRectangle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ES has no 1D targets: 1D textures are stored as 2D (height 1) and 1D arrays as 2D arrays
|
// ES has none of the desktop-only targets: 1D textures are stored as 2D (height 1), 1D
|
||||||
// (height 1, layers in depth). Must match SPIRV-Cross's ES 1D-as-2D shader emulation.
|
// arrays as 2D arrays (height 1, layers in depth), and rectangle textures as plain 2D -
|
||||||
|
// they are single-level and already clamp, so only the non-normalized coordinates differ.
|
||||||
|
// Must match the shader-side emulation: SPIRV-Cross handles 1D/1D-array itself, and
|
||||||
|
// ShaderCompiler::LowerRectImages rewrites rectangle images (declining any module
|
||||||
|
// whose lookups are not integer-coordinate, which SPIRV-Cross then still rejects).
|
||||||
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
|
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
|
||||||
switch (target) {
|
switch (target) {
|
||||||
case TextureTarget::Texture1D:
|
case TextureTarget::Texture1D:
|
||||||
|
case TextureTarget::TextureRectangle:
|
||||||
return TextureTarget::Texture2D;
|
return TextureTarget::Texture2D;
|
||||||
case TextureTarget::Texture1DArray:
|
case TextureTarget::Texture1DArray:
|
||||||
return TextureTarget::Texture2DArray;
|
return TextureTarget::Texture2DArray;
|
||||||
@@ -297,6 +488,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) {
|
inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) {
|
||||||
switch (uploadTarget) {
|
switch (uploadTarget) {
|
||||||
case TextureUploadTarget::Texture1D:
|
case TextureUploadTarget::Texture1D:
|
||||||
|
case TextureUploadTarget::TextureRectangle:
|
||||||
return GL_TEXTURE_2D;
|
return GL_TEXTURE_2D;
|
||||||
case TextureUploadTarget::Texture1DArray:
|
case TextureUploadTarget::Texture1DArray:
|
||||||
return GL_TEXTURE_2D_ARRAY;
|
return GL_TEXTURE_2D_ARRAY;
|
||||||
@@ -323,6 +515,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return target == TextureTarget::Texture3D || target == TextureTarget::TextureCubeMap;
|
return target == TextureTarget::Texture3D || target == TextureTarget::TextureCubeMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Components per texel the frontend format's client data carries, for the three-channel
|
||||||
|
// formats that can be widened to a four-channel colour-renderable target; 0 for everything
|
||||||
|
// else. See PrepareChannelWidenedUpload.
|
||||||
|
Uint GetWidenableClientComponentCount(TextureInternalFormat format);
|
||||||
|
|
||||||
|
// True when a widenable format's components are integer rather than normalized, which is
|
||||||
|
// what decides the synthetic alpha's value: GL_RGB8I and GL_RGB8_SNORM are both uploaded
|
||||||
|
// as GL_BYTE, but their 1.0 is 1 and 0x7F respectively.
|
||||||
|
Bool IsIntegerWidenableFormat(TextureInternalFormat format);
|
||||||
|
|
||||||
|
// Repacks three-component client data as four components with an alpha of 1.0 in
|
||||||
|
// `uploadType`, for a format the backend widened to keep a colour attachment renderable.
|
||||||
|
// 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
|
||||||
|
// scratch buffer and has to outlive the returned pointer.
|
||||||
|
const void* PrepareChannelWidenedUpload(Uint componentCount, const IntVec3& texelSize, const void* data,
|
||||||
|
SizeT byteSize, GLenum uploadType, Vector<Uint8>& widenedData,
|
||||||
|
Bool integerData = false);
|
||||||
|
|
||||||
struct StateTextureBasicInfo { // Used for tracking texture state changes
|
struct StateTextureBasicInfo { // Used for tracking texture state changes
|
||||||
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
|
||||||
SizeT width = 0;
|
SizeT width = 0;
|
||||||
@@ -360,6 +571,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void Bind(GLenum target, Uint unit = TempTextureUnit);
|
void Bind(GLenum target, Uint unit = TempTextureUnit);
|
||||||
Uint GetBackendTextureId() const;
|
Uint GetBackendTextureId() const;
|
||||||
|
|
||||||
|
// Aggregate first-level clean gate for the per-draw trio
|
||||||
|
// SyncTextureParamsToBackend + SyncBuiltinSamplerToBackend +
|
||||||
|
// SyncMipmapsToBackend: EXACTLY the conjunction of their own early-outs
|
||||||
|
// (params version == synced params version; builtin-sampler version ==
|
||||||
|
// synced sampler version; and SyncMipmapsToBackend's cheap gate - stamped
|
||||||
|
// trio + content version + Mipmap storage). True means each of the three
|
||||||
|
// would provably return without work, so the caller may skip the calls;
|
||||||
|
// false only falls through to the three calls, whose own gates re-decide
|
||||||
|
// individually - this gate must never be MORE permissive than they are.
|
||||||
|
// `contextId`/`samplingGeneration` are the frontend context's current
|
||||||
|
// values, hoisted by the caller so a per-draw list walk reads them once
|
||||||
|
// instead of per texture. `t` must be the live frontend texture.
|
||||||
|
Bool IsDrawSyncClean(const MG_State::GLState::ITextureObject* t, Uint64 contextId,
|
||||||
|
Uint64 samplingGeneration) const {
|
||||||
|
if (!m_isInitialized || m_syncedShapeContextId == 0 || m_syncedShapeContextId != contextId ||
|
||||||
|
m_syncedShapeGeneration != samplingGeneration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const Uint16 paramsVersion = t->GetTextureParamsVersion();
|
||||||
|
if (m_syncedShapeParamsVersion != paramsVersion || m_syncedTextureParamsVersion != paramsVersion) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (m_syncedContentVersion == 0 || m_syncedContentVersion != t->GetContentVersion()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& samplerObject = t->GetSamplerObject();
|
||||||
|
if (!samplerObject || m_syncedSamplerVersion != samplerObject->GetVersion()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return t->GetStorageType() == TextureStorageType::Mipmap;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void RecreateBackendTexture();
|
void RecreateBackendTexture();
|
||||||
|
|
||||||
@@ -371,6 +614,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool m_imageBindableStorageRequired = false;
|
Bool m_imageBindableStorageRequired = false;
|
||||||
Bool m_backendStorageImmutable = false;
|
Bool m_backendStorageImmutable = false;
|
||||||
StateTextureBasicInfo m_prevTextureInfo;
|
StateTextureBasicInfo m_prevTextureInfo;
|
||||||
|
// Frontend content version at the last completed mipmap sync. The per-draw
|
||||||
|
// clean probe compares this before rebuilding shape info and scanning
|
||||||
|
// per-level dirty flags; 0 never matches a real version (they start at 1).
|
||||||
|
Uint64 m_syncedContentVersion = 0;
|
||||||
|
// First-level clean gate for SyncMipmapsToBackend, checked before even the
|
||||||
|
// IsComplete()/shape-probe walk. Valid only as a trio with the content and
|
||||||
|
// texture-params versions: the context's sampling-resolution generation moves on
|
||||||
|
// EVERY texture-shape mutation (BumpShapeVersion is the only writer of shape and
|
||||||
|
// unconditionally bumps it), the content version on every CPU pixel mutation, and
|
||||||
|
// the params version covers SetSamples/SetFixedSampleLocations, which bump neither
|
||||||
|
// of the other two but feed the shape probe. The context id pins the generation to
|
||||||
|
// the context that produced it - generations restart at 0 with a new context, and a
|
||||||
|
// texture is owned by exactly one context (share groups are not implemented), so a
|
||||||
|
// mutation can never happen under a context this key does not name. 0 = never
|
||||||
|
// stamped (real context ids start at 1). Backend-side invalidation rides on
|
||||||
|
// m_isInitialized: RequireImageBindableStorage and RecreateBackendTexture clear it.
|
||||||
|
Uint64 m_syncedShapeContextId = 0;
|
||||||
|
Uint64 m_syncedShapeGeneration = 0;
|
||||||
|
Uint16 m_syncedShapeParamsVersion = 0;
|
||||||
SamplerParameters m_cacheSamplerParameters;
|
SamplerParameters m_cacheSamplerParameters;
|
||||||
UintVec2 m_cacheLodRange = {0, 1000};
|
UintVec2 m_cacheLodRange = {0, 1000};
|
||||||
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||||
@@ -387,6 +649,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
|
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
|
||||||
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||||
Bool imageBindableStorageRequired = false);
|
Bool imageBindableStorageRequired = false);
|
||||||
|
// Brings every texture the next draw reads - the touched units' bindings and the draw
|
||||||
|
// FBO's texture attachments - onto the backend, through the two borrowed-pair memos
|
||||||
|
// documented at their definitions. Declared here so tests can drive those memos directly.
|
||||||
|
void SyncNeccessaryTextures();
|
||||||
extern Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
|
extern Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
|
||||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
||||||
g_boundTexturesCache;
|
g_boundTexturesCache;
|
||||||
@@ -429,6 +695,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
this array could be provided as data directly to ES `glDrawBuffers` function
|
this array could be provided as data directly to ES `glDrawBuffers` function
|
||||||
*/
|
*/
|
||||||
GLenum m_backendDrawBuffers[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS] = {GL_NONE};
|
GLenum m_backendDrawBuffers[MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS] = {GL_NONE};
|
||||||
|
|
||||||
|
static constexpr Uint MAX_COLOR_ATTACHMENT_SLOTS =
|
||||||
|
static_cast<Uint>(FramebufferAttachmentType::Color31) -
|
||||||
|
static_cast<Uint>(FramebufferAttachmentType::Color0) + 1;
|
||||||
|
/* Where each frontend GL_COLOR_ATTACHMENTn image physically lives in the backend ES
|
||||||
|
framebuffer, as a GL_COLOR_ATTACHMENTm enum. ES only accepts glDrawBuffers bufs[s] ==
|
||||||
|
GL_COLOR_ATTACHMENTs, so a GL draw-buffer slot s naming attachment a forces a's image
|
||||||
|
under backend slot s. This table is the single owner of that decision and is kept a
|
||||||
|
PERMUTATION of the backend colour slots: every other attachment keeps its identity
|
||||||
|
slot when that slot survived, and is parked on the lowest free slot when it did not.
|
||||||
|
Deriving the point per-query from the draw-buffer array instead handed the identity
|
||||||
|
point to any attachment that was not a draw buffer - i.e. exactly the point a
|
||||||
|
relocated draw buffer had just taken over. The permutation is only true of the
|
||||||
|
PHYSICAL framebuffer because the attachment loop detaches a point whose frontend
|
||||||
|
owner is empty; do not remove that detach. */
|
||||||
|
GLenum m_backendColorSlots[MAX_COLOR_ATTACHMENT_SLOTS] = {GL_NONE};
|
||||||
|
/* Rebuild m_backendColorSlots from the frontend draw-buffer array. Returns true when any
|
||||||
|
attachment moved, i.e. when the physical attachments and the memoised read buffer have
|
||||||
|
to be re-applied. */
|
||||||
|
Bool RecomputeBackendColorSlots(
|
||||||
|
const MG_State::GLState::FramebufferObject::FramebufferAttachmentArray& stateDrawBuffers);
|
||||||
|
|
||||||
FramebufferAttachmentType m_frontendReadBuffer = FramebufferAttachmentType::Color0;
|
FramebufferAttachmentType m_frontendReadBuffer = FramebufferAttachmentType::Color0;
|
||||||
GLenum m_backendReadBuffer = GL_COLOR_ATTACHMENT0;
|
GLenum m_backendReadBuffer = GL_COLOR_ATTACHMENT0;
|
||||||
|
|
||||||
@@ -438,11 +726,88 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
|
|
||||||
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
|
||||||
g_backendFramebufferObjects;
|
g_backendFramebufferObjects;
|
||||||
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions;
|
// True when the read buffer names a fixed-point (norm/snorm) attachment that the
|
||||||
|
// backend actually stores in a floating-point format. GL clamps a read from a
|
||||||
|
// fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR defaults to
|
||||||
|
// GL_FIXED_ONLY); the substituted float storage would not, so the readback path
|
||||||
|
// has to apply the clamp itself.
|
||||||
|
Bool IsFixedPointFallbackReadAttachment();
|
||||||
|
|
||||||
|
// True when the read buffer names a three-channel attachment the backend actually stores
|
||||||
|
// in a four-channel format (the colour-renderable widening). A format without alpha reads
|
||||||
|
// back as 1.0, so the readback path has to overwrite the alpha the draw left behind -
|
||||||
|
// unconditionally, since this is the format's own semantics rather than the
|
||||||
|
// GL_CLAMP_READ_COLOR rule the clamp above implements.
|
||||||
|
Bool IsAlphaWidenedFallbackReadAttachment();
|
||||||
|
|
||||||
|
// True when this attachment's storage carries an alpha channel its frontend format does
|
||||||
|
// not (the three-channel colour-renderable widening).
|
||||||
|
Bool IsAlphaWidenedColorAttachment(const MG_State::GLState::FramebufferAttachmentObject& attachmentObject);
|
||||||
|
|
||||||
|
// Bit i set = DRAW BUFFER i of `fbo` resolves to a colour attachment the backend widened
|
||||||
|
// from three channels to four. Indexed by draw-buffer slot, not by attachment point,
|
||||||
|
// because that is what glColorMaski / glClearBufferfv address.
|
||||||
|
Uint32 ComputeAlphaWidenedDrawBufferMask(const MG_State::GLState::FramebufferObject& fbo);
|
||||||
|
|
||||||
|
// The same mask for whatever is currently bound to GL_DRAW_FRAMEBUFFER, recomputed by
|
||||||
|
// SyncCurrentFBO (BackendFramebufferObject::SyncToBackend for the DRAW target, and reset
|
||||||
|
// to 0 on the default framebuffer). Read by the draw/clear state sync, so it is only
|
||||||
|
// trustworthy after SyncCurrentFBO has run in the same entry point.
|
||||||
|
//
|
||||||
|
// WHY IT EXISTS (the dst-alpha discipline). A widened attachment has a real alpha channel
|
||||||
|
// the application's format does not, and GL says a missing channel reads as 1.0. Readback
|
||||||
|
// can paper over that (ForceWideReadAlphaToOne), but GL_DST_ALPHA /
|
||||||
|
// GL_ONE_MINUS_DST_ALPHA blending and glBlitFramebuffer read the STORED alpha inside the
|
||||||
|
// driver where no interception is possible. So the stored alpha is kept at 1.0 instead:
|
||||||
|
// a clear touching a widened buffer writes alpha 1.0, and every draw into it has its
|
||||||
|
// alpha write mask forced off, so nothing can ever move it again. The application's own
|
||||||
|
// colour mask is untouched - glGet(GL_COLOR_WRITEMASK) still reports what it set.
|
||||||
|
extern Uint32 g_alphaWidenedDrawBufferMask;
|
||||||
|
|
||||||
|
// Bit i set = DRAW BUFFER i of the framebuffer bound as DRAW resolves to a colour
|
||||||
|
// attachment with an INTEGER format. Recomputed beside the mask above and for its sake:
|
||||||
|
// glClearBufferfv on an integer colour buffer is GL_INVALID_OPERATION, so the
|
||||||
|
// per-draw-buffer clear route the widening needs has to stand down when one is present.
|
||||||
|
// (glClear on an integer colour buffer is left undefined by ES in the first place, and
|
||||||
|
// an application that wants a defined answer has to call glClearBufferuiv/iv - which does
|
||||||
|
// carry the widened alpha substitution.)
|
||||||
|
extern Uint32 g_integerColorDrawBufferMask;
|
||||||
|
|
||||||
|
// The colour a clear has to hand the driver for one draw buffer: the application's value,
|
||||||
|
// except that a widened attachment's alpha is replaced by the 1.0 its three-channel
|
||||||
|
// format implies. `one` is 1.0 encoded in the clear call's own component type - the
|
||||||
|
// integer clears carry the integer 1, the float clear carries 1.0f.
|
||||||
|
//
|
||||||
|
// Returns `value` itself when nothing is substituted, so the ordinary path allocates and
|
||||||
|
// copies nothing; `scratch` is the caller's buffer and has to outlive the returned
|
||||||
|
// pointer. Free of GL state on purpose, so the substitution can be unit-tested exactly as
|
||||||
|
// the driver sees it.
|
||||||
|
template <typename T>
|
||||||
|
const T* SubstituteWidenedClearAlpha(const T* value, Bool widened, T one, T (&scratch)[4]) {
|
||||||
|
if (!widened || value == nullptr) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
scratch[0] = value[0];
|
||||||
|
scratch[1] = value[1];
|
||||||
|
scratch[2] = value[2];
|
||||||
|
scratch[3] = one;
|
||||||
|
return scratch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// What SyncCurrentFBO last pushed for each target, as a (binding, object, revision)
|
||||||
|
// triple; it re-syncs unless all three still match. Stamped by SyncCurrentFBO and
|
||||||
|
// ForceBindCurrentFBO, cleared by InvalidateFramebufferBindingCache. The three are
|
||||||
|
// only meaningful together - see SyncCurrentFBO.
|
||||||
|
//
|
||||||
|
// The binding slot's own version, which changes whenever a different object is bound
|
||||||
|
// to this target. Distinguishes a rebind from an in-place edit, and keeps the raw
|
||||||
|
// pointer below from matching an address the allocator recycled for a new FBO.
|
||||||
|
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedSlotVersions;
|
||||||
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
|
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
|
||||||
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
|
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
|
||||||
// must re-sync it even when the binding-slot version has not moved.
|
// must re-sync it even when the binding-slot version has not moved.
|
||||||
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions;
|
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions;
|
||||||
|
// Which object was synced. Raw and never dereferenced: only compared for identity.
|
||||||
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
|
||||||
g_fboSyncedObjects;
|
g_fboSyncedObjects;
|
||||||
|
|
||||||
@@ -538,6 +903,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void InvalidatePackStateCache();
|
void InvalidatePackStateCache();
|
||||||
} // namespace PixelStoreImpl
|
} // namespace PixelStoreImpl
|
||||||
|
|
||||||
|
namespace SamplerImpl {
|
||||||
|
class BackendSamplerObject; // for PrgramImpl's sampler-pass memo rows below
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -576,6 +945,49 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Int backendLocation = -1;
|
Int backendLocation = -1;
|
||||||
GLenum uniformType = 0;
|
GLenum uniformType = 0;
|
||||||
Int lastAssignedUnit = -1;
|
Int lastAssignedUnit = -1;
|
||||||
|
// Location of this sampler's emulated GL_TEXTURE_LOD_BIAS uniform
|
||||||
|
// (PrgramImpl::EmulateTextureLodBias), -1 when the shader has none.
|
||||||
|
// lastAssignedLodBias mirrors the value the program currently holds,
|
||||||
|
// so an unbiased shader issues no per-draw glUniform1f at all.
|
||||||
|
Int lodBiasLocation = -1;
|
||||||
|
Float lastAssignedLodBias = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Memo of the whole per-draw sampler-uniform pass (glUniform1i unit
|
||||||
|
// assignments, lod-bias uniform, raw-depth-fetch substitution and the
|
||||||
|
// per-unit sampler-object binds) in BindCurrentProgramWithResources.
|
||||||
|
// The pass is a pure function of the keys below, and its only driver-side
|
||||||
|
// effect is the sampler binding of each sampled unit, so replaying it as
|
||||||
|
// "do nothing" additionally requires those bindings to still be on the
|
||||||
|
// driver - the per-entry row compare against g_boundSamplersCache (the
|
||||||
|
// shadow every sampler bind in this backend already routes through).
|
||||||
|
//
|
||||||
|
// Invalidation enumeration:
|
||||||
|
// * sampler-uniform unit assignment (glUniform1i) and uniform-block
|
||||||
|
// binding edits -> frontend backendStateVersion;
|
||||||
|
// * any texture/sampler bind moving on any unit (incl. the high-water
|
||||||
|
// mark moving) -> unitBindingsEpoch;
|
||||||
|
// * any sampler parameter (incl. lod bias, compare mode) or texture
|
||||||
|
// shape/format change -> samplingGeneration;
|
||||||
|
// * another frontend context -> contextId (never-reused id);
|
||||||
|
// * ES context recreation -> textureContextGeneration;
|
||||||
|
// * relink / backend program rebuild -> SyncToBackend resets `valid`
|
||||||
|
// (it rebuilds m_samplerUniformBindings, whose lastAssignedUnit /
|
||||||
|
// lastAssignedLodBias dedup state this memo leans on);
|
||||||
|
// * any other writer moving a sampled unit's sampler binding
|
||||||
|
// (BindCurrentUnitSamplers on a unit-sampler change, scratch binds)
|
||||||
|
// -> the row snapshot compare.
|
||||||
|
struct SamplerPassMemo {
|
||||||
|
static constexpr SizeT kMaxEntries = 16;
|
||||||
|
Bool valid = false;
|
||||||
|
Uint8 count = 0;
|
||||||
|
Uint64 contextId = 0;
|
||||||
|
Uint64 unitBindingsEpoch = 0;
|
||||||
|
Uint64 samplingGeneration = 0;
|
||||||
|
Uint32 backendStateVersion = 0;
|
||||||
|
Uint textureContextGeneration = 0;
|
||||||
|
Array<Uint8, kMaxEntries> units{};
|
||||||
|
Array<SamplerImpl::BackendSamplerObject*, kMaxEntries> rows{};
|
||||||
};
|
};
|
||||||
|
|
||||||
BackendProgramObjectImpl();
|
BackendProgramObjectImpl();
|
||||||
@@ -585,11 +997,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
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;
|
||||||
|
// True when the transpiled program kept a gl_DrawID uniform, i.e. SetDrawID
|
||||||
|
// actually reaches a shader read rather than being discarded.
|
||||||
|
Bool ReadsDrawID() const { return m_drawIdUniformLocation >= 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
|
||||||
|
// shader failed to transpile or compile, or the link itself failed). Use()
|
||||||
|
// must not leave the previously bound program current in that case.
|
||||||
|
Bool IsBackendProgramUsable() const { return m_backendProgramUsable; }
|
||||||
Uint GetBackendGlobalUBOId() const { return m_backendGlobalUBOId; }
|
Uint GetBackendGlobalUBOId() const { return m_backendGlobalUBOId; }
|
||||||
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
|
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
|
||||||
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
|
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
|
||||||
|
Uint GetFragColorBroadcastCount() const { return m_fragColorBroadcastCount; }
|
||||||
|
|
||||||
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; }
|
||||||
@@ -601,6 +1021,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// reflected size when the transpiled block pads differently).
|
// reflected size when the transpiled block pads differently).
|
||||||
Int GetGlobalUboBackendBlockSize() const { return m_globalUboBackendBlockSize; }
|
Int GetGlobalUboBackendBlockSize() const { return m_globalUboBackendBlockSize; }
|
||||||
BufferImpl::UboRingAllocation& GetGlobalUboRingAllocation() { return m_globalUboRingAllocation; }
|
BufferImpl::UboRingAllocation& GetGlobalUboRingAllocation() { return m_globalUboRingAllocation; }
|
||||||
|
SamplerPassMemo& GetSamplerPassMemo() { return m_samplerPassMemo; }
|
||||||
// Frontend link version this backend program (and its resource caches) was
|
// Frontend link version this backend program (and its resource caches) was
|
||||||
// built from; a mismatch means every link-derived cache here is stale.
|
// built from; a mismatch means every link-derived cache here is stale.
|
||||||
Uint32 GetSyncedLinkVersion() const { return m_syncedLinkVersion; }
|
Uint32 GetSyncedLinkVersion() const { return m_syncedLinkVersion; }
|
||||||
@@ -616,7 +1037,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Int m_indirectParamsBinding = -1;
|
Int m_indirectParamsBinding = -1;
|
||||||
Uint32 m_snormFallbackClampOutputMask = 0;
|
Uint32 m_snormFallbackClampOutputMask = 0;
|
||||||
Uint32 m_unormFallbackClampOutputMask = 0;
|
Uint32 m_unormFallbackClampOutputMask = 0;
|
||||||
|
// Draw buffers a legacy gl_FragColor write has to reach (see
|
||||||
|
// PrgramImpl::BroadcastLegacyFragColor); 1 keeps the plain single-output shader.
|
||||||
|
Uint m_fragColorBroadcastCount = 1;
|
||||||
Bool m_isInitialized = false;
|
Bool m_isInitialized = false;
|
||||||
|
Bool m_backendProgramUsable = false;
|
||||||
|
|
||||||
Int m_globalUboBackendBlockIndex = -1;
|
Int m_globalUboBackendBlockIndex = -1;
|
||||||
Int m_globalUboBackendBlockSize = 0;
|
Int m_globalUboBackendBlockSize = 0;
|
||||||
@@ -625,16 +1050,37 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint32 m_lastUploadedGlobalUboVersion = ~0u;
|
Uint32 m_lastUploadedGlobalUboVersion = ~0u;
|
||||||
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
||||||
Uint32 m_syncedLinkVersion = ~0u;
|
Uint32 m_syncedLinkVersion = ~0u;
|
||||||
|
SamplerPassMemo m_samplerPassMemo;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Uint32 g_snormFallbackClampOutputMask;
|
extern Uint32 g_snormFallbackClampOutputMask;
|
||||||
extern Uint32 g_unormFallbackClampOutputMask;
|
extern Uint32 g_unormFallbackClampOutputMask;
|
||||||
|
// Draw buffers the current draw framebuffer enables. Like the clamp masks above it
|
||||||
|
// is framebuffer state that the shader has to be compiled against, so a program
|
||||||
|
// whose snapshot no longer matches is relinked.
|
||||||
|
extern Uint g_fragColorBroadcastCount;
|
||||||
// Backend id of the last glUseProgram issued through this backend; lets Use()
|
// Backend id of the last glUseProgram issued through this backend; lets Use()
|
||||||
// skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the
|
// skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the
|
||||||
// ES context is recreated.
|
// ES context is recreated.
|
||||||
extern Uint g_lastUsedBackendProgramId;
|
extern Uint g_lastUsedBackendProgramId;
|
||||||
extern StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl>
|
extern StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl>
|
||||||
g_backendProgramObjects;
|
g_backendProgramObjects;
|
||||||
|
|
||||||
|
// Points one shader storage block of an ALREADY-LINKED backend program at
|
||||||
|
// `binding`. `blockName` is the frontend interface-query spelling; the real
|
||||||
|
// driver's own index for it is looked up here, because the transpiled ESSL's
|
||||||
|
// block order is not the frontend's. Returns false when the block does not exist
|
||||||
|
// on the backend program (eliminated as unused, or the driver lacks the entry
|
||||||
|
// points), which is not an error - GL_BUFFER_BINDING is served from the frontend
|
||||||
|
// record either way.
|
||||||
|
Bool ApplyShaderStorageBlockBinding(Uint backendProgramId, const String& blockName, Uint binding);
|
||||||
|
// Replays every glShaderStorageBlockBinding recorded on the program onto a backend
|
||||||
|
// program that was just built. The frontend record is authoritative (only the
|
||||||
|
// shader's DECLARED binding survives in the SPIR-V), so without this replay any
|
||||||
|
// rebuild would silently revert rebound blocks. Mirrors DirectVulkan's
|
||||||
|
// reseed-on-rebuild in BuildProgramResourceCache.
|
||||||
|
void ReseedShaderStorageBlockBindings(Uint backendProgramId,
|
||||||
|
const MG_State::GLState::ProgramObject& stateProgramObject);
|
||||||
} // namespace PrgramImpl
|
} // namespace PrgramImpl
|
||||||
|
|
||||||
namespace SamplerImpl {
|
namespace SamplerImpl {
|
||||||
|
|||||||
@@ -0,0 +1,894 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "MultiDraw.h"
|
||||||
|
#include "Managers.h"
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||||
|
using MG_Config::GLESMultiDrawMode;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Batch shape
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SizeT IndexTypeSize(GLenum type) {
|
||||||
|
switch (type) {
|
||||||
|
case GL_UNSIGNED_BYTE: return 1;
|
||||||
|
case GL_UNSIGNED_SHORT: return 2;
|
||||||
|
case GL_UNSIGNED_INT: return 4;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The all-ones value of an index type, which is what GL restarts on once
|
||||||
|
// primitive restart is in play. CheckPrimitiveRestartSupported has already
|
||||||
|
// rejected the arbitrary-index form of GL_PRIMITIVE_RESTART, so an enabled
|
||||||
|
// restart always restarts here and nowhere else.
|
||||||
|
Uint32 RestartSentinelFor(GLenum type) {
|
||||||
|
switch (type) {
|
||||||
|
case GL_UNSIGNED_BYTE: return 0xFFu;
|
||||||
|
case GL_UNSIGNED_SHORT: return 0xFFFFu;
|
||||||
|
default: return 0xFFFFFFFFu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool RestartActive() {
|
||||||
|
return MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||||
|
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertices per primitive for the modes whose sub-draws may be concatenated into a
|
||||||
|
// single draw without changing the primitive stream. Zero for strip/loop/fan modes
|
||||||
|
// (concatenation would weld one sub-draw's last primitive to the next sub-draw's
|
||||||
|
// first) and for GL_PATCHES, whose primitive size is dynamic tessellation state.
|
||||||
|
Uint32 ConcatenablePrimitiveSize(GLenum mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case GL_POINTS: return 1;
|
||||||
|
case GL_LINES: return 2;
|
||||||
|
case GL_TRIANGLES: return 3;
|
||||||
|
case GL_LINES_ADJACENCY: return 4;
|
||||||
|
case GL_TRIANGLES_ADJACENCY: return 6;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beyond this an emulated batch would ask for a scratch allocation measured in
|
||||||
|
// hundreds of megabytes (and the scratch ring never shrinks again); decline and let
|
||||||
|
// a per-sub-draw tier handle it instead of trying and failing inside the driver.
|
||||||
|
constexpr SizeT kMaxFlattenedIndices = SizeT{1} << 24;
|
||||||
|
|
||||||
|
// The flattening dispatch is one invocation per output index. ES 3.1 only
|
||||||
|
// guarantees 65535 work groups per dimension, and exceeding it makes
|
||||||
|
// glDispatchCompute an INVALID_VALUE no-op - which would leave the draw reading an
|
||||||
|
// uninitialised index buffer rather than failing visibly. Cap the tier there
|
||||||
|
// instead of querying: 4.19M indices is far past any real multi-draw batch, and
|
||||||
|
// beyond it the per-sub-draw tiers are the better answer anyway.
|
||||||
|
constexpr SizeT kComputeWorkGroupSize = 64;
|
||||||
|
constexpr SizeT kMaxComputeWorkGroups = 65535;
|
||||||
|
constexpr SizeT kMaxComputeFlattenedIndices = kMaxComputeWorkGroups * kComputeWorkGroupSize;
|
||||||
|
|
||||||
|
Uint BoundDrawIndirectBufferId() {
|
||||||
|
const auto& indirect =
|
||||||
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||||
|
if (!indirect) return 0;
|
||||||
|
const auto* resource = BufferImpl::EnsureBufferResource(indirect);
|
||||||
|
return resource ? resource->id : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() {
|
||||||
|
static const SharedPtr<MG_State::GLState::BufferObject> none;
|
||||||
|
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||||
|
if (!vao) return none;
|
||||||
|
return vao->GetIndexBufferBindingSlot().GetBoundObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The GL name PrepareForDraw left on GL_ELEMENT_ARRAY_BUFFER, i.e. what a tier
|
||||||
|
// that swaps in a scratch index buffer has to put back. Restoring the exact name
|
||||||
|
// matters beyond tidiness: the VAO twin memoises that it already synced this
|
||||||
|
// index binding and will not re-issue it on the next draw.
|
||||||
|
Uint BoundIndexBufferId() {
|
||||||
|
const auto& ibo = BoundIndexBuffer();
|
||||||
|
if (!ibo) return 0;
|
||||||
|
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
|
||||||
|
return resource ? resource->id : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Scratch GL objects
|
||||||
|
//
|
||||||
|
// All of them belong to the ES context and are abandoned (not deleted) when it
|
||||||
|
// dies, exactly like XfbImpl's scatter buffer: the names are the dead context's
|
||||||
|
// to reclaim, and deleting them would target whatever the successor context
|
||||||
|
// handed out for the same name.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct ScratchBuffer {
|
||||||
|
Uint id = 0;
|
||||||
|
SizeT capacity = 0;
|
||||||
|
SizeT cursor = 0; // ring buffers only: next free byte
|
||||||
|
};
|
||||||
|
|
||||||
|
ScratchBuffer g_indirectCommands; // synthesized DrawElementsIndirectCommand array
|
||||||
|
ScratchBuffer g_rebasedIndices; // CPU-rebased index stream
|
||||||
|
ScratchBuffer g_drawInfo; // compute tier: per-sub-draw descriptors
|
||||||
|
ScratchBuffer g_flattenedIndices; // compute tier: flattened index stream
|
||||||
|
|
||||||
|
Uint g_computeProgram = 0;
|
||||||
|
Bool g_computeProgramFailed = false;
|
||||||
|
GLint g_uElementSize = -1;
|
||||||
|
GLint g_uDrawCount = -1;
|
||||||
|
GLint g_uTotalIndices = -1;
|
||||||
|
|
||||||
|
// Reused staging, so a steady stream of batches allocates nothing.
|
||||||
|
Vector<DrawElementsIndirectCommand> g_commandStaging;
|
||||||
|
Vector<Uint32> g_indexStaging;
|
||||||
|
Vector<Uint32> g_drawInfoStaging;
|
||||||
|
Vector<GLint> g_zeroBaseVertices;
|
||||||
|
|
||||||
|
// Everything below stages through GL_ARRAY_BUFFER, the manager-wide staging target
|
||||||
|
// (BufferImpl::TempBufferTarget); binding it disturbs no VAO state.
|
||||||
|
Bool EnsureScratchName(ScratchBuffer& buffer) {
|
||||||
|
if (buffer.id != 0) return true;
|
||||||
|
GLuint id = 0;
|
||||||
|
g_GLESFuncs.glGenBuffers(1, &id);
|
||||||
|
if (id == 0) return false;
|
||||||
|
buffer.id = id;
|
||||||
|
buffer.capacity = 0;
|
||||||
|
buffer.cursor = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whole-buffer upload, for the two buffers that are read from offset 0 because they
|
||||||
|
// are bound as storage blocks. Respecifies rather than sub-updates: glBufferData
|
||||||
|
// orphans the previous store, so the upload never waits on a dispatch still reading
|
||||||
|
// the old contents out of the same name.
|
||||||
|
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) {
|
||||||
|
if (bytes == 0) return true;
|
||||||
|
if (!EnsureScratchName(buffer)) return false;
|
||||||
|
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
|
||||||
|
// Grow in powers of two so a batch that creeps up in size stops respecifying.
|
||||||
|
SizeT capacity = buffer.capacity == 0 ? bytes : buffer.capacity;
|
||||||
|
while (capacity < bytes) capacity *= 2;
|
||||||
|
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
|
||||||
|
GL_STREAM_DRAW);
|
||||||
|
buffer.capacity = capacity;
|
||||||
|
buffer.cursor = 0;
|
||||||
|
if (data) {
|
||||||
|
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ring upload, for the buffers whose consumers can address a byte offset (indirect
|
||||||
|
// commands and rewritten index streams). Respecifying per batch is what an
|
||||||
|
// orphan-every-time scheme costs, and on a desktop-class driver that allocation
|
||||||
|
// dominated the tiers that use these buffers - a multi-draw of 32 sub-draws stages
|
||||||
|
// 640 bytes and paid for a fresh store to hold them. Bump-allocating instead means
|
||||||
|
// one respecify per wrap; every byte between two wraps is written exactly once, so
|
||||||
|
// nothing in flight is overwritten, and the wrap itself orphans.
|
||||||
|
constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal
|
||||||
|
constexpr SizeT kMinRingBytes = 1u << 16;
|
||||||
|
|
||||||
|
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data, SizeT& outOffset) {
|
||||||
|
outOffset = 0;
|
||||||
|
if (bytes == 0) return true;
|
||||||
|
if (!EnsureScratchName(buffer)) return false;
|
||||||
|
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
|
||||||
|
|
||||||
|
const SizeT aligned = (bytes + kRingAlignment - 1) & ~(kRingAlignment - 1);
|
||||||
|
if (buffer.capacity < aligned) {
|
||||||
|
SizeT capacity = buffer.capacity == 0 ? kMinRingBytes : buffer.capacity;
|
||||||
|
while (capacity < aligned) capacity *= 2;
|
||||||
|
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(capacity), nullptr,
|
||||||
|
GL_STREAM_DRAW);
|
||||||
|
buffer.capacity = capacity;
|
||||||
|
buffer.cursor = 0;
|
||||||
|
} else if (buffer.cursor + aligned > buffer.capacity) {
|
||||||
|
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(buffer.capacity),
|
||||||
|
nullptr, GL_STREAM_DRAW);
|
||||||
|
buffer.cursor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
outOffset = buffer.cursor;
|
||||||
|
if (data) {
|
||||||
|
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(outOffset),
|
||||||
|
static_cast<GLsizeiptr>(bytes), data);
|
||||||
|
}
|
||||||
|
buffer.cursor += aligned;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tier resolution
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Best-first, and measured rather than assumed. MobileGlues orders its own Auto
|
||||||
|
// multiindirect -> indirect -> basevertex; on both ES drivers available here that
|
||||||
|
// is backwards, because staging a command buffer per batch costs more than the
|
||||||
|
// driver entries it saves. mc_sodium_multidraw (132 batches x 32 sub-draws),
|
||||||
|
// ns/op, median of three:
|
||||||
|
//
|
||||||
|
// NVIDIA ES 3.2 Mesa llvmpipe ES 3.2
|
||||||
|
// ext n/a 19300
|
||||||
|
// basevertex 2500 25200
|
||||||
|
// multiindirect 5700 27600
|
||||||
|
// drawelements 5600 28700
|
||||||
|
// indirect 5800 31000
|
||||||
|
//
|
||||||
|
// Ring-allocating the command staging (instead of respecifying per batch) was
|
||||||
|
// tried first and moved the indirect tiers by less than noise, so the cost is the
|
||||||
|
// indirect draw path itself, not the upload. Only "ext" - a real multi-draw entry
|
||||||
|
// point rather than an indirect one - actually beats replaying the sub-draws.
|
||||||
|
//
|
||||||
|
// The compute tier is deliberately absent from the ladder: it rewrites the
|
||||||
|
// primitive stream rather than replaying it, and it measured slowest of all here,
|
||||||
|
// so it stays opt-in behind the env knob (the same call MobileGlues makes - its
|
||||||
|
// Auto never selects Compute either).
|
||||||
|
constexpr GLESMultiDrawMode kAutoLadder[] = {
|
||||||
|
GLESMultiDrawMode::Ext, GLESMultiDrawMode::BaseVertex, GLESMultiDrawMode::MultiIndirect,
|
||||||
|
GLESMultiDrawMode::Indirect, GLESMultiDrawMode::DrawElements,
|
||||||
|
};
|
||||||
|
|
||||||
|
Bool SupportsTier(GLESMultiDrawMode tier) {
|
||||||
|
return IsTierSupported(g_GLESCapabilities, g_GLESFuncs, tier);
|
||||||
|
}
|
||||||
|
|
||||||
|
GLESMultiDrawMode g_resolvedTier = GLESMultiDrawMode::Auto;
|
||||||
|
Bool g_tierResolved = false;
|
||||||
|
String g_tierResolution;
|
||||||
|
|
||||||
|
void ResolveTierOnce() {
|
||||||
|
if (g_tierResolved) return;
|
||||||
|
g_tierResolved = true;
|
||||||
|
g_resolvedTier =
|
||||||
|
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
|
||||||
|
&g_tierResolution);
|
||||||
|
MGLOG_I("DirectGLES multi-draw: %s", g_tierResolution.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which tiers have already announced themselves, one bit per GLESMultiDrawMode.
|
||||||
|
// The resolution line above says which tier was CHOSEN; this says which one a
|
||||||
|
// batch actually went through, and the two differ whenever a batch's shape
|
||||||
|
// demotes it. Worth a line each: a multi-draw path that resolves to a tier and
|
||||||
|
// then quietly runs a different one is exactly how "the batch drew nothing"
|
||||||
|
// hides.
|
||||||
|
Uint32 g_announcedTiers = 0;
|
||||||
|
|
||||||
|
void NoteTierExecuted(GLESMultiDrawMode tier) {
|
||||||
|
const Uint32 bit = 1u << static_cast<Uint32>(tier);
|
||||||
|
if (g_announcedTiers & bit) return;
|
||||||
|
g_announcedTiers |= bit;
|
||||||
|
MGLOG_I("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tier this particular batch can actually take. A tier is demoted here when
|
||||||
|
// the batch's own shape - not the driver - rules it out; the compute tier keeps
|
||||||
|
// its remaining feasibility checks inside its implementation, where the data it
|
||||||
|
// has to walk is already in hand.
|
||||||
|
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool hasIndexBuffer) {
|
||||||
|
ResolveTierOnce();
|
||||||
|
GLESMultiDrawMode tier = g_resolvedTier;
|
||||||
|
|
||||||
|
// Batched tiers issue one driver entry for the whole batch, so the emulated
|
||||||
|
// gl_DrawID uniform can only hold one value across every sub-draw. A program
|
||||||
|
// that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its
|
||||||
|
// own index (the spec's value); nothing else observes the difference.
|
||||||
|
const Bool batched = tier == GLESMultiDrawMode::Ext || tier == GLESMultiDrawMode::MultiIndirect ||
|
||||||
|
tier == GLESMultiDrawMode::Compute;
|
||||||
|
if (batched && programReadsDrawID) {
|
||||||
|
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
|
||||||
|
: GLESMultiDrawMode::DrawElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The indirect tiers describe each sub-draw as an element offset into the
|
||||||
|
// bound element array buffer. A client-memory index array has no such buffer,
|
||||||
|
// and indirect draws are not defined without one.
|
||||||
|
if (!hasIndexBuffer &&
|
||||||
|
(tier == GLESMultiDrawMode::MultiIndirect || tier == GLESMultiDrawMode::Indirect)) {
|
||||||
|
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
|
||||||
|
: GLESMultiDrawMode::DrawElements;
|
||||||
|
}
|
||||||
|
return tier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Index rewriting, shared by the two tiers that fold base vertices into indices
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Both of those tiers emit GL_UNSIGNED_INT regardless of the source type. Keeping
|
||||||
|
// the source width would be wrong, not merely tight: GL adds baseVertex to the
|
||||||
|
// index at full precision, so a GL_UNSIGNED_SHORT index plus a base vertex past
|
||||||
|
// 65535 addresses a vertex the source type cannot spell. Widening also gives the
|
||||||
|
// rewritten stream a restart sentinel (0xFFFFFFFF) that survives the rebase.
|
||||||
|
void RebaseIndices(const Uint8* source, SizeT sourceIndexCount, SizeT indexSize, Int32 baseVertex,
|
||||||
|
Bool restartActive, Uint32 restartSentinel, Uint32* out) {
|
||||||
|
const Uint32 baseVertexBits = static_cast<Uint32>(baseVertex);
|
||||||
|
for (SizeT i = 0; i < sourceIndexCount; ++i) {
|
||||||
|
Uint32 value = 0;
|
||||||
|
switch (indexSize) {
|
||||||
|
case 1: value = source[i]; break;
|
||||||
|
case 2: {
|
||||||
|
Uint16 narrow = 0;
|
||||||
|
std::memcpy(&narrow, source + i * 2, sizeof(narrow));
|
||||||
|
value = narrow;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: std::memcpy(&value, source + i * 4, sizeof(value)); break;
|
||||||
|
}
|
||||||
|
// Unsigned wraparound is the defined behaviour for a negative base vertex.
|
||||||
|
out[i] = (restartActive && value == restartSentinel) ? 0xFFFFFFFFu : value + baseVertexBits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU-readable bytes of one sub-draw's indices, from the frontend shadow of the
|
||||||
|
// bound index buffer or straight from the client array. Null when the sub-draw
|
||||||
|
// would read outside the buffer.
|
||||||
|
const Uint8* ResolveSubDrawIndices(const SharedPtr<MG_State::GLState::BufferObject>& indexBuffer,
|
||||||
|
const Uint8* indexBufferBytes, SizeT indexBufferSize, const void* indices,
|
||||||
|
SizeT indexCount, SizeT indexSize) {
|
||||||
|
if (!indexBuffer) {
|
||||||
|
return static_cast<const Uint8*>(indices);
|
||||||
|
}
|
||||||
|
if (!indexBufferBytes) return nullptr;
|
||||||
|
const SizeT byteOffset = reinterpret_cast<SizeT>(indices);
|
||||||
|
const SizeT byteEnd = byteOffset + indexCount * indexSize;
|
||||||
|
if (byteEnd > indexBufferSize || byteEnd < byteOffset) return nullptr;
|
||||||
|
return indexBufferBytes + byteOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tier: Ext - one glMultiDrawElementsBaseVertexEXT
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Bool RunExt(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, GLsizei drawcount,
|
||||||
|
const GLint* basevertex) {
|
||||||
|
if (!SupportsTier(GLESMultiDrawMode::Ext)) return false;
|
||||||
|
const GLint* baseVertices = basevertex;
|
||||||
|
if (!baseVertices) {
|
||||||
|
// glMultiDrawElements: every base vertex is 0, but the entry point still
|
||||||
|
// wants an array. One permanently-zero vector serves every such batch.
|
||||||
|
if (g_zeroBaseVertices.size() < static_cast<SizeT>(drawcount)) {
|
||||||
|
g_zeroBaseVertices.resize(static_cast<SizeT>(drawcount), 0);
|
||||||
|
}
|
||||||
|
baseVertices = g_zeroBaseVertices.data();
|
||||||
|
}
|
||||||
|
g_GLESFuncs.glMultiDrawElementsBaseVertexEXT(mode, count, type, indices, drawcount, baseVertices);
|
||||||
|
NoteTierExecuted(GLESMultiDrawMode::Ext);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tiers: MultiIndirect / Indirect - synthesized indirect commands
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Bool RunIndirect(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID) {
|
||||||
|
if (!SupportsTier(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect)) return false;
|
||||||
|
const SizeT indexSize = IndexTypeSize(type);
|
||||||
|
if (indexSize == 0) return false;
|
||||||
|
// Indirect commands address indices as an element offset into the bound element
|
||||||
|
// array buffer, and an indirect draw is not defined without one.
|
||||||
|
const auto& indexBuffer = BoundIndexBuffer();
|
||||||
|
if (!indexBuffer) return false;
|
||||||
|
|
||||||
|
g_commandStaging.resize(static_cast<SizeT>(drawcount));
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
const SizeT byteOffset = reinterpret_cast<SizeT>(indices[i]);
|
||||||
|
// firstIndex counts elements, so an offset that is not a whole number of
|
||||||
|
// them cannot be expressed as a command at all.
|
||||||
|
if (byteOffset % indexSize != 0) return false;
|
||||||
|
auto& command = g_commandStaging[static_cast<SizeT>(i)];
|
||||||
|
command.count = count[i] > 0 ? static_cast<Uint32>(count[i]) : 0u;
|
||||||
|
command.instanceCount = 1;
|
||||||
|
command.firstIndex = static_cast<Uint32>(byteOffset / indexSize);
|
||||||
|
command.baseVertex = basevertex ? basevertex[i] : 0;
|
||||||
|
command.baseInstance = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand);
|
||||||
|
SizeT commandBase = 0;
|
||||||
|
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(), commandBase)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every synthesized command carries baseInstance 0. Say so through the direct
|
||||||
|
// path, which also clears the indirect-params word index a preceding real
|
||||||
|
// indirect draw may have left pointing into its own command buffer.
|
||||||
|
SetCurrentBaseInstance(0);
|
||||||
|
|
||||||
|
const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
|
||||||
|
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
|
||||||
|
if (batched) {
|
||||||
|
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
|
||||||
|
drawcount, 0);
|
||||||
|
} else {
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||||
|
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
|
||||||
|
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
||||||
|
}
|
||||||
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
|
}
|
||||||
|
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding);
|
||||||
|
NoteTierExecuted(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tier: BaseVertex - the per-sub-draw replay
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Bool RunBaseVertexLoop(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) {
|
||||||
|
if (!SupportsTier(GLESMultiDrawMode::BaseVertex)) return false;
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (count[i] <= 0) continue;
|
||||||
|
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||||
|
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||||
|
basevertex ? basevertex[i] : 0);
|
||||||
|
}
|
||||||
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
|
NoteTierExecuted(GLESMultiDrawMode::BaseVertex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tier: DrawElements - base vertices folded into a scratch index stream
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Bool RunRebasedDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) {
|
||||||
|
const SizeT indexSize = IndexTypeSize(type);
|
||||||
|
if (indexSize == 0) return false;
|
||||||
|
|
||||||
|
SizeT total = 0;
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (count[i] > 0) total += static_cast<SizeT>(count[i]);
|
||||||
|
}
|
||||||
|
if (total == 0) return true;
|
||||||
|
if (total > kMaxFlattenedIndices) return false;
|
||||||
|
|
||||||
|
const auto& indexBuffer = BoundIndexBuffer();
|
||||||
|
const Uint8* indexBufferBytes = nullptr;
|
||||||
|
SizeT indexBufferSize = 0;
|
||||||
|
if (indexBuffer) {
|
||||||
|
// The shadow is the source of truth for CPU reads, but a persistent map or
|
||||||
|
// a shader write may have moved past it since the last sync.
|
||||||
|
indexBuffer->SyncPersistentMappedRange();
|
||||||
|
indexBuffer->SyncGpuWrites();
|
||||||
|
indexBufferBytes = indexBuffer->MappedData();
|
||||||
|
indexBufferSize = indexBuffer->GetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
const Bool restartActive = RestartActive();
|
||||||
|
const Uint32 restartSentinel = RestartSentinelFor(type);
|
||||||
|
g_indexStaging.resize(total);
|
||||||
|
SizeT cursor = 0;
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (count[i] <= 0) continue;
|
||||||
|
const SizeT subDrawCount = static_cast<SizeT>(count[i]);
|
||||||
|
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
|
||||||
|
subDrawCount, indexSize);
|
||||||
|
if (!source) {
|
||||||
|
MGLOG_E("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
|
||||||
|
"buffer; skipping the batch",
|
||||||
|
i);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RebaseIndices(source, subDrawCount, indexSize, basevertex ? basevertex[i] : 0, restartActive,
|
||||||
|
restartSentinel, g_indexStaging.data() + cursor);
|
||||||
|
cursor += subDrawCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
SizeT indexBase = 0;
|
||||||
|
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uint previousIndexBinding = BoundIndexBufferId();
|
||||||
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_rebasedIndices.id);
|
||||||
|
cursor = 0;
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (count[i] <= 0) continue;
|
||||||
|
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||||
|
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
|
||||||
|
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
||||||
|
cursor += static_cast<SizeT>(count[i]);
|
||||||
|
}
|
||||||
|
if (feedDrawID) SetCurrentDrawID(0);
|
||||||
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
||||||
|
NoteTierExecuted(GLESMultiDrawMode::DrawElements);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tier: Compute - the whole batch flattened into one rebased index stream
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// One index per invocation. The sub-draw an output slot belongs to is found by
|
||||||
|
// binary search over the inclusive prefix sums of the sub-draw counts, which is
|
||||||
|
// why the descriptors are sorted by construction. Sub-draws with a zero count
|
||||||
|
// repeat the previous prefix sum and are therefore skipped by the search.
|
||||||
|
//
|
||||||
|
// Three storage blocks, not the five the shape suggests: ES 3.1 only guarantees
|
||||||
|
// four per compute stage, so the per-sub-draw descriptors share one buffer.
|
||||||
|
constexpr const char* kFlattenComputeSource = R"(#version 310 es
|
||||||
|
layout(local_size_x = 64) in;
|
||||||
|
|
||||||
|
uniform uint uElementSize;
|
||||||
|
uniform uint uDrawCount;
|
||||||
|
uniform uint uTotalIndices;
|
||||||
|
|
||||||
|
layout(std430, binding = 0) readonly buffer SourceIndices { uint sourceWords[]; };
|
||||||
|
layout(std430, binding = 1) readonly buffer DrawInfo { uint drawInfo[]; };
|
||||||
|
layout(std430, binding = 2) writeonly buffer FlatIndices { uint flatIndices[]; };
|
||||||
|
|
||||||
|
uint ReadSourceIndex(uint element) {
|
||||||
|
if (uElementSize == 4u) {
|
||||||
|
return sourceWords[element];
|
||||||
|
}
|
||||||
|
if (uElementSize == 2u) {
|
||||||
|
uint word = sourceWords[element >> 1u];
|
||||||
|
return (word >> ((element & 1u) * 16u)) & 0xFFFFu;
|
||||||
|
}
|
||||||
|
uint word = sourceWords[element >> 2u];
|
||||||
|
return (word >> ((element & 3u) * 8u)) & 0xFFu;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
uint outIndex = gl_GlobalInvocationID.x;
|
||||||
|
if (outIndex >= uTotalIndices) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint low = 0u;
|
||||||
|
uint high = uDrawCount - 1u;
|
||||||
|
while (low < high) {
|
||||||
|
uint mid = low + (high - low) / 2u;
|
||||||
|
if (drawInfo[mid * 3u + 2u] > outIndex) {
|
||||||
|
high = mid;
|
||||||
|
} else {
|
||||||
|
low = mid + 1u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint localIndex = outIndex - (low == 0u ? 0u : drawInfo[(low - 1u) * 3u + 2u]);
|
||||||
|
// Unsigned wraparound is the defined behaviour for a negative base vertex. No
|
||||||
|
// restart sentinel handling: the tier declines outright while restart is enabled.
|
||||||
|
flatIndices[outIndex] = ReadSourceIndex(localIndex + drawInfo[low * 3u]) + drawInfo[low * 3u + 1u];
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct FlattenedStream {
|
||||||
|
Uint bufferId = 0;
|
||||||
|
SizeT indexCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
Bool EnsureComputeProgram() {
|
||||||
|
if (g_computeProgram != 0) return true;
|
||||||
|
if (g_computeProgramFailed) return false;
|
||||||
|
g_computeProgramFailed = true; // cleared again only on a complete success
|
||||||
|
|
||||||
|
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
|
||||||
|
if (shader == 0) {
|
||||||
|
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const char* source = kFlattenComputeSource;
|
||||||
|
g_GLESFuncs.glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
g_GLESFuncs.glCompileShader(shader);
|
||||||
|
GLint status = GL_FALSE;
|
||||||
|
g_GLESFuncs.glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||||
|
if (status != GL_TRUE) {
|
||||||
|
char log[1024] = {};
|
||||||
|
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||||
|
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
|
||||||
|
g_GLESFuncs.glDeleteShader(shader);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint program = g_GLESFuncs.glCreateProgram();
|
||||||
|
if (program == 0) {
|
||||||
|
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateProgram failed");
|
||||||
|
g_GLESFuncs.glDeleteShader(shader);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_GLESFuncs.glAttachShader(program, shader);
|
||||||
|
g_GLESFuncs.glLinkProgram(program);
|
||||||
|
g_GLESFuncs.glDeleteShader(shader);
|
||||||
|
g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||||
|
if (status != GL_TRUE) {
|
||||||
|
char log[1024] = {};
|
||||||
|
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||||
|
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
|
||||||
|
g_GLESFuncs.glDeleteProgram(program);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_computeProgram = program;
|
||||||
|
g_uElementSize = g_GLESFuncs.glGetUniformLocation(program, "uElementSize");
|
||||||
|
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
|
||||||
|
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
|
||||||
|
g_computeProgramFailed = false;
|
||||||
|
MGLOG_I("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds the flattened stream, or leaves `out` empty when this batch's shape rules
|
||||||
|
// the tier out. Runs BEFORE PrepareForDraw - see the call site - so it may leave
|
||||||
|
// the compute program current and the first storage points unbound; the
|
||||||
|
// preparation that follows re-establishes both.
|
||||||
|
void FlattenWithCompute(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount, const GLint* basevertex, FlattenedStream& out) {
|
||||||
|
if (!SupportsTier(GLESMultiDrawMode::Compute)) return;
|
||||||
|
const SizeT indexSize = IndexTypeSize(type);
|
||||||
|
if (indexSize == 0) return;
|
||||||
|
|
||||||
|
// Merging sub-draws into a single draw only reproduces the original primitive
|
||||||
|
// stream for list-shaped modes: a strip, loop or fan would gain primitives
|
||||||
|
// spanning the seam between two sub-draws.
|
||||||
|
const Uint32 primitiveSize = ConcatenablePrimitiveSize(mode);
|
||||||
|
if (primitiveSize == 0) return;
|
||||||
|
|
||||||
|
// Primitive restart defeats the whole-multiple-of-a-primitive argument below,
|
||||||
|
// even for a list mode. A restart ends the current primitive, so a sub-draw of
|
||||||
|
// six GL_TRIANGLES indices with a restart after the third emits ONE triangle
|
||||||
|
// and drops the two leftover vertices - and once concatenated those leftovers
|
||||||
|
// find a third vertex in the next sub-draw and become a triangle that GL never
|
||||||
|
// draws. Splicing separator sentinels into the flattened stream could fix it,
|
||||||
|
// at the cost of a per-sub-draw offset the prefix-sum layout does not carry;
|
||||||
|
// declining is the honest trade for a tier that is already opt-in.
|
||||||
|
if (RestartActive()) return;
|
||||||
|
|
||||||
|
// The shader reads the source indices as a storage buffer, so there has to be
|
||||||
|
// a real buffer to read - a client-memory index array has none.
|
||||||
|
const auto& indexBuffer = BoundIndexBuffer();
|
||||||
|
if (!indexBuffer) return;
|
||||||
|
|
||||||
|
// A dispatch inside an open capture span is not legal, and the span would also
|
||||||
|
// observe one merged draw rather than the batch it asked for.
|
||||||
|
if (XfbImpl::IsCaptureSpanOpen()) return;
|
||||||
|
|
||||||
|
auto* sourceResource = BufferImpl::EnsureBufferResource(indexBuffer);
|
||||||
|
if (!sourceResource || sourceResource->id == 0) return;
|
||||||
|
const SizeT sourceSize = indexBuffer->GetSize();
|
||||||
|
// std430 addresses the source as uint[]; a tail shorter than a word is not
|
||||||
|
// reachable, so a narrow index type needs a word-multiple buffer.
|
||||||
|
if (indexSize < 4 && (sourceSize % 4) != 0) return;
|
||||||
|
|
||||||
|
g_drawInfoStaging.resize(3 * static_cast<SizeT>(drawcount));
|
||||||
|
SizeT total = 0;
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
const SizeT subDrawCount = count[i] > 0 ? static_cast<SizeT>(count[i]) : 0;
|
||||||
|
// GL drops a trailing partial primitive per sub-draw; concatenation would
|
||||||
|
// instead splice it onto the next sub-draw's first vertices.
|
||||||
|
if (subDrawCount % primitiveSize != 0) return;
|
||||||
|
const SizeT byteOffset = reinterpret_cast<SizeT>(indices[i]);
|
||||||
|
if (byteOffset % indexSize != 0) return;
|
||||||
|
if (subDrawCount != 0) {
|
||||||
|
const SizeT byteEnd = byteOffset + subDrawCount * indexSize;
|
||||||
|
if (byteEnd > sourceSize || byteEnd < byteOffset) return;
|
||||||
|
}
|
||||||
|
total += subDrawCount;
|
||||||
|
if (total > kMaxComputeFlattenedIndices) return;
|
||||||
|
const SizeT slot = 3 * static_cast<SizeT>(i);
|
||||||
|
g_drawInfoStaging[slot] = static_cast<Uint32>(byteOffset / indexSize);
|
||||||
|
g_drawInfoStaging[slot + 1] = static_cast<Uint32>(basevertex ? basevertex[i] : 0);
|
||||||
|
g_drawInfoStaging[slot + 2] = static_cast<Uint32>(total);
|
||||||
|
}
|
||||||
|
if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well
|
||||||
|
|
||||||
|
if (!EnsureComputeProgram()) return;
|
||||||
|
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr)) return;
|
||||||
|
|
||||||
|
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id);
|
||||||
|
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id);
|
||||||
|
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 2, g_flattenedIndices.id);
|
||||||
|
|
||||||
|
g_GLESFuncs.glUseProgram(g_computeProgram);
|
||||||
|
PrgramImpl::g_lastUsedBackendProgramId = g_computeProgram;
|
||||||
|
if (g_uElementSize >= 0) g_GLESFuncs.glUniform1ui(g_uElementSize, static_cast<GLuint>(indexSize));
|
||||||
|
if (g_uDrawCount >= 0) g_GLESFuncs.glUniform1ui(g_uDrawCount, static_cast<GLuint>(drawcount));
|
||||||
|
if (g_uTotalIndices >= 0) g_GLESFuncs.glUniform1ui(g_uTotalIndices, static_cast<GLuint>(total));
|
||||||
|
|
||||||
|
g_GLESFuncs.glDispatchCompute(
|
||||||
|
static_cast<GLuint>((total + kComputeWorkGroupSize - 1) / kComputeWorkGroupSize), 1, 1);
|
||||||
|
g_GLESFuncs.glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT);
|
||||||
|
|
||||||
|
// Hand the storage points back to their GL default. PrepareForDraw re-syncs
|
||||||
|
// only the points the app has actually touched, so leaving a scratch buffer on
|
||||||
|
// an untouched point would keep it visible to the next shader that declares one.
|
||||||
|
for (Uint point = 0; point < 3; ++point) {
|
||||||
|
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, point, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteTierExecuted(GLESMultiDrawMode::Compute);
|
||||||
|
out.bufferId = g_flattenedIndices.id;
|
||||||
|
out.indexCount = total;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// Public surface
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Bool IsTierSupported(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& funcs,
|
||||||
|
GLESMultiDrawMode tier) {
|
||||||
|
const Bool esAtLeast31 =
|
||||||
|
caps.GLESVersion.Major > 3 || (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1);
|
||||||
|
switch (tier) {
|
||||||
|
case GLESMultiDrawMode::Ext:
|
||||||
|
return caps.SupportsMultiDrawElementsBaseVertex;
|
||||||
|
case GLESMultiDrawMode::MultiIndirect:
|
||||||
|
return caps.SupportsMultiDrawIndirect && esAtLeast31 && funcs.glDrawElementsIndirect != nullptr;
|
||||||
|
case GLESMultiDrawMode::Indirect:
|
||||||
|
return esAtLeast31 && funcs.glDrawElementsIndirect != nullptr;
|
||||||
|
case GLESMultiDrawMode::BaseVertex:
|
||||||
|
return caps.SupportsDrawElementsBaseVertex;
|
||||||
|
case GLESMultiDrawMode::DrawElements:
|
||||||
|
// Plain glDrawElements over a rewritten index stream: ES 2 core, so this is
|
||||||
|
// the floor every other tier can fall back to.
|
||||||
|
return true;
|
||||||
|
case GLESMultiDrawMode::Compute:
|
||||||
|
// Three storage blocks, which is inside the four ES 3.1 guarantees per stage.
|
||||||
|
return caps.SupportsComputeShader && caps.MaxComputeShaderStorageBlocks >= 3 &&
|
||||||
|
funcs.glBindBufferBase != nullptr;
|
||||||
|
case GLESMultiDrawMode::Auto:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLESMultiDrawMode ResolveTier(const MG_External::GLESCapabilities& caps,
|
||||||
|
const MG_External::GLESFunctionsTable& funcs, GLESMultiDrawMode requested,
|
||||||
|
String* explanation) {
|
||||||
|
const auto bestAuto = [&]() {
|
||||||
|
for (const GLESMultiDrawMode tier : kAutoLadder) {
|
||||||
|
if (IsTierSupported(caps, funcs, tier)) return tier;
|
||||||
|
}
|
||||||
|
return GLESMultiDrawMode::DrawElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
GLESMultiDrawMode resolved = GLESMultiDrawMode::DrawElements;
|
||||||
|
String line;
|
||||||
|
if (requested == GLESMultiDrawMode::Auto) {
|
||||||
|
resolved = bestAuto();
|
||||||
|
line = String("auto -> ") + TierName(resolved);
|
||||||
|
} else if (IsTierSupported(caps, funcs, requested)) {
|
||||||
|
resolved = requested;
|
||||||
|
line = String("MOBILEGL_ESPRYT_MULTIDRAW_MODE=") + TierName(requested) + " -> " + TierName(resolved);
|
||||||
|
} else {
|
||||||
|
resolved = bestAuto();
|
||||||
|
line = String("MOBILEGL_ESPRYT_MULTIDRAW_MODE=") + TierName(requested) +
|
||||||
|
" requested but unsupported by this driver -> " + TierName(resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (explanation) {
|
||||||
|
String supported;
|
||||||
|
for (const GLESMultiDrawMode tier : kAutoLadder) {
|
||||||
|
if (!IsTierSupported(caps, funcs, tier)) continue;
|
||||||
|
if (!supported.empty()) supported += ", ";
|
||||||
|
supported += TierName(tier);
|
||||||
|
}
|
||||||
|
if (IsTierSupported(caps, funcs, GLESMultiDrawMode::Compute)) {
|
||||||
|
supported += supported.empty() ? "compute (opt-in)" : ", compute (opt-in)";
|
||||||
|
}
|
||||||
|
*explanation = line + " (driver supports: " + supported + ")";
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* TierName(GLESMultiDrawMode tier) {
|
||||||
|
switch (tier) {
|
||||||
|
case GLESMultiDrawMode::Auto: return "auto";
|
||||||
|
case GLESMultiDrawMode::Ext: return "ext";
|
||||||
|
case GLESMultiDrawMode::MultiIndirect: return "multiindirect";
|
||||||
|
case GLESMultiDrawMode::Indirect: return "indirect";
|
||||||
|
case GLESMultiDrawMode::BaseVertex: return "basevertex";
|
||||||
|
case GLESMultiDrawMode::DrawElements: return "drawelements";
|
||||||
|
case GLESMultiDrawMode::Compute: return "compute";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
GLESMultiDrawMode ResolvedTier() {
|
||||||
|
ResolveTierOnce();
|
||||||
|
return g_resolvedTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
String DescribeTierResolution() {
|
||||||
|
ResolveTierOnce();
|
||||||
|
return g_tierResolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnBackendContextDestroyed() {
|
||||||
|
g_indirectCommands = {};
|
||||||
|
g_rebasedIndices = {};
|
||||||
|
g_drawInfo = {};
|
||||||
|
g_flattenedIndices = {};
|
||||||
|
g_computeProgram = 0;
|
||||||
|
g_computeProgramFailed = false;
|
||||||
|
g_uElementSize = -1;
|
||||||
|
g_uDrawCount = -1;
|
||||||
|
g_uTotalIndices = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount, const GLint* basevertex) {
|
||||||
|
if (drawcount <= 0 || !count || !indices) return;
|
||||||
|
// State-independent and possibly throwing, so it runs before any GL work.
|
||||||
|
CheckPrimitiveRestartSupported(type);
|
||||||
|
|
||||||
|
const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr;
|
||||||
|
|
||||||
|
// The compute tier dispatches BEFORE the draw state is established: doing it
|
||||||
|
// afterwards would mean unpicking the program, SSBO and index bindings
|
||||||
|
// PrepareForDraw just made, and a dispatch inside an open transform feedback
|
||||||
|
// span is not legal at all. On success it hands back a flattened index stream.
|
||||||
|
FlattenedStream flattened;
|
||||||
|
if (ResolvedTier() == GLESMultiDrawMode::Compute && !CurrentProgramReadsDrawID()) {
|
||||||
|
FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened);
|
||||||
|
}
|
||||||
|
|
||||||
|
PrepareForDraw(DrawSyncBit::IndexBuffer);
|
||||||
|
|
||||||
|
if (flattened.indexCount != 0) {
|
||||||
|
const Uint previousIndexBinding = BoundIndexBufferId();
|
||||||
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId);
|
||||||
|
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
|
||||||
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Bool feedDrawID = CurrentProgramReadsDrawID();
|
||||||
|
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, hasIndexBuffer);
|
||||||
|
|
||||||
|
Bool drawn = false;
|
||||||
|
switch (tier) {
|
||||||
|
case GLESMultiDrawMode::Ext:
|
||||||
|
drawn = RunExt(mode, count, type, indices, drawcount, basevertex);
|
||||||
|
break;
|
||||||
|
case GLESMultiDrawMode::MultiIndirect:
|
||||||
|
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID);
|
||||||
|
break;
|
||||||
|
case GLESMultiDrawMode::Indirect:
|
||||||
|
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID);
|
||||||
|
break;
|
||||||
|
case GLESMultiDrawMode::BaseVertex:
|
||||||
|
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||||
|
break;
|
||||||
|
case GLESMultiDrawMode::DrawElements:
|
||||||
|
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||||
|
break;
|
||||||
|
case GLESMultiDrawMode::Compute:
|
||||||
|
// Its pre-pass ran above; reaching here means it declined this batch's shape.
|
||||||
|
break;
|
||||||
|
case GLESMultiDrawMode::Auto:
|
||||||
|
break; // resolution never yields Auto
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every tier above may decline a batch whose shape it cannot express. The two
|
||||||
|
// below are the floor: a base-vertex replay where the driver has one, and the
|
||||||
|
// rewritten index stream where it does not. Both are safe for any batch these
|
||||||
|
// entry points can receive.
|
||||||
|
if (!drawn) drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||||
|
if (!drawn) drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||||
|
if (!drawn) {
|
||||||
|
MGLOG_E("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
|
||||||
|
"the batch was dropped",
|
||||||
|
drawcount, mode, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Backend/DirectGLES/MultiDraw.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
#include <Config.h>
|
||||||
|
#include "DirectGLES.h"
|
||||||
|
|
||||||
|
// Emulation of the desktop glMultiDrawElements / glMultiDrawElementsBaseVertex entry
|
||||||
|
// points on OpenGL ES, which has neither in core.
|
||||||
|
//
|
||||||
|
// Every strategy below is an emulation; they differ only in which driver capability
|
||||||
|
// they lean on and in how many driver entries a batch of N sub-draws costs. The design
|
||||||
|
// follows MobileGlues (MobileGL-Dev/MobileGlues, gl/multidraw.cpp) tier for tier, plus
|
||||||
|
// the native GL_EXT_multi_draw_arrays interaction that MobileGL already had:
|
||||||
|
//
|
||||||
|
// Ext one glMultiDrawElementsBaseVertexEXT 1 driver entry
|
||||||
|
// MultiIndirect one glMultiDrawElementsIndirectEXT 1 driver entry + 1 upload
|
||||||
|
// Indirect N x glDrawElementsIndirect N + 1 upload
|
||||||
|
// BaseVertex N x glDrawElementsBaseVertex N
|
||||||
|
// DrawElements N x glDrawElements over CPU-rebased indices N + 1 upload
|
||||||
|
// Compute 1 x glDrawElements over a GPU-flattened, 1 dispatch + 1 entry
|
||||||
|
// rebased index stream
|
||||||
|
//
|
||||||
|
// Which one runs is resolved once per ES context from the driver's capabilities,
|
||||||
|
// capped by MOBILEGL_ESPRYT_MULTIDRAW_MODE, and can additionally be demoted per batch
|
||||||
|
// when the batch's own shape rules a tier out (see ResolveTierForBatch in the .cpp).
|
||||||
|
namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||||
|
// The tier this ES context resolved to, computed on first use and stable after.
|
||||||
|
MG_Config::GLESMultiDrawMode ResolvedTier();
|
||||||
|
// "multiindirect", "compute", ... - stable identifiers, also used by the POST row.
|
||||||
|
const char* TierName(MG_Config::GLESMultiDrawMode tier);
|
||||||
|
// One line naming the resolved tier, the tiers the driver can support, and the env
|
||||||
|
// clamp if one applied. For DriverPost and the startup log.
|
||||||
|
String DescribeTierResolution();
|
||||||
|
|
||||||
|
// The resolution itself, as a pure function of a capability set: the backend feeds
|
||||||
|
// it the live ES context's capabilities, DriverPost feeds it the ones it probed
|
||||||
|
// standalone, and both therefore report the same tier. `explanation`, when non-null,
|
||||||
|
// receives the "requested -> resolved (driver supports: ...)" line.
|
||||||
|
MG_Config::GLESMultiDrawMode ResolveTier(const MG_External::GLESCapabilities& caps,
|
||||||
|
const MG_External::GLESFunctionsTable& funcs,
|
||||||
|
MG_Config::GLESMultiDrawMode requested, String* explanation);
|
||||||
|
// Whether one tier is runnable on the given capability set, for per-row POST output.
|
||||||
|
Bool IsTierSupported(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& funcs,
|
||||||
|
MG_Config::GLESMultiDrawMode tier);
|
||||||
|
|
||||||
|
// Runs `drawcount` indexed sub-draws as one glMultiDrawElements(BaseVertex) call
|
||||||
|
// would. `basevertex` is null for the plain glMultiDrawElements entry point (every
|
||||||
|
// base vertex is 0). Owns the whole draw, preparation included: callers must not
|
||||||
|
// have run PrepareForDraw, because the compute tier has to dispatch before the
|
||||||
|
// draw state is established.
|
||||||
|
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount, const GLint* basevertex);
|
||||||
|
|
||||||
|
// The ES context is gone: every scratch buffer and the compute program belonged to
|
||||||
|
// it, so drop the names without deleting them (the dead context reclaims them).
|
||||||
|
void OnBackendContextDestroyed();
|
||||||
|
} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl
|
||||||
@@ -22,6 +22,9 @@
|
|||||||
#include <MG_Util/Math/SmallFloat.h>
|
#include <MG_Util/Math/SmallFloat.h>
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstring>
|
||||||
|
#include <regex>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectGLES {
|
namespace MobileGL::MG_Backend::DirectGLES {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -45,15 +48,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
Flags<PixelFormatNormalizeOptionBit> GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat) {
|
Flags<PixelFormatNormalizeOptionBit>
|
||||||
|
GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat,
|
||||||
|
Flags<PixelFormatNormalizeOptionBit> extraOptions) {
|
||||||
using namespace MG_Util::TextureFormatProcessor;
|
using namespace MG_Util::TextureFormatProcessor;
|
||||||
const Flags<PixelFormatNormalizeOptionBit> forcedOptions =
|
const Flags<PixelFormatNormalizeOptionBit> forcedOptions = GetApplicablePixelFormatNormalizeOptions(
|
||||||
GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat, GetForcedPixelFormatNormalizeOptions());
|
requestedInternalFormat, GetForcedPixelFormatNormalizeOptions() | extraOptions);
|
||||||
if (forcedOptions) {
|
if (forcedOptions) {
|
||||||
return forcedOptions;
|
return forcedOptions;
|
||||||
}
|
}
|
||||||
return GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat,
|
return GetApplicablePixelFormatNormalizeOptions(
|
||||||
GetDriverPixelFormatNormalizeOptions());
|
requestedInternalFormat, GetDriverPixelFormatNormalizeOptions() | extraOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool HasCachedFormatCapability(TextureInternalFormat internalFormat,
|
Bool HasCachedFormatCapability(TextureInternalFormat internalFormat,
|
||||||
@@ -113,13 +118,61 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||||
Flags<PixelFormatNormalizeOptionBit> options;
|
Flags<PixelFormatNormalizeOptionBit> options;
|
||||||
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||||
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat);
|
options = GetRuntimeFallbackNormalizeOptions(
|
||||||
|
requestedInternalFormat,
|
||||||
|
TextureImpl::GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
|
||||||
}
|
}
|
||||||
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
|
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace TextureImpl {
|
namespace TextureImpl {
|
||||||
|
// Every image that can back a colour attachment needs a colour-renderable storage format,
|
||||||
|
// and ES has no renderable three-channel format at all: a three-channel float fallback is
|
||||||
|
// a legal ES texture but neither legal multisample storage nor a legal attachment, so
|
||||||
|
// GL_RGB8_SNORM / GL_RGB16F / ... have to be widened to four channels for any of them.
|
||||||
|
// This used to cover the multisample pair alone, on the grounds that only those can never
|
||||||
|
// be uploaded to; the transfer paths now expand three-channel client data themselves
|
||||||
|
// (Managers.cpp PrepareFallbackUpload) and hide the added alpha again on sample and
|
||||||
|
// readback, so the same substitution is available everywhere.
|
||||||
|
//
|
||||||
|
// The widening only ever *happens* where the driver refuses the native form (see
|
||||||
|
// PopulateFormatCapabilitiesImpl: outside multisample storage it rides the driver branch,
|
||||||
|
// behind the native probe), so a driver that does render to a three-channel image keeps
|
||||||
|
// allocating it byte for byte.
|
||||||
|
//
|
||||||
|
// Do NOT read that as "nothing changes off-device". Measured on Mesa 26.1.6 llvmpipe
|
||||||
|
// (the headless CI driver), an ES 3.2 GL_TEXTURE_2D colour attachment is COMPLETE for
|
||||||
|
// GL_RGB8 and GL_RGB16F but INCOMPLETE_ATTACHMENT for GL_RGB8_SNORM, GL_SRGB8 and every
|
||||||
|
// RGB integer format, and UNSUPPORTED for GL_RGB32F. Those eight formats therefore DO
|
||||||
|
// take the widened path on llvmpipe, which is where the retrace fixtures and the glcts
|
||||||
|
// green suites run - the substitution is driver-conditional, not desktop-exempt.
|
||||||
|
//
|
||||||
|
// A buffer texture is the one image that can never be an attachment; its storage is the
|
||||||
|
// buffer object's, and widening it would misdescribe the application's data.
|
||||||
|
Bool TargetRequiresRenderableFormat(SizeT targetIndex) {
|
||||||
|
if (targetIndex >= kFormatCapabilityTargetCount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (targetIndex == kFormatCapabilityRenderbufferTargetIndex) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return static_cast<TextureTarget>(targetIndex) != TextureTarget::TextureBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
|
||||||
|
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex) {
|
||||||
|
Flags<PixelFormatNormalizeOptionBit> options;
|
||||||
|
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
options |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
|
||||||
|
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
|
||||||
|
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||||
GLenum* outFormat, GLenum* outType, TextureTarget target) {
|
GLenum* outFormat, GLenum* outType, TextureTarget target) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
@@ -148,6 +201,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat) {
|
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat) {
|
||||||
return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Bool BackendFormatAddsAlpha(TextureInternalFormat internalFormat, SizeT targetIndex) {
|
||||||
|
if (!TargetRequiresRenderableFormat(targetIndex)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (pActiveBackendObject && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
|
||||||
|
const Flags<PixelFormatNormalizeOptionBit> options = GetRuntimeFallbackNormalizeOptions(
|
||||||
|
requestedInternalFormat, GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
|
||||||
|
return static_cast<Bool>(options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target) {
|
||||||
|
const SizeT targetIndex =
|
||||||
|
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
|
||||||
|
return BackendFormatAddsAlpha(internalFormat, targetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) {
|
||||||
|
return BackendFormatAddsAlpha(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
|
||||||
|
}
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
namespace PrgramImpl {
|
namespace PrgramImpl {
|
||||||
String ProcessOutColorLocations(const String& glslCode) {
|
String ProcessOutColorLocations(const String& glslCode) {
|
||||||
@@ -276,6 +354,55 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return glslCode;
|
return glslCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount) {
|
||||||
|
#ifdef TRACY_ENABLE
|
||||||
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
|
#endif
|
||||||
|
// The name is the marker: ShaderSourceProcessor only emits it when the source
|
||||||
|
// wrote gl_FragColor, and such a shader can have no other output.
|
||||||
|
static const char* const kLoweredName = "mg_FragColor";
|
||||||
|
if (shaderType != GL_FRAGMENT_SHADER || drawBufferCount <= 1) {
|
||||||
|
return glslCode;
|
||||||
|
}
|
||||||
|
static const std::regex declRegex(
|
||||||
|
R"(layout\s*\(\s*location\s*=\s*0\s*\)\s*out\s+((?:lowp|mediump|highp)\s+)?vec4\s+mg_FragColor\s*;)");
|
||||||
|
std::smatch declMatch;
|
||||||
|
if (!std::regex_search(glslCode, declMatch, declRegex)) {
|
||||||
|
return glslCode;
|
||||||
|
}
|
||||||
|
const String precision = declMatch[1].matched ? declMatch[1].str() : String();
|
||||||
|
|
||||||
|
String replicaDecls;
|
||||||
|
String replicaCopies;
|
||||||
|
for (Uint location = 1; location < drawBufferCount; ++location) {
|
||||||
|
const String name = String(kLoweredName) + "_" + std::to_string(location);
|
||||||
|
replicaDecls += "\nlayout(location = " + std::to_string(location) + ") out " + precision + "vec4 " +
|
||||||
|
name + ";";
|
||||||
|
replicaCopies += "\n " + name + " = " + kLoweredName + ";";
|
||||||
|
}
|
||||||
|
|
||||||
|
static const std::regex mainRegex(R"(void\s+main\s*\([^)]*\)\s*\{)");
|
||||||
|
std::smatch mainMatch;
|
||||||
|
if (!std::regex_search(glslCode, mainMatch, mainRegex)) {
|
||||||
|
return glslCode;
|
||||||
|
}
|
||||||
|
SizeT bracePos = static_cast<SizeT>(mainMatch.position(0) + mainMatch.length(0) - 1);
|
||||||
|
Int depth = 0;
|
||||||
|
for (SizeT pos = bracePos; pos < glslCode.size(); ++pos) {
|
||||||
|
if (glslCode[pos] == '{') {
|
||||||
|
++depth;
|
||||||
|
} else if (glslCode[pos] == '}') {
|
||||||
|
--depth;
|
||||||
|
if (depth == 0) {
|
||||||
|
glslCode.insert(pos, replicaCopies + "\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glslCode.insert(static_cast<SizeT>(declMatch.position(0)) + declMatch[0].str().size(), replicaDecls);
|
||||||
|
return glslCode;
|
||||||
|
}
|
||||||
|
|
||||||
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType) {
|
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
@@ -342,6 +469,167 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// How a lookup carries its level of detail, and how many arguments it takes
|
||||||
|
// before the optional bias.
|
||||||
|
struct LodLookupForm {
|
||||||
|
const char* name;
|
||||||
|
Int requiredArgs; // arguments before the optional bias (implicit form)
|
||||||
|
Int explicitLodArg; // index of the explicit LOD argument, -1 for implicit
|
||||||
|
};
|
||||||
|
|
||||||
|
// texelFetch* is deliberately absent: an integer fetch names its level directly
|
||||||
|
// and takes no LOD bias. textureGather has no bias either. textureGrad* derives
|
||||||
|
// the LOD from gradients and offers no argument to fold a bias into, so it is
|
||||||
|
// left alone rather than rewritten incorrectly.
|
||||||
|
constexpr LodLookupForm LOD_LOOKUP_FORMS[] = {
|
||||||
|
{"textureProjLodOffset", 0, 2}, {"textureProjOffset", 4, -1}, {"textureProjLod", 0, 2},
|
||||||
|
{"textureLodOffset", 0, 2}, {"textureOffset", 3, -1}, {"textureProj", 2, -1},
|
||||||
|
{"textureLod", 0, 2}, {"texture", 2, -1},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sampler types with no mip chain, or whose GLSL lookups have no bias overload
|
||||||
|
// at all (the array-shadow forms), so nothing can or should be folded in.
|
||||||
|
Bool IsBiasableSamplerType(const String& samplerType) {
|
||||||
|
if (samplerType.find("MS") != String::npos) return false; // multisample
|
||||||
|
if (samplerType.find("Buffer") != String::npos) return false; // texture buffer
|
||||||
|
if (samplerType.find("Rect") != String::npos) return false; // rectangle: no mips
|
||||||
|
if (samplerType == "sampler2DArrayShadow") return false;
|
||||||
|
if (samplerType == "samplerCubeArrayShadow") return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool IsIdentifierChar(char c) { return std::isalnum(static_cast<unsigned char>(c)) || c == '_'; }
|
||||||
|
|
||||||
|
// Byte offsets of the top-level argument separators and of the closing paren,
|
||||||
|
// starting from the '(' at openParen. Empty when the parentheses do not balance.
|
||||||
|
Vector<SizeT> SplitCallArguments(const String& code, SizeT openParen) {
|
||||||
|
Vector<SizeT> marks;
|
||||||
|
Int depth = 0;
|
||||||
|
for (SizeT i = openParen; i < code.size(); ++i) {
|
||||||
|
const char c = code[i];
|
||||||
|
if (c == '(' || c == '[') {
|
||||||
|
++depth;
|
||||||
|
} else if (c == ']') {
|
||||||
|
--depth;
|
||||||
|
} else if (c == ')') {
|
||||||
|
--depth;
|
||||||
|
if (depth == 0) {
|
||||||
|
marks.push_back(i);
|
||||||
|
return marks;
|
||||||
|
}
|
||||||
|
} else if (c == ',' && depth == 1) {
|
||||||
|
marks.push_back(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
String EmulateTextureLodBias(const String& glslCode) {
|
||||||
|
#ifdef TRACY_ENABLE
|
||||||
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
|
#endif
|
||||||
|
if (glslCode.find("sampler") == String::npos || glslCode.find("texture") == String::npos) {
|
||||||
|
return glslCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect the mip-capable sampler uniforms this shader declares.
|
||||||
|
static const std::regex samplerDeclRegex(
|
||||||
|
R"(uniform\s+(?:(?:highp|mediump|lowp)\s+)?([iu]?sampler[A-Za-z0-9]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*;)");
|
||||||
|
UnorderedMap<String, String> samplerNames; // name -> bias uniform name
|
||||||
|
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), samplerDeclRegex), end; it != end; ++it) {
|
||||||
|
const String samplerType = (*it)[1].str();
|
||||||
|
if (!IsBiasableSamplerType(samplerType)) continue;
|
||||||
|
const String name = (*it)[2].str();
|
||||||
|
samplerNames.emplace(name, String(LOD_BIAS_UNIFORM_PREFIX) + name);
|
||||||
|
}
|
||||||
|
if (samplerNames.empty()) {
|
||||||
|
return glslCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite the lookups. Right-to-left so earlier offsets stay valid, and only for
|
||||||
|
// samplers named directly as the first argument (SPIRV-Cross never produces an
|
||||||
|
// expression there for ES output, which has no separate sampler objects).
|
||||||
|
String result = glslCode;
|
||||||
|
Vector<String> usedSamplers;
|
||||||
|
for (SizeT scan = result.size(); scan-- > 0;) {
|
||||||
|
if (result[scan] != 't') continue;
|
||||||
|
if (scan > 0 && IsIdentifierChar(result[scan - 1])) continue;
|
||||||
|
|
||||||
|
const LodLookupForm* form = nullptr;
|
||||||
|
SizeT openParen = 0;
|
||||||
|
for (const auto& candidate : LOD_LOOKUP_FORMS) {
|
||||||
|
const SizeT nameLength = std::strlen(candidate.name);
|
||||||
|
if (result.compare(scan, nameLength, candidate.name) != 0) continue;
|
||||||
|
SizeT after = result.find_first_not_of(" \t", scan + nameLength);
|
||||||
|
if (after == String::npos || result[after] != '(') continue;
|
||||||
|
form = &candidate;
|
||||||
|
openParen = after;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (form == nullptr) continue;
|
||||||
|
|
||||||
|
const Vector<SizeT> marks = SplitCallArguments(result, openParen);
|
||||||
|
if (marks.empty()) continue;
|
||||||
|
const SizeT argCount = marks.size();
|
||||||
|
const SizeT closeParen = marks.back();
|
||||||
|
|
||||||
|
// First argument must be one of our samplers.
|
||||||
|
const SizeT firstArgStart = result.find_first_not_of(" \t", openParen + 1);
|
||||||
|
SizeT firstArgEnd = marks.front();
|
||||||
|
while (firstArgEnd > firstArgStart && (result[firstArgEnd - 1] == ' ' || result[firstArgEnd - 1] == '\t')) {
|
||||||
|
--firstArgEnd;
|
||||||
|
}
|
||||||
|
if (firstArgStart == String::npos || firstArgEnd <= firstArgStart) continue;
|
||||||
|
const String samplerName = result.substr(firstArgStart, firstArgEnd - firstArgStart);
|
||||||
|
const auto samplerIt = samplerNames.find(samplerName);
|
||||||
|
if (samplerIt == samplerNames.end()) continue;
|
||||||
|
|
||||||
|
const String& biasName = samplerIt->second;
|
||||||
|
if (form->explicitLodArg >= 0) {
|
||||||
|
// Explicit LOD: the bias adds to it, as Vulkan does for
|
||||||
|
// OpImageSampleExplicitLod and as the CTS reference expects.
|
||||||
|
const SizeT lodIndex = static_cast<SizeT>(form->explicitLodArg);
|
||||||
|
if (argCount <= lodIndex) continue;
|
||||||
|
const SizeT lodStart = marks[lodIndex - 1] + 1;
|
||||||
|
const SizeT lodEnd = marks[lodIndex];
|
||||||
|
result.insert(lodEnd, String(") + ") + biasName + ")");
|
||||||
|
result.insert(lodStart, "((");
|
||||||
|
} else {
|
||||||
|
const SizeT required = static_cast<SizeT>(form->requiredArgs);
|
||||||
|
if (argCount == required) {
|
||||||
|
result.insert(closeParen, String(", ") + biasName);
|
||||||
|
} else if (argCount == required + 1) {
|
||||||
|
const SizeT biasStart = marks[argCount - 2] + 1;
|
||||||
|
result.insert(closeParen, String(") + ") + biasName + ")");
|
||||||
|
result.insert(biasStart, "((");
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usedSamplers.push_back(samplerName);
|
||||||
|
}
|
||||||
|
if (usedSamplers.empty()) {
|
||||||
|
return glslCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare the bias uniforms that were actually referenced, right after the
|
||||||
|
// sampler declaration line they belong to.
|
||||||
|
for (const auto& samplerName : usedSamplers) {
|
||||||
|
const String& biasName = samplerNames[samplerName];
|
||||||
|
if (result.find(String("float ") + biasName + ";") != String::npos) continue;
|
||||||
|
const std::regex declRegex(
|
||||||
|
R"(uniform\s+(?:(?:highp|mediump|lowp)\s+)?[iu]?sampler[A-Za-z0-9]*\s+)" + samplerName + R"(\s*;)");
|
||||||
|
std::smatch match;
|
||||||
|
if (!std::regex_search(result, match, declRegex)) continue;
|
||||||
|
const SizeT declEnd = static_cast<SizeT>(match.position(0)) + match[0].str().size();
|
||||||
|
result.insert(declEnd, String("\nuniform highp float ") + biasName + ";");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace PrgramImpl
|
} // namespace PrgramImpl
|
||||||
|
|
||||||
namespace Utils {
|
namespace Utils {
|
||||||
@@ -852,6 +1140,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (pixelPackBufferObject) {
|
||||||
|
// WritebackFromBackend bumps change serials with no backend op; re-open
|
||||||
|
// the buffer draw-clean memos (once for the whole row loop).
|
||||||
|
BufferImpl::BumpBufferMutationEpoch();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} // namespace ReadbackImpl
|
} // namespace ReadbackImpl
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||||
|
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectGLES {
|
namespace MobileGL::MG_Backend::DirectGLES {
|
||||||
namespace DebugImpl {
|
namespace DebugImpl {
|
||||||
@@ -34,12 +36,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
} // namespace VertexArrayImpl
|
} // namespace VertexArrayImpl
|
||||||
|
|
||||||
namespace TextureImpl {
|
namespace TextureImpl {
|
||||||
|
// Whether images on this format-capability target can back a colour attachment, and so
|
||||||
|
// need a colour-renderable storage format even when the frontend asked for a
|
||||||
|
// three-channel one ES never renders to. Shared by the capability probe (which passes the
|
||||||
|
// capabilities it has just queried, before the globals are published) and by the
|
||||||
|
// allocation path (which reads the active backend's), so the format the cache was probed
|
||||||
|
// with is always the format the image is created with.
|
||||||
|
Bool TargetRequiresRenderableFormat(SizeT targetIndex);
|
||||||
|
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
|
||||||
|
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);
|
||||||
|
|
||||||
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||||
GLenum* outFormat, GLenum* outType,
|
GLenum* outFormat, GLenum* outType,
|
||||||
TextureTarget target = TextureTarget::Unknown);
|
TextureTarget target = TextureTarget::Unknown);
|
||||||
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||||
GLenum* outFormat, GLenum* outType);
|
GLenum* outFormat, GLenum* outType);
|
||||||
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
|
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
|
||||||
|
|
||||||
|
// True when the format the image is actually created with has an alpha channel the
|
||||||
|
// frontend format does not (the three-channel colour-renderable widening). GL reads such
|
||||||
|
// a channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE and
|
||||||
|
// any readback of the image has to overwrite the alpha the draw happened to leave there.
|
||||||
|
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
|
||||||
|
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
|
||||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
||||||
} // namespace TextureImpl
|
} // namespace TextureImpl
|
||||||
|
|
||||||
@@ -104,7 +123,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
|
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
|
||||||
Uint32 unormOutputMask);
|
Uint32 unormOutputMask);
|
||||||
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
|
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
|
||||||
|
// Legacy GLSL's gl_FragColor is broadcast to every enabled draw buffer (GL 4.6
|
||||||
|
// 15.2.3), but ShaderSourceProcessor lowers it to the single output mg_FragColor,
|
||||||
|
// which only ever reaches draw buffer 0. Replicates it across `drawBufferCount`
|
||||||
|
// outputs and copies the value into them at the end of main. A no-op for
|
||||||
|
// drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
|
||||||
|
// enables several draw buffers, so the ordinary single-target shader is untouched.
|
||||||
|
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
|
||||||
String RemoveLayoutBinding(const String& glslCode);
|
String RemoveLayoutBinding(const String& glslCode);
|
||||||
|
// 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.
|
||||||
|
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
|
||||||
|
// ES has no per-texture/sampler LOD bias at all (GL_TEXTURE_LOD_BIAS is desktop
|
||||||
|
// only; Vulkan spells it VkSamplerCreateInfo::mipLodBias), so it has to reach the
|
||||||
|
// shader as a uniform and be folded into every lookup's level of detail. Declares
|
||||||
|
// one `uniform highp float mg_lodBias_<sampler>;` per mip-capable sampler and adds
|
||||||
|
// it to the bias / explicit-LOD argument of every lookup that takes one. Draws push
|
||||||
|
// the bound texture's (or sampler object's) value into it; a shader whose samplers
|
||||||
|
// all have a zero bias is therefore unaffected. Returns the source unchanged when
|
||||||
|
// there is nothing to rewrite.
|
||||||
|
String EmulateTextureLodBias(const String& glslCode);
|
||||||
} // namespace PrgramImpl
|
} // namespace PrgramImpl
|
||||||
|
|
||||||
namespace Utils {
|
namespace Utils {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||||
|
#include "MG_Util/Async/ShaderCompilePool.h"
|
||||||
|
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -41,13 +42,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool IsLayeredTarget(TextureTarget target) {
|
Bool IsLayeredTarget(TextureTarget target) {
|
||||||
return target == TextureTarget::Texture3D || target == TextureTarget::Texture1DArray ||
|
return target == TextureTarget::Texture3D || target == TextureTarget::Texture1DArray ||
|
||||||
target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMap ||
|
target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMap ||
|
||||||
target == TextureTarget::TextureCubeMapArray ||
|
target == TextureTarget::TextureCubeMapArray || target == TextureTarget::Texture2DMultisampleArray;
|
||||||
target == TextureTarget::Texture2DMultisampleArray;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool IsMultisampleTarget(TextureTarget target) {
|
Bool IsMultisampleTarget(TextureTarget target) {
|
||||||
return target == TextureTarget::Texture2DMultisample ||
|
return target == TextureTarget::Texture2DMultisample || target == TextureTarget::Texture2DMultisampleArray;
|
||||||
target == TextureTarget::Texture2DMultisampleArray;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool IsTextureBufferTarget(TextureTarget target) {
|
Bool IsTextureBufferTarget(TextureTarget target) {
|
||||||
@@ -59,8 +58,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLenum normalizedInternalFormat = glFormat;
|
GLenum normalizedInternalFormat = glFormat;
|
||||||
GLenum imageFormat = GL_RGBA;
|
GLenum imageFormat = GL_RGBA;
|
||||||
GLenum imageType = GL_UNSIGNED_BYTE;
|
GLenum imageType = GL_UNSIGNED_BYTE;
|
||||||
MG_Util::TextureFormatProcessor::NormalizePixelFormat(
|
MG_Util::TextureFormatProcessor::NormalizePixelFormat(glFormat, PixelFormatNormalizeOptionBit::None,
|
||||||
glFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat, &imageFormat, &imageType);
|
&normalizedInternalFormat, &imageFormat, &imageType);
|
||||||
return imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER || imageFormat == GL_RGB_INTEGER ||
|
return imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER || imageFormat == GL_RGB_INTEGER ||
|
||||||
imageFormat == GL_RGBA_INTEGER;
|
imageFormat == GL_RGBA_INTEGER;
|
||||||
}
|
}
|
||||||
@@ -81,8 +80,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return caps;
|
return caps;
|
||||||
}
|
}
|
||||||
|
|
||||||
FormatCapabilityFlags BuildVulkanCaps(TextureInternalFormat logicalFormat,
|
FormatCapabilityFlags BuildVulkanCaps(TextureInternalFormat logicalFormat, TextureTarget target,
|
||||||
TextureTarget target,
|
|
||||||
VkFormatFeatureFlags features) {
|
VkFormatFeatureFlags features) {
|
||||||
FormatCapabilityFlags caps;
|
FormatCapabilityFlags caps;
|
||||||
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
|
const Bool isDepth = MG_Util::IsDepthFormatInternalFormat(logicalFormat);
|
||||||
@@ -101,8 +99,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const Bool sampled = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0;
|
const Bool sampled = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0;
|
||||||
const Bool linearFilter = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0;
|
const Bool linearFilter = (features & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0;
|
||||||
const Bool colorRenderable = (features & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
|
const Bool colorRenderable = (features & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
|
||||||
const Bool depthStencilRenderable =
|
const Bool depthStencilRenderable = (features & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0;
|
||||||
(features & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0;
|
|
||||||
const Bool renderable = (isDepth || isStencil) ? depthStencilRenderable : colorRenderable;
|
const Bool renderable = (isDepth || isStencil) ? depthStencilRenderable : colorRenderable;
|
||||||
|
|
||||||
if (sampled || renderable) {
|
if (sampled || renderable) {
|
||||||
@@ -199,21 +196,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
Bool HasNewCaveatFormatCaps(FormatCapabilityFlags nativeCaps, FormatCapabilityFlags fallbackCaps) {
|
Bool HasNewCaveatFormatCaps(FormatCapabilityFlags nativeCaps, FormatCapabilityFlags fallbackCaps) {
|
||||||
for (FormatCapability capability : kReportedFormatCapabilities) {
|
for (FormatCapability capability : kReportedFormatCapabilities) {
|
||||||
if (HasFormatCapability(fallbackCaps, capability) &&
|
if (HasFormatCapability(fallbackCaps, capability) && !HasFormatCapability(nativeCaps, capability)) {
|
||||||
!HasFormatCapability(nativeCaps, capability)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LogVulkanFormatCaveat(TextureInternalFormat logicalFormat,
|
void LogVulkanFormatCaveat(TextureInternalFormat logicalFormat, SizeT targetIndex,
|
||||||
SizeT targetIndex,
|
|
||||||
TextureInternalFormat fallbackFormat) {
|
TextureInternalFormat fallbackFormat) {
|
||||||
MGLOG_D("Caveat: %s %s not fully supported. Reason: native Vulkan format is not fully supported. Fallback: %s",
|
MGLOG_D(
|
||||||
GetFormatCapabilityTargetName(targetIndex).c_str(),
|
"Caveat: %s %s not fully supported. Reason: native Vulkan format is not fully supported. Fallback: %s",
|
||||||
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
|
GetFormatCapabilityTargetName(targetIndex).c_str(),
|
||||||
MG_Util::ConvertTextureInternalFormatToString(fallbackFormat).c_str());
|
MG_Util::ConvertTextureInternalFormatToString(logicalFormat).c_str(),
|
||||||
|
MG_Util::ConvertTextureInternalFormatToString(fallbackFormat).c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<Int> BuildSampleCounts(Int maxSamples) {
|
Vector<Int> BuildSampleCounts(Int maxSamples) {
|
||||||
@@ -257,15 +253,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
|
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
|
||||||
const auto target = static_cast<TextureTarget>(targetIndex);
|
const auto target = static_cast<TextureTarget>(targetIndex);
|
||||||
const VkFormatFeatureFlags nativeFeatures =
|
const VkFormatFeatureFlags nativeFeatures = IsTextureBufferTarget(target)
|
||||||
IsTextureBufferTarget(target) ? nativeProperties.bufferFeatures
|
? nativeProperties.bufferFeatures
|
||||||
: nativeProperties.optimalTilingFeatures;
|
: nativeProperties.optimalTilingFeatures;
|
||||||
FormatCapabilityFlags nativeCaps = BuildVulkanCaps(logicalFormat, target, nativeFeatures);
|
FormatCapabilityFlags nativeCaps = BuildVulkanCaps(logicalFormat, target, nativeFeatures);
|
||||||
cache.FullCaps[targetIndex][formatIndex] |= nativeCaps;
|
cache.FullCaps[targetIndex][formatIndex] |= nativeCaps;
|
||||||
|
|
||||||
const VkFormatFeatureFlags fallbackFeatures =
|
const VkFormatFeatureFlags fallbackFeatures = IsTextureBufferTarget(target)
|
||||||
IsTextureBufferTarget(target) ? fallbackProperties.bufferFeatures
|
? fallbackProperties.bufferFeatures
|
||||||
: fallbackProperties.optimalTilingFeatures;
|
: fallbackProperties.optimalTilingFeatures;
|
||||||
FormatCapabilityFlags fallbackCaps = BuildVulkanCaps(logicalFormat, target, fallbackFeatures);
|
FormatCapabilityFlags fallbackCaps = BuildVulkanCaps(logicalFormat, target, fallbackFeatures);
|
||||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
|
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
|
||||||
cache.CaveatCaps[targetIndex][formatIndex] |= fallbackCaps;
|
cache.CaveatCaps[targetIndex][formatIndex] |= fallbackCaps;
|
||||||
@@ -300,9 +296,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
cache.FullCaps[renderbufferTargetIndex][formatIndex] |= renderbufferCaps;
|
cache.FullCaps[renderbufferTargetIndex][formatIndex] |= renderbufferCaps;
|
||||||
|
|
||||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
|
if (fallbackFormat != VK_FORMAT_UNDEFINED && fallbackFormat != nativeFormat) {
|
||||||
FormatCapabilityFlags fallbackRenderbufferCaps =
|
FormatCapabilityFlags fallbackRenderbufferCaps = BuildVulkanCaps(
|
||||||
BuildVulkanCaps(logicalFormat, TextureTarget::Texture2D,
|
logicalFormat, TextureTarget::Texture2D, fallbackProperties.optimalTilingFeatures);
|
||||||
fallbackProperties.optimalTilingFeatures);
|
|
||||||
fallbackRenderbufferCaps &= FormatCapability::Creatable;
|
fallbackRenderbufferCaps &= FormatCapability::Creatable;
|
||||||
if ((fallbackProperties.optimalTilingFeatures &
|
if ((fallbackProperties.optimalTilingFeatures &
|
||||||
(VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) !=
|
(VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) !=
|
||||||
@@ -311,8 +306,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
fallbackRenderbufferCaps |= FormatCapability::MultisampleRenderbuffer;
|
fallbackRenderbufferCaps |= FormatCapability::MultisampleRenderbuffer;
|
||||||
}
|
}
|
||||||
cache.CaveatCaps[renderbufferTargetIndex][formatIndex] |= fallbackRenderbufferCaps;
|
cache.CaveatCaps[renderbufferTargetIndex][formatIndex] |= fallbackRenderbufferCaps;
|
||||||
if (fallbackLogicalFormat &&
|
if (fallbackLogicalFormat && HasNewCaveatFormatCaps(renderbufferCaps, fallbackRenderbufferCaps)) {
|
||||||
HasNewCaveatFormatCaps(renderbufferCaps, fallbackRenderbufferCaps)) {
|
|
||||||
LogVulkanFormatCaveat(logicalFormat, renderbufferTargetIndex, *fallbackLogicalFormat);
|
LogVulkanFormatCaveat(logicalFormat, renderbufferTargetIndex, *fallbackLogicalFormat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,14 +323,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
void PopulateFormatCapabilities(VkPhysicalDevice physicalDevice,
|
void PopulateFormatCapabilities(VkPhysicalDevice physicalDevice,
|
||||||
PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties,
|
PFN_vkGetPhysicalDeviceFormatProperties getFormatProperties,
|
||||||
const MG_External::VulkanCapabilities& capabilities,
|
const MG_External::VulkanCapabilities& capabilities, FormatCapabilityCache& cache) {
|
||||||
FormatCapabilityCache& cache) {
|
|
||||||
PopulateFormatCapabilitiesImpl(physicalDevice, getFormatProperties, capabilities, cache);
|
PopulateFormatCapabilitiesImpl(physicalDevice, getFormatProperties, capabilities, cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
|
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
|
||||||
|
|
||||||
BackendObject_DirectVulkan::BackendObject_DirectVulkan(): m_rendererInfo{GetRendererIdentity()} {}
|
BackendObject_DirectVulkan::BackendObject_DirectVulkan() : m_rendererInfo{GetRendererIdentity()} {}
|
||||||
|
|
||||||
Bool BackendObject_DirectVulkan::InitWindowSurface() {
|
Bool BackendObject_DirectVulkan::InitWindowSurface() {
|
||||||
if (!m_windowHandle.Handle) {
|
if (!m_windowHandle.Handle) {
|
||||||
@@ -410,10 +403,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MGLOG_E("DirectVulkan backend not initialized");
|
MGLOG_E("DirectVulkan backend not initialized");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!handle.Handle || (handle.Backend != WindowBackend::Android &&
|
if (!handle.Handle || (handle.Backend != WindowBackend::Android && handle.Backend != WindowBackend::X11 &&
|
||||||
handle.Backend != WindowBackend::X11 &&
|
handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::Win32)) {
|
||||||
handle.Backend != WindowBackend::MetalLayer &&
|
|
||||||
handle.Backend != WindowBackend::Win32)) {
|
|
||||||
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
|
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -504,36 +495,50 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
.RendererName = "Magma",
|
.RendererName = "Magma",
|
||||||
.BackendName = "Direct (Vulkan)",
|
.BackendName = "Direct (Vulkan)",
|
||||||
.ExtraVendor = Nullopt,
|
.ExtraVendor = Nullopt,
|
||||||
.RendererGLInfo =
|
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
||||||
{
|
.TargetGLSLVersion = {4, 6, 0},
|
||||||
.TargetGLVersion = {3, 3, 0},
|
// Baseline advertisement (no shader subgroup, no timer queries); a
|
||||||
.TargetGLSLVersion = {4, 6, 0},
|
// live backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||||
// Baseline advertisement (no shader subgroup, no timer queries); a
|
.Extensions = BuildAdvertisedExtensions(false, false, false),
|
||||||
// live backend reconciles its copy in UpdateAdvertisedExtensions.
|
.IsCompatibilityProfile = false},
|
||||||
.Extensions = BuildAdvertisedExtensions(false, false, false),
|
|
||||||
.IsCompatibilityProfile = false
|
|
||||||
},
|
|
||||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||||
return rendererInfo;
|
return rendererInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||||
Bool anisotropicFilteringSupported) {
|
Bool anisotropicFilteringSupported) {
|
||||||
Vector<GLExtension> extensions = {V_OpenGL30, V_OpenGL31, V_OpenGL32,
|
Vector<GLExtension> extensions = {
|
||||||
V_OpenGL33, E_GL_ARB_draw_buffers_blend, E_GL_ARB_compute_shader,
|
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||||
E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
|
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_multi_draw_indirect,
|
||||||
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
|
E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||||
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample,
|
||||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
|
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
|
||||||
E_GL_ARB_texture_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
|
E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
||||||
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
|
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
|
||||||
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
|
E_GL_ARB_explicit_attrib_location,
|
||||||
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
|
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||||
E_GL_ARB_explicit_attrib_location};
|
// extension explicitly permits. It is also the only thing that
|
||||||
|
// exposes glProgramParameteri before GL 4.1.
|
||||||
|
E_GL_ARB_get_program_binary};
|
||||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||||
}
|
}
|
||||||
|
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan
|
||||||
|
// device's: the compiler threads belong to MobileGL's shader pool and
|
||||||
|
// glCompileShader/glLinkProgram are serviced entirely inside the frontend, so there
|
||||||
|
// is no device feature to condition this on.
|
||||||
|
//
|
||||||
|
// Gated on the async flag deliberately, and this is the whole reason the gate
|
||||||
|
// exists. Advertising the string is the one part of asynchronous compilation that a
|
||||||
|
// recorded trace can never cover: Iris and Sodium change their SUBMISSION SCHEDULE
|
||||||
|
// the moment they see it - they enqueue whole pipeline batches and poll
|
||||||
|
// GL_COMPLETION_STATUS_KHR instead of compiling one program at a time - so
|
||||||
|
// MOBILEGL_ASYNC_SHADER_COMPILE=0 has to withdraw the application-visible behaviour
|
||||||
|
// change as well as the threading, or the kill switch would only be half a switch.
|
||||||
|
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||||
|
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
||||||
|
}
|
||||||
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension string);
|
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension string);
|
||||||
// only advertised when the device actually supports timestamp queries and the
|
// only advertised when the device actually supports timestamp queries and the
|
||||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
||||||
@@ -593,6 +598,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
funcsTable.GL.ClearBufferiv = ClearBufferiv;
|
funcsTable.GL.ClearBufferiv = ClearBufferiv;
|
||||||
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
|
funcsTable.GL.ClearNamedFramebufferfv = ClearNamedFramebufferfv;
|
||||||
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
|
funcsTable.GL.ClearNamedFramebufferfi = ClearNamedFramebufferfi;
|
||||||
|
funcsTable.GL.ClearNamedFramebufferiv = ClearNamedFramebufferiv;
|
||||||
|
funcsTable.GL.ClearNamedFramebufferuiv = ClearNamedFramebufferuiv;
|
||||||
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
|
funcsTable.GL.BlitFramebuffer = BlitFramebuffer;
|
||||||
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
|
funcsTable.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
|
||||||
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
|
funcsTable.GL.CopyTexImage2D = CopyTexImage2D;
|
||||||
@@ -610,12 +617,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
||||||
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
||||||
funcsTable.GL.GetProgramiv = GetProgramiv;
|
funcsTable.GL.GetProgramiv = GetProgramiv;
|
||||||
funcsTable.GL.GetProgramInterfaceiv = GetProgramInterfaceiv;
|
|
||||||
funcsTable.GL.GetProgramResourceIndex = GetProgramResourceIndex;
|
|
||||||
funcsTable.GL.GetProgramResourceName = GetProgramResourceName;
|
|
||||||
funcsTable.GL.GetProgramResourceiv = GetProgramResourceiv;
|
|
||||||
funcsTable.GL.GetProgramResourceLocation = GetProgramResourceLocation;
|
|
||||||
funcsTable.GL.GetProgramResourceLocationIndex = GetProgramResourceLocationIndex;
|
|
||||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||||
funcsTable.GL.FenceSync = FenceSync;
|
funcsTable.GL.FenceSync = FenceSync;
|
||||||
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
||||||
@@ -725,7 +726,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// rather than a maximum the sampler manager will never apply.
|
// rather than a maximum the sampler manager will never apply.
|
||||||
m_dynamicParameters.MaxTextureMaxAnisotropy =
|
m_dynamicParameters.MaxTextureMaxAnisotropy =
|
||||||
(pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported()) ? m_vulkanCaps.MaxSamplerAnisotropy
|
(pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported()) ? m_vulkanCaps.MaxSamplerAnisotropy
|
||||||
: 1.0f;
|
: 1.0f;
|
||||||
m_dynamicParameters.SmoothLineWidthRangeMin = m_vulkanCaps.SmoothLineWidthRangeMin;
|
m_dynamicParameters.SmoothLineWidthRangeMin = m_vulkanCaps.SmoothLineWidthRangeMin;
|
||||||
m_dynamicParameters.SmoothLineWidthRangeMax = m_vulkanCaps.SmoothLineWidthRangeMax;
|
m_dynamicParameters.SmoothLineWidthRangeMax = m_vulkanCaps.SmoothLineWidthRangeMax;
|
||||||
m_dynamicParameters.SmoothLineWidthGranularity = m_vulkanCaps.SmoothLineWidthGranularity;
|
m_dynamicParameters.SmoothLineWidthGranularity = m_vulkanCaps.SmoothLineWidthGranularity;
|
||||||
@@ -746,8 +747,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_dynamicParameters.MaxIntegerSamples = m_vulkanCaps.MaxIntegerSamples;
|
m_dynamicParameters.MaxIntegerSamples = m_vulkanCaps.MaxIntegerSamples;
|
||||||
m_dynamicParameters.MaxSamples = m_vulkanCaps.MaxSamples;
|
m_dynamicParameters.MaxSamples = m_vulkanCaps.MaxSamples;
|
||||||
m_dynamicParameters.MaxSampleMaskWords = m_vulkanCaps.MaxSampleMaskWords;
|
m_dynamicParameters.MaxSampleMaskWords = m_vulkanCaps.MaxSampleMaskWords;
|
||||||
const Int maxSupportedTextureUnits =
|
const Int maxSupportedTextureUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||||
static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
|
||||||
// GL_MAX_TEXTURE_IMAGE_UNITS is a *per-stage* sampler limit. Adreno/Qualcomm report a huge
|
// GL_MAX_TEXTURE_IMAGE_UNITS is a *per-stage* sampler limit. Adreno/Qualcomm report a huge
|
||||||
// maxPerStageDescriptorSampledImages (descriptor-indexing scale), so clamping it only to our
|
// maxPerStageDescriptorSampledImages (descriptor-indexing scale), so clamping it only to our
|
||||||
// combined array capacity (192) still advertises 192 per stage. Host code treats this value as
|
// combined array capacity (192) still advertises 192 per stage. Host code treats this value as
|
||||||
@@ -757,8 +757,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// limits while keeping the combined limit at our texture-unit array capacity.
|
// limits while keeping the combined limit at our texture-unit array capacity.
|
||||||
constexpr Int maxPerStageTextureUnits =
|
constexpr Int maxPerStageTextureUnits =
|
||||||
static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS);
|
static_cast<Int>(MG_State::GLState::TextureState::MAX_PER_STAGE_TEXTURE_IMAGE_UNITS);
|
||||||
m_dynamicParameters.MaxTextureImageUnits =
|
m_dynamicParameters.MaxTextureImageUnits = std::min(m_vulkanCaps.MaxTextureImageUnits, maxPerStageTextureUnits);
|
||||||
std::min(m_vulkanCaps.MaxTextureImageUnits, maxPerStageTextureUnits);
|
|
||||||
m_dynamicParameters.MaxVertexTextureImageUnits =
|
m_dynamicParameters.MaxVertexTextureImageUnits =
|
||||||
std::min(m_vulkanCaps.MaxVertexTextureImageUnits, maxPerStageTextureUnits);
|
std::min(m_vulkanCaps.MaxVertexTextureImageUnits, maxPerStageTextureUnits);
|
||||||
m_dynamicParameters.MaxComputeTextureImageUnits =
|
m_dynamicParameters.MaxComputeTextureImageUnits =
|
||||||
@@ -767,19 +766,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
std::min(m_vulkanCaps.MaxCombinedTextureImageUnits, maxSupportedTextureUnits);
|
std::min(m_vulkanCaps.MaxCombinedTextureImageUnits, maxSupportedTextureUnits);
|
||||||
// Never advertise more attributes than the state layer can store: the current-value array and
|
// Never advertise more attributes than the state layer can store: the current-value array and
|
||||||
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
|
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
|
||||||
m_dynamicParameters.MaxVertexAttribs =
|
m_dynamicParameters.MaxVertexAttribs = std::min(
|
||||||
std::min(m_vulkanCaps.MaxVertexAttribs,
|
m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
||||||
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
|
||||||
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_vulkanCaps.MaxComputeShaderStorageBlocks;
|
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_vulkanCaps.MaxComputeShaderStorageBlocks;
|
||||||
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_vulkanCaps.MaxCombinedShaderStorageBlocks;
|
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_vulkanCaps.MaxCombinedShaderStorageBlocks;
|
||||||
m_dynamicParameters.MaxComputeUniformBlocks = m_vulkanCaps.MaxComputeUniformBlocks;
|
m_dynamicParameters.MaxComputeUniformBlocks = m_vulkanCaps.MaxComputeUniformBlocks;
|
||||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
||||||
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
|
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
|
||||||
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
|
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
|
||||||
|
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
|
||||||
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
|
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
|
||||||
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
||||||
m_dynamicParameters.MaxImageUnits =
|
m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||||
std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
|
||||||
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
||||||
const Int maxPerStageImageUniforms =
|
const Int maxPerStageImageUniforms =
|
||||||
std::min(m_dynamicParameters.MaxImageUnits, m_dynamicParameters.MaxCombinedImageUniforms);
|
std::min(m_dynamicParameters.MaxImageUnits, m_dynamicParameters.MaxCombinedImageUniforms);
|
||||||
@@ -796,8 +794,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_vulkanCaps.SupportsFragmentStoresAndAtomics ? maxPerStageImageUniforms : 0;
|
m_vulkanCaps.SupportsFragmentStoresAndAtomics ? maxPerStageImageUniforms : 0;
|
||||||
m_dynamicParameters.MaxComputeImageUniforms =
|
m_dynamicParameters.MaxComputeImageUniforms =
|
||||||
std::min(std::max(m_vulkanCaps.MaxComputeImageUniforms, 0), maxPerStageImageUniforms);
|
std::min(std::max(m_vulkanCaps.MaxComputeImageUniforms, 0), maxPerStageImageUniforms);
|
||||||
const Int maxSupportedDrawBuffers =
|
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
||||||
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);
|
||||||
m_dynamicParameters.MaxClipDistances = m_vulkanCaps.MaxClipDistances;
|
m_dynamicParameters.MaxClipDistances = m_vulkanCaps.MaxClipDistances;
|
||||||
@@ -816,21 +813,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
|
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
|
||||||
if (m_vulkanCaps.FragmentInterpolationOffsetBits >= 4 &&
|
if (m_vulkanCaps.FragmentInterpolationOffsetBits >= 4 &&
|
||||||
std::isfinite(m_vulkanCaps.MaxFragmentInterpolationOffset)) {
|
std::isfinite(m_vulkanCaps.MaxFragmentInterpolationOffset)) {
|
||||||
const Float requiredMaxOffset =
|
const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -m_vulkanCaps.FragmentInterpolationOffsetBits);
|
||||||
0.5f - std::ldexp(1.0f, -m_vulkanCaps.FragmentInterpolationOffsetBits);
|
|
||||||
if (m_vulkanCaps.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
|
if (m_vulkanCaps.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
|
||||||
m_dynamicParameters.MaxFragmentInterpolationOffset = m_vulkanCaps.MaxFragmentInterpolationOffset;
|
m_dynamicParameters.MaxFragmentInterpolationOffset = m_vulkanCaps.MaxFragmentInterpolationOffset;
|
||||||
m_dynamicParameters.FragmentInterpolationOffsetBits =
|
m_dynamicParameters.FragmentInterpolationOffsetBits = m_vulkanCaps.FragmentInterpolationOffsetBits;
|
||||||
m_vulkanCaps.FragmentInterpolationOffsetBits;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
|
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
|
||||||
|
// A 2D or 2D multisample array texture is a VK_IMAGE_TYPE_2D image whose GL depth IS its
|
||||||
|
// arrayLayers, so a GL layer is a Vulkan array layer with nothing to translate.
|
||||||
|
// ResolveAttachmentBaseArrayLayer already passes the attachment's layer through. The other
|
||||||
|
// layered targets are declared separately as their own machinery lands.
|
||||||
|
{
|
||||||
|
using DynParams = MG_Backend::DynamicBackendParameters;
|
||||||
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
|
||||||
|
// A cube map array is one 2D image with arrayLayers = 6 * cubeCount, so a GL layer is a
|
||||||
|
// Vulkan array layer here too - but the image cannot be created without imageCubeArray.
|
||||||
|
// A 3D texture's GL layer is a z slice, which only a 2D view over a 2D-array-compatible
|
||||||
|
// image can name. Optimistic: a format that refuses the flag is caught at image creation
|
||||||
|
// and declines the slice view there, which the clear path handles as a soft miss.
|
||||||
|
if (m_vulkanCaps.Supports2DArrayCompatible3DImages) {
|
||||||
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D);
|
||||||
|
}
|
||||||
|
if (m_vulkanCaps.SupportsImageCubeArray) {
|
||||||
|
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
|
||||||
|
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_dynamicParameters.SupportsFloat64VertexAttributes = m_vulkanCaps.SupportsShaderFloat64;
|
||||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||||
if (m_vulkanCaps.SupportsShaderSubgroup) {
|
if (m_vulkanCaps.SupportsShaderSubgroup) {
|
||||||
m_dynamicParameters.SubgroupSize = m_vulkanCaps.SubgroupSize;
|
m_dynamicParameters.SubgroupSize = m_vulkanCaps.SubgroupSize;
|
||||||
m_dynamicParameters.SubgroupSupportedStages = mapShaderStages(m_vulkanCaps.SubgroupSupportedStages);
|
m_dynamicParameters.SubgroupSupportedStages = mapShaderStages(m_vulkanCaps.SubgroupSupportedStages);
|
||||||
m_dynamicParameters.SubgroupSupportedFeatures = mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
|
m_dynamicParameters.SubgroupSupportedFeatures =
|
||||||
|
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
|
||||||
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
|
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
|
||||||
} else {
|
} else {
|
||||||
m_dynamicParameters.SubgroupSize = 0;
|
m_dynamicParameters.SubgroupSize = 0;
|
||||||
@@ -840,8 +860,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
if (m_dynamicParameters.MaxShaderStorageBlockSize != m_vulkanCaps.MaxShaderStorageBlockSize) {
|
if (m_dynamicParameters.MaxShaderStorageBlockSize != m_vulkanCaps.MaxShaderStorageBlockSize) {
|
||||||
MGLOG_I("DirectVulkan: clamped GL_MAX_SHADER_STORAGE_BLOCK_SIZE from %zu to %zu",
|
MGLOG_I("DirectVulkan: clamped GL_MAX_SHADER_STORAGE_BLOCK_SIZE from %zu to %zu",
|
||||||
m_vulkanCaps.MaxShaderStorageBlockSize,
|
m_vulkanCaps.MaxShaderStorageBlockSize, m_dynamicParameters.MaxShaderStorageBlockSize);
|
||||||
m_dynamicParameters.MaxShaderStorageBlockSize);
|
|
||||||
}
|
}
|
||||||
switch (m_vulkanCaps.VendorId) {
|
switch (m_vulkanCaps.VendorId) {
|
||||||
case 0x5143u: // VK_VENDOR_ID: Qualcomm
|
case 0x5143u: // VK_VENDOR_ID: Qualcomm
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||||
#include "MG_Util/Miscellany/IndexGenerator.h"
|
#include "MG_Util/Miscellany/IndexGenerator.h"
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <bit>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <spirv_reflect.h>
|
#include <spirv_reflect.h>
|
||||||
|
|
||||||
@@ -231,6 +232,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
StorageBlockResource block{};
|
StorageBlockResource block{};
|
||||||
block.name = blockName;
|
block.name = blockName;
|
||||||
block.binding = binding->binding;
|
block.binding = binding->binding;
|
||||||
|
// glShaderStorageBlockBinding survives every rebuild of this cache: the
|
||||||
|
// authoritative record of a rebound block lives on the program (it is what
|
||||||
|
// GL_BUFFER_BINDING reports), and only the shader's declared binding is
|
||||||
|
// recoverable from the SPIR-V. Without this, any unrelated state-version
|
||||||
|
// bump would silently revert the block to its declared binding.
|
||||||
|
const Int rebound = program.GetShaderStorageBlockBindingOverride(blockName);
|
||||||
|
if (rebound >= 0) block.binding = static_cast<Uint32>(rebound);
|
||||||
block.dataSize = static_cast<GLint>(binding->block.size);
|
block.dataSize = static_cast<GLint>(binding->block.size);
|
||||||
const GLuint blockIndex = static_cast<GLuint>(cache.storageBlocks.size());
|
const GLuint blockIndex = static_cast<GLuint>(cache.storageBlocks.size());
|
||||||
AddBufferVariablesRecursive(binding->block, blockName, blockIndex, cache.bufferVariables,
|
AddBufferVariablesRecursive(binding->block, blockName, blockIndex, cache.bufferVariables,
|
||||||
@@ -255,18 +263,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return programObject.get();
|
return programObject.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CopyResourceName(const String& source, GLsizei bufSize, GLsizei* length, GLchar* name) {
|
|
||||||
const GLsizei writtenLength = static_cast<GLsizei>(source.size());
|
|
||||||
if (length) {
|
|
||||||
*length = writtenLength;
|
|
||||||
}
|
|
||||||
if (name && bufSize > 0) {
|
|
||||||
const GLsizei copyLength = std::min<GLsizei>(bufSize - 1, writtenLength);
|
|
||||||
std::memcpy(name, source.data(), static_cast<SizeT>(copyLength));
|
|
||||||
name[copyLength] = '\0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
|
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
|
||||||
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||||
if (drawBuffer) {
|
if (drawBuffer) {
|
||||||
@@ -287,100 +283,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return reinterpret_cast<const Uint8*>(indirect);
|
return reinterpret_cast<const Uint8*>(indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<GLuint> GetUniformBlockActiveVariables(const MG_State::GLState::ProgramObject& program,
|
|
||||||
GLuint blockIndex) {
|
|
||||||
Vector<GLuint> activeVariables;
|
|
||||||
const Uint uniformCount = program.GetUniformCount();
|
|
||||||
activeVariables.reserve(uniformCount);
|
|
||||||
for (Uint uniformIndex = 0; uniformIndex < uniformCount; ++uniformIndex) {
|
|
||||||
if (program.GetActiveUniformBlockIndex(uniformIndex) == static_cast<Int>(blockIndex)) {
|
|
||||||
activeVariables.push_back(uniformIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return activeVariables;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint FindProgramInputIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
|
||||||
const Int activeCount = program.GetActiveAttributesCount();
|
|
||||||
for (Int index = 0; index < activeCount; ++index) {
|
|
||||||
if (program.GetActiveAttribName(index) == name) {
|
|
||||||
return static_cast<GLuint>(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return GL_INVALID_INDEX;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint FindProgramOutputIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
|
||||||
const Int activeCount = program.GetActiveFragmentOutputCount();
|
|
||||||
for (Int index = 0; index < activeCount; ++index) {
|
|
||||||
if (program.GetActiveFragmentOutputName(index) == name) {
|
|
||||||
return static_cast<GLuint>(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return GL_INVALID_INDEX;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint GetProgramOutputLocation(const MG_State::GLState::ProgramObject& program, const String& name) {
|
|
||||||
const Int activeCount = program.GetActiveFragmentOutputCount();
|
|
||||||
for (Int index = 0; index < activeCount; ++index) {
|
|
||||||
if (program.GetActiveFragmentOutputName(index) == name) {
|
|
||||||
return program.GetFragmentOutputLocation(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint GetProgramResourceActiveCount(const MG_State::GLState::ProgramObject& program, GLenum programInterface,
|
|
||||||
const ProgramResourceCache& cache) {
|
|
||||||
switch (programInterface) {
|
|
||||||
case GL_SHADER_STORAGE_BLOCK:
|
|
||||||
return static_cast<GLint>(cache.storageBlocks.size());
|
|
||||||
case GL_BUFFER_VARIABLE:
|
|
||||||
return static_cast<GLint>(cache.bufferVariables.size());
|
|
||||||
case GL_UNIFORM_BLOCK:
|
|
||||||
return program.GetActiveUniformBlocksCount();
|
|
||||||
case GL_UNIFORM:
|
|
||||||
return static_cast<GLint>(program.GetUniformCount());
|
|
||||||
case GL_PROGRAM_INPUT:
|
|
||||||
return program.GetActiveAttributesCount();
|
|
||||||
case GL_PROGRAM_OUTPUT:
|
|
||||||
return program.GetActiveFragmentOutputCount();
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint GetProgramResourceMaxNameLength(const MG_State::GLState::ProgramObject& program, GLenum programInterface,
|
|
||||||
const ProgramResourceCache& cache) {
|
|
||||||
switch (programInterface) {
|
|
||||||
case GL_SHADER_STORAGE_BLOCK: {
|
|
||||||
SizeT maxLength = 0;
|
|
||||||
for (const auto& block : cache.storageBlocks) maxLength = std::max(maxLength, block.name.size() + 1);
|
|
||||||
return static_cast<GLint>(maxLength);
|
|
||||||
}
|
|
||||||
case GL_BUFFER_VARIABLE: {
|
|
||||||
SizeT maxLength = 0;
|
|
||||||
for (const auto& var : cache.bufferVariables) maxLength = std::max(maxLength, var.name.size() + 1);
|
|
||||||
return static_cast<GLint>(maxLength);
|
|
||||||
}
|
|
||||||
case GL_UNIFORM_BLOCK:
|
|
||||||
return program.GetActiveUniformBlocksMaxNameLength() + 1;
|
|
||||||
case GL_UNIFORM:
|
|
||||||
return program.GetUniformMaxLength() + 1;
|
|
||||||
case GL_PROGRAM_INPUT:
|
|
||||||
return program.GetActiveAttributesMaxLength() + 1;
|
|
||||||
case GL_PROGRAM_OUTPUT: {
|
|
||||||
SizeT maxLength = 0;
|
|
||||||
const Int activeCount = program.GetActiveFragmentOutputCount();
|
|
||||||
for (Int index = 0; index < activeCount; ++index) {
|
|
||||||
maxLength = std::max(maxLength, program.GetActiveFragmentOutputName(index).size() + 1);
|
|
||||||
}
|
|
||||||
return static_cast<GLint>(maxLength);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void ClearProgramResourceCaches() {
|
void ClearProgramResourceCaches() {
|
||||||
@@ -394,11 +296,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
|
||||||
auto& cache = GetProgramResourceCache(program);
|
auto& cache = GetProgramResourceCache(program);
|
||||||
const auto it = std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
|
auto find = [&cache](const String& key) {
|
||||||
[&](const StorageBlockResource& block) { return block.name == name; });
|
return std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
|
||||||
return it == cache.storageBlocks.end()
|
[&](const StorageBlockResource& block) { return block.name == key; });
|
||||||
? GL_INVALID_INDEX
|
};
|
||||||
: static_cast<GLuint>(std::distance(cache.storageBlocks.begin(), it));
|
auto it = find(name);
|
||||||
|
if (it == cache.storageBlocks.end()) {
|
||||||
|
// Cache names are normalized (NormalizeDescriptorName drops the array suffix), so
|
||||||
|
// an arrayed block that GL enumerates per element - "B[0]", "B[1]" - is one entry
|
||||||
|
// here, spelled "B". Retry against the bare name before giving up.
|
||||||
|
const auto bracket = name.rfind('[');
|
||||||
|
if (bracket == String::npos || name.empty() || name.back() != ']') return GL_INVALID_INDEX;
|
||||||
|
it = find(name.substr(0, bracket));
|
||||||
|
if (it == cache.storageBlocks.end()) return GL_INVALID_INDEX;
|
||||||
|
}
|
||||||
|
return static_cast<GLuint>(std::distance(cache.storageBlocks.begin(), it));
|
||||||
}
|
}
|
||||||
|
|
||||||
GLuint GetShaderStorageBlockBinding(const MG_State::GLState::ProgramObject& program, GLuint blockIndex) {
|
GLuint GetShaderStorageBlockBinding(const MG_State::GLState::ProgramObject& program, GLuint blockIndex) {
|
||||||
@@ -440,6 +352,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
pVulkanRenderer->ClearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
pVulkanRenderer->ClearNamedFramebufferfv(framebuffer, buffer, drawbuffer, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||||
|
GLint drawbuffer, const GLint* value) {
|
||||||
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferiv called with null VulkanRenderer");
|
||||||
|
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferiv called with null GL context");
|
||||||
|
pVulkanRenderer->ClearNamedFramebufferiv(framebuffer, buffer, drawbuffer, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||||
|
GLint drawbuffer, const GLuint* value) {
|
||||||
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferuiv called with null VulkanRenderer");
|
||||||
|
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::ClearNamedFramebufferuiv called with null GL context");
|
||||||
|
pVulkanRenderer->ClearNamedFramebufferuiv(framebuffer, buffer, drawbuffer, value);
|
||||||
|
}
|
||||||
|
|
||||||
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||||
GLint drawbuffer, GLfloat depth, GLint stencil) {
|
GLint drawbuffer, GLfloat depth, GLint stencil) {
|
||||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfi called with null VulkanRenderer");
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ClearNamedFramebufferfi called with null VulkanRenderer");
|
||||||
@@ -859,357 +785,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params) {
|
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) {
|
||||||
if (!params) return;
|
|
||||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
auto* programObject = TryGetDirectVulkanProgram(program);
|
||||||
if (!programObject) return;
|
if (!programObject || storageBlockName == nullptr) return;
|
||||||
auto& cache = GetProgramResourceCache(*programObject);
|
|
||||||
switch (pname) {
|
|
||||||
case GL_ACTIVE_RESOURCES:
|
|
||||||
*params = GetProgramResourceActiveCount(*programObject, programInterface, cache);
|
|
||||||
return;
|
|
||||||
case GL_MAX_NAME_LENGTH:
|
|
||||||
*params = GetProgramResourceMaxNameLength(*programObject, programInterface, cache);
|
|
||||||
return;
|
|
||||||
case GL_MAX_NUM_ACTIVE_VARIABLES:
|
|
||||||
if (programInterface == GL_SHADER_STORAGE_BLOCK) {
|
|
||||||
SizeT maxCount = 0;
|
|
||||||
for (const auto& block : cache.storageBlocks) {
|
|
||||||
maxCount = std::max(maxCount, block.activeVariables.size());
|
|
||||||
}
|
|
||||||
*params = static_cast<GLint>(maxCount);
|
|
||||||
} else if (programInterface == GL_UNIFORM_BLOCK) {
|
|
||||||
GLint maxCount = 0;
|
|
||||||
const Int activeBlocks = programObject->GetActiveUniformBlocksCount();
|
|
||||||
for (Int index = 0; index < activeBlocks; ++index) {
|
|
||||||
maxCount = std::max(maxCount, programObject->GetUniformBlockActiveUniformCount(index));
|
|
||||||
}
|
|
||||||
*params = maxCount;
|
|
||||||
} else {
|
|
||||||
*params = 0;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
*params = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name) {
|
|
||||||
if (!name) return GL_INVALID_INDEX;
|
|
||||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
|
||||||
if (!programObject) return GL_INVALID_INDEX;
|
|
||||||
auto& cache = GetProgramResourceCache(*programObject);
|
|
||||||
const String resourceName = name;
|
|
||||||
if (programInterface == GL_SHADER_STORAGE_BLOCK) {
|
|
||||||
return GetShaderStorageBlockIndex(*programObject, name);
|
|
||||||
}
|
|
||||||
if (programInterface == GL_BUFFER_VARIABLE) {
|
|
||||||
const auto it = std::find_if(cache.bufferVariables.begin(), cache.bufferVariables.end(),
|
|
||||||
[&](const BufferVariableResource& var) { return var.name == resourceName; });
|
|
||||||
return it == cache.bufferVariables.end()
|
|
||||||
? GL_INVALID_INDEX
|
|
||||||
: static_cast<GLuint>(std::distance(cache.bufferVariables.begin(), it));
|
|
||||||
}
|
|
||||||
if (programInterface == GL_UNIFORM_BLOCK) {
|
|
||||||
return programObject->GetUniformBlockIndex(name);
|
|
||||||
}
|
|
||||||
if (programInterface == GL_UNIFORM) {
|
|
||||||
const Int activeUniformIndex = programObject->GetActiveUniformIndex(resourceName);
|
|
||||||
return activeUniformIndex >= 0 ? static_cast<GLuint>(activeUniformIndex) : GL_INVALID_INDEX;
|
|
||||||
}
|
|
||||||
if (programInterface == GL_PROGRAM_INPUT) {
|
|
||||||
return FindProgramInputIndex(*programObject, resourceName);
|
|
||||||
}
|
|
||||||
if (programInterface == GL_PROGRAM_OUTPUT) {
|
|
||||||
return FindProgramOutputIndex(*programObject, resourceName);
|
|
||||||
}
|
|
||||||
return GL_INVALID_INDEX;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
|
|
||||||
GLsizei* length, GLchar* name) {
|
|
||||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
|
||||||
if (!programObject) return;
|
|
||||||
auto& cache = GetProgramResourceCache(*programObject);
|
|
||||||
if (programInterface == GL_SHADER_STORAGE_BLOCK && index < cache.storageBlocks.size()) {
|
|
||||||
CopyResourceName(cache.storageBlocks[index].name, bufSize, length, name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (programInterface == GL_BUFFER_VARIABLE && index < cache.bufferVariables.size()) {
|
|
||||||
CopyResourceName(cache.bufferVariables[index].name, bufSize, length, name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (programInterface == GL_UNIFORM_BLOCK && programObject->IsActiveUniformBlock(index)) {
|
|
||||||
CopyResourceName(programObject->GetUniformBlockName(index), bufSize, length, name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (programInterface == GL_UNIFORM && index < programObject->GetUniformCount()) {
|
|
||||||
CopyResourceName(programObject->GetActiveUniformName(index), bufSize, length, name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (programInterface == GL_PROGRAM_INPUT && index < static_cast<GLuint>(programObject->GetActiveAttributesCount())) {
|
|
||||||
CopyResourceName(programObject->GetActiveAttribName(index), bufSize, length, name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (programInterface == GL_PROGRAM_OUTPUT &&
|
|
||||||
index < static_cast<GLuint>(programObject->GetActiveFragmentOutputCount())) {
|
|
||||||
CopyResourceName(programObject->GetActiveFragmentOutputName(index), bufSize, length, name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (length) *length = 0;
|
|
||||||
if (name && bufSize > 0) name[0] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
|
||||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) {
|
|
||||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
|
||||||
if (!programObject || !props || !params || bufSize <= 0) return;
|
|
||||||
auto& cache = GetProgramResourceCache(*programObject);
|
|
||||||
GLsizei written = 0;
|
|
||||||
auto writeValue = [&](GLint value) {
|
|
||||||
if (written < bufSize) {
|
|
||||||
params[written++] = value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (GLsizei propIndex = 0; propIndex < propCount; ++propIndex) {
|
|
||||||
const GLenum prop = props[propIndex];
|
|
||||||
if (programInterface == GL_SHADER_STORAGE_BLOCK && index < cache.storageBlocks.size()) {
|
|
||||||
const auto& block = cache.storageBlocks[index];
|
|
||||||
switch (prop) {
|
|
||||||
case GL_NAME_LENGTH:
|
|
||||||
writeValue(static_cast<GLint>(block.name.size() + 1));
|
|
||||||
break;
|
|
||||||
case GL_BUFFER_BINDING:
|
|
||||||
writeValue(static_cast<GLint>(block.binding));
|
|
||||||
break;
|
|
||||||
case GL_BUFFER_DATA_SIZE:
|
|
||||||
writeValue(block.dataSize);
|
|
||||||
break;
|
|
||||||
case GL_NUM_ACTIVE_VARIABLES:
|
|
||||||
writeValue(static_cast<GLint>(block.activeVariables.size()));
|
|
||||||
break;
|
|
||||||
case GL_ACTIVE_VARIABLES:
|
|
||||||
for (const auto variable : block.activeVariables) writeValue(static_cast<GLint>(variable));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (programInterface == GL_BUFFER_VARIABLE && index < cache.bufferVariables.size()) {
|
|
||||||
const auto& var = cache.bufferVariables[index];
|
|
||||||
switch (prop) {
|
|
||||||
case GL_NAME_LENGTH:
|
|
||||||
writeValue(static_cast<GLint>(var.name.size() + 1));
|
|
||||||
break;
|
|
||||||
case GL_TYPE:
|
|
||||||
writeValue(GL_FLOAT);
|
|
||||||
break;
|
|
||||||
case GL_ARRAY_SIZE:
|
|
||||||
writeValue(1);
|
|
||||||
break;
|
|
||||||
case GL_OFFSET:
|
|
||||||
writeValue(var.offset);
|
|
||||||
break;
|
|
||||||
case GL_BLOCK_INDEX:
|
|
||||||
writeValue(static_cast<GLint>(var.blockIndex));
|
|
||||||
break;
|
|
||||||
case GL_ARRAY_STRIDE:
|
|
||||||
case GL_MATRIX_STRIDE:
|
|
||||||
case GL_TOP_LEVEL_ARRAY_SIZE:
|
|
||||||
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
|
||||||
case GL_IS_ROW_MAJOR:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (programInterface == GL_UNIFORM_BLOCK &&
|
|
||||||
programObject->IsActiveUniformBlock(index)) {
|
|
||||||
const auto activeVariables = GetUniformBlockActiveVariables(*programObject, index);
|
|
||||||
switch (prop) {
|
|
||||||
case GL_NAME_LENGTH:
|
|
||||||
writeValue(static_cast<GLint>(programObject->GetUniformBlockName(index).size() + 1));
|
|
||||||
break;
|
|
||||||
case GL_BUFFER_BINDING:
|
|
||||||
writeValue(static_cast<GLint>(programObject->GetUniformBlockBinding(index)));
|
|
||||||
break;
|
|
||||||
case GL_BUFFER_DATA_SIZE:
|
|
||||||
writeValue(static_cast<GLint>(programObject->GetUBOSizeAt(index)));
|
|
||||||
break;
|
|
||||||
case GL_NUM_ACTIVE_VARIABLES:
|
|
||||||
writeValue(static_cast<GLint>(activeVariables.size()));
|
|
||||||
break;
|
|
||||||
case GL_ACTIVE_VARIABLES:
|
|
||||||
for (const GLuint variableIndex : activeVariables) {
|
|
||||||
writeValue(static_cast<GLint>(variableIndex));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
|
||||||
writeValue(programObject->IsUniformBlockReferencedByStage(index, EShLangVertex) ? GL_TRUE
|
|
||||||
: GL_FALSE);
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
|
||||||
writeValue(programObject->IsUniformBlockReferencedByStage(index, EShLangFragment) ? GL_TRUE
|
|
||||||
: GL_FALSE);
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
|
||||||
writeValue(programObject->IsUniformBlockReferencedByStage(index, EShLangCompute) ? GL_TRUE
|
|
||||||
: GL_FALSE);
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
|
||||||
writeValue(GL_FALSE);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (programInterface == GL_UNIFORM && index < programObject->GetUniformCount()) {
|
|
||||||
const auto& uniformName = programObject->GetActiveUniformName(index);
|
|
||||||
const GLint location = programObject->GetUniformLocation(uniformName);
|
|
||||||
switch (prop) {
|
|
||||||
case GL_NAME_LENGTH:
|
|
||||||
writeValue(static_cast<GLint>(uniformName.size() + 1));
|
|
||||||
break;
|
|
||||||
case GL_TYPE:
|
|
||||||
writeValue(static_cast<GLint>(programObject->GetActiveUniformType(index)));
|
|
||||||
break;
|
|
||||||
case GL_ARRAY_SIZE:
|
|
||||||
writeValue(programObject->GetActiveUniformArraySize(index));
|
|
||||||
break;
|
|
||||||
case GL_BLOCK_INDEX:
|
|
||||||
writeValue(programObject->GetActiveUniformBlockIndex(index));
|
|
||||||
break;
|
|
||||||
case GL_LOCATION:
|
|
||||||
writeValue(location);
|
|
||||||
break;
|
|
||||||
case GL_OFFSET:
|
|
||||||
writeValue(location >= 0 && programObject->IsValidUniformLocation(location)
|
|
||||||
? static_cast<GLint>(programObject->GetUniformOffset(location))
|
|
||||||
: 0);
|
|
||||||
break;
|
|
||||||
case GL_ARRAY_STRIDE:
|
|
||||||
case GL_MATRIX_STRIDE:
|
|
||||||
case GL_IS_ROW_MAJOR:
|
|
||||||
case GL_TOP_LEVEL_ARRAY_SIZE:
|
|
||||||
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
|
||||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
|
||||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
|
||||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
|
||||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (programInterface == GL_PROGRAM_INPUT &&
|
|
||||||
index < static_cast<GLuint>(programObject->GetActiveAttributesCount())) {
|
|
||||||
const auto& resourceName = programObject->GetActiveAttribName(index);
|
|
||||||
switch (prop) {
|
|
||||||
case GL_NAME_LENGTH:
|
|
||||||
writeValue(static_cast<GLint>(resourceName.size() + 1));
|
|
||||||
break;
|
|
||||||
case GL_TYPE:
|
|
||||||
writeValue(static_cast<GLint>(programObject->GetActiveAttribType(index)));
|
|
||||||
break;
|
|
||||||
case GL_ARRAY_SIZE:
|
|
||||||
writeValue(programObject->GetActiveAttribArraySize(index));
|
|
||||||
break;
|
|
||||||
case GL_LOCATION:
|
|
||||||
writeValue(programObject->GetAttributeLocation(resourceName));
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
|
||||||
writeValue(GL_TRUE);
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
|
||||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
|
||||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
|
||||||
case GL_IS_PER_PATCH:
|
|
||||||
case GL_LOCATION_INDEX:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (programInterface == GL_PROGRAM_OUTPUT &&
|
|
||||||
index < static_cast<GLuint>(programObject->GetActiveFragmentOutputCount())) {
|
|
||||||
const auto& resourceName = programObject->GetActiveFragmentOutputName(index);
|
|
||||||
switch (prop) {
|
|
||||||
case GL_NAME_LENGTH:
|
|
||||||
writeValue(static_cast<GLint>(resourceName.size() + 1));
|
|
||||||
break;
|
|
||||||
case GL_TYPE:
|
|
||||||
writeValue(static_cast<GLint>(programObject->GetFragmentOutputType(index)));
|
|
||||||
break;
|
|
||||||
case GL_ARRAY_SIZE:
|
|
||||||
writeValue(programObject->GetActiveFragmentOutputArraySize(index));
|
|
||||||
break;
|
|
||||||
case GL_LOCATION:
|
|
||||||
writeValue(programObject->GetFragmentOutputLocation(index));
|
|
||||||
break;
|
|
||||||
case GL_LOCATION_INDEX:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
|
||||||
writeValue(GL_TRUE);
|
|
||||||
break;
|
|
||||||
case GL_REFERENCED_BY_VERTEX_SHADER:
|
|
||||||
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
|
||||||
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
|
||||||
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
|
||||||
case GL_IS_PER_PATCH:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
writeValue(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
writeValue(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (length) *length = written;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name) {
|
|
||||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
|
||||||
if (!programObject || !name) return -1;
|
|
||||||
if (programInterface == GL_UNIFORM) {
|
|
||||||
return programObject->GetUniformLocation(name);
|
|
||||||
}
|
|
||||||
if (programInterface == GL_PROGRAM_INPUT) {
|
|
||||||
return programObject->GetAttributeLocation(name);
|
|
||||||
}
|
|
||||||
if (programInterface == GL_PROGRAM_OUTPUT) {
|
|
||||||
return GetProgramOutputLocation(*programObject, name);
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name) {
|
|
||||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
|
||||||
if (!programObject || !name) return -1;
|
|
||||||
if (programInterface == GL_PROGRAM_OUTPUT) {
|
|
||||||
return GetProgramOutputLocation(*programObject, name) >= 0 ? 0 : -1;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) {
|
|
||||||
auto* programObject = TryGetDirectVulkanProgram(program);
|
|
||||||
if (!programObject) return;
|
|
||||||
auto& cache = GetProgramResourceCache(*programObject);
|
|
||||||
const Int maxBindings = pActiveBackendObject
|
const Int maxBindings = pActiveBackendObject
|
||||||
? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings
|
? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings
|
||||||
: 0;
|
: 0;
|
||||||
@@ -1219,13 +797,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage binding is out of range."));
|
MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage binding is out of range."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (storageBlockIndex >= cache.storageBlocks.size()) {
|
// The frontend already validated that the name denotes an active block, and has
|
||||||
MG_State::pGLContext->RecordError(
|
// already recorded the new binding on the program - which is what reseeds this cache
|
||||||
ErrorCode::InvalidValue,
|
// whenever it is rebuilt. Writing the entry here as well keeps an ALREADY-BUILT cache
|
||||||
MakeUnique<GenericErrorInfo>("DirectVulkan", __func__, "Shader storage block index is not active."));
|
// (the common case: the very next draw reads it) from having to be thrown away.
|
||||||
return;
|
auto& cache = GetProgramResourceCache(*programObject);
|
||||||
}
|
const GLuint blockIndex = GetShaderStorageBlockIndex(*programObject, storageBlockName);
|
||||||
cache.storageBlocks[storageBlockIndex].binding = storageBlockBinding;
|
if (blockIndex == GL_INVALID_INDEX) return;
|
||||||
|
cache.storageBlocks[blockIndex].binding = storageBlockBinding;
|
||||||
}
|
}
|
||||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
|
||||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ReadPixels called with null VulkanRenderer");
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::ReadPixels called with null VulkanRenderer");
|
||||||
@@ -1378,31 +957,66 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
pVulkanRenderer->MultiDrawArrays(payload);
|
pVulkanRenderer->MultiDrawArrays(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared body of glMultiDrawElements (basevertex == nullptr) and
|
||||||
|
// glMultiDrawElementsBaseVertex: identical calls except for the per-draw
|
||||||
|
// vertex offset, which VkMultiDrawIndexedInfoEXT / VkDrawIndexedIndirectCommand /
|
||||||
|
// vkCmdDrawIndexed all carry natively.
|
||||||
|
static void MultiDrawElementsImpl(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
|
GLsizei drawcount, const GLint* basevertex) {
|
||||||
|
if (drawcount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MultiDrawIndexedCmd payload{};
|
||||||
|
payload.mode = mode;
|
||||||
|
payload.indexBufferView.indexType = type;
|
||||||
|
|
||||||
|
// Loop-invariant: the index type is fixed for the whole multi-draw, so resolve
|
||||||
|
// its byte size once instead of twice per sub-draw (a cross-TU switch that
|
||||||
|
// showed up in per-frame profiles of sodium-style 132x32 multi-draws). Index
|
||||||
|
// sizes are 1/2/4, so the per-sub-draw offset division below reduces to a
|
||||||
|
// shift - the hardware divide was the hottest instruction of this loop.
|
||||||
|
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
|
||||||
|
if (indexSize == 0) {
|
||||||
|
MGLOG_E("MultiDrawElements skipped: unsupported index type 0x%x", type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
|
||||||
|
|
||||||
|
// TODO: allocate draw cmd buf elsewhere
|
||||||
|
static Vector<DrawIndexedCmdParam> params;
|
||||||
|
params.clear();
|
||||||
|
params.resize(drawcount);
|
||||||
|
|
||||||
|
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||||
|
if (count[i] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this index view needs a redesign, now there's a lotta redundant uploads
|
||||||
|
|
||||||
|
payload.indexBufferView.indexByteOffset = 0;
|
||||||
|
payload.indexBufferView.indexByteSize =
|
||||||
|
std::max(reinterpret_cast<SizeT>(indices[i]) + count[i] * indexSize,
|
||||||
|
payload.indexBufferView.indexByteSize);
|
||||||
|
|
||||||
|
auto& param = params[i];
|
||||||
|
|
||||||
|
param.indexCount = count[i];
|
||||||
|
param.instanceCount = 1;
|
||||||
|
param.firstIndex = reinterpret_cast<SizeT>(indices[i]) >> indexSizeShift;
|
||||||
|
param.vertexOffset = basevertex != nullptr ? basevertex[i] : 0;
|
||||||
|
param.firstInstance = 0;
|
||||||
|
}
|
||||||
|
payload.drawCount = drawcount;
|
||||||
|
payload.pParams = params.data();
|
||||||
|
pVulkanRenderer->MultiDrawElements(payload);
|
||||||
|
}
|
||||||
|
|
||||||
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
GLsizei drawcount) {
|
GLsizei drawcount) {
|
||||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElements called with null VulkanRenderer");
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElements called with null VulkanRenderer");
|
||||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElements called with null GL context");
|
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElements called with null GL context");
|
||||||
|
MultiDrawElementsImpl(mode, count, type, indices, drawcount, nullptr);
|
||||||
// Vector<DrawElementCmd> cmds;
|
|
||||||
// cmds.reserve(static_cast<SizeT>(drawcount));
|
|
||||||
// for (GLsizei i = 0; i < drawcount; ++i) {
|
|
||||||
// if (count[i] == 0) {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// DrawElementCmd payload{};
|
|
||||||
// payload.mode = mode;
|
|
||||||
// payload.firstVertex = 0;
|
|
||||||
// payload.indexCount = count[i];
|
|
||||||
// payload.indexType = type;
|
|
||||||
// payload.indexByteOffset = reinterpret_cast<SizeT>(indices[i]);
|
|
||||||
// cmds.push_back(payload);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (cmds.empty()) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// pVulkanRenderer->MultiDrawElements(cmds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
|
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
|
||||||
@@ -1430,40 +1044,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||||
GLsizei drawcount, const GLint* basevertex) {
|
GLsizei drawcount, const GLint* basevertex) {
|
||||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElements called with null VulkanRenderer");
|
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::MultiDrawElementsBaseVertex called with null VulkanRenderer");
|
||||||
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElements called with null GL context");
|
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::MultiDrawElementsBaseVertex called with null GL context");
|
||||||
MultiDrawIndexedCmd payload{};
|
MultiDrawElementsImpl(mode, count, type, indices, drawcount, basevertex);
|
||||||
payload.mode = mode;
|
|
||||||
payload.indexBufferView.indexType = type;
|
|
||||||
|
|
||||||
// TODO: allocate draw cmd buf elsewhere
|
|
||||||
static Vector<DrawIndexedCmdParam> params;
|
|
||||||
params.clear();
|
|
||||||
params.resize(drawcount);
|
|
||||||
|
|
||||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
|
||||||
if (count[i] == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this index view needs a redesign, now there's a lotta redundant uploads
|
|
||||||
|
|
||||||
payload.indexBufferView.indexByteOffset = 0;
|
|
||||||
payload.indexBufferView.indexByteSize =
|
|
||||||
std::max(reinterpret_cast<SizeT>(indices[i]) + count[i] * MG_Util::GetGLTypeSize(type),
|
|
||||||
payload.indexBufferView.indexByteSize);
|
|
||||||
|
|
||||||
auto& param = params[i];
|
|
||||||
|
|
||||||
param.indexCount = count[i];
|
|
||||||
param.instanceCount = 1;
|
|
||||||
param.firstIndex = reinterpret_cast<SizeT>(indices[i]) / MG_Util::GetGLTypeSize(type);
|
|
||||||
param.vertexOffset = basevertex[i];
|
|
||||||
param.firstInstance = 0;
|
|
||||||
}
|
|
||||||
payload.drawCount = drawcount;
|
|
||||||
payload.pParams = params.data();
|
|
||||||
pVulkanRenderer->MultiDrawElements(payload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -1578,6 +1161,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// (and, via the SharedPtrs, the records), never pool slots, so
|
// (and, via the SharedPtrs, the records), never pool slots, so
|
||||||
// stale queries are always safe to delete.
|
// stale queries are always safe to delete.
|
||||||
Uint64 rendererGeneration = 0;
|
Uint64 rendererGeneration = 0;
|
||||||
|
// Kind::XfbGenerated - the frontend's paused-draw primitive counter when the
|
||||||
|
// query began. VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT counts only what the
|
||||||
|
// capture saw, so a draw made while the span was paused is invisible to it -
|
||||||
|
// but GL_PRIMITIVES_GENERATED counts what the last vertex processing stage
|
||||||
|
// emitted regardless. The delta closes that gap at result time.
|
||||||
|
Uint64 pausedPrimitiveSnapshot = 0;
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -1676,6 +1265,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
primitives)) {
|
primitives)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (query->kind == VulkanTimerQuery::Kind::XfbGenerated && MG_State::pGLContext != nullptr) {
|
||||||
|
primitives += MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() -
|
||||||
|
query->pausedPrimitiveSnapshot;
|
||||||
|
}
|
||||||
*outNanoseconds = primitives;
|
*outNanoseconds = primitives;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1719,6 +1312,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto* query = new VulkanTimerQuery{};
|
auto* query = new VulkanTimerQuery{};
|
||||||
query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten;
|
query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten;
|
||||||
query->rendererGeneration = GetRendererGeneration();
|
query->rendererGeneration = GetRendererGeneration();
|
||||||
|
query->pausedPrimitiveSnapshot =
|
||||||
|
MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0;
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
|
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
|
||||||
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||||
GLint drawbuffer, const GLfloat* value);
|
GLint drawbuffer, const GLfloat* value);
|
||||||
|
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||||
|
GLint drawbuffer, const GLint* value);
|
||||||
|
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||||
|
GLint drawbuffer, const GLuint* value);
|
||||||
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
|
||||||
GLint drawbuffer, GLfloat depth, GLint stencil);
|
GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||||
void Clear(GLbitfield mask);
|
void Clear(GLbitfield mask);
|
||||||
@@ -93,15 +97,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||||
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
|
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||||
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
|
||||||
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
|
|
||||||
GLsizei* length, GLchar* name);
|
|
||||||
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
|
|
||||||
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
|
|
||||||
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
|
|
||||||
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
|
|
||||||
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
|
||||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||||
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
|
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
|
||||||
|
|||||||
@@ -205,9 +205,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
|
||||||
XXHASH_VERIFY(
|
XXHASH_VERIFY(
|
||||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||||
|
XXHASH_VERIFY(
|
||||||
|
XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
|
||||||
@@ -380,6 +383,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ia.topology = payload.topology;
|
ia.topology = payload.topology;
|
||||||
ia.primitiveRestartEnable = payload.primitiveRestartEnable ? VK_TRUE : VK_FALSE;
|
ia.primitiveRestartEnable = payload.primitiveRestartEnable ? VK_TRUE : VK_FALSE;
|
||||||
|
|
||||||
|
// Only a patch topology has a tessellation stage to configure; leaving the pointer null
|
||||||
|
// otherwise is what the spec expects.
|
||||||
|
VkPipelineTessellationStateCreateInfo tessellation{VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO};
|
||||||
|
tessellation.patchControlPoints = payload.patchControlPoints;
|
||||||
|
|
||||||
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||||
vpci.viewportCount = 1;
|
vpci.viewportCount = 1;
|
||||||
vpci.scissorCount = 1;
|
vpci.scissorCount = 1;
|
||||||
@@ -391,6 +399,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
raster.depthBiasEnable = payload.depthBiasEnable ? VK_TRUE : VK_FALSE;
|
raster.depthBiasEnable = payload.depthBiasEnable ? VK_TRUE : VK_FALSE;
|
||||||
raster.rasterizerDiscardEnable = payload.rasterizerDiscardEnable ? VK_TRUE : VK_FALSE;
|
raster.rasterizerDiscardEnable = payload.rasterizerDiscardEnable ? VK_TRUE : VK_FALSE;
|
||||||
raster.lineWidth = 1.0f;
|
raster.lineWidth = 1.0f;
|
||||||
|
// Only chain the struct when the mode is not Vulkan's implicit default: a device without
|
||||||
|
// VK_EXT_provoking_vertex enabled must never see this pNext entry, and the renderer's
|
||||||
|
// selector already collapses to FIRST in exactly that case - so a device without the
|
||||||
|
// extension produces a byte-identical VkGraphicsPipelineCreateInfo to before.
|
||||||
|
VkPipelineRasterizationProvokingVertexStateCreateInfoEXT provokingVertexState{
|
||||||
|
VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_PROVOKING_VERTEX_STATE_CREATE_INFO_EXT};
|
||||||
|
if (payload.provokingVertexMode != VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) {
|
||||||
|
provokingVertexState.provokingVertexMode = payload.provokingVertexMode;
|
||||||
|
provokingVertexState.pNext = raster.pNext;
|
||||||
|
raster.pNext = &provokingVertexState;
|
||||||
|
}
|
||||||
|
|
||||||
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||||
ms.rasterizationSamples = payload.rasterizationSamples;
|
ms.rasterizationSamples = payload.rasterizationSamples;
|
||||||
@@ -444,6 +463,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
gpi.pStages = payload.stages->data();
|
gpi.pStages = payload.stages->data();
|
||||||
gpi.pVertexInputState = payload.vertexInputState;
|
gpi.pVertexInputState = payload.vertexInputState;
|
||||||
gpi.pInputAssemblyState = &ia;
|
gpi.pInputAssemblyState = &ia;
|
||||||
|
gpi.pTessellationState =
|
||||||
|
payload.topology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST ? &tessellation : nullptr;
|
||||||
gpi.pViewportState = &vpci;
|
gpi.pViewportState = &vpci;
|
||||||
gpi.pRasterizationState = &raster;
|
gpi.pRasterizationState = &raster;
|
||||||
gpi.pMultisampleState = &ms;
|
gpi.pMultisampleState = &ms;
|
||||||
|
|||||||
@@ -30,9 +30,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 subpass = 0;
|
Uint32 subpass = 0;
|
||||||
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||||
Bool primitiveRestartEnable = false;
|
Bool primitiveRestartEnable = false;
|
||||||
|
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||||
|
Uint32 patchControlPoints = 3;
|
||||||
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
||||||
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
||||||
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
||||||
|
// GL's provoking vertex, baked into the pipeline (VK_EXT_provoking_vertex). It selects
|
||||||
|
// which vertex a flat varying takes AND the vertex order transform feedback records for
|
||||||
|
// strips/fans, so it is part of the pipeline's identity, not dynamic state. Defaults to
|
||||||
|
// Vulkan's own convention, which is what a device without the extension gets.
|
||||||
|
VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
|
||||||
Bool depthTestEnable = false;
|
Bool depthTestEnable = false;
|
||||||
Bool depthWriteEnable = false;
|
Bool depthWriteEnable = false;
|
||||||
Bool depthBiasEnable = false;
|
Bool depthBiasEnable = false;
|
||||||
|
|||||||
@@ -1761,6 +1761,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &binding, sizeof(binding)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &binding, sizeof(binding)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The transform feedback capture layout is baked into the modules by
|
||||||
|
// XfbCaptureDecoratePass rather than coming from the SPIR-V, so it has to be part of
|
||||||
|
// the key: two programs can share every shader and still capture differently, which
|
||||||
|
// is exactly what changing the buffer mode does (glTransformFeedbackVaryings with the
|
||||||
|
// same varyings but GL_SEPARATE_ATTRIBS instead of GL_INTERLEAVED_ATTRIBS). Only
|
||||||
|
// hashed for a capturing compile, so nothing else changes key.
|
||||||
|
if (flags & CompileOptionBit::XfbCapture) {
|
||||||
|
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, varying.name.data(), varying.name.size()));
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.bufferIndex, sizeof(varying.bufferIndex)));
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &varying.offsetBytes, sizeof(varying.offsetBytes)));
|
||||||
|
}
|
||||||
|
const SizeT bufferCount = program.GetTransformFeedbackBufferCount();
|
||||||
|
for (SizeT i = 0; i < bufferCount; ++i) {
|
||||||
|
const Uint32 stride = program.GetTransformFeedbackStride(static_cast<Uint32>(i));
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &stride, sizeof(stride)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HashType hash = XXH64_digest(m_hashState);
|
HashType hash = XXH64_digest(m_hashState);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
@@ -2329,6 +2348,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
pipelineLayoutInfo.pSetLayouts = &entry.descriptorSetLayout;
|
pipelineLayoutInfo.pSetLayouts = &entry.descriptorSetLayout;
|
||||||
VK_VERIFY(vkCreatePipelineLayout(m_device, &pipelineLayoutInfo, nullptr, &entry.pipelineLayout),
|
VK_VERIFY(vkCreatePipelineLayout(m_device, &pipelineLayoutInfo, nullptr, &entry.pipelineLayout),
|
||||||
"ProgramFactory::ReflectLayout, vkCreatePipelineLayout");
|
"ProgramFactory::ReflectLayout, vkCreatePipelineLayout");
|
||||||
|
|
||||||
|
// Built here rather than where bindingKinds is sized: at that point the vector is only
|
||||||
|
// zero-initialised and the kinds are assigned further down, so a list built there would be
|
||||||
|
// empty. Ascending by construction because the index walks upward.
|
||||||
|
entry.activeBindings.clear();
|
||||||
|
for (Uint32 binding = 0; binding < static_cast<Uint32>(entry.bindingKinds.size()); ++binding) {
|
||||||
|
if (entry.bindingKinds[binding] != DescriptorBindingKind::None) {
|
||||||
|
entry.activeBindings.push_back(binding);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram(
|
const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram(
|
||||||
@@ -2350,6 +2379,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Structural change: the insert below can move every entry of this
|
||||||
|
// open-addressing map, so all memoised entry pointers die here.
|
||||||
|
++m_cacheStructureEpoch;
|
||||||
auto& entry = m_cache[hash];
|
auto& entry = m_cache[hash];
|
||||||
entry.hash = hash;
|
entry.hash = hash;
|
||||||
entry.lastUsedFrame = m_frameCounter;
|
entry.lastUsedFrame = m_frameCounter;
|
||||||
@@ -2388,6 +2420,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// stored as - which addresses [0,1] where the application addressed texels.
|
||||||
|
{
|
||||||
|
Vector<Uint> rectLoweredSpirv;
|
||||||
|
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(moduleSpirvs[i], rectLoweredSpirv) &&
|
||||||
|
!rectLoweredSpirv.empty()) {
|
||||||
|
moduleSpirvs[i] = Move(rectLoweredSpirv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GL apps depend on cross-program position invariance for multi-pass equality
|
// GL apps depend on cross-program position invariance for multi-pass equality
|
||||||
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
|
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
|
||||||
// depth its own first pass wrote); decorate Position outputs Invariant so
|
// depth its own first pass wrote); decorate Position outputs Invariant so
|
||||||
@@ -2428,6 +2471,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A 64-bit vertex input has to arrive as its 32-bit word pair: VK_FORMAT_R64*_SFLOAT is
|
||||||
|
// optional and lavapipe advertises none of them at all. The pass is unconditional so it
|
||||||
|
// always agrees with the Float64 case in VertexInputStateFactory::ToVkVertexFormat, and
|
||||||
|
// ReflectVertexInputs below then sees an ordinary uvec2/uvec4 input.
|
||||||
|
//
|
||||||
|
// Failure here is not recoverable and must not be swallowed: ToVkVertexFormat has already
|
||||||
|
// 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
|
||||||
|
// double input - garbage with no diagnostic anywhere.
|
||||||
|
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) {
|
||||||
|
Vector<Uint> packedSpirv;
|
||||||
|
const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan(
|
||||||
|
moduleSpirvs[i], packedSpirv);
|
||||||
|
MOBILEGL_ASSERT(packOk,
|
||||||
|
"ProgramFactory: 64-bit vertex input packing failed for program %u; the "
|
||||||
|
"vertex-input format and the shader input type now disagree",
|
||||||
|
program.GetExternalIndex());
|
||||||
|
if (packOk) {
|
||||||
|
moduleSpirvs[i] = std::move(packedSpirv);
|
||||||
|
} else {
|
||||||
|
MGLOG_E("ProgramFactory: failed to pack 64-bit vertex inputs for program %u; "
|
||||||
|
"double-typed vertex attributes will be fetched as uint32 words and not "
|
||||||
|
"reinterpreted",
|
||||||
|
program.GetExternalIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// When Vulkan can legally access storage images without a statically declared
|
// When Vulkan can legally access storage images without a statically declared
|
||||||
// format, let GL's glBindImageTexture format select the runtime image view. This
|
// format, let GL's glBindImageTexture format select the runtime image view. This
|
||||||
// provides desktop-driver-compatible behavior for packs such as iterationRP, whose
|
// provides desktop-driver-compatible behavior for packs such as iterationRP, whose
|
||||||
@@ -2513,6 +2583,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// erase runs ~VkProgramObject (modules/layouts destroyed); notify after
|
// erase runs ~VkProgramObject (modules/layouts destroyed); notify after
|
||||||
// so an observer never observes a half-destroyed entry through a lookup.
|
// so an observer never observes a half-destroyed entry through a lookup.
|
||||||
// Observers only need the handle values to purge their keyed caches.
|
// Observers only need the handle values to purge their keyed caches.
|
||||||
|
++m_cacheStructureEpoch; // erase moves/kills entries: memoised pointers die
|
||||||
it = m_cache.erase(it);
|
it = m_cache.erase(it);
|
||||||
if (m_evictionObserver != nullptr) {
|
if (m_evictionObserver != nullptr) {
|
||||||
m_evictionObserver->OnProgramEvicted(hash, descriptorSetLayout);
|
m_evictionObserver->OnProgramEvicted(hash, descriptorSetLayout);
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||||
Vector<DescriptorBindingKind> bindingKinds;
|
Vector<DescriptorBindingKind> bindingKinds;
|
||||||
|
// The bindings this program actually declares, ascending. bindingKinds is sized to the
|
||||||
|
// 256-binding cap while a real GL program uses 1-8, so the per-draw descriptor walk was
|
||||||
|
// scanning 256 slots to find a handful. MUST stay ascending: Vulkan consumes
|
||||||
|
// pDynamicOffsets in binding order and the writer pushes them in iteration order, so an
|
||||||
|
// unordered list would silently mis-pair dynamic offsets with their uniform blocks.
|
||||||
|
Vector<Uint32> activeBindings;
|
||||||
Vector<Uint32> dynamicBindings;
|
Vector<Uint32> dynamicBindings;
|
||||||
Vector<Int> uniformBlockIndexByBinding;
|
Vector<Int> uniformBlockIndexByBinding;
|
||||||
// Descriptor count per binding (1 except for UBO instance arrays, which occupy one
|
// Descriptor count per binding (1 except for UBO instance arrays, which occupy one
|
||||||
@@ -99,8 +105,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
||||||
Bool fragmentReplacesDepth = false;
|
Bool fragmentReplacesDepth = false;
|
||||||
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||||
// cache eviction (see OnFrameBoundary).
|
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
||||||
Uint64 lastUsedFrame = 0;
|
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||||
|
mutable Uint64 lastUsedFrame = 0;
|
||||||
|
|
||||||
static inline VkDevice s_device = VK_NULL_HANDLE;
|
static inline VkDevice s_device = VK_NULL_HANDLE;
|
||||||
|
|
||||||
@@ -114,6 +121,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
descriptorSetLayout = other.descriptorSetLayout;
|
descriptorSetLayout = other.descriptorSetLayout;
|
||||||
pipelineLayout = other.pipelineLayout;
|
pipelineLayout = other.pipelineLayout;
|
||||||
bindingKinds = std::move(other.bindingKinds);
|
bindingKinds = std::move(other.bindingKinds);
|
||||||
|
activeBindings = std::move(other.activeBindings);
|
||||||
dynamicBindings = std::move(other.dynamicBindings);
|
dynamicBindings = std::move(other.dynamicBindings);
|
||||||
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
||||||
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
|
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
|
||||||
@@ -162,6 +170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
descriptorSetLayout = other.descriptorSetLayout;
|
descriptorSetLayout = other.descriptorSetLayout;
|
||||||
pipelineLayout = other.pipelineLayout;
|
pipelineLayout = other.pipelineLayout;
|
||||||
bindingKinds = std::move(other.bindingKinds);
|
bindingKinds = std::move(other.bindingKinds);
|
||||||
|
activeBindings = std::move(other.activeBindings);
|
||||||
dynamicBindings = std::move(other.dynamicBindings);
|
dynamicBindings = std::move(other.dynamicBindings);
|
||||||
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
|
||||||
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
|
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
|
||||||
@@ -254,6 +263,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const VkProgramObject& GetOrCreateProgram(
|
const VkProgramObject& GetOrCreateProgram(
|
||||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
||||||
|
|
||||||
|
// Bumped whenever m_cache's STRUCTURE changes (any insert or erase): the cache is
|
||||||
|
// an open-addressing map holding entries by value, so both moves existing entries.
|
||||||
|
// A caller that memoised a VkProgramObject* may keep dereferencing it only while
|
||||||
|
// this is unchanged; on a bump it must re-run GetOrCreateProgram.
|
||||||
|
Uint64 GetCacheStructureEpoch() const { return m_cacheStructureEpoch; }
|
||||||
|
// A memoised entry pointer bypasses GetOrCreateProgram, whose per-lookup stamp is
|
||||||
|
// what keeps an in-use entry out of OnFrameBoundary's idle sweep - so such a
|
||||||
|
// caller must re-stamp the entry itself, at least once per frame boundary.
|
||||||
|
void StampProgramUse(const VkProgramObject& entry) const { entry.lastUsedFrame = m_frameCounter; }
|
||||||
|
|
||||||
// Observer may be null (no notifications). Not owned.
|
// Observer may be null (no notifications). Not owned.
|
||||||
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
|
||||||
// Frame boundary hook: ages the program cache and evicts long-unused entries
|
// Frame boundary hook: ages the program cache and evicts long-unused entries
|
||||||
@@ -304,6 +323,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
mutable ProgramLookupCache m_lastLookup;
|
mutable ProgramLookupCache m_lastLookup;
|
||||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||||
Uint64 m_frameCounter = 0;
|
Uint64 m_frameCounter = 0;
|
||||||
|
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
|
||||||
|
Uint64 m_cacheStructureEpoch = 1;
|
||||||
IEvictionObserver* m_evictionObserver = nullptr;
|
IEvictionObserver* m_evictionObserver = nullptr;
|
||||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -116,6 +116,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_frameCount = frameCount;
|
m_frameCount = frameCount;
|
||||||
m_maxBindings = maxBindings;
|
m_maxBindings = maxBindings;
|
||||||
m_samplerResolveMemo.assign(m_maxBindings, SamplerResolveMemo{});
|
m_samplerResolveMemo.assign(m_maxBindings, SamplerResolveMemo{});
|
||||||
|
// Every entry is freshly constructed (all-invalid), so nothing needs sweeping until
|
||||||
|
// a resolve writes one.
|
||||||
|
m_samplerResolveMemoHighWater = 0;
|
||||||
m_setsPerFrame = setsPerFrame;
|
m_setsPerFrame = setsPerFrame;
|
||||||
m_peakDescriptorSetsObserved = 0;
|
m_peakDescriptorSetsObserved = 0;
|
||||||
m_textureManager = textureManager;
|
m_textureManager = textureManager;
|
||||||
@@ -174,6 +177,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_minDynamicOffsetAlignment = 1;
|
m_minDynamicOffsetAlignment = 1;
|
||||||
m_frameCount = 0;
|
m_frameCount = 0;
|
||||||
m_maxBindings = 0;
|
m_maxBindings = 0;
|
||||||
|
m_samplerResolveMemo.clear();
|
||||||
|
m_samplerResolveMemoHighWater = 0;
|
||||||
m_setsPerFrame = 0;
|
m_setsPerFrame = 0;
|
||||||
m_peakDescriptorSetsObserved = 0;
|
m_peakDescriptorSetsObserved = 0;
|
||||||
m_textureManager = nullptr;
|
m_textureManager = nullptr;
|
||||||
@@ -202,14 +207,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (auto& cacheEntryPair : frame.descriptorSetCacheByLayout) {
|
for (auto& cacheEntryPair : frame.descriptorSetCacheByLayout) {
|
||||||
cacheEntryPair.second.cursor = 0;
|
cacheEntryPair.second.cursor = 0;
|
||||||
}
|
}
|
||||||
// The frame's descriptor sets are recycled above, so last frame's reuse target
|
// The frame's descriptor sets are recycled above, so last frame's reuse targets
|
||||||
// is gone: start the per-draw descriptor-reuse cache fresh this frame.
|
// are gone: start the per-draw descriptor-reuse cache fresh this frame.
|
||||||
m_hasLastDescriptor = false;
|
for (auto& entry : m_descriptorReuseMemo) {
|
||||||
|
entry.valid = false;
|
||||||
|
}
|
||||||
|
m_fastRebindMemo.valid = false;
|
||||||
m_lastBindValid = false;
|
m_lastBindValid = false;
|
||||||
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
|
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
|
||||||
// reuse cannot outlive a single frame (see SamplerResolveMemo).
|
// reuse cannot outlive a single frame (see SamplerResolveMemo). Only the entries a
|
||||||
for (auto& memo : m_samplerResolveMemo) {
|
// resolve has actually written can be valid, so the high-water mark bounds the
|
||||||
memo.valid = false;
|
// sweep - the vector itself is sized to the device's binding cap (256 here), which
|
||||||
|
// is ~30x more entries than any program declares.
|
||||||
|
const Uint32 touchedBindings =
|
||||||
|
std::min<Uint32>(m_samplerResolveMemoHighWater, static_cast<Uint32>(m_samplerResolveMemo.size()));
|
||||||
|
for (Uint32 binding = 0; binding < touchedBindings; ++binding) {
|
||||||
|
m_samplerResolveMemo[binding].valid = false;
|
||||||
|
m_samplerResolveMemo[binding].infoValid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,8 +255,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
if (purgedSets > 0) {
|
if (purgedSets > 0) {
|
||||||
// The per-draw reuse memo folds the layout handle into its signature; drop
|
// The per-draw reuse memo folds the layout handle into its signature; drop
|
||||||
// it so a recycled handle value cannot revive a purged set mid-frame.
|
// every entry so a recycled handle value cannot revive a purged set mid-frame.
|
||||||
m_hasLastDescriptor = false;
|
for (auto& entry : m_descriptorReuseMemo) {
|
||||||
|
entry.valid = false;
|
||||||
|
}
|
||||||
|
// The rebind memo's set may be among the freed ones.
|
||||||
|
m_fastRebindMemo.valid = false;
|
||||||
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
|
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,9 +268,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
|
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
Uint32 binding, VkDescriptorImageInfo& outImageInfo) const {
|
Uint32 binding, VkDescriptorImageInfo& outImageInfo,
|
||||||
|
Bool trustUnchangedHint) const {
|
||||||
MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveSamplerDescriptor: texture manager is null");
|
MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveSamplerDescriptor: texture manager is null");
|
||||||
MOBILEGL_ASSERT(m_samplerManager != nullptr, "ResolveSamplerDescriptor: sampler manager is null");
|
MOBILEGL_ASSERT(m_samplerManager != nullptr, "ResolveSamplerDescriptor: sampler manager is null");
|
||||||
|
// The caller proved every input of this binding's resolution unchanged since the
|
||||||
|
// last full resolve (which also filled the cache), so the whole chain below -
|
||||||
|
// texture/sampler resolution, completeness probe, sync, layout handling, sampler
|
||||||
|
// and view lookups - would recompute the identical descriptor.
|
||||||
|
if (trustUnchangedHint && binding < m_samplerResolveMemo.size() &&
|
||||||
|
m_samplerResolveMemo[binding].infoValid) {
|
||||||
|
outImageInfo = m_samplerResolveMemo[binding].info;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
MOBILEGL_ASSERT(binding < programObj.samplerNameByBinding.size(),
|
MOBILEGL_ASSERT(binding < programObj.samplerNameByBinding.size(),
|
||||||
"ResolveSamplerDescriptor: sampler binding %u name lookup out of range", binding);
|
"ResolveSamplerDescriptor: sampler binding %u name lookup out of range", binding);
|
||||||
// Raw-pointer resolve to skip the SharedPtr atomic refcount churn: the bound texture stays
|
// Raw-pointer resolve to skip the SharedPtr atomic refcount churn: the bound texture stays
|
||||||
@@ -266,12 +294,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const auto& samplerOverride = textureUnit.GetSamplerObject();
|
const auto& samplerOverride = textureUnit.GetSamplerObject();
|
||||||
const auto preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
const auto preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||||
SharedPtr<MG_State::GLState::ITextureObject> fallbackHolder;
|
SharedPtr<MG_State::GLState::ITextureObject> fallbackHolder;
|
||||||
|
// A texture that fails the completeness rules for the filter in effect reads
|
||||||
|
// (0, 0, 0, 1), which is exactly what the fallback texture holds - so it takes the
|
||||||
|
// same route as a sampler with nothing bound.
|
||||||
|
if (texture != nullptr &&
|
||||||
|
MG_State::GLState::SamplesAsIncompleteTexture(
|
||||||
|
texture, samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get())) {
|
||||||
|
texture = nullptr;
|
||||||
|
}
|
||||||
if (texture == nullptr) {
|
if (texture == nullptr) {
|
||||||
fallbackHolder = GetFallbackTexture(preferredTarget);
|
fallbackHolder = GetFallbackTexture(preferredTarget);
|
||||||
texture = fallbackHolder.get();
|
texture = fallbackHolder.get();
|
||||||
MOBILEGL_ASSERT(texture != nullptr,
|
if (texture == nullptr) {
|
||||||
"ResolveSamplerDescriptor: no fallback texture available for binding=%u location=%d unit=%d target=%d",
|
MGLOG_E("ResolveSamplerDescriptor: no fallback texture available for binding=%u ('%s') "
|
||||||
binding, location, unit, static_cast<Int>(preferredTarget));
|
"location=%d unit=%d target=%d",
|
||||||
|
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
||||||
|
static_cast<Int>(preferredTarget));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
MGLOG_W(
|
MGLOG_W(
|
||||||
"ResolveSamplerDescriptor: using fallback texture for unbound sampler binding=%u ('%s') location=%d unit=%d target=%d",
|
"ResolveSamplerDescriptor: using fallback texture for unbound sampler binding=%u ('%s') location=%d unit=%d target=%d",
|
||||||
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
binding, programObj.samplerNameByBinding[binding].c_str(), location, unit,
|
||||||
@@ -349,6 +389,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
viewFormatMemo->viewFormatDomain = numericDomain;
|
viewFormatMemo->viewFormatDomain = numericDomain;
|
||||||
viewFormatMemo->viewFormat = sampledViewFormat;
|
viewFormatMemo->viewFormat = sampledViewFormat;
|
||||||
viewFormatMemo->viewFormatValid = true;
|
viewFormatMemo->viewFormatValid = true;
|
||||||
|
NoteSamplerResolveMemoTouched(binding);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
|
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
|
||||||
@@ -404,6 +445,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
memo.viewLevelCount = viewLevelCount;
|
memo.viewLevelCount = viewLevelCount;
|
||||||
memo.sampler = resolvedSampler;
|
memo.sampler = resolvedSampler;
|
||||||
memo.valid = true;
|
memo.valid = true;
|
||||||
|
NoteSamplerResolveMemoTouched(binding);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering,
|
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering,
|
||||||
@@ -414,7 +456,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
.imageView = sampledImageView,
|
.imageView = sampledImageView,
|
||||||
.imageLayout = resource->layout,
|
.imageLayout = resource->layout,
|
||||||
};
|
};
|
||||||
return outImageInfo.sampler != VK_NULL_HANDLE;
|
if (outImageInfo.sampler == VK_NULL_HANDLE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (binding < m_samplerResolveMemo.size()) {
|
||||||
|
m_samplerResolveMemo[binding].info = outImageInfo;
|
||||||
|
m_samplerResolveMemo[binding].infoValid = true;
|
||||||
|
NoteSamplerResolveMemoTouched(binding);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool UniformManager::ResolveSamplerDescriptorOverride(
|
Bool UniformManager::ResolveSamplerDescriptorOverride(
|
||||||
@@ -592,7 +642,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
const VkDeviceSize texelSize =
|
const VkDeviceSize texelSize =
|
||||||
static_cast<VkDeviceSize>(MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat));
|
static_cast<VkDeviceSize>(MG_Util::GetSizedInternalFormatSizeInBytes(internalFormat));
|
||||||
VkDeviceSize viewRange = slice.size;
|
// glTextureBufferRange addresses a window of the buffer, not all of it; the whole-buffer
|
||||||
|
// forms report the buffer's current size here, so both go through the same clamp.
|
||||||
|
const VkDeviceSize rangeOffset = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeOffset());
|
||||||
|
const VkDeviceSize rangeSize = static_cast<VkDeviceSize>(textureBuffer->GetBufferRangeSizeInBytes());
|
||||||
|
VkDeviceSize viewRange = std::min(rangeSize, slice.size > rangeOffset ? slice.size - rangeOffset : 0);
|
||||||
if (texelSize > 0) {
|
if (texelSize > 0) {
|
||||||
viewRange = (viewRange / texelSize) * texelSize;
|
viewRange = (viewRange / texelSize) * texelSize;
|
||||||
}
|
}
|
||||||
@@ -605,7 +659,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
|
viewInfo.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
|
||||||
viewInfo.buffer = slice.buffer;
|
viewInfo.buffer = slice.buffer;
|
||||||
viewInfo.format = vkFormat;
|
viewInfo.format = vkFormat;
|
||||||
viewInfo.offset = slice.offset;
|
viewInfo.offset = slice.offset + rangeOffset;
|
||||||
viewInfo.range = viewRange;
|
viewInfo.range = viewRange;
|
||||||
|
|
||||||
VkBufferView bufferView = VK_NULL_HANDLE;
|
VkBufferView bufferView = VK_NULL_HANDLE;
|
||||||
@@ -650,6 +704,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The shader may write this buffer, and those writes land in GPU memory behind the
|
||||||
|
// frontend's CPU shadow - which is what MapBuffer and GetBufferSubData read.
|
||||||
|
// Host-visible coherent GPU residency makes the shadow BE that memory, so the
|
||||||
|
// results are visible without a readback path, exactly as for a capture buffer.
|
||||||
|
bufferObject->EnsureGpuResidentStorage();
|
||||||
|
// ... and the read that follows has to wait for this draw or dispatch to retire.
|
||||||
|
bufferObject->MarkGpuWritten();
|
||||||
|
|
||||||
BufferSlice slice{};
|
BufferSlice slice{};
|
||||||
if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) {
|
if (!m_bufferManager->AcquireResidentSlice(BufferKind::ShaderStorage, bufferObject, slice) || !slice.IsValid()) {
|
||||||
MGLOG_E("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
|
MGLOG_E("ResolveStorageBufferDescriptor: failed to sync GL buffer %u for block '%s'",
|
||||||
@@ -752,15 +814,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(TextureTarget target) const {
|
SharedPtr<MG_State::GLState::ITextureObject> UniformManager::GetFallbackTexture(TextureTarget target) const {
|
||||||
MOBILEGL_ASSERT(target == TextureTarget::Texture2D || target == TextureTarget::TextureRectangle,
|
// The fallback is a single-sampled 2D image, so it can only stand in for a sampler that
|
||||||
"UniformManager::GetFallbackTexture: unsupported fallback target=%d",
|
// would accept one. A multisample sampler in particular cannot: its descriptor demands a
|
||||||
static_cast<Int>(target));
|
// multisample view, and handing it this one is invalid Vulkan, not a degraded picture.
|
||||||
|
// Report that there is no fallback and let the caller decline the draw - aborting the
|
||||||
|
// process over an unbound sampler is never the right answer.
|
||||||
|
if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) {
|
||||||
|
MGLOG_E("UniformManager::GetFallbackTexture: no fallback exists for target=%d",
|
||||||
|
static_cast<Int>(target));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_fallbackTexture2D == nullptr) {
|
if (m_fallbackTexture2D == nullptr) {
|
||||||
auto fallbackTexture = MakeShared<MG_State::GLState::TextureObject2D>(kFallbackTexture2DExternalIndex);
|
auto fallbackTexture = MakeShared<MG_State::GLState::TextureObject2D>(kFallbackTexture2DExternalIndex);
|
||||||
fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8);
|
fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||||
fallbackTexture->AllocateStorage(TextureUploadTarget::Texture2D, 0,
|
fallbackTexture->AllocateStorage(TextureUploadTarget::Texture2D, 0,
|
||||||
{.texelSize = {1, 1, 1}, .byteSize = 4});
|
{.texelSize = {1, 1, 1}, .byteSize = 4});
|
||||||
|
// (0, 0, 0, 1): what GL reads from a texture that is not complete, and the only
|
||||||
|
// sensible answer for a sampler with nothing bound.
|
||||||
|
static Uint8 kOpaqueBlackTexel[4] = {0, 0, 0, 255};
|
||||||
|
fallbackTexture->UpdateMipmapSubData(TextureUploadTarget::Texture2D, 0,
|
||||||
|
{kOpaqueBlackTexel, sizeof(kOpaqueBlackTexel)});
|
||||||
fallbackTexture->MarkStorageDirty(TextureUploadTarget::Texture2D, 0, true);
|
fallbackTexture->MarkStorageDirty(TextureUploadTarget::Texture2D, 0, true);
|
||||||
m_fallbackTexture2D = fallbackTexture;
|
m_fallbackTexture2D = fallbackTexture;
|
||||||
}
|
}
|
||||||
@@ -768,10 +842,55 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return m_fallbackTexture2D;
|
return m_fallbackTexture2D;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool UniformManager::ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
Uint32 binding,
|
||||||
|
MG_State::GLState::ITextureObject*& outTexture,
|
||||||
|
const MG_State::GLState::SamplerObject*& outSampler) const {
|
||||||
|
// Open-coded ResolveSamplerTextureRaw so the unit is resolved once for both the
|
||||||
|
// texture and the sampler override - this runs per binding per full-path draw,
|
||||||
|
// and program-alternating draw streams take the full path on every draw.
|
||||||
|
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveSampledBinding: GL context is null");
|
||||||
|
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
||||||
|
"ResolveSampledBinding: sampler location binding %u out of range", binding);
|
||||||
|
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
|
||||||
|
"ResolveSampledBinding: sampler target binding %u out of range", binding);
|
||||||
|
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
||||||
|
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
||||||
|
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||||
|
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||||
|
MG_State::GLState::ITextureObject* texture =
|
||||||
|
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
|
||||||
|
// Undefined default texture (name 0, no image) resolves as "unbound", exactly
|
||||||
|
// like ResolveSamplerTextureRaw reports it.
|
||||||
|
if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) {
|
||||||
|
texture = nullptr;
|
||||||
|
}
|
||||||
|
if (texture == nullptr) {
|
||||||
|
// ResolveSamplerDescriptor will substitute the fallback texture for this binding;
|
||||||
|
// include it in the sampled set so the pre-render-pass sync/transition pass covers
|
||||||
|
// its first use instead of leaving that work to happen inside an active pass.
|
||||||
|
if (preferredTarget != TextureTarget::Texture2D &&
|
||||||
|
preferredTarget != TextureTarget::TextureRectangle) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
texture = GetFallbackTexture(preferredTarget).get();
|
||||||
|
}
|
||||||
|
const auto& samplerOverride = textureUnit.GetSamplerObject();
|
||||||
|
outTexture = texture;
|
||||||
|
outSampler = samplerOverride ? samplerOverride.get()
|
||||||
|
: (texture != nullptr ? texture->GetSamplerObject().get() : nullptr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Bool UniformManager::CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
Bool UniformManager::CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) {
|
Vector<MG_State::GLState::ITextureObject*>& outTextures,
|
||||||
|
Vector<SampledBindingRecord>* outBindingRecords) {
|
||||||
outTextures.clear();
|
outTextures.clear();
|
||||||
|
if (outBindingRecords != nullptr) {
|
||||||
|
outBindingRecords->clear();
|
||||||
|
}
|
||||||
|
|
||||||
const Uint32 bindingCount =
|
const Uint32 bindingCount =
|
||||||
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
|
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
|
||||||
@@ -780,17 +899,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
MG_State::GLState::ITextureObject* texture = ResolveSamplerTextureRaw(program, programObj, binding);
|
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||||
if (!texture) {
|
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||||
// ResolveSamplerDescriptor will substitute the fallback texture for this binding;
|
if (!ResolveSampledBinding(program, programObj, binding, texture, sampler)) {
|
||||||
// include it in the sampled set so the pre-render-pass sync/transition pass covers
|
continue;
|
||||||
// its first use instead of leaving that work to happen inside an active pass.
|
}
|
||||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
if (outBindingRecords != nullptr) {
|
||||||
if (preferredTarget != TextureTarget::Texture2D &&
|
outBindingRecords->push_back({texture != nullptr ? texture->GetLifetimeId() : 0,
|
||||||
preferredTarget != TextureTarget::TextureRectangle) {
|
sampler != nullptr ? sampler->GetLifetimeId() : 0});
|
||||||
continue;
|
|
||||||
}
|
|
||||||
texture = GetFallbackTexture(preferredTarget).get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto found = std::find(outTextures.begin(), outTextures.end(), texture);
|
auto found = std::find(outTextures.begin(), outTextures.end(), texture);
|
||||||
@@ -801,6 +917,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool UniformManager::SampledBindingsUnchanged(const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
const Vector<SampledBindingRecord>& previousRecords) const {
|
||||||
|
SizeT recordIndex = 0;
|
||||||
|
// Iterate only the bindings this program declares (ascending), exactly like
|
||||||
|
// BindProgramUniformBuffers: this runs per draw whenever the texture bind
|
||||||
|
// generation moved, and walking all m_maxBindings slots to find the 1-8 real
|
||||||
|
// ones dominated it.
|
||||||
|
for (const Uint32 binding : programObj.activeBindings) {
|
||||||
|
if (binding >= m_maxBindings) {
|
||||||
|
break; // ascending, so nothing past the cap can follow
|
||||||
|
}
|
||||||
|
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||||
|
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||||
|
if (!ResolveSampledBinding(program, programObj, binding, texture, sampler)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (recordIndex >= previousRecords.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const SampledBindingRecord& record = previousRecords[recordIndex++];
|
||||||
|
if (record.textureLifetimeId != (texture != nullptr ? texture->GetLifetimeId() : 0) ||
|
||||||
|
record.samplerLifetimeId != (sampler != nullptr ? sampler->GetLifetimeId() : 0)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return recordIndex == previousRecords.size();
|
||||||
|
}
|
||||||
|
|
||||||
Bool UniformManager::CollectStorageImageTextures(
|
Bool UniformManager::CollectStorageImageTextures(
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
@@ -972,7 +1120,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Uint64 descriptorCount64 = static_cast<Uint64>(maxSets) * static_cast<Uint64>(m_maxBindings);
|
// Sized from what a real program declares, not from the 256-binding cap. A GL program's
|
||||||
|
// single descriptor set holds the bindings shader reflection found - typically 2 to 8 - so
|
||||||
|
// scaling by m_maxBindings declared 5 x 64 x 256 = 81,920 descriptors per pool and 245,760
|
||||||
|
// across the three frames in flight, which drivers that reserve backing store proportional
|
||||||
|
// to the declared count pay for at init. An outlier program is absorbed by the existing
|
||||||
|
// VK_ERROR_OUT_OF_POOL_MEMORY -> GrowFrameDescriptorPool path: pool sizes are aggregate
|
||||||
|
// budgets rather than per-set limits, and vkAllocateDescriptorSets is spec-required to
|
||||||
|
// report that error rather than fail hard.
|
||||||
|
static constexpr Uint32 kEstimatedBindingsPerSet = 8;
|
||||||
|
const Uint64 descriptorCount64 =
|
||||||
|
static_cast<Uint64>(maxSets) * static_cast<Uint64>(std::min(m_maxBindings, kEstimatedBindingsPerSet));
|
||||||
if (descriptorCount64 > static_cast<Uint64>(std::numeric_limits<Uint32>::max())) {
|
if (descriptorCount64 > static_cast<Uint64>(std::numeric_limits<Uint32>::max())) {
|
||||||
MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow");
|
MGLOG_E("UniformDescriptorBinder::CreateDescriptorPool failed: descriptorCount overflow");
|
||||||
return false;
|
return false;
|
||||||
@@ -1105,12 +1263,101 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return VK_SUCCESS;
|
return VK_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool UniformManager::ResolveDynamicUboDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
Uint32 binding, Uint32 arrayElement, Uint32 frameIndex,
|
||||||
|
VkBuffer& outBuffer, VkDeviceSize& outRange,
|
||||||
|
Uint32& outDynamicOffset) {
|
||||||
|
UboBindResult ubo{};
|
||||||
|
const Bool hasPayload = ResolveUniformBufferPayload(program, programObj, binding, arrayElement, ubo);
|
||||||
|
MOBILEGL_ASSERT(hasPayload && (ubo.directBindable || (ubo.payload != nullptr && ubo.payloadSize > 0)),
|
||||||
|
"UniformDescriptorBinder::ResolveDynamicUboDescriptor failed: missing UBO payload on binding %u element %u",
|
||||||
|
binding, arrayElement);
|
||||||
|
if (ubo.directBindable) {
|
||||||
|
// Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy.
|
||||||
|
outBuffer = ubo.buffer;
|
||||||
|
outRange = ubo.range;
|
||||||
|
outDynamicOffset = static_cast<Uint32>(ubo.dynamicOffset);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Global-UBO slice reuse (see GlobalUboSliceMemo): unchanged
|
||||||
|
// uniform bytes re-use the slice already uploaded this frame.
|
||||||
|
const Bool isGlobalUbo = programObj.globalUboBinding == static_cast<Int>(binding) && arrayElement == 0;
|
||||||
|
const Uint64 uboFrameSerial = m_bufferManager->GetFrameSerial();
|
||||||
|
const Uint64 uboProgramLifetimeId = program.GetLifetimeId();
|
||||||
|
const Uint32 uboContentVersion = program.GetUBOContentVersion();
|
||||||
|
if (isGlobalUbo) {
|
||||||
|
for (const auto& memo : m_globalUboMemo) {
|
||||||
|
if (memo.buffer != VK_NULL_HANDLE && memo.programLifetimeId == uboProgramLifetimeId &&
|
||||||
|
memo.frameSerial == uboFrameSerial && memo.uboContentVersion == uboContentVersion &&
|
||||||
|
memo.range == static_cast<VkDeviceSize>(ubo.payloadSize)) {
|
||||||
|
outBuffer = memo.buffer;
|
||||||
|
outRange = memo.range;
|
||||||
|
outDynamicOffset = static_cast<Uint32>(memo.offset);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BufferSlice slice{};
|
||||||
|
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload, ubo.payloadSize,
|
||||||
|
m_minDynamicOffsetAlignment, slice)) {
|
||||||
|
MOBILEGL_ASSERT(false,
|
||||||
|
"UniformDescriptorBinder::ResolveDynamicUboDescriptor failed: UBO upload failed on binding %u element %u",
|
||||||
|
binding, arrayElement);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
outBuffer = slice.buffer;
|
||||||
|
outRange = ubo.payloadSize;
|
||||||
|
outDynamicOffset = static_cast<Uint32>(slice.offset);
|
||||||
|
if (isGlobalUbo) {
|
||||||
|
m_globalUboMemo[m_globalUboMemoNext] =
|
||||||
|
GlobalUboSliceMemo{uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
|
||||||
|
slice.buffer, slice.offset, static_cast<VkDeviceSize>(ubo.payloadSize)};
|
||||||
|
m_globalUboMemoNext = (m_globalUboMemoNext + 1) % kGlobalUboMemoSize;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UniformManager::BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
|
||||||
|
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
|
||||||
|
const Vector<Uint32>& dynamicOffsets) {
|
||||||
|
// Skip the driver call when this exact binding is already live on the
|
||||||
|
// command buffer (see the bind-dedup shadow in the header).
|
||||||
|
const Uint32 offsetCount = static_cast<Uint32>(dynamicOffsets.size());
|
||||||
|
Bool identicalBind = m_lastBindValid && m_lastBindSet == descriptorSet &&
|
||||||
|
m_lastBindLayout == pipelineLayout && m_lastBindPoint == bindPoint &&
|
||||||
|
m_lastBindOffsetCount == offsetCount && offsetCount <= kMaxShadowedDynamicOffsets;
|
||||||
|
if (identicalBind) {
|
||||||
|
for (Uint32 i = 0; i < offsetCount; ++i) {
|
||||||
|
if (m_lastBindOffsets[i] != dynamicOffsets[i]) {
|
||||||
|
identicalBind = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!identicalBind) {
|
||||||
|
vkCmdBindDescriptorSets(commandBuffer, bindPoint, pipelineLayout, 0, 1,
|
||||||
|
&descriptorSet, offsetCount, dynamicOffsets.data());
|
||||||
|
if (offsetCount <= kMaxShadowedDynamicOffsets) {
|
||||||
|
m_lastBindValid = true;
|
||||||
|
m_lastBindSet = descriptorSet;
|
||||||
|
m_lastBindLayout = pipelineLayout;
|
||||||
|
m_lastBindPoint = bindPoint;
|
||||||
|
m_lastBindOffsetCount = offsetCount;
|
||||||
|
std::copy_n(dynamicOffsets.data(), offsetCount, m_lastBindOffsets);
|
||||||
|
} else {
|
||||||
|
m_lastBindValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Bool UniformManager::BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
Bool UniformManager::BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
Uint32 frameIndex,
|
Uint32 frameIndex,
|
||||||
VkPipelineBindPoint bindPoint,
|
VkPipelineBindPoint bindPoint,
|
||||||
const SamplerBindingOverride* samplerBindingOverride) {
|
const SamplerBindingOverride* samplerBindingOverride,
|
||||||
|
Bool samplerDescriptorsUnchangedHint) {
|
||||||
auto& frame = m_frames[frameIndex];
|
auto& frame = m_frames[frameIndex];
|
||||||
if (frame.descriptorPools.empty()) {
|
if (frame.descriptorPools.empty()) {
|
||||||
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid");
|
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid");
|
||||||
@@ -1120,6 +1367,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
frame.activeDescriptorPoolIndex = 0;
|
frame.activeDescriptorPoolIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dynamic-offset-only rebind (see FastRebindMemo in the header): the last
|
||||||
|
// cacheable walk of this exact program selected a set whose contents are
|
||||||
|
// provably still what this walk would write - the hint covers every
|
||||||
|
// sampler binding, and an unchanged (buffer, range) for the single
|
||||||
|
// dynamic UBO covers the rest - except the dynamic offset, which rebinding
|
||||||
|
// the SAME set delivers without any descriptor write.
|
||||||
|
const Bool cacheable = (samplerBindingOverride == nullptr);
|
||||||
|
if (cacheable && samplerDescriptorsUnchangedHint && m_fastRebindMemo.valid &&
|
||||||
|
m_fastRebindMemo.frameIndex == frameIndex &&
|
||||||
|
m_fastRebindMemo.programLifetimeId == program.GetLifetimeId() &&
|
||||||
|
m_fastRebindMemo.programHash == programObj.hash) {
|
||||||
|
VkBuffer uboBuffer = VK_NULL_HANDLE;
|
||||||
|
VkDeviceSize uboRange = 0;
|
||||||
|
Uint32 uboDynamicOffset = 0;
|
||||||
|
if (ResolveDynamicUboDescriptor(program, programObj, m_fastRebindMemo.uboBinding, 0, frameIndex,
|
||||||
|
uboBuffer, uboRange, uboDynamicOffset) &&
|
||||||
|
uboBuffer == m_fastRebindMemo.uboBuffer && uboRange == m_fastRebindMemo.uboRange) {
|
||||||
|
auto& fastOffsets = m_dynamicOffsetsScratch;
|
||||||
|
fastOffsets.clear();
|
||||||
|
fastOffsets.push_back(uboDynamicOffset);
|
||||||
|
BindDescriptorSetDeduped(commandBuffer, bindPoint, programObj.pipelineLayout,
|
||||||
|
m_fastRebindMemo.set, fastOffsets);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Any mismatch (arena wrap or growth, direct-bind retarget, upload
|
||||||
|
// failure) falls through to the full walk, which re-records the memo.
|
||||||
|
}
|
||||||
|
|
||||||
// The descriptor set is chosen AFTER the writes are built (below), so a draw
|
// The descriptor set is chosen AFTER the writes are built (below), so a draw
|
||||||
// whose resolved descriptor content matches the previous draw can reuse that
|
// whose resolved descriptor content matches the previous draw can reuse that
|
||||||
// set and skip both AcquireDescriptorSet and vkUpdateDescriptorSets.
|
// set and skip both AcquireDescriptorSet and vkUpdateDescriptorSets.
|
||||||
@@ -1151,13 +1426,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
texelBufferViews.reserve(m_maxBindings);
|
texelBufferViews.reserve(m_maxBindings);
|
||||||
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
|
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
|
||||||
|
|
||||||
const Uint32 bindingCount =
|
// Eligibility probe for FastRebindMemo, filled by this walk: exactly one
|
||||||
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
|
// dynamic-UBO descriptor (no arrayed elements) and otherwise only
|
||||||
for (Uint32 binding = 0; binding < bindingCount; ++binding) {
|
// combined-image samplers, so the whole set's content is pinned by the
|
||||||
const auto kind = programObj.bindingKinds[binding];
|
// sampler hint plus one (buffer, range) compare.
|
||||||
if (kind == ProgramFactory::DescriptorBindingKind::None) {
|
Uint32 dynamicUboDescriptorCount = 0;
|
||||||
continue;
|
Uint32 fastRebindUboBinding = 0;
|
||||||
|
Bool fastRebindKindsEligible = true;
|
||||||
|
|
||||||
|
// Iterate only the bindings this program declares. The old walk covered all 256 slots of
|
||||||
|
// bindingKinds on every draw to find the 1-8 a real program uses.
|
||||||
|
for (const Uint32 binding : programObj.activeBindings) {
|
||||||
|
if (binding >= m_maxBindings) {
|
||||||
|
break; // ascending, so nothing past the cap can follow
|
||||||
}
|
}
|
||||||
|
const auto kind = programObj.bindingKinds[binding];
|
||||||
|
|
||||||
VkWriteDescriptorSet write{};
|
VkWriteDescriptorSet write{};
|
||||||
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||||
@@ -1171,67 +1454,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
binding < programObj.bindingDescriptorCounts.size()
|
binding < programObj.bindingDescriptorCounts.size()
|
||||||
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding])
|
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding])
|
||||||
: 1u;
|
: 1u;
|
||||||
|
dynamicUboDescriptorCount += descriptorCount;
|
||||||
|
fastRebindUboBinding = binding;
|
||||||
const SizeT firstBufferInfoIndex = bufferInfos.size();
|
const SizeT firstBufferInfoIndex = bufferInfos.size();
|
||||||
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
for (Uint32 element = 0; element < descriptorCount; ++element) {
|
||||||
UboBindResult ubo{};
|
|
||||||
const Bool hasPayload =
|
|
||||||
ResolveUniformBufferPayload(program, programObj, binding, element, ubo);
|
|
||||||
MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0,
|
|
||||||
"UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u element %u",
|
|
||||||
binding, element);
|
|
||||||
|
|
||||||
VkDescriptorBufferInfo bufferInfo{};
|
VkDescriptorBufferInfo bufferInfo{};
|
||||||
// Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo
|
// Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo
|
||||||
// is stable across draws and the descriptor-set reuse cache keeps hitting.
|
// is stable across draws and the descriptor-set reuse cache keeps hitting.
|
||||||
bufferInfo.offset = 0;
|
bufferInfo.offset = 0;
|
||||||
Uint32 dynOffset;
|
Uint32 dynOffset = 0;
|
||||||
if (ubo.directBindable) {
|
if (!ResolveDynamicUboDescriptor(program, programObj, binding, element, frameIndex,
|
||||||
// Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy.
|
bufferInfo.buffer, bufferInfo.range, dynOffset)) {
|
||||||
bufferInfo.buffer = ubo.buffer;
|
return false;
|
||||||
bufferInfo.range = ubo.range;
|
|
||||||
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
|
|
||||||
} else {
|
|
||||||
// Global-UBO slice reuse (see GlobalUboSliceMemo): unchanged
|
|
||||||
// uniform bytes re-use the slice already uploaded this frame.
|
|
||||||
const Bool isGlobalUbo =
|
|
||||||
programObj.globalUboBinding == static_cast<Int>(binding) && element == 0;
|
|
||||||
const Uint64 uboFrameSerial = m_bufferManager->GetFrameSerial();
|
|
||||||
const Uint64 uboProgramLifetimeId = program.GetLifetimeId();
|
|
||||||
const Uint32 uboContentVersion = program.GetUBOContentVersion();
|
|
||||||
Bool reusedSlice = false;
|
|
||||||
if (isGlobalUbo) {
|
|
||||||
for (const auto& memo : m_globalUboMemo) {
|
|
||||||
if (memo.buffer != VK_NULL_HANDLE &&
|
|
||||||
memo.programLifetimeId == uboProgramLifetimeId &&
|
|
||||||
memo.frameSerial == uboFrameSerial &&
|
|
||||||
memo.uboContentVersion == uboContentVersion &&
|
|
||||||
memo.range == static_cast<VkDeviceSize>(ubo.payloadSize)) {
|
|
||||||
bufferInfo.buffer = memo.buffer;
|
|
||||||
bufferInfo.range = memo.range;
|
|
||||||
dynOffset = static_cast<Uint32>(memo.offset);
|
|
||||||
reusedSlice = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!reusedSlice) {
|
|
||||||
BufferSlice slice{};
|
|
||||||
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
|
|
||||||
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
|
|
||||||
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u",
|
|
||||||
binding, element);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
bufferInfo.buffer = slice.buffer;
|
|
||||||
bufferInfo.range = ubo.payloadSize;
|
|
||||||
dynOffset = static_cast<Uint32>(slice.offset);
|
|
||||||
if (isGlobalUbo) {
|
|
||||||
m_globalUboMemo[m_globalUboMemoNext] = GlobalUboSliceMemo{
|
|
||||||
uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
|
|
||||||
slice.buffer, slice.offset, static_cast<VkDeviceSize>(ubo.payloadSize)};
|
|
||||||
m_globalUboMemoNext = (m_globalUboMemoNext + 1) % kGlobalUboMemoSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
bufferInfos.push_back(bufferInfo);
|
bufferInfos.push_back(bufferInfo);
|
||||||
// Dynamic offsets are consumed in binding order, then array element order,
|
// Dynamic offsets are consumed in binding order, then array element order,
|
||||||
@@ -1254,6 +1488,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
texelBufferViews.push_back(bufferView);
|
texelBufferViews.push_back(bufferView);
|
||||||
|
fastRebindKindsEligible = false;
|
||||||
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
|
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
|
||||||
write.pTexelBufferView = &texelBufferViews.back();
|
write.pTexelBufferView = &texelBufferViews.back();
|
||||||
writes.push_back(write);
|
writes.push_back(write);
|
||||||
@@ -1267,6 +1502,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bufferInfos.push_back(bufferInfo);
|
bufferInfos.push_back(bufferInfo);
|
||||||
|
fastRebindKindsEligible = false;
|
||||||
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||||
write.pBufferInfo = &bufferInfos.back();
|
write.pBufferInfo = &bufferInfos.back();
|
||||||
writes.push_back(write);
|
writes.push_back(write);
|
||||||
@@ -1279,6 +1515,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
imageInfos.push_back(imageInfo);
|
imageInfos.push_back(imageInfo);
|
||||||
|
fastRebindKindsEligible = false;
|
||||||
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
|
write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
|
||||||
write.pImageInfo = &imageInfos.back();
|
write.pImageInfo = &imageInfos.back();
|
||||||
writes.push_back(write);
|
writes.push_back(write);
|
||||||
@@ -1291,7 +1528,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
samplerBindingOverride->sampler != nullptr) {
|
samplerBindingOverride->sampler != nullptr) {
|
||||||
hasImage = ResolveSamplerDescriptorOverride(*samplerBindingOverride, imageInfo);
|
hasImage = ResolveSamplerDescriptorOverride(*samplerBindingOverride, imageInfo);
|
||||||
} else {
|
} else {
|
||||||
hasImage = ResolveSamplerDescriptor(commandBuffer, program, programObj, binding, imageInfo);
|
hasImage = ResolveSamplerDescriptor(commandBuffer, program, programObj, binding, imageInfo,
|
||||||
|
samplerDescriptorsUnchangedHint);
|
||||||
}
|
}
|
||||||
if (!hasImage) {
|
if (!hasImage) {
|
||||||
MGLOG_E(
|
MGLOG_E(
|
||||||
@@ -1312,17 +1550,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reuse the previous draw's descriptor set when the resolved content is
|
// Reuse a recent draw's descriptor set when the resolved content is
|
||||||
// byte-identical (only the bind-time dynamic offsets differ). The signature
|
// byte-identical (only the bind-time dynamic offsets differ). The signature
|
||||||
// covers the descriptor-set layout + every write's binding/type/count + the
|
// covers the descriptor-set layout + every write's binding/type/count + the
|
||||||
// pointed-to buffer/image/texel-buffer infos (all value-initialized, so no
|
// pointed-to buffer/image/texel-buffer infos (all value-initialized, so no
|
||||||
// padding noise). Correctness: bindings are re-resolved every draw, so the
|
// padding noise). Correctness: bindings are re-resolved every draw, so the
|
||||||
// signature always reflects the current state and reuse happens only on an
|
// signature always reflects the current state and reuse happens only on an
|
||||||
// exact match; the reused set is never re-acquired within a frame (the acquire
|
// exact match; a reused set is never re-acquired within a frame (the acquire
|
||||||
// cursor only advances), so its written contents survive; the layout is part of
|
// cursor only advances), so its written contents survive; the layout is part of
|
||||||
// the signature so reuse never crosses programs. Sampler overrides (blits)
|
// the signature so reuse never crosses programs. Sampler overrides (blits)
|
||||||
// bypass and invalidate the cache.
|
// bypass and invalidate the cache.
|
||||||
const Bool cacheable = (samplerBindingOverride == nullptr);
|
|
||||||
Uint64 signature = 0xcbf29ce484222325ULL;
|
Uint64 signature = 0xcbf29ce484222325ULL;
|
||||||
{
|
{
|
||||||
const auto mix64 = [&signature](Uint64 word) {
|
const auto mix64 = [&signature](Uint64 word) {
|
||||||
@@ -1350,8 +1587,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
mixWords(texelBufferViews.data(), texelBufferViews.size() * sizeof(VkBufferView));
|
mixWords(texelBufferViews.data(), texelBufferViews.size() * sizeof(VkBufferView));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cacheable && m_hasLastDescriptor && signature == m_lastDescriptorSignature) {
|
VkDescriptorSet reusedSet = VK_NULL_HANDLE;
|
||||||
descriptorSet = m_lastBoundDescriptorSet;
|
if (cacheable) {
|
||||||
|
for (const auto& entry : m_descriptorReuseMemo) {
|
||||||
|
if (entry.valid && entry.signature == signature) {
|
||||||
|
reusedSet = entry.set;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reusedSet != VK_NULL_HANDLE) {
|
||||||
|
descriptorSet = reusedSet;
|
||||||
} else {
|
} else {
|
||||||
VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet);
|
VkResult allocResult = AcquireDescriptorSet(frameIndex, programObj, descriptorSet);
|
||||||
if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) {
|
if (allocResult != VK_SUCCESS || descriptorSet == VK_NULL_HANDLE) {
|
||||||
@@ -1365,39 +1611,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (!writes.empty()) {
|
if (!writes.empty()) {
|
||||||
vkUpdateDescriptorSets(m_device, static_cast<Uint32>(writes.size()), writes.data(), 0, nullptr);
|
vkUpdateDescriptorSets(m_device, static_cast<Uint32>(writes.size()), writes.data(), 0, nullptr);
|
||||||
}
|
}
|
||||||
m_lastBoundDescriptorSet = descriptorSet;
|
if (cacheable) {
|
||||||
m_lastDescriptorSignature = signature;
|
m_descriptorReuseMemo[m_descriptorReuseMemoNext] =
|
||||||
m_hasLastDescriptor = cacheable;
|
DescriptorReuseEntry{signature, descriptorSet, true};
|
||||||
}
|
m_descriptorReuseMemoNext = (m_descriptorReuseMemoNext + 1) % kDescriptorReuseMemoSize;
|
||||||
|
} else {
|
||||||
// Skip the driver call when this exact binding is already live on the
|
for (auto& entry : m_descriptorReuseMemo) {
|
||||||
// command buffer (see the bind-dedup shadow in the header).
|
entry.valid = false;
|
||||||
const Uint32 offsetCount = static_cast<Uint32>(dynamicOffsets.size());
|
|
||||||
Bool identicalBind = m_lastBindValid && m_lastBindSet == descriptorSet &&
|
|
||||||
m_lastBindLayout == programObj.pipelineLayout && m_lastBindPoint == bindPoint &&
|
|
||||||
m_lastBindOffsetCount == offsetCount && offsetCount <= kMaxShadowedDynamicOffsets;
|
|
||||||
if (identicalBind) {
|
|
||||||
for (Uint32 i = 0; i < offsetCount; ++i) {
|
|
||||||
if (m_lastBindOffsets[i] != dynamicOffsets[i]) {
|
|
||||||
identicalBind = false;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!identicalBind) {
|
|
||||||
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
|
// (Re)record the dynamic-offset-only rebind memo. Recording on every
|
||||||
&descriptorSet, offsetCount, dynamicOffsets.data());
|
// cacheable walk (allocated or reused set alike - both hold exactly the
|
||||||
if (offsetCount <= kMaxShadowedDynamicOffsets) {
|
// content just computed) keeps the single slot tracking the most recent
|
||||||
m_lastBindValid = true;
|
// program; a non-cacheable override walk drops it alongside the reuse
|
||||||
m_lastBindSet = descriptorSet;
|
// memo above.
|
||||||
m_lastBindLayout = programObj.pipelineLayout;
|
if (cacheable && fastRebindKindsEligible && dynamicUboDescriptorCount == 1) {
|
||||||
m_lastBindPoint = bindPoint;
|
m_fastRebindMemo = FastRebindMemo{
|
||||||
m_lastBindOffsetCount = offsetCount;
|
/*valid=*/true, frameIndex, program.GetLifetimeId(), programObj.hash,
|
||||||
std::copy_n(dynamicOffsets.data(), offsetCount, m_lastBindOffsets);
|
fastRebindUboBinding, bufferInfos[0].buffer,
|
||||||
} else {
|
bufferInfos[0].range, descriptorSet};
|
||||||
m_lastBindValid = false;
|
} else {
|
||||||
}
|
m_fastRebindMemo.valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BindDescriptorSetDeduped(commandBuffer, bindPoint, programObj.pipelineLayout, descriptorSet,
|
||||||
|
dynamicOffsets);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -53,18 +53,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// caches - a live layout's entry must never be purged (its sets would be
|
// caches - a live layout's entry must never be purged (its sets would be
|
||||||
// unreachable pool slots), so there is deliberately no age-based sweep here.
|
// unreachable pool slots), so there is deliberately no age-based sweep here.
|
||||||
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
|
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
|
||||||
|
// One record per visited CombinedImageSampler binding (post fallback substitution,
|
||||||
|
// in binding order): the resolved texture and effective sampler, as never-reused
|
||||||
|
// lifetime ids so a freed-and-reallocated object at the same heap address can only
|
||||||
|
// MISS a comparison, never false-hit it (same ABA rule as SamplerResolveMemo).
|
||||||
|
struct SampledBindingRecord {
|
||||||
|
Uint64 textureLifetimeId = 0;
|
||||||
|
Uint64 samplerLifetimeId = 0;
|
||||||
|
};
|
||||||
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
Vector<MG_State::GLState::ITextureObject*>& outTextures);
|
Vector<MG_State::GLState::ITextureObject*>& outTextures,
|
||||||
|
Vector<SampledBindingRecord>* outBindingRecords = nullptr);
|
||||||
|
// Shadow-compare for the SetupDraw fast path: re-runs the CollectSampledTextures
|
||||||
|
// walk and reports whether every visited binding still resolves to the recorded
|
||||||
|
// (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a
|
||||||
|
// redundant glBindSampler, which always bumps it) does not prove the sampled set
|
||||||
|
// moved; this walk does, without rebuilding the set or falling off the fast path.
|
||||||
|
Bool SampledBindingsUnchanged(const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
|
const Vector<SampledBindingRecord>& previousRecords) const;
|
||||||
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;
|
||||||
|
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
|
||||||
|
// every input of every combined-image-sampler resolution is unchanged since the
|
||||||
|
// previous draw's resolve - same (texture, sampler) per binding, texture params
|
||||||
|
// sum, sampling-resolution generation (sampler params + texture shape), image
|
||||||
|
// epochs AND per-resource layout values - so the per-binding cached
|
||||||
|
// VkDescriptorImageInfo may be reused without re-running the resolve chain.
|
||||||
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
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);
|
||||||
|
|
||||||
// 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
|
||||||
@@ -114,6 +138,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
|
static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||||
|
// Shared per-binding resolution for CollectSampledTextures and
|
||||||
|
// SampledBindingsUnchanged, so membership and comparison can never diverge:
|
||||||
|
// texture after the fallback substitution (may still be null when no fallback
|
||||||
|
// exists), effective sampler = unit override else the texture's own sampler.
|
||||||
|
// False = the binding is skipped (unbound with a non-2D fallback target).
|
||||||
|
Bool ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
|
MG_State::GLState::ITextureObject*& outTexture,
|
||||||
|
const MG_State::GLState::SamplerObject*& outSampler) const;
|
||||||
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
|
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
|
||||||
// the bound texture stays alive through the draw via GL binding state, so callers that
|
// the bound texture stays alive through the draw via GL binding state, so callers that
|
||||||
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
|
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
|
||||||
@@ -121,9 +154,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding);
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding);
|
||||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
|
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
|
||||||
|
// trustUnchangedHint: reuse this binding's cached VkDescriptorImageInfo outright
|
||||||
|
// (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof
|
||||||
|
// obligations the caller carries).
|
||||||
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program,
|
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
VkDescriptorImageInfo& outImageInfo) const;
|
VkDescriptorImageInfo& outImageInfo,
|
||||||
|
Bool trustUnchangedHint = false) const;
|
||||||
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
|
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
|
||||||
VkDescriptorImageInfo& outImageInfo) const;
|
VkDescriptorImageInfo& outImageInfo) const;
|
||||||
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
@@ -149,6 +186,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
Bool 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;
|
||||||
|
// Shared resolution of one dynamic-UBO binding element into the
|
||||||
|
// (buffer, range, dynamicOffset) triple the descriptor consumes: direct
|
||||||
|
// bind, global-slice reuse, or transient upload. Used by the full walk
|
||||||
|
// and by the dynamic-offset-only rebind (see FastRebindMemo).
|
||||||
|
Bool ResolveDynamicUboDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||||
|
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||||
|
Uint32 arrayElement, Uint32 frameIndex, VkBuffer& outBuffer,
|
||||||
|
VkDeviceSize& outRange, Uint32& outDynamicOffset);
|
||||||
|
// The vkCmdBindDescriptorSets tail shared by the full walk and the
|
||||||
|
// dynamic-offset-only rebind: skips the driver call when this exact
|
||||||
|
// binding is already live on the command buffer (see the bind-dedup
|
||||||
|
// shadow below), otherwise binds and refreshes the shadow.
|
||||||
|
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
|
||||||
|
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
|
||||||
|
const Vector<Uint32>& dynamicOffsets);
|
||||||
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
|
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
|
||||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
|
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
|
||||||
VkResult AllocateDescriptorSetsFromActivePool(
|
VkResult AllocateDescriptorSetsFromActivePool(
|
||||||
@@ -179,14 +231,53 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<VkBufferView> m_texelBufferViewsScratch;
|
Vector<VkBufferView> m_texelBufferViewsScratch;
|
||||||
Vector<Uint32> m_dynamicOffsetsScratch;
|
Vector<Uint32> m_dynamicOffsetsScratch;
|
||||||
|
|
||||||
// Descriptor-set reuse across consecutive draws (see BindProgramUniformBuffers).
|
// Descriptor-set reuse across recent draws (see BindProgramUniformBuffers).
|
||||||
// When a draw's resolved descriptor content is byte-identical to the previous
|
// When a draw's resolved descriptor content is byte-identical to one memoized
|
||||||
// draw's, reuse the same VkDescriptorSet and skip AcquireDescriptorSet +
|
// earlier, reuse that VkDescriptorSet and skip AcquireDescriptorSet +
|
||||||
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Reset each
|
// vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Four
|
||||||
// frame in BeginFrame because the frame's descriptor sets are recycled there.
|
// entries with round-robin replacement rather than one: draws alternating
|
||||||
VkDescriptorSet m_lastBoundDescriptorSet = VK_NULL_HANDLE;
|
// between two programs (MC's chunk<->entity ping-pong) would thrash a single
|
||||||
Uint64 m_lastDescriptorSignature = 0;
|
// slot into a full re-allocate+write every draw. Reset each frame in BeginFrame
|
||||||
Bool m_hasLastDescriptor = false;
|
// because the frame's descriptor sets are recycled there.
|
||||||
|
struct DescriptorReuseEntry {
|
||||||
|
Uint64 signature = 0;
|
||||||
|
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||||
|
Bool valid = false;
|
||||||
|
};
|
||||||
|
static constexpr Uint32 kDescriptorReuseMemoSize = 4;
|
||||||
|
DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize];
|
||||||
|
Uint32 m_descriptorReuseMemoNext = 0;
|
||||||
|
|
||||||
|
// Dynamic-offset-only rebind (see BindProgramUniformBuffers): records the
|
||||||
|
// descriptor set selected by the last cacheable full walk of a program
|
||||||
|
// whose active bindings are exactly one dynamic UBO (single descriptor)
|
||||||
|
// plus combined-image samplers. When the next call proves every sampler
|
||||||
|
// descriptor input unchanged (samplerDescriptorsUnchangedHint) and the
|
||||||
|
// UBO re-resolves to the SAME VkBuffer+range - only the dynamic offset
|
||||||
|
// moved, the per-draw glUniform case - the walk collapses to: resolve one
|
||||||
|
// offset, rebind the recorded set with new pDynamicOffsets (Vulkan allows
|
||||||
|
// rebinding the same set with different dynamic offsets).
|
||||||
|
// Invalidation inventory: BeginFrame clears it (the frame's sets are
|
||||||
|
// recycled) and the frameIndex field guards cross-frame confusion on top;
|
||||||
|
// OnDescriptorSetLayoutDestroyed clears it (the set may be freed); a
|
||||||
|
// sampler-override walk clears it (mirrors m_descriptorReuseMemo); a
|
||||||
|
// program relink bumps the backend state version and thus programObj.hash
|
||||||
|
// so the key misses; the program lifetime id is never reused, so a
|
||||||
|
// deleted-and-recreated program misses; a texture/sampler/binding change
|
||||||
|
// drops the hint upstream; an arena wrap or growth resolves a different
|
||||||
|
// VkBuffer and misses. AcquireDescriptorSet's per-frame cursor only
|
||||||
|
// advances, so the recorded set is never re-written within its frame.
|
||||||
|
struct FastRebindMemo {
|
||||||
|
Bool valid = false;
|
||||||
|
Uint32 frameIndex = 0;
|
||||||
|
Uint64 programLifetimeId = 0;
|
||||||
|
ProgramFactory::HashType programHash = 0;
|
||||||
|
Uint32 uboBinding = 0;
|
||||||
|
VkBuffer uboBuffer = VK_NULL_HANDLE;
|
||||||
|
VkDeviceSize uboRange = 0;
|
||||||
|
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||||
|
};
|
||||||
|
FastRebindMemo m_fastRebindMemo;
|
||||||
|
|
||||||
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
|
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
|
||||||
// block resolve to the same set AND the same dynamic offsets, so the
|
// block resolve to the same set AND the same dynamic offsets, so the
|
||||||
@@ -245,7 +336,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
SamplerNumericDomain viewFormatDomain = SamplerNumericDomain::Unknown;
|
SamplerNumericDomain viewFormatDomain = SamplerNumericDomain::Unknown;
|
||||||
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
||||||
Bool viewFormatValid = false;
|
Bool viewFormatValid = false;
|
||||||
|
// Whole resolved descriptor from this binding's last full resolve. Reused
|
||||||
|
// ONLY under ResolveSamplerDescriptor's trustUnchangedHint, whose caller
|
||||||
|
// proves every resolve input unchanged; cleared with the per-frame reset
|
||||||
|
// (the cached VkSampler outlives a frame only via a fresh resolve, which
|
||||||
|
// also re-stamps it against VkSamplerManager's frame-boundary sweep).
|
||||||
|
VkDescriptorImageInfo info{};
|
||||||
|
Bool infoValid = false;
|
||||||
};
|
};
|
||||||
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
|
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
|
||||||
|
// Exclusive upper bound on the entries of m_samplerResolveMemo that any resolve
|
||||||
|
// has ever written. The vector is sized to the DEVICE binding cap (256 on desktop
|
||||||
|
// NVIDIA), but a program declares 1-8 bindings, so the per-frame reset below was
|
||||||
|
// memsetting ~22 KB of never-touched entries every frame - a measurable slice of
|
||||||
|
// the per-frame fixed cost on draw-light frames. Every site that can turn any of
|
||||||
|
// an entry's *Valid flags on raises this mark first, so entries at or above it are
|
||||||
|
// provably still in their constructed (all-invalid) state and clearing them is a
|
||||||
|
// no-op. Never lowered except by Initialize/Shutdown, which rebuild the vector.
|
||||||
|
mutable Uint32 m_samplerResolveMemoHighWater = 0;
|
||||||
|
void NoteSamplerResolveMemoTouched(Uint32 binding) const {
|
||||||
|
if (binding >= m_samplerResolveMemoHighWater) {
|
||||||
|
m_samplerResolveMemoHighWater = binding + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -29,17 +29,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Stride, sizeof(attr.Stride)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Offset, sizeof(attr.Offset)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsInteger, sizeof(attr.IsInteger)));
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsLong, sizeof(attr.IsLong)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
|
||||||
|
|
||||||
// The buffer's heap address is an identity component of the key: a freed
|
// The bound buffer's IDENTITY is a component of the key, and it has to be the
|
||||||
// buffer's reused address can alias an old cache entry, but only under a
|
// buffer's never-reused lifetime id - NOT its heap address, which this used to
|
||||||
// byte-identical attribute layout - and the entry payload is a pure function
|
// hash. An address is recycled by the allocator, so a deleted-and-recreated
|
||||||
// of the hashed inputs, with the draw path re-resolving bindingBufferKeys
|
// buffer reproduces it; combined with a byte-identical attribute layout that
|
||||||
// against the live VAO attribute pointers, so an aliased hit returns exactly
|
// reproduces the WHOLE content hash, and the hash is what
|
||||||
// what a rebuild would. Address drift only grows the map; the OnFrameBoundary
|
// TryBindResolvedVertexBindings accepts as proof that a memoised binding still
|
||||||
// aging sweep bounds that.
|
// reads the buffer it was resolved from. It did not: a destroyed buffer's GPU
|
||||||
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
|
// slice was bound for its successor's draw, which is how a transform-feedback
|
||||||
|
// capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous
|
||||||
|
// test's positions) instead of its own.
|
||||||
|
// Zero for client memory (no buffer), which is a distinct identity of its own.
|
||||||
|
const Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +75,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||||
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
|
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
|
||||||
|
// Also mirror the layout identity and the two per-draw masks into the VAO's aux
|
||||||
|
// memo (pure VALUES derived from the VAO configuration, so config-version
|
||||||
|
// guarding alone is sound). The draw fast path reads them from the VAO object it
|
||||||
|
// already touched instead of chasing into this entry - see PackVertexInputAuxMemo.
|
||||||
|
vao.SetBackendAuxMemo(entry.layoutHash,
|
||||||
|
PackVertexInputAuxMasks(entry.unsupportedAttribMask, entry.attributeLocationMask));
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +98,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<Uint32> bindingAttributeLocations;
|
Vector<Uint32> bindingAttributeLocations;
|
||||||
Vector<Bool> bindingUsesClientMemory;
|
Vector<Bool> bindingUsesClientMemory;
|
||||||
Vector<VertexStreamConversion> bindingConversions;
|
Vector<VertexStreamConversion> bindingConversions;
|
||||||
|
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
|
||||||
Uint32 unsupportedAttribMask = 0;
|
Uint32 unsupportedAttribMask = 0;
|
||||||
|
|
||||||
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
|
for (Uint32 location = 0; location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; ++location) {
|
||||||
@@ -96,7 +108,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VkFormat sourceVkFormat =
|
const VkFormat sourceVkFormat =
|
||||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra);
|
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
||||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
||||||
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
MGLOG_E("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",
|
||||||
@@ -180,6 +192,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
bindingConversions.push_back(conversion);
|
bindingConversions.push_back(conversion);
|
||||||
builder.AddBinding(binding, stride, inputRate);
|
builder.AddBinding(binding, stride, inputRate);
|
||||||
builder.AddAttribute(location, binding, vkFormat, 0);
|
builder.AddAttribute(location, binding, vkFormat, 0);
|
||||||
|
// Divisor 1 is what VK_VERTEX_INPUT_RATE_INSTANCE already means; only anything
|
||||||
|
// else needs the extension to say it.
|
||||||
|
if (inputRate == VK_VERTEX_INPUT_RATE_INSTANCE && attr.Divisor != 1) {
|
||||||
|
bindingDivisors.push_back({binding, static_cast<Uint32>(attr.Divisor)});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& state = builder.Build();
|
const auto& state = builder.Build();
|
||||||
@@ -191,6 +208,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
BackendVertexInputState& entry = *slot;
|
BackendVertexInputState& entry = *slot;
|
||||||
entry.hash = hash;
|
entry.hash = hash;
|
||||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||||
|
entry.bindingDivisors = Move(bindingDivisors);
|
||||||
entry.bindings = builder.GetBindings();
|
entry.bindings = builder.GetBindings();
|
||||||
entry.attributes = builder.GetAttributes();
|
entry.attributes = builder.GetAttributes();
|
||||||
// See the layoutHash declaration: hash only the resolved layout, never
|
// See the layoutHash declaration: hash only the resolved layout, never
|
||||||
@@ -207,6 +225,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
|
||||||
}
|
}
|
||||||
|
for (const auto& divisor : entry.bindingDivisors) {
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.binding, sizeof(divisor.binding)));
|
||||||
|
XXHASH_VERIFY(XXH64_update(m_hashState, &divisor.divisor, sizeof(divisor.divisor)));
|
||||||
|
}
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
|
||||||
entry.layoutHash = XXH64_digest(m_hashState);
|
entry.layoutHash = XXH64_digest(m_hashState);
|
||||||
entry.attributeLocationMask = 0;
|
entry.attributeLocationMask = 0;
|
||||||
@@ -224,6 +246,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
entry.state = state;
|
entry.state = state;
|
||||||
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
|
entry.state.pVertexBindingDescriptions = entry.bindings.empty() ? nullptr : entry.bindings.data();
|
||||||
entry.state.pVertexAttributeDescriptions = entry.attributes.empty() ? nullptr : entry.attributes.data();
|
entry.state.pVertexAttributeDescriptions = entry.attributes.empty() ? nullptr : entry.attributes.data();
|
||||||
|
if (!entry.bindingDivisors.empty()) {
|
||||||
|
entry.divisorState.vertexBindingDivisorCount = static_cast<Uint32>(entry.bindingDivisors.size());
|
||||||
|
entry.divisorState.pVertexBindingDivisors = entry.bindingDivisors.data();
|
||||||
|
entry.state.pNext = &entry.divisorState;
|
||||||
|
} else {
|
||||||
|
entry.state.pNext = nullptr;
|
||||||
|
}
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +284,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
|
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
|
||||||
Bool isBgra) {
|
Bool isBgra, Bool isLong) {
|
||||||
if (isBgra) {
|
if (isBgra) {
|
||||||
// GL_BGRA: four reversed-order components, always normalized (enforced at validation), only
|
// GL_BGRA: four reversed-order components, always normalized (enforced at validation), only
|
||||||
// legal with GL_UNSIGNED_BYTE or a 2_10_10_10 type. The reversed VkFormats put the
|
// legal with GL_UNSIGNED_BYTE or a 2_10_10_10 type. The reversed VkFormats put the
|
||||||
@@ -280,6 +309,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
case DataType::Int2101010Rev:
|
case DataType::Int2101010Rev:
|
||||||
if (isInteger || size != 4) return VK_FORMAT_UNDEFINED;
|
if (isInteger || size != 4) return VK_FORMAT_UNDEFINED;
|
||||||
return normalized ? VK_FORMAT_A2B10G10R10_SNORM_PACK32 : VK_FORMAT_A2B10G10R10_SSCALED_PACK32;
|
return normalized ? VK_FORMAT_A2B10G10R10_SNORM_PACK32 : VK_FORMAT_A2B10G10R10_SSCALED_PACK32;
|
||||||
|
case DataType::Float64:
|
||||||
|
// A 64-bit attribute is fetched as its 32-bit word pair and bitcast back to double in the
|
||||||
|
// shader (PackDoubleVertexInputsPass does the shader half). That is bit-exact and, unlike
|
||||||
|
// VK_FORMAT_R64*_SFLOAT, needs no format capability: lavapipe reports bufferFeatures = 0
|
||||||
|
// 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,
|
||||||
|
// so they always agree without extra plumbing.
|
||||||
|
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
|
||||||
|
switch (size) {
|
||||||
|
case 1: return VK_FORMAT_R32G32_UINT;
|
||||||
|
case 2: return VK_FORMAT_R32G32B32A32_UINT;
|
||||||
|
// A dvec3/dvec4 input is 6/8 uint32 components: no single VkFormat, and GL spreads it
|
||||||
|
// over two attribute locations, which the location-per-VAO-index model here does not
|
||||||
|
// express. Declined rather than fetched wrong.
|
||||||
|
default: return VK_FORMAT_UNDEFINED;
|
||||||
|
}
|
||||||
case DataType::Float32:
|
case DataType::Float32:
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 1: return VK_FORMAT_R32_SFLOAT;
|
case 1: return VK_FORMAT_R32_SFLOAT;
|
||||||
|
|||||||
@@ -28,11 +28,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
struct BackendVertexInputState {
|
struct BackendVertexInputState {
|
||||||
HashType hash = 0;
|
HashType hash = 0;
|
||||||
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
|
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
|
||||||
// unsupported mask) - NO buffer identities. `hash` mixes buffer heap
|
// unsupported mask) - NO buffer identities. `hash` mixes each bound
|
||||||
// addresses so per-chunk VBOs mint a fresh identity per buffer; keying
|
// buffer's never-reused LIFETIME ID, so per-chunk VBOs mint a fresh
|
||||||
// pipelines on that minted one VkPipeline per chunk section for an
|
// identity per buffer; keying pipelines on that minted one VkPipeline per
|
||||||
// identical layout, defeating pipeline reuse and the per-draw memo.
|
// chunk section for an identical layout, defeating pipeline reuse and the
|
||||||
// Pipelines depend only on the layout, so they key on this instead.
|
// per-draw memo. Pipelines depend only on the layout, so they key on this
|
||||||
|
// instead.
|
||||||
HashType layoutHash = 0;
|
HashType layoutHash = 0;
|
||||||
// Frame boundary of the last cache hit; entries idle past the
|
// Frame boundary of the last cache hit; entries idle past the
|
||||||
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
||||||
@@ -53,6 +54,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Bitmask of `attributes[i].location` - the draw path needs it up to
|
// Bitmask of `attributes[i].location` - the draw path needs it up to
|
||||||
// three times per draw, so it is baked once at build time.
|
// three times per draw, so it is baked once at build time.
|
||||||
Uint32 attributeLocationMask = 0;
|
Uint32 attributeLocationMask = 0;
|
||||||
|
// Per-binding glVertexAttribDivisor values other than 1. Vulkan's instance input
|
||||||
|
// rate advances once per instance and nothing else, so anything else has to be
|
||||||
|
// stated through VK_EXT_vertex_attribute_divisor. Empty when every instanced
|
||||||
|
// binding uses divisor 1, which is what the plain input rate already means.
|
||||||
|
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
|
||||||
|
VkPipelineVertexInputDivisorStateCreateInfoEXT divisorState{
|
||||||
|
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_DIVISOR_STATE_CREATE_INFO_EXT
|
||||||
|
};
|
||||||
VkPipelineVertexInputStateCreateInfo state{
|
VkPipelineVertexInputStateCreateInfo state{
|
||||||
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
|
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
|
||||||
};
|
};
|
||||||
@@ -63,6 +72,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
~VertexInputStateFactory() = default;
|
~VertexInputStateFactory() = default;
|
||||||
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
||||||
|
|
||||||
|
// The VAO aux-memo payload GetOrCreateVertexInputState(vao) stamps: aux0 is the
|
||||||
|
// entry's layoutHash, aux1 packs (unsupportedAttribMask << 32) | attributeLocationMask.
|
||||||
|
// Readers that find the aux memo valid can use these without resolving the entry.
|
||||||
|
static Uint64 PackVertexInputAuxMasks(Uint32 unsupportedAttribMask, Uint32 attributeLocationMask) {
|
||||||
|
return (static_cast<Uint64>(unsupportedAttribMask) << 32) | attributeLocationMask;
|
||||||
|
}
|
||||||
|
|
||||||
HashType ComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
HashType ComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||||
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
|
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
|
||||||
// is unchanged. Use this on per-draw paths.
|
// is unchanged. Use this on per-draw paths.
|
||||||
@@ -71,8 +87,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
||||||
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
||||||
// Frame boundary hook: ages the cache and evicts entries not hit for many
|
// Frame boundary hook: ages the cache and evicts entries not hit for many
|
||||||
// frames. The key mixes buffer heap addresses, so buffer/VAO churn keeps
|
// frames. The key mixes each bound buffer's never-reused lifetime id, so
|
||||||
// minting fresh keys; without eviction the map grows for the whole session.
|
// buffer/VAO churn keeps minting fresh keys - and does so by construction,
|
||||||
|
// not by luck: a recreated buffer can no longer land back on its dead
|
||||||
|
// predecessor's key. Without eviction the map grows for the whole session.
|
||||||
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
|
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
|
||||||
// and the draw path's entry reference never spans a frame boundary, so
|
// and the draw path's entry reference never spans a frame boundary, so
|
||||||
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
|
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
|
||||||
@@ -85,7 +103,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
|
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false);
|
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
|
||||||
|
Bool isLong = false);
|
||||||
static Bool IsScaledIntegerVertexFormat(VkFormat format);
|
static Bool IsScaledIntegerVertexFormat(VkFormat format);
|
||||||
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
||||||
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "VkBufferManager.h"
|
#include "VkBufferManager.h"
|
||||||
|
#include "../DirectVulkan.h"
|
||||||
|
#include "VulkanRenderer.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -57,6 +59,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The CPU is about to read a buffer a shader wrote. Its bytes live in coherent
|
||||||
|
// host-visible GPU storage (EnsureGpuResidentStorage adopts it when the buffer is
|
||||||
|
// bound as a shader storage buffer), so nothing needs copying - but coherence only
|
||||||
|
// says the writes are visible once they have happened, so the work has to retire
|
||||||
|
// first.
|
||||||
|
void Ops_ReadbackFromGpu(BufferObject& bufferObject) {
|
||||||
|
(void)bufferObject;
|
||||||
|
if (pVulkanRenderer) {
|
||||||
|
pVulkanRenderer->FinishPendingGpuWork();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void* Ops_AcquirePersistentMap(BufferObject& bufferObject) {
|
void* Ops_AcquirePersistentMap(BufferObject& bufferObject) {
|
||||||
if (g_activeBufferManager) {
|
if (g_activeBufferManager) {
|
||||||
return g_activeBufferManager->AcquirePersistentMap(bufferObject);
|
return g_activeBufferManager->AcquirePersistentMap(bufferObject);
|
||||||
@@ -80,6 +94,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
.FlushMappedRange = Ops_FlushMappedRange,
|
.FlushMappedRange = Ops_FlushMappedRange,
|
||||||
.OnDestroy = Ops_OnDestroy,
|
.OnDestroy = Ops_OnDestroy,
|
||||||
.AcquirePersistentMap = Ops_AcquirePersistentMap,
|
.AcquirePersistentMap = Ops_AcquirePersistentMap,
|
||||||
|
.ReadbackFromGpu = Ops_ReadbackFromGpu,
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -227,8 +242,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) {
|
void VkBufferManager::TrackLiveResource(const SharedPtr<VkBufferResource>& resource) {
|
||||||
if (m_liveResources.size() >= kLiveResourcePruneThreshold) {
|
// Sweep on a doubling watermark rather than on every insert past the threshold. The old
|
||||||
|
// form walked the whole vector for each new buffer once the list passed 256, and when the
|
||||||
|
// buffers are all live the walk removes nothing and the list grows by one - so creating N
|
||||||
|
// live buffers cost ~N^2/2 expired() checks. Reclamation semantics are unchanged: the sweep
|
||||||
|
// still removes exactly the expired entries, just less often and with the same bound on how
|
||||||
|
// much dead weight can accumulate (at most as many entries as were live at the last sweep).
|
||||||
|
if (m_liveResources.size() >= std::max<SizeT>(kLiveResourcePruneThreshold, 2 * m_liveResourcesLastPruned)) {
|
||||||
std::erase_if(m_liveResources, [](const WeakPtr<VkBufferResource>& weak) { return weak.expired(); });
|
std::erase_if(m_liveResources, [](const WeakPtr<VkBufferResource>& weak) { return weak.expired(); });
|
||||||
|
m_liveResourcesLastPruned = m_liveResources.size();
|
||||||
}
|
}
|
||||||
m_liveResources.push_back(resource);
|
m_liveResources.push_back(resource);
|
||||||
}
|
}
|
||||||
@@ -236,6 +258,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void VkBufferManager::ReleaseAllLiveResources() {
|
void VkBufferManager::ReleaseAllLiveResources() {
|
||||||
for (auto& weak : m_liveResources) {
|
for (auto& weak : m_liveResources) {
|
||||||
if (auto resource = weak.lock()) {
|
if (auto resource = weak.lock()) {
|
||||||
|
BumpSliceEpoch(*resource);
|
||||||
resource->buffer.Destroy();
|
resource->buffer.Destroy();
|
||||||
resource->storageSize = 0;
|
resource->storageSize = 0;
|
||||||
resource->usageFlags = 0;
|
resource->usageFlags = 0;
|
||||||
@@ -250,6 +273,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
Bool VkBufferManager::CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size,
|
Bool VkBufferManager::CreateResidentStorage(VkBufferResource& resource, VkDeviceSize size,
|
||||||
VkBufferUsageFlags usage, VkMemoryPropertyFlags requiredFlags) {
|
VkBufferUsageFlags usage, VkMemoryPropertyFlags requiredFlags) {
|
||||||
|
// The only place a resident VkBuffer handle is minted, so every resident slice
|
||||||
|
// change funnels through here (callers release the old handle first).
|
||||||
|
BumpSliceEpoch(resource);
|
||||||
// Staged range copies write resident storage with vkCmdCopyBuffer.
|
// Staged range copies write resident storage with vkCmdCopyBuffer.
|
||||||
usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||||
const Bool created = resource.buffer.Create({
|
const Bool created = resource.buffer.Create({
|
||||||
@@ -336,6 +362,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (!resource) {
|
if (!resource) {
|
||||||
return; // lazy: AcquireResidentSlice performs a full upload on creation
|
return; // lazy: AcquireResidentSlice performs a full upload on creation
|
||||||
}
|
}
|
||||||
|
// A respecify can change the size, the usage hint (so the resident/streamed
|
||||||
|
// route), and the contents at once; retire every memo before deciding what to
|
||||||
|
// do about the storage.
|
||||||
|
BumpSliceEpoch(*resource);
|
||||||
// Any cached streaming slice refers to the previous contents.
|
// Any cached streaming slice refers to the previous contents.
|
||||||
resource->transientFrameSerial = 0;
|
resource->transientFrameSerial = 0;
|
||||||
if (!resource->buffer.IsValid()) {
|
if (!resource->buffer.IsValid()) {
|
||||||
@@ -368,6 +398,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (!resource) {
|
if (!resource) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Drops the streaming memo below and may end in a storage swap or a deferred
|
||||||
|
// full re-upload, so no memoised slice survives this.
|
||||||
|
BumpSliceEpoch(*resource);
|
||||||
resource->transientFrameSerial = 0;
|
resource->transientFrameSerial = 0;
|
||||||
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
|
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
|
||||||
return;
|
return;
|
||||||
@@ -400,6 +433,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (!resource) {
|
if (!resource) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
BumpSliceEpoch(*resource);
|
||||||
resource->transientFrameSerial = 0;
|
resource->transientFrameSerial = 0;
|
||||||
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
|
if (!resource->buffer.IsValid() || resource->pendingFullUpload) {
|
||||||
return;
|
return;
|
||||||
@@ -459,6 +493,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
TrackLiveResource(resource);
|
TrackLiveResource(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bumped for the request, not just for the storage it may create. This is the
|
||||||
|
// one call the frontend makes when a buffer becomes persistently mapped for
|
||||||
|
// writing (BufferObject::AcquireMemoryRange), and a map the backend declines
|
||||||
|
// keeps mutating its shadow with no further API call - so it is what lets
|
||||||
|
// GetSliceEpochCounter stand for "no buffer needs a persistent-map range push".
|
||||||
|
BumpSliceEpoch(*resource);
|
||||||
|
|
||||||
// Idempotent: an already-backed buffer returns the same mapped base.
|
// Idempotent: an already-backed buffer returns the same mapped base.
|
||||||
if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) {
|
if (resource->persistentMapped && resource->buffer.IsValid() && resource->storageSize == size) {
|
||||||
return resource->buffer.GetMappedData();
|
return resource->buffer.GetMappedData();
|
||||||
@@ -546,6 +587,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
auto resource = GetOrCreateResource(bufferObject);
|
auto resource = GetOrCreateResource(bufferObject);
|
||||||
bufferObject->SyncPersistentMappedRange();
|
bufferObject->SyncPersistentMappedRange();
|
||||||
|
|
||||||
|
// A persistently mapped resource's storage IS the application's copy of the bytes -
|
||||||
|
// the frontend adopted it in place of the shadow and hands out pointers into it, and
|
||||||
|
// a shader can have written bytes the shadow never saw (a transform feedback
|
||||||
|
// capture). Streaming a second copy would feed this draw the stale shadow, and the
|
||||||
|
// downgrade below would release the storage the application still points at,
|
||||||
|
// breaking the "never recreated" promise AcquirePersistentMap makes.
|
||||||
|
if (resource->persistentMapped) {
|
||||||
|
return AcquireResidentSlice(kind, bufferObject, outSlice);
|
||||||
|
}
|
||||||
|
|
||||||
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
const VkDeviceSize size = static_cast<VkDeviceSize>(bufferObject->GetSize());
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
|
MGLOG_E("VkBufferManager::AcquireStreamedSlice failed: buffer size is zero");
|
||||||
@@ -559,6 +610,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Idle-content promotion: see the field comments in VkBufferResource. The
|
||||||
|
// streak counts frame BOUNDARIES survived unchanged (the same-frame memo
|
||||||
|
// above swallows repeat draws), so a promotion needs the content stable
|
||||||
|
// for kStreamedPromotionStreak whole frames - one no-op frame does not
|
||||||
|
// trigger the resident round-trip, whose creation upload is itself a
|
||||||
|
// staged copy worth avoiding for content that is about to change again.
|
||||||
|
constexpr Uint32 kStreamedPromotionStreak = 2;
|
||||||
|
if (resource->promotedResident) {
|
||||||
|
if (resource->promotedChangeSerial == changeSerial &&
|
||||||
|
static_cast<VkDeviceSize>(bufferObject->GetSize()) == size) {
|
||||||
|
return AcquireResidentSlice(kind, bufferObject, outSlice);
|
||||||
|
}
|
||||||
|
resource->promotedResident = false;
|
||||||
|
resource->unchangedStreak = 0;
|
||||||
|
} else if (resource->transientChangeSerial == changeSerial && resource->transientSize == size &&
|
||||||
|
resource->transientFrameSerial != 0) {
|
||||||
|
if (++resource->unchangedStreak >= kStreamedPromotionStreak) {
|
||||||
|
// Promotion moves the buffer off the arena and onto resident storage.
|
||||||
|
resource->promotedResident = true;
|
||||||
|
resource->promotedChangeSerial = changeSerial;
|
||||||
|
BumpSliceEpoch(*resource);
|
||||||
|
if (AcquireResidentSlice(kind, bufferObject, outSlice)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
resource->promotedResident = false; // resident creation failed: stream as before
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resource->unchangedStreak = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A fresh arena allocation: a different slice than the last call handed back,
|
||||||
|
// and (below) the point where a promoted buffer's resident storage is released.
|
||||||
|
// The stable-promotion exit above returns before this, so a buffer the app has
|
||||||
|
// stopped touching keeps one slice for as long as it keeps its resident storage.
|
||||||
|
BumpSliceEpoch(*resource);
|
||||||
if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->MappedData(), size, 16,
|
if (!m_transientUploadArena.Upload(m_currentFrameIndex, bufferObject->MappedData(), size, 16,
|
||||||
outSlice)) {
|
outSlice)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -57,11 +57,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// never orphaned or recreated. Draw-time acquire binds it directly, no re-upload.
|
// never orphaned or recreated. Draw-time acquire binds it directly, no re-upload.
|
||||||
Bool persistentMapped = false;
|
Bool persistentMapped = false;
|
||||||
|
|
||||||
|
// Bumped from a manager-wide counter every time anything that decides which
|
||||||
|
// BufferSlice an Acquire*Slice call hands back changes: storage created or
|
||||||
|
// released, a full re-upload becoming due, a promotion/demotion between
|
||||||
|
// resident and streamed storage, or a new per-frame arena slice. Callers that
|
||||||
|
// memoise a resolved slice compare this to prove the memo still describes the
|
||||||
|
// buffer. The counter is manager-wide (never per-resource) so a freshly
|
||||||
|
// created resource - including one that replaces a destroyed resource at the
|
||||||
|
// same address - can never reproduce a value some memo already holds. 0 means
|
||||||
|
// "no slice has ever been handed out", which no memo can match.
|
||||||
|
Uint64 sliceEpoch = 0;
|
||||||
|
|
||||||
// Cached transient (streaming) slice for the current frame.
|
// Cached transient (streaming) slice for the current frame.
|
||||||
BufferSlice transientSlice{};
|
BufferSlice transientSlice{};
|
||||||
Uint64 transientFrameSerial = 0;
|
Uint64 transientFrameSerial = 0;
|
||||||
Uint64 transientChangeSerial = 0;
|
Uint64 transientChangeSerial = 0;
|
||||||
VkDeviceSize transientSize = 0;
|
VkDeviceSize transientSize = 0;
|
||||||
|
|
||||||
|
// Streaming re-copies the whole store into the per-frame arena on every
|
||||||
|
// frame, which is right for genuinely per-frame data but pure waste for a
|
||||||
|
// Dynamic-hinted buffer the app stopped touching. After the content
|
||||||
|
// survives kStreamedPromotionStreak frame boundaries unchanged it is
|
||||||
|
// promoted to resident storage (one final upload, then zero per-frame
|
||||||
|
// cost); the first content change demotes it back to streaming, and the
|
||||||
|
// streaming path's existing downgrade releases the resident store.
|
||||||
|
Uint32 unchangedStreak = 0;
|
||||||
|
Bool promotedResident = false;
|
||||||
|
Uint64 promotedChangeSerial = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Supplies a command buffer that is recording and outside any render pass,
|
// Supplies a command buffer that is recording and outside any render pass,
|
||||||
@@ -121,6 +143,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource);
|
void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource);
|
||||||
|
|
||||||
Uint64 GetFrameSerial() const { return m_frameSerial; }
|
Uint64 GetFrameSerial() const { return m_frameSerial; }
|
||||||
|
// Highest value handed to any VkBufferResource::sliceEpoch. Unchanged since a
|
||||||
|
// memo was taken means no buffer this manager owns changed which slice it hands
|
||||||
|
// back, and none was persistently mapped, in between - so a memo of resolved
|
||||||
|
// slices needs no per-buffer re-check. See AcquirePersistentMap for the mapping half.
|
||||||
|
Uint64 GetSliceEpochCounter() const { return m_sliceEpochCounter; }
|
||||||
// Highest frame serial whose GPU work is known complete; serials at or
|
// Highest frame serial whose GPU work is known complete; serials at or
|
||||||
// below it may be considered signaled. Drives IsResourceBusy and the
|
// below it may be considered signaled. Drives IsResourceBusy and the
|
||||||
// backend GL fence objects.
|
// backend GL fence objects.
|
||||||
@@ -147,6 +174,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void DestroyAllDeferredReleases();
|
void DestroyAllDeferredReleases();
|
||||||
void TrackLiveResource(const SharedPtr<VkBufferResource>& resource);
|
void TrackLiveResource(const SharedPtr<VkBufferResource>& resource);
|
||||||
void ReleaseAllLiveResources();
|
void ReleaseAllLiveResources();
|
||||||
|
// See VkBufferResource::sliceEpoch.
|
||||||
|
void BumpSliceEpoch(VkBufferResource& resource) { resource.sliceEpoch = ++m_sliceEpochCounter; }
|
||||||
|
|
||||||
VkBufferManagerInitInfo m_initInfo{};
|
VkBufferManagerInitInfo m_initInfo{};
|
||||||
BufferArena m_transientUploadArena;
|
BufferArena m_transientUploadArena;
|
||||||
@@ -154,8 +183,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
|
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
|
||||||
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
|
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
|
||||||
Vector<WeakPtr<VkBufferResource>> m_liveResources;
|
Vector<WeakPtr<VkBufferResource>> m_liveResources;
|
||||||
|
// Size m_liveResources had just after the last sweep; the next sweep waits for it to double.
|
||||||
|
SizeT m_liveResourcesLastPruned = 0;
|
||||||
Uint32 m_currentFrameIndex = 0;
|
Uint32 m_currentFrameIndex = 0;
|
||||||
Uint64 m_frameSerial = 1;
|
Uint64 m_frameSerial = 1;
|
||||||
Uint64 m_completedSerialFloor = 0;
|
Uint64 m_completedSerialFloor = 0;
|
||||||
|
// Never reset (not even by Shutdown): a value handed to a resource must stay
|
||||||
|
// unique for the process, or a memo taken before a re-initialize could match
|
||||||
|
// a different resource's state after it.
|
||||||
|
Uint64 m_sliceEpochCounter = 0;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -176,16 +176,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferSlice VkBufferObject::GetSlice(VkDeviceSize offset, VkDeviceSize size) const {
|
|
||||||
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
|
|
||||||
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
|
|
||||||
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::GetSlice range out of bounds");
|
|
||||||
|
|
||||||
BufferSlice slice{};
|
|
||||||
slice.buffer = m_buffer;
|
|
||||||
slice.offset = offset;
|
|
||||||
slice.size = resolvedSize;
|
|
||||||
slice.mapped = (m_mappedData != nullptr) ? static_cast<Uint8*>(m_mappedData) + offset : nullptr;
|
|
||||||
return slice;
|
|
||||||
}
|
|
||||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||||
|
|||||||
@@ -48,7 +48,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
|
|
||||||
VkBuffer GetHandle() const { return m_buffer; }
|
VkBuffer GetHandle() const { return m_buffer; }
|
||||||
VkDeviceSize GetSize() const { return m_size; }
|
VkDeviceSize GetSize() const { return m_size; }
|
||||||
BufferSlice GetSlice(VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) const;
|
// Inline: runs on the per-draw acquire path (a resident buffer bind is a
|
||||||
|
// GetSlice per binding), where an out-of-line call was measurable.
|
||||||
|
BufferSlice GetSlice(VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE) const {
|
||||||
|
MOBILEGL_ASSERT(offset <= m_size, "VkBufferObject::GetSlice offset out of range");
|
||||||
|
const VkDeviceSize resolvedSize = (size == VK_WHOLE_SIZE) ? (m_size - offset) : size;
|
||||||
|
MOBILEGL_ASSERT(offset + resolvedSize <= m_size, "VkBufferObject::GetSlice range out of bounds");
|
||||||
|
|
||||||
|
BufferSlice slice{};
|
||||||
|
slice.buffer = m_buffer;
|
||||||
|
slice.offset = offset;
|
||||||
|
slice.size = resolvedSize;
|
||||||
|
slice.mapped = (m_mappedData != nullptr) ? static_cast<Uint8*>(m_mappedData) + offset : nullptr;
|
||||||
|
return slice;
|
||||||
|
}
|
||||||
void* GetMappedData() const { return m_mappedData; }
|
void* GetMappedData() const { return m_mappedData; }
|
||||||
Bool IsMapped() const { return m_mappedData != nullptr; }
|
Bool IsMapped() const { return m_mappedData != nullptr; }
|
||||||
Bool IsValid() const { return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; }
|
Bool IsValid() const { return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; }
|
||||||
|
|||||||
@@ -8,15 +8,75 @@
|
|||||||
|
|
||||||
#include "VkClearManager.h"
|
#include "VkClearManager.h"
|
||||||
|
|
||||||
|
#include "MG_State/GLState/Core.h"
|
||||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
|
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
|
||||||
return target >= TextureUploadTarget::CubeMapPositiveX &&
|
return target >= TextureUploadTarget::CubeMapPositiveX &&
|
||||||
target <= TextureUploadTarget::CubeMapNegativeZ;
|
target <= TextureUploadTarget::CubeMapNegativeZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VkClearColorValue MakeVkClearColorValue(const ClearAttachmentPayload& payload, Bool formatLacksAlpha) {
|
||||||
|
VkClearColorValue clearValue{};
|
||||||
|
switch (payload.colorEncoding) {
|
||||||
|
case ClearColorEncoding::Int:
|
||||||
|
clearValue.int32[0] = payload.colorInt.x();
|
||||||
|
clearValue.int32[1] = payload.colorInt.y();
|
||||||
|
clearValue.int32[2] = payload.colorInt.z();
|
||||||
|
clearValue.int32[3] = formatLacksAlpha ? 1 : payload.colorInt.w();
|
||||||
|
break;
|
||||||
|
case ClearColorEncoding::Uint:
|
||||||
|
clearValue.uint32[0] = payload.colorUint.x();
|
||||||
|
clearValue.uint32[1] = payload.colorUint.y();
|
||||||
|
clearValue.uint32[2] = payload.colorUint.z();
|
||||||
|
clearValue.uint32[3] = formatLacksAlpha ? 1u : payload.colorUint.w();
|
||||||
|
break;
|
||||||
|
case ClearColorEncoding::Float:
|
||||||
|
clearValue.float32[0] = payload.color.x();
|
||||||
|
clearValue.float32[1] = payload.color.y();
|
||||||
|
clearValue.float32[2] = payload.color.z();
|
||||||
|
clearValue.float32[3] = formatLacksAlpha ? 1.0f : payload.color.w();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return clearValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PreCompensateSrgbClearColor(ClearAttachmentPayload& payload, VkFormat destinationFormat) {
|
||||||
|
if (payload.colorEncoding != ClearColorEncoding::Float) return;
|
||||||
|
// With GL_FRAMEBUFFER_SRGB enabled GL performs the encoding itself, so the driver doing it
|
||||||
|
// is exactly right and there is nothing to undo.
|
||||||
|
if (MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
|
||||||
|
if (ResolveSrgbAttachmentWriteFormat(destinationFormat, false) == destinationFormat) return;
|
||||||
|
|
||||||
|
// sRGB -> linear (GL 4.6 core 8.24), applied to the colour channels only: alpha is stored
|
||||||
|
// linearly in an sRGB format and must pass through untouched.
|
||||||
|
const auto toLinear = [](Float encoded) {
|
||||||
|
const Float value = std::clamp(encoded, 0.0f, 1.0f);
|
||||||
|
return value <= 0.04045f ? value / 12.92f : std::pow((value + 0.055f) / 1.055f, 2.4f);
|
||||||
|
};
|
||||||
|
payload.color = FloatVec4(toLinear(payload.color.x()), toLinear(payload.color.y()),
|
||||||
|
toLinear(payload.color.z()), payload.color.w());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload) {
|
||||||
|
switch (payload.colorEncoding) {
|
||||||
|
case ClearColorEncoding::Int:
|
||||||
|
payload.colorInt = IntVec4(payload.colorInt.x(), payload.colorInt.y(), payload.colorInt.z(), 1);
|
||||||
|
break;
|
||||||
|
case ClearColorEncoding::Uint:
|
||||||
|
payload.colorUint = UintVec4(payload.colorUint.x(), payload.colorUint.y(), payload.colorUint.z(), 1u);
|
||||||
|
break;
|
||||||
|
case ClearColorEncoding::Float:
|
||||||
|
payload.color = FloatVec4(payload.color.x(), payload.color.y(), payload.color.z(), 1.0f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Bool PendingClearMatchesTextureIdentity(const PendingClearKey& key, const TextureIdentity& identity) {
|
static Bool PendingClearMatchesTextureIdentity(const PendingClearKey& key, const TextureIdentity& identity) {
|
||||||
return key.texture == identity.texture && key.textureLifetimeId == identity.lifetimeId;
|
return key.texture == identity.texture && key.textureLifetimeId == identity.lifetimeId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint32 stencil{};
|
Uint32 stencil{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A colour clear reaches us from one of glClear/ClearBufferfv, ClearBufferiv or
|
||||||
|
// ClearBufferuiv, and Vulkan reads VkClearColorValue's union according to the destination
|
||||||
|
// image's format rather than converting between the members - a float written where an
|
||||||
|
// integer format is expected is reinterpreted bit for bit, not rounded. Remember which entry
|
||||||
|
// point supplied the value so the member written when the clear is materialized matches.
|
||||||
|
enum class ClearColorEncoding : Uint8 { Float, Int, Uint };
|
||||||
|
|
||||||
struct ClearAttachmentPayload {
|
struct ClearAttachmentPayload {
|
||||||
GLbitfield mask = 0;
|
GLbitfield mask = 0;
|
||||||
FloatVec4 color = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
|
FloatVec4 color = FloatVec4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
ClearColorEncoding colorEncoding = ClearColorEncoding::Float;
|
||||||
|
IntVec4 colorInt = IntVec4(0, 0, 0, 0);
|
||||||
|
UintVec4 colorUint = UintVec4(0u, 0u, 0u, 0u);
|
||||||
Float depth = 1.0f;
|
Float depth = 1.0f;
|
||||||
Uint32 stencil = 0;
|
Uint32 stencil = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Builds the clear value for `payload` in the union member its encoding calls for.
|
||||||
|
// `formatLacksAlpha` applies GL's rule that a format without an alpha channel reads as one,
|
||||||
|
// expressed in whichever type matches (GL 4.6 core 15.2.3).
|
||||||
|
VkClearColorValue MakeVkClearColorValue(const ClearAttachmentPayload& payload, Bool formatLacksAlpha);
|
||||||
|
|
||||||
|
// Applies that same rule in place, for the paths that have to bake it into the payload before
|
||||||
|
// the destination is known.
|
||||||
|
void ForceOpaqueClearAlpha(ClearAttachmentPayload& payload);
|
||||||
|
|
||||||
|
// vkCmdClearColorImage names the image, so the driver applies the destination format's transfer
|
||||||
|
// function to whatever value it is handed. Every other write path in this backend goes through
|
||||||
|
// the UNORM twin view while GL_FRAMEBUFFER_SRGB is off (ResolveSrgbAttachmentWriteFormat) and
|
||||||
|
// therefore stores the raw value GL asked for. Rewrites `payload` to the linear colour whose
|
||||||
|
// encoding is that raw value, so a direct image clear of an sRGB destination agrees with them.
|
||||||
|
// A no-op for every other format, for integer clear encodings, and when GL is doing the
|
||||||
|
// encoding itself.
|
||||||
|
void PreCompensateSrgbClearColor(ClearAttachmentPayload& payload, VkFormat destinationFormat);
|
||||||
|
|
||||||
struct PendingClearKey {
|
struct PendingClearKey {
|
||||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||||
Uint64 textureLifetimeId = 0;
|
Uint64 textureLifetimeId = 0;
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Float ResolveColorClearAlpha(const MG_State::GLState::ITextureObject* texture, Float requestedAlpha) {
|
static Bool ColorFormatLacksAlpha(const MG_State::GLState::ITextureObject* texture) {
|
||||||
|
return texture != nullptr && MG_Util::GetBaseInternalFormatComponentCount(texture->GetFormat()) == 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[maybe_unused]] static Float ResolveColorClearAlpha(const MG_State::GLState::ITextureObject* texture, Float requestedAlpha) {
|
||||||
if (texture != nullptr && MG_Util::GetBaseInternalFormatComponentCount(texture->GetFormat()) == 3) {
|
if (texture != nullptr && MG_Util::GetBaseInternalFormatComponentCount(texture->GetFormat()) == 3) {
|
||||||
return 1.0f;
|
return 1.0f;
|
||||||
}
|
}
|
||||||
@@ -83,9 +87,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static VkImageViewType ResolveAttachmentViewType(
|
static VkImageViewType ResolveAttachmentViewType(
|
||||||
const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||||
const VkTextureManager::TextureResource& resource) {
|
const VkTextureManager::TextureResource& resource) {
|
||||||
return !attachment.IsLayered() && IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ?
|
if (attachment.IsLayered()) {
|
||||||
VK_IMAGE_VIEW_TYPE_2D :
|
return resource.viewType;
|
||||||
resource.viewType;
|
}
|
||||||
|
// A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever
|
||||||
|
// the image's own view type is. The cube-face upload targets always meant this; a cube map
|
||||||
|
// array attached through glFramebufferTextureLayer means it too, and a CUBE_ARRAY view over
|
||||||
|
// a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path
|
||||||
|
// produces a non-layered cube attachment without a face upload target - and is kept for
|
||||||
|
// symmetry with CUBE_ARRAY.
|
||||||
|
if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ||
|
||||||
|
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE) {
|
||||||
|
return VK_IMAGE_VIEW_TYPE_2D;
|
||||||
|
}
|
||||||
|
return resource.viewType;
|
||||||
}
|
}
|
||||||
|
|
||||||
static MG_State::GLState::ITextureObject* ResolveCompleteColorAttachmentTexture(
|
static MG_State::GLState::ITextureObject* ResolveCompleteColorAttachmentTexture(
|
||||||
@@ -526,7 +541,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
pending.renderbuffer = renderbuffer;
|
pending.renderbuffer = renderbuffer;
|
||||||
pending.payload.mask |= clearPayload.mask;
|
pending.payload.mask |= clearPayload.mask;
|
||||||
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||||
|
// The whole colour description, not just the float vector: an integer clear keeps its
|
||||||
|
// value in colorInt/colorUint, and dropping the encoding here would leave the pending
|
||||||
|
// clear reading as an all-zero float one.
|
||||||
pending.payload.color = clearPayload.color;
|
pending.payload.color = clearPayload.color;
|
||||||
|
pending.payload.colorEncoding = clearPayload.colorEncoding;
|
||||||
|
pending.payload.colorInt = clearPayload.colorInt;
|
||||||
|
pending.payload.colorUint = clearPayload.colorUint;
|
||||||
}
|
}
|
||||||
if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||||
pending.payload.depth = clearPayload.depth;
|
pending.payload.depth = clearPayload.depth;
|
||||||
@@ -924,9 +945,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (rbHasClear &&
|
if (rbHasClear &&
|
||||||
MG_Util::GetBaseInternalFormatComponentCount(renderbuffer->GetInternalFormat()) == 3) {
|
MG_Util::GetBaseInternalFormatComponentCount(renderbuffer->GetInternalFormat()) == 3) {
|
||||||
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
|
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
|
||||||
rbClearPayload.color =
|
ForceOpaqueClearAlpha(rbClearPayload);
|
||||||
FloatVec4(rbClearPayload.color.x(), rbClearPayload.color.y(),
|
|
||||||
rbClearPayload.color.z(), 1.0f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const VkImageLayout trackedRbLayout = rbResource->layout;
|
const VkImageLayout trackedRbLayout = rbResource->layout;
|
||||||
@@ -1496,12 +1515,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||||
clearValues[pending.attachmentIndex].color = {
|
clearValues[pending.attachmentIndex].color =
|
||||||
clearPayload.color.x(),
|
MakeVkClearColorValue(clearPayload, ColorFormatLacksAlpha(liveTexture.get()));
|
||||||
clearPayload.color.y(),
|
|
||||||
clearPayload.color.z(),
|
|
||||||
ResolveColorClearAlpha(liveTexture.get(), clearPayload.color.w())
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
if ((clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||||
clearValues[pending.attachmentIndex].depthStencil.depth = clearPayload.depth;
|
clearValues[pending.attachmentIndex].depthStencil.depth = clearPayload.depth;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||||
|
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vk_mem_alloc.h>
|
#include <vk_mem_alloc.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||||
@@ -314,7 +315,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 deferredAtFrame = 0;
|
Uint64 deferredAtFrame = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
// Node-based std::unordered_map, deliberately not FastSTL's open-addressing UnorderedMap:
|
||||||
|
// callers cache a RenderbufferResource* - or a bare &resource->layout - and then make further
|
||||||
|
// calls that touch this map. BlitFramebuffer is the one that bit: it resolves the source and
|
||||||
|
// destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then
|
||||||
|
// materializes the source's pending clear, which looks that same resource up again. FastSTL's
|
||||||
|
// operator[] runs its load-factor check before find_key and reallocates the whole bucket array
|
||||||
|
// when occupancy crosses it, so even a plain lookup relocates every element; erase only
|
||||||
|
// tombstones and never decrements the occupancy, so the doubling keeps firing. After a
|
||||||
|
// relocation the cached pointer names freed storage still holding the pre-clear
|
||||||
|
// VK_IMAGE_LAYOUT_UNDEFINED, and BlitFramebuffer bails out at "source image layout is
|
||||||
|
// undefined", silently dropping the blit - renderbuffers_storage_multisample read back zero
|
||||||
|
// instead of the clear colour on exactly the iterations that grew the table.
|
||||||
|
//
|
||||||
|
// Reordering the materialize ahead of the resolves - the fix ReadPixels got - does not cover
|
||||||
|
// this: the destination resolve still runs after the source pointer is taken. The depth blit,
|
||||||
|
// GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same
|
||||||
|
// kind of pointer, so the invariant belongs in the container rather than in a per-call-site
|
||||||
|
// ordering rule. m_textureResources is node-based for the same reason. This buys stability
|
||||||
|
// across rehash and insert only - erase still invalidates the erased element, which is safe
|
||||||
|
// here because a renderbuffer that is an FBO attachment is held alive by that attachment.
|
||||||
|
std::unordered_map<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
||||||
// Supported sample counts per attachment format, so per-draw resource lookups
|
// Supported sample counts per attachment format, so per-draw resource lookups
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &maxAnisotropy, sizeof(maxAnisotropy)));
|
||||||
const auto compareMode = sampler.GetCompareMode();
|
const auto compareMode = sampler.GetCompareMode();
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
|
||||||
const auto compareFunc = ResolveCompareFunc(sampler, texture);
|
const auto compareFunc = sampler.GetSamplerCompareFunc();
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
|
||||||
const auto borderColor = ResolveVkBorderColor(sampler, texture);
|
const auto borderColor = ResolveVkBorderColor(sampler, texture);
|
||||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
|
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
|
||||||
@@ -207,7 +207,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE;
|
samplerInfo.anisotropyEnable = maxAnisotropy > 1.0f ? VK_TRUE : VK_FALSE;
|
||||||
samplerInfo.maxAnisotropy = maxAnisotropy;
|
samplerInfo.maxAnisotropy = maxAnisotropy;
|
||||||
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
|
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
|
||||||
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
|
samplerInfo.compareOp = ToVkCompareOp(sampler.GetSamplerCompareFunc());
|
||||||
// Must match BuildSamplerKey's resolution exactly.
|
// Must match BuildSamplerKey's resolution exactly.
|
||||||
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||||
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
|
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
|
||||||
@@ -281,24 +281,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SamplerCompareFunc VkSamplerManager::ResolveCompareFunc(const MG_State::GLState::SamplerObject& sampler,
|
|
||||||
const MG_State::GLState::ITextureObject& texture) {
|
|
||||||
const auto compareFunc = sampler.GetSamplerCompareFunc();
|
|
||||||
if (sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture &&
|
|
||||||
IsDepthTextureFormat(texture.GetFormat()) && compareFunc == SamplerCompareFunc::Always) {
|
|
||||||
return SamplerCompareFunc::LessEqual;
|
|
||||||
}
|
|
||||||
|
|
||||||
return compareFunc;
|
|
||||||
}
|
|
||||||
|
|
||||||
VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||||
const MG_State::GLState::ITextureObject& texture) {
|
const MG_State::GLState::ITextureObject& texture) {
|
||||||
if (!UsesBorderColor(sampler)) {
|
if (!UsesBorderColor(sampler)) {
|
||||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& borderColor = texture.GetBorderColor();
|
// Border colour is sampler state: a bound sampler object supplies its own, and a texture
|
||||||
|
// with none reaches the very same value through the sampler object it owns.
|
||||||
|
const auto& borderColor = sampler.GetBorderColor();
|
||||||
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat());
|
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat());
|
||||||
|
|
||||||
if (isDepthTexture) {
|
if (isDepthTexture) {
|
||||||
|
|||||||
@@ -69,8 +69,6 @@ private:
|
|||||||
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
|
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
|
||||||
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
|
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
|
||||||
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
|
static VkCompareOp ToVkCompareOp(SamplerCompareFunc func);
|
||||||
static SamplerCompareFunc ResolveCompareFunc(const MG_State::GLState::SamplerObject& sampler,
|
|
||||||
const MG_State::GLState::ITextureObject& texture);
|
|
||||||
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
static VkBorderColor ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||||
const MG_State::GLState::ITextureObject& texture);
|
const MG_State::GLState::ITextureObject& texture);
|
||||||
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
|
// The anisotropy Vulkan will actually apply: 1.0 (i.e. disabled) unless the feature is on and
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||||
|
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
|
#include <algorithm>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -25,9 +26,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule
|
// Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule
|
||||||
// only covers rendering commands; e.g. Flywheel's Hi-Z depth pyramid downsample samples the
|
// only covers rendering commands; e.g. Flywheel's Hi-Z depth pyramid downsample samples the
|
||||||
// depth attachment of the bound draw framebuffer), so sampled-read barriers must cover the
|
// depth attachment of the bound draw framebuffer), so sampled-read barriers must cover the
|
||||||
// compute stage in addition to the graphics stages.
|
// compute stage in addition to the graphics stages. Set at Initialize from the renderer's
|
||||||
static constexpr VkPipelineStageFlags kSampledReadStages =
|
// device-feature-derived mask: geometry/tessellation stage bits are invalid in a barrier when
|
||||||
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
// their feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and ALL_GRAPHICS
|
||||||
|
// would also serialize against non-shader stages. The default only matters before a device
|
||||||
|
// exists, when nothing records barriers.
|
||||||
|
static VkPipelineStageFlags s_sampledReadStages =
|
||||||
|
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||||
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||||
|
|
||||||
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
|
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
|
||||||
Int maxDimension = std::max<Int>(baseTexelSize.x(),
|
Int maxDimension = std::max<Int>(baseTexelSize.x(),
|
||||||
@@ -193,7 +199,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
|
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
|
||||||
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
|
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
|
||||||
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
|
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
|
||||||
outSrcStageMask = kSampledReadStages;
|
outSrcStageMask = s_sampledReadStages;
|
||||||
outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||||
return;
|
return;
|
||||||
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
|
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
|
||||||
@@ -241,7 +247,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
|
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
|
||||||
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
|
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
|
||||||
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
|
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
|
||||||
outDstStageMask = kSampledReadStages;
|
outDstStageMask = s_sampledReadStages;
|
||||||
outDstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
outDstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||||
return;
|
return;
|
||||||
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
|
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
|
||||||
@@ -564,6 +570,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
outShape.depth = 1;
|
outShape.depth = 1;
|
||||||
outShape.arrayLayers = 6;
|
outShape.arrayLayers = 6;
|
||||||
return true;
|
return true;
|
||||||
|
case TextureUploadTarget::CubeMapArray:
|
||||||
|
case TextureUploadTarget::ProxyCubeMapArray:
|
||||||
|
// GL_TEXTURE_CUBE_MAP_ARRAY is an array texture whose layers happen to be cube faces:
|
||||||
|
// one 2D image with arrayLayers = 6 * cubeCount, CUBE_COMPATIBLE so the whole thing can
|
||||||
|
// be sampled as a samplerCubeArray. glTexStorage3D hands the 6*n through as the GL depth
|
||||||
|
// and the upload path's depthSelectsArrayLayer already lists VK_IMAGE_VIEW_TYPE_CUBE_ARRAY,
|
||||||
|
// so the copies address layers correctly.
|
||||||
|
//
|
||||||
|
// A depth that is not a whole number of cubes, or a non-square level, has no Vulkan shape
|
||||||
|
// - declined the way every other unrepresentable target is. This function's Bool return
|
||||||
|
// exists for exactly that; asserting here would abort the process on ordinary application
|
||||||
|
// input, GL_PROXY_TEXTURE_CUBE_MAP_ARRAY above all.
|
||||||
|
if (texelSize.z() <= 0 || (texelSize.z() % 6) != 0 || texelSize.x() != texelSize.y()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
outShape.imageType = VK_IMAGE_TYPE_2D;
|
||||||
|
outShape.viewType = VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
|
||||||
|
outShape.imageFlags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
|
||||||
|
outShape.depth = 1;
|
||||||
|
outShape.arrayLayers = static_cast<Uint32>(texelSize.z());
|
||||||
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -578,6 +605,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_commandPool = initInfo.commandPool;
|
m_commandPool = initInfo.commandPool;
|
||||||
m_graphicsQueue = initInfo.graphicsQueue;
|
m_graphicsQueue = initInfo.graphicsQueue;
|
||||||
m_imageFormatListSupported = initInfo.imageFormatListSupported;
|
m_imageFormatListSupported = initInfo.imageFormatListSupported;
|
||||||
|
s_sampledReadStages = initInfo.sampledReadStageMask;
|
||||||
m_currentFrameIndex = 0;
|
m_currentFrameIndex = 0;
|
||||||
m_deferredReleases.clear();
|
m_deferredReleases.clear();
|
||||||
m_deferredReleases.resize(initInfo.frameCount);
|
m_deferredReleases.resize(initInfo.frameCount);
|
||||||
@@ -593,12 +621,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
TextureResource::s_device = m_device;
|
TextureResource::s_device = m_device;
|
||||||
TextureResource::s_allocator = m_allocator;
|
TextureResource::s_allocator = m_allocator;
|
||||||
|
|
||||||
|
// Own pool for the recycled upload-batch command buffers. Parking a
|
||||||
|
// dozen reset-but-alive command buffers in the renderer's shared pool
|
||||||
|
// interleaves their retained chunks with the frame command buffers
|
||||||
|
// allocated/freed there every frame; isolating them keeps both pools'
|
||||||
|
// internal allocators dense.
|
||||||
|
VkCommandPoolCreateInfo uploadPoolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||||
|
uploadPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT |
|
||||||
|
VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
|
||||||
|
uploadPoolInfo.queueFamilyIndex = initInfo.graphicsQueueFamilyIndex;
|
||||||
|
VK_VERIFY(vkCreateCommandPool(m_device, &uploadPoolInfo, nullptr, &m_uploadCommandPool),
|
||||||
|
"vkCreateCommandPool(texture upload batch)");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VkTextureManager::Shutdown() {
|
void VkTextureManager::Shutdown() {
|
||||||
if (m_device != VK_NULL_HANDLE) {
|
if (m_device != VK_NULL_HANDLE) {
|
||||||
|
// A still-open (never-submitted) batch is discarded, not submitted:
|
||||||
|
// the renderer has already drained the device and the data has no
|
||||||
|
// observer. Submitted batches are waited and recycled, then the
|
||||||
|
// pools they recycled into are destroyed.
|
||||||
|
DiscardPendingUploadBatch();
|
||||||
ReclaimCompletedUploads(/*waitAll=*/true);
|
ReclaimCompletedUploads(/*waitAll=*/true);
|
||||||
|
DestroyUploadPools();
|
||||||
|
if (m_uploadCommandPool != VK_NULL_HANDLE) {
|
||||||
|
vkDestroyCommandPool(m_device, m_uploadCommandPool, nullptr);
|
||||||
|
m_uploadCommandPool = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DestroyDeferredReleases();
|
DestroyDeferredReleases();
|
||||||
++m_resourceEraseEpoch; // every memoized resource pointer dies with the map
|
++m_resourceEraseEpoch; // every memoized resource pointer dies with the map
|
||||||
@@ -822,8 +872,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
|
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
|
||||||
return VK_NULL_HANDLE;
|
return VK_NULL_HANDLE;
|
||||||
}
|
}
|
||||||
if (layerCount == 0 || baseArrayLayer >= resource->arrayLayers ||
|
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so a per-slice
|
||||||
baseArrayLayer + layerCount > resource->arrayLayers) {
|
// attachment view is a 2D view whose "array layer" is the slice - legal only on a
|
||||||
|
// 2D-array-compatible image (VUID-VkImageViewCreateInfo-image-04970), which
|
||||||
|
// SyncTextureResource asks for and may have had refused per format.
|
||||||
|
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D && viewType == VK_IMAGE_VIEW_TYPE_2D) {
|
||||||
|
const Uint32 sliceCount = std::max(resource->depth >> mipLevel, 1u);
|
||||||
|
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) == 0 ||
|
||||||
|
layerCount == 0 || baseArrayLayer >= sliceCount || baseArrayLayer + layerCount > sliceCount) {
|
||||||
|
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d (mip %u has %u slices, "
|
||||||
|
"2D-array-compatible=%d)",
|
||||||
|
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
|
||||||
|
mipLevel, sliceCount,
|
||||||
|
(int)((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0));
|
||||||
|
return VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
} else if (layerCount == 0 || baseArrayLayer >= resource->arrayLayers ||
|
||||||
|
baseArrayLayer + layerCount > resource->arrayLayers) {
|
||||||
MGLOG_D("%s: invalid layer span [%u, %u) for textureId=%d arrayLayers=%u",
|
MGLOG_D("%s: invalid layer span [%u, %u) for textureId=%d arrayLayers=%u",
|
||||||
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
|
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
|
||||||
resource->arrayLayers);
|
resource->arrayLayers);
|
||||||
@@ -1190,7 +1255,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
||||||
kSampledReadStages, srcAccessMask,
|
s_sampledReadStages, srcAccessMask,
|
||||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
|
||||||
resource->arrayLayers);
|
resource->arrayLayers);
|
||||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||||
@@ -1246,6 +1311,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
!it->second.storageUsageResolved;
|
!it->second.storageUsageResolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool VkTextureManager::NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const {
|
||||||
|
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||||
|
const auto it = m_textureResources.find(identity);
|
||||||
|
// No image yet: the first sync sizes the chain from the levels the texture already
|
||||||
|
// defines, so nothing is recreated and there is nothing to order against.
|
||||||
|
if (it == m_textureResources.end() || it->second.image == VK_NULL_HANDLE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const TextureResource& resource = it->second;
|
||||||
|
const IntVec3 extent = {static_cast<Int>(resource.extent.width), static_cast<Int>(resource.extent.height),
|
||||||
|
static_cast<Int>(resource.depth)};
|
||||||
|
return resource.mipLevels < ComputeFullMipLevelCount(extent);
|
||||||
|
}
|
||||||
|
|
||||||
Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const {
|
Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const {
|
||||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
||||||
const auto it = m_textureResources.find(identity);
|
const auto it = m_textureResources.find(identity);
|
||||||
@@ -1478,20 +1557,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// preserve-copy path below carries the pixels over), so sequentially-
|
// preserve-copy path below carries the pixels over), so sequentially-
|
||||||
// defined atlas mips do not recreate per level, and glGenerateMipmap -
|
// defined atlas mips do not recreate per level, and glGenerateMipmap -
|
||||||
// which defines every level before syncing - works unchanged.
|
// which defines every level before syncing - works unchanged.
|
||||||
const Uint32 backingMipLevels =
|
|
||||||
isMultisampleTexture ? 1u
|
|
||||||
: (mipLevels > 1 ? std::max(mipLevels, ComputeFullMipLevelCount(texelSize)) : 1u);
|
|
||||||
TextureShapeInfo shapeInfo{};
|
TextureShapeInfo shapeInfo{};
|
||||||
const Bool supportedShape = TryResolveTextureShapeInfo(texture, uploadTarget, texelSize, shapeInfo);
|
const Bool supportedShape = TryResolveTextureShapeInfo(texture, uploadTarget, texelSize, shapeInfo);
|
||||||
MOBILEGL_ASSERT(supportedShape,
|
// ComputeFullMipLevelCount takes max(x, y, z), and for every ARRAY shape z is the layer
|
||||||
"SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
|
// count, not a mip-able axis: a 4x4 array with 192 layers asked for 6 levels on an image
|
||||||
"mipLevels=%u vkViewType=%d",
|
// whose legal maximum is 3 (VUID-VkImageCreateInfo-mipLevels-00958). Only the image's own
|
||||||
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
// extent - width, height and shapeInfo.depth, which is 1 for every array - can bound it.
|
||||||
MG_Util::ConvertTextureTargetToString(texture.GetTarget()).c_str(),
|
// lavapipe has been letting this through unvalidated; a strict driver would not.
|
||||||
texture.GetExternalIndex(), texelSize.x(), texelSize.y(), texelSize.z(), mipLevels,
|
const IntVec3 mipExtent{texelSize.x(), texelSize.y(), static_cast<Int>(shapeInfo.depth)};
|
||||||
static_cast<Int>(MG_Util::ConvertTextureUploadTargetToVkEnum(uploadTarget)));
|
const Uint32 fullMipLevels = ComputeFullMipLevelCount(mipExtent);
|
||||||
|
const Uint32 backingMipLevels =
|
||||||
|
isMultisampleTexture ? 1u : (mipLevels > 1 ? std::min(std::max(mipLevels, fullMipLevels), fullMipLevels) : 1u);
|
||||||
if (!supportedShape) {
|
if (!supportedShape) {
|
||||||
MGLOG_D("%s: not Texture2D, unsupported", __func__);
|
// A gap in this backend's coverage, not a broken invariant: the GL front end accepts
|
||||||
|
// targets this manager has no Vulkan image shape for yet (cube map arrays above all).
|
||||||
|
// Declining the sync leaves the texture unbacked - wrong, but recoverable - where an
|
||||||
|
// assertion would take the whole process down instead.
|
||||||
|
MGLOG_W("SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
|
||||||
|
"mipLevels=%u vkViewType=%d",
|
||||||
|
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
|
||||||
|
MG_Util::ConvertTextureTargetToString(texture.GetTarget()).c_str(), texture.GetExternalIndex(),
|
||||||
|
texelSize.x(), texelSize.y(), texelSize.z(), mipLevels,
|
||||||
|
static_cast<Int>(MG_Util::ConvertTextureUploadTargetToVkEnum(uploadTarget)));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
VkSampleCountFlagBits resolvedSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
VkSampleCountFlagBits resolvedSampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||||
@@ -1502,6 +1589,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str());
|
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// glTexStorage*Multisample(samples = 1) is legal GL, but a one-sample image cannot back a
|
||||||
|
// sampler2DMS: VUID-RuntimeSpirv-samples-08726 forbids an OpTypeImage with MS = 1 from
|
||||||
|
// reading an image created with VK_SAMPLE_COUNT_1_BIT, and the fetch returns undefined data
|
||||||
|
// rather than an error. GL only promises "at least the requested number of samples", so
|
||||||
|
// giving a multisample texture two is both legal and the only way to keep the shader's view
|
||||||
|
// of it honest. GL_TEXTURE_SAMPLES still reports what the application asked for - that is
|
||||||
|
// read off the texture object, not off the image.
|
||||||
|
if (isMultisampleTexture && resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT) {
|
||||||
|
resolvedSampleCount = VK_SAMPLE_COUNT_2_BIT;
|
||||||
|
}
|
||||||
|
|
||||||
const VkImageAspectFlags aspect = GetAspectMaskForFormat(format);
|
const VkImageAspectFlags aspect = GetAspectMaskForFormat(format);
|
||||||
VkFormatProperties formatProperties{};
|
VkFormatProperties formatProperties{};
|
||||||
@@ -1528,6 +1625,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
|
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
|
||||||
const Bool supportsStorageImage = storageImageCapable && markedAsStorageImage;
|
const Bool supportsStorageImage = storageImageCapable && markedAsStorageImage;
|
||||||
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
|
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
|
||||||
|
// One z slice of a 3D texture can only be attached to a framebuffer through a 2D view over
|
||||||
|
// it, which needs the image to be 2D-array-compatible (Vulkan 1.1 core, promoted from
|
||||||
|
// VK_KHR_maintenance1). Asked for optimistically and withdrawn per format below if the
|
||||||
|
// driver refuses - losing it only costs per-slice attachment, while failing creation would
|
||||||
|
// lose the texture entirely.
|
||||||
|
if (shapeInfo.imageType == VK_IMAGE_TYPE_3D && !isMultisampleTexture &&
|
||||||
|
m_2dArrayCompatibleUnsupported.find(format) == m_2dArrayCompatibleUnsupported.end()) {
|
||||||
|
imageCreateFlags |= VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
|
||||||
|
}
|
||||||
if (storageImageCapable && IsMutableStorageImageFormat(format) &&
|
if (storageImageCapable && IsMutableStorageImageFormat(format) &&
|
||||||
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
|
||||||
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
|
||||||
@@ -1577,7 +1683,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (rounded == 0) {
|
if (rounded == 0) {
|
||||||
for (Uint32 bit = static_cast<Uint32>(resolvedSampleCount) >> 1; bit != 0; bit >>= 1) {
|
// Never land on one sample: that is the VUID-RuntimeSpirv-samples-08726
|
||||||
|
// violation the floor above exists to avoid, and it would come back silently
|
||||||
|
// for any format whose only supported count is 1.
|
||||||
|
for (Uint32 bit = static_cast<Uint32>(resolvedSampleCount) >> 1;
|
||||||
|
bit > static_cast<Uint32>(VK_SAMPLE_COUNT_1_BIT); bit >>= 1) {
|
||||||
if ((supported & bit) != 0) {
|
if ((supported & bit) != 0) {
|
||||||
rounded = bit;
|
rounded = bit;
|
||||||
break;
|
break;
|
||||||
@@ -1681,7 +1791,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
imageInfo.pNext = &formatListInfo;
|
imageInfo.pNext = &formatListInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
|
if (isMultisampleTexture || (imageInfo.flags & (VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT |
|
||||||
|
VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT)) != 0) {
|
||||||
VkImageFormatProperties imageFormatProperties{};
|
VkImageFormatProperties imageFormatProperties{};
|
||||||
VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
|
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
|
||||||
@@ -1704,6 +1815,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
|
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
|
||||||
imageInfo.flags, &imageFormatProperties);
|
imageInfo.flags, &imageFormatProperties);
|
||||||
}
|
}
|
||||||
|
if (imageFormatResult != VK_SUCCESS && !isMultisampleTexture &&
|
||||||
|
(imageInfo.flags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0) {
|
||||||
|
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
|
||||||
|
// format; failing creation would lose the texture entirely. Remembered so later syncs
|
||||||
|
// neither reprobe nor flag-mismatch against this image and recreate it.
|
||||||
|
MGLOG_W("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
|
||||||
|
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
|
||||||
|
"unavailable for it)",
|
||||||
|
__func__, static_cast<Int>(format), texture.GetExternalIndex());
|
||||||
|
m_2dArrayCompatibleUnsupported.insert(format);
|
||||||
|
imageInfo.flags &= ~VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
|
||||||
|
imageCreateFlags = imageInfo.flags;
|
||||||
|
imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
|
||||||
|
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
|
||||||
|
imageInfo.flags, &imageFormatProperties);
|
||||||
|
}
|
||||||
if (imageFormatResult != VK_SUCCESS ||
|
if (imageFormatResult != VK_SUCCESS ||
|
||||||
(isMultisampleTexture && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) {
|
(isMultisampleTexture && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) {
|
||||||
MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for textureId=%d target=%s "
|
MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for textureId=%d target=%s "
|
||||||
@@ -1755,6 +1882,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
resource.syncedTextureParamsVersion = 0;
|
resource.syncedTextureParamsVersion = 0;
|
||||||
|
|
||||||
if (preservedResource) {
|
if (preservedResource) {
|
||||||
|
// The preserve copy reads the OLD image on its own immediately-
|
||||||
|
// submitted-and-waited command buffer; a batched upload into that
|
||||||
|
// image still sitting in the open batch must reach the queue first
|
||||||
|
// or the copy carries pre-upload texels forward.
|
||||||
|
FlushPendingUploads();
|
||||||
const Bool preserved = PreserveTextureContentsOnRecreate(
|
const Bool preserved = PreserveTextureContentsOnRecreate(
|
||||||
m_device, m_commandPool, m_graphicsQueue, *preservedResource, resource);
|
m_device, m_commandPool, m_graphicsQueue, *preservedResource, resource);
|
||||||
MOBILEGL_ASSERT(preserved,
|
MOBILEGL_ASSERT(preserved,
|
||||||
@@ -1765,6 +1897,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VkTextureManager::DeferResourceRelease(TextureResource&& resource) {
|
void VkTextureManager::DeferResourceRelease(TextureResource&& resource) {
|
||||||
|
// The deferred-release queues are drained under fence/queue-idle proofs
|
||||||
|
// that only cover SUBMITTED work; a recorded-but-unsubmitted upload
|
||||||
|
// batch referencing this image would escape them. Push the batch onto
|
||||||
|
// the queue first so every later proof covers it. Rare (only recreate/
|
||||||
|
// erase of an image uploaded this very frame), so the flush is cheap.
|
||||||
|
if (m_uploadBatchOpen && resource.image != VK_NULL_HANDLE &&
|
||||||
|
std::find(m_uploadBatchImages.begin(), m_uploadBatchImages.end(), resource.image) !=
|
||||||
|
m_uploadBatchImages.end()) {
|
||||||
|
FlushPendingUploads();
|
||||||
|
}
|
||||||
if (resource.image == VK_NULL_HANDLE && resource.fullView == VK_NULL_HANDLE &&
|
if (resource.image == VK_NULL_HANDLE && resource.fullView == VK_NULL_HANDLE &&
|
||||||
resource.sampledView == VK_NULL_HANDLE &&
|
resource.sampledView == VK_NULL_HANDLE &&
|
||||||
resource.perMipViews.empty() && resource.perMipSampledViews.empty() &&
|
resource.perMipViews.empty() && resource.perMipSampledViews.empty() &&
|
||||||
@@ -1815,14 +1957,211 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
} else if (vkGetFenceStatus(m_device, entry.fence) != VK_SUCCESS) {
|
} else if (vkGetFenceStatus(m_device, entry.fence) != VK_SUCCESS) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
vkDestroyFence(m_device, entry.fence, nullptr);
|
// Recycle, don't destroy: the fence resets into the fence pool,
|
||||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &entry.commandBuffer);
|
// the command buffer resets into the CB pool (m_uploadCommandPool
|
||||||
vmaDestroyBuffer(m_allocator, entry.stagingBuffer, entry.stagingAllocation);
|
// carries RESET_COMMAND_BUFFER_BIT), and the staging blocks
|
||||||
|
// return to the block pool for the next batch to bump-allocate.
|
||||||
|
// This is where the mc_tex_stream win comes from: the per-upload
|
||||||
|
// fence create/destroy + command-buffer alloc/free ioctl traffic
|
||||||
|
// was the measured 41%-in-kernel cost, not the submit itself.
|
||||||
|
if (vkResetFences(m_device, 1, &entry.fence) == VK_SUCCESS) {
|
||||||
|
m_freeUploadFences.push_back(entry.fence);
|
||||||
|
} else {
|
||||||
|
vkDestroyFence(m_device, entry.fence, nullptr);
|
||||||
|
}
|
||||||
|
if (vkResetCommandBuffer(entry.commandBuffer, 0) == VK_SUCCESS) {
|
||||||
|
m_freeUploadCommandBuffers.push_back(entry.commandBuffer);
|
||||||
|
} else {
|
||||||
|
vkFreeCommandBuffers(m_device, m_uploadCommandPool, 1, &entry.commandBuffer);
|
||||||
|
}
|
||||||
|
for (auto& block : entry.stagingBlocks) {
|
||||||
|
RecycleUploadStagingBlock(Move(block));
|
||||||
|
}
|
||||||
|
entry.stagingBlocks.clear();
|
||||||
}
|
}
|
||||||
m_pendingUploadReclaims.erase(m_pendingUploadReclaims.begin(),
|
m_pendingUploadReclaims.erase(m_pendingUploadReclaims.begin(),
|
||||||
m_pendingUploadReclaims.begin() + static_cast<std::ptrdiff_t>(completed));
|
m_pendingUploadReclaims.begin() + static_cast<std::ptrdiff_t>(completed));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VkTextureManager::RecycleUploadStagingBlock(UploadStagingBlock&& block) {
|
||||||
|
if (block.buffer == VK_NULL_HANDLE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Bound the idle pool: a one-off giant upload (initial atlas define)
|
||||||
|
// must not pin its staging memory forever.
|
||||||
|
constexpr VkDeviceSize kMaxFreeUploadStagingBytes = 32u * 1024u * 1024u;
|
||||||
|
if (m_allocator == nullptr || m_freeUploadStagingBytes + block.capacity > kMaxFreeUploadStagingBytes) {
|
||||||
|
vmaDestroyBuffer(m_allocator, block.buffer, block.allocation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
block.cursor = 0;
|
||||||
|
m_freeUploadStagingBytes += block.capacity;
|
||||||
|
m_freeUploadStagingBlocks.push_back(Move(block));
|
||||||
|
}
|
||||||
|
|
||||||
|
VkCommandBuffer VkTextureManager::EnsureUploadBatchOpen() {
|
||||||
|
if (m_uploadBatchOpen) {
|
||||||
|
return m_uploadBatchCommandBuffer;
|
||||||
|
}
|
||||||
|
if (!m_freeUploadCommandBuffers.empty()) {
|
||||||
|
m_uploadBatchCommandBuffer = m_freeUploadCommandBuffers.back();
|
||||||
|
m_freeUploadCommandBuffers.pop_back();
|
||||||
|
} else {
|
||||||
|
VkCommandBufferAllocateInfo allocInfo{};
|
||||||
|
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||||||
|
allocInfo.commandPool = m_uploadCommandPool;
|
||||||
|
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||||
|
allocInfo.commandBufferCount = 1;
|
||||||
|
VK_VERIFY(vkAllocateCommandBuffers(m_device, &allocInfo, &m_uploadBatchCommandBuffer),
|
||||||
|
"vkAllocateCommandBuffers(texture upload batch)");
|
||||||
|
}
|
||||||
|
VkCommandBufferBeginInfo beginInfo{};
|
||||||
|
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
||||||
|
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||||
|
VK_VERIFY(vkBeginCommandBuffer(m_uploadBatchCommandBuffer, &beginInfo),
|
||||||
|
"vkBeginCommandBuffer(texture upload batch)");
|
||||||
|
m_uploadBatchOpen = true;
|
||||||
|
return m_uploadBatchCommandBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8* VkTextureManager::AcquireUploadStagingSpace(VkDeviceSize size, VkBuffer& outBuffer,
|
||||||
|
VkDeviceSize& outBaseOffset) {
|
||||||
|
// 16 covers every uncompressed texel size in use (1..16 bytes) and the
|
||||||
|
// bufferOffset multiple-of-4 rule; per-item offsets inside the span
|
||||||
|
// keep the pre-batching tight packing.
|
||||||
|
constexpr VkDeviceSize kUploadStagingAlignment = 16;
|
||||||
|
constexpr VkDeviceSize kUploadStagingBlockSize = 1u * 1024u * 1024u;
|
||||||
|
UploadStagingBlock* current = m_uploadBatchBlocks.empty() ? nullptr : &m_uploadBatchBlocks.back();
|
||||||
|
VkDeviceSize alignedCursor = 0;
|
||||||
|
if (current != nullptr) {
|
||||||
|
alignedCursor = (current->cursor + (kUploadStagingAlignment - 1)) & ~(kUploadStagingAlignment - 1);
|
||||||
|
if (alignedCursor + size > current->capacity) {
|
||||||
|
current = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current == nullptr) {
|
||||||
|
UploadStagingBlock block;
|
||||||
|
for (SizeT i = 0; i < m_freeUploadStagingBlocks.size(); ++i) {
|
||||||
|
if (m_freeUploadStagingBlocks[i].capacity >= size) {
|
||||||
|
block = Move(m_freeUploadStagingBlocks[i]);
|
||||||
|
m_freeUploadStagingBytes -= block.capacity;
|
||||||
|
m_freeUploadStagingBlocks.erase(m_freeUploadStagingBlocks.begin() +
|
||||||
|
static_cast<std::ptrdiff_t>(i));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (block.buffer == VK_NULL_HANDLE) {
|
||||||
|
VkBufferCreateInfo bufferInfo{};
|
||||||
|
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
||||||
|
bufferInfo.size = std::max(kUploadStagingBlockSize, size);
|
||||||
|
bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
||||||
|
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||||
|
VmaAllocationCreateInfo stagingAllocationInfo{};
|
||||||
|
stagingAllocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST;
|
||||||
|
stagingAllocationInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
|
||||||
|
VMA_ALLOCATION_CREATE_MAPPED_BIT;
|
||||||
|
stagingAllocationInfo.requiredFlags =
|
||||||
|
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
||||||
|
VmaAllocationInfo allocationResult{};
|
||||||
|
VK_VERIFY(vmaCreateBuffer(m_allocator, &bufferInfo, &stagingAllocationInfo, &block.buffer,
|
||||||
|
&block.allocation, &allocationResult),
|
||||||
|
"vmaCreateBuffer(texture upload staging block)");
|
||||||
|
block.mapped = static_cast<Uint8*>(allocationResult.pMappedData);
|
||||||
|
block.capacity = bufferInfo.size;
|
||||||
|
MOBILEGL_ASSERT(block.mapped != nullptr,
|
||||||
|
"AcquireUploadStagingSpace: staging block is not persistently mapped");
|
||||||
|
}
|
||||||
|
block.cursor = 0;
|
||||||
|
m_uploadBatchBlocks.push_back(Move(block));
|
||||||
|
current = &m_uploadBatchBlocks.back();
|
||||||
|
alignedCursor = 0;
|
||||||
|
}
|
||||||
|
outBuffer = current->buffer;
|
||||||
|
outBaseOffset = alignedCursor;
|
||||||
|
current->cursor = alignedCursor + size;
|
||||||
|
return current->mapped + alignedCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VkTextureManager::FlushPendingUploads() {
|
||||||
|
if (!m_uploadBatchOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
VK_VERIFY(vkEndCommandBuffer(m_uploadBatchCommandBuffer), "vkEndCommandBuffer(texture upload batch)");
|
||||||
|
|
||||||
|
VkFence uploadFence = VK_NULL_HANDLE;
|
||||||
|
if (!m_freeUploadFences.empty()) {
|
||||||
|
uploadFence = m_freeUploadFences.back();
|
||||||
|
m_freeUploadFences.pop_back();
|
||||||
|
} else {
|
||||||
|
VkFenceCreateInfo fenceInfo{};
|
||||||
|
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
||||||
|
VK_VERIFY(vkCreateFence(m_device, &fenceInfo, nullptr, &uploadFence), "vkCreateFence(texture upload)");
|
||||||
|
}
|
||||||
|
|
||||||
|
VkSubmitInfo submitInfo{};
|
||||||
|
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||||||
|
submitInfo.commandBufferCount = 1;
|
||||||
|
submitInfo.pCommandBuffers = &m_uploadBatchCommandBuffer;
|
||||||
|
VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, uploadFence), "vkQueueSubmit(texture upload batch)");
|
||||||
|
|
||||||
|
PendingUploadReclaim reclaim;
|
||||||
|
reclaim.fence = uploadFence;
|
||||||
|
reclaim.commandBuffer = m_uploadBatchCommandBuffer;
|
||||||
|
reclaim.stagingBlocks = Move(m_uploadBatchBlocks);
|
||||||
|
m_pendingUploadReclaims.push_back(Move(reclaim));
|
||||||
|
m_uploadBatchCommandBuffer = VK_NULL_HANDLE;
|
||||||
|
m_uploadBatchOpen = false;
|
||||||
|
m_uploadBatchBlocks.clear();
|
||||||
|
m_uploadBatchImages.clear();
|
||||||
|
m_uploadBatchStagingBytes = 0;
|
||||||
|
|
||||||
|
ReclaimCompletedUploads();
|
||||||
|
// Backstop for pathological upload storms: bound in-flight staging
|
||||||
|
// memory by blocking on the oldest batch only once the list is deep.
|
||||||
|
constexpr SizeT kMaxPendingTextureUploads = 16;
|
||||||
|
if (m_pendingUploadReclaims.size() > kMaxPendingTextureUploads) {
|
||||||
|
VK_VERIFY(vkWaitForFences(m_device, 1, &m_pendingUploadReclaims.front().fence, VK_TRUE, UINT64_MAX),
|
||||||
|
"vkWaitForFences(texture upload backstop)");
|
||||||
|
ReclaimCompletedUploads();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VkTextureManager::DiscardPendingUploadBatch() {
|
||||||
|
if (!m_uploadBatchOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The batch was never submitted, so the command buffer is in the
|
||||||
|
// recording state, not pending - freeing it is legal.
|
||||||
|
vkFreeCommandBuffers(m_device, m_uploadCommandPool, 1, &m_uploadBatchCommandBuffer);
|
||||||
|
m_uploadBatchCommandBuffer = VK_NULL_HANDLE;
|
||||||
|
m_uploadBatchOpen = false;
|
||||||
|
for (auto& block : m_uploadBatchBlocks) {
|
||||||
|
RecycleUploadStagingBlock(Move(block));
|
||||||
|
}
|
||||||
|
m_uploadBatchBlocks.clear();
|
||||||
|
m_uploadBatchImages.clear();
|
||||||
|
m_uploadBatchStagingBytes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VkTextureManager::DestroyUploadPools() {
|
||||||
|
for (auto& block : m_freeUploadStagingBlocks) {
|
||||||
|
if (block.buffer != VK_NULL_HANDLE) {
|
||||||
|
vmaDestroyBuffer(m_allocator, block.buffer, block.allocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_freeUploadStagingBlocks.clear();
|
||||||
|
m_freeUploadStagingBytes = 0;
|
||||||
|
if (!m_freeUploadCommandBuffers.empty()) {
|
||||||
|
vkFreeCommandBuffers(m_device, m_uploadCommandPool, static_cast<Uint32>(m_freeUploadCommandBuffers.size()),
|
||||||
|
m_freeUploadCommandBuffers.data());
|
||||||
|
m_freeUploadCommandBuffers.clear();
|
||||||
|
}
|
||||||
|
for (const VkFence fence : m_freeUploadFences) {
|
||||||
|
vkDestroyFence(m_device, fence, nullptr);
|
||||||
|
}
|
||||||
|
m_freeUploadFences.clear();
|
||||||
|
}
|
||||||
|
|
||||||
void VkTextureManager::DestroyDeferredReleases() {
|
void VkTextureManager::DestroyDeferredReleases() {
|
||||||
for (auto& deferredReleases : m_deferredReleases) {
|
for (auto& deferredReleases : m_deferredReleases) {
|
||||||
deferredReleases.clear();
|
deferredReleases.clear();
|
||||||
@@ -1957,6 +2296,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const void* source = nullptr;
|
const void* source = nullptr;
|
||||||
Vector<Uint8> expandedData;
|
Vector<Uint8> expandedData;
|
||||||
VkDeviceSize offset = 0;
|
VkDeviceSize offset = 0;
|
||||||
|
// Sub-region upload (a small sprite in a big atlas): only the dirty box
|
||||||
|
// is staged and copied. texelSize keeps the LEVEL extent - the staging
|
||||||
|
// row copy needs it for the shadow's stride. Plain color formats only;
|
||||||
|
// the RGB-expand and depth(+stencil) conversion passes rewrite whole
|
||||||
|
// levels and stay full-size.
|
||||||
|
Bool subRegion = false;
|
||||||
|
IntVec3 regionLo = {0, 0, 0};
|
||||||
|
IntVec3 regionSize = {0, 0, 0};
|
||||||
|
SizeT texelBytes = 0;
|
||||||
|
// Scatter refinement of the single dirty box: when the storage's rect
|
||||||
|
// list reports the writes' true footprint (~100 sprites whose union box
|
||||||
|
// spans the whole atlas), each rect is staged tightly and copied with
|
||||||
|
// its own VkBufferImageCopy in ONE vkCmdCopyBufferToImage. Empty means
|
||||||
|
// "stage the one box above". Only set while subRegion.
|
||||||
|
Vector<MG_State::GLState::MipmapDirtyRegion> rects;
|
||||||
};
|
};
|
||||||
|
|
||||||
Vector<UploadItem> uploadItems;
|
Vector<UploadItem> uploadItems;
|
||||||
@@ -2003,6 +2357,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
uploadItem.source = source;
|
uploadItem.source = source;
|
||||||
uploadItem.offset = stagingSize;
|
uploadItem.offset = stagingSize;
|
||||||
uploadItem.uploadByteSize = byteSize;
|
uploadItem.uploadByteSize = byteSize;
|
||||||
|
if (!formatInfo.expandRgbToRgba &&
|
||||||
|
GetAspectMaskForFormat(outResource.format) == VK_IMAGE_ASPECT_COLOR_BIT) {
|
||||||
|
const auto region = mipmapTexture.GetStorageDirtyRegion(target, level);
|
||||||
|
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
|
||||||
|
static_cast<SizeT>(texelSize.y()) *
|
||||||
|
static_cast<SizeT>(std::max(texelSize.z(), 1));
|
||||||
|
if (!region.Empty() && !region.CoversWholeLevel(texelSize) && texelCount > 0 &&
|
||||||
|
byteSize % texelCount == 0) {
|
||||||
|
uploadItem.subRegion = true;
|
||||||
|
uploadItem.regionLo = region.lo;
|
||||||
|
uploadItem.regionSize = {region.hi.x() - region.lo.x(), region.hi.y() - region.lo.y(),
|
||||||
|
region.hi.z() - region.lo.z()};
|
||||||
|
uploadItem.texelBytes = byteSize / texelCount;
|
||||||
|
uploadItem.uploadByteSize = static_cast<SizeT>(uploadItem.regionSize.x()) *
|
||||||
|
static_cast<SizeT>(uploadItem.regionSize.y()) *
|
||||||
|
static_cast<SizeT>(uploadItem.regionSize.z()) *
|
||||||
|
uploadItem.texelBytes;
|
||||||
|
// Scatter refinement: the storage only hands out its rect list
|
||||||
|
// when the rects' summed area is materially smaller than the
|
||||||
|
// union box (0 otherwise), so taking it always stages fewer
|
||||||
|
// bytes than the box - the very amplification this path exists
|
||||||
|
// to avoid paying twice.
|
||||||
|
MG_State::GLState::MipmapDirtyRegion
|
||||||
|
dirtyRects[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
|
||||||
|
const SizeT dirtyRectCount = mipmapTexture.GetStorageDirtyRects(
|
||||||
|
target, level, dirtyRects, MG_State::GLState::MipmapStorage::kMaxDirtyRects);
|
||||||
|
if (dirtyRectCount >= 2) {
|
||||||
|
uploadItem.rects.assign(dirtyRects, dirtyRects + dirtyRectCount);
|
||||||
|
SizeT rectTexels = 0;
|
||||||
|
for (const auto& rect : uploadItem.rects) {
|
||||||
|
rectTexels += rect.TexelCount();
|
||||||
|
}
|
||||||
|
uploadItem.uploadByteSize = rectTexels * uploadItem.texelBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (formatInfo.expandRgbToRgba) {
|
if (formatInfo.expandRgbToRgba) {
|
||||||
const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo,
|
const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo,
|
||||||
uploadItem.expandedData);
|
uploadItem.expandedData);
|
||||||
@@ -2139,41 +2529,66 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VkBuffer stagingBuffer = VK_NULL_HANDLE;
|
// Rare mid-frame hazard, kept at parity with the old per-upload
|
||||||
VmaAllocation stagingAllocation = nullptr;
|
// submits: this image already has an upload recorded in the OPEN batch
|
||||||
|
// and has since been referenced by the frame's open recording (drawn).
|
||||||
VkBufferCreateInfo bufferInfo{};
|
// Appending here would merge both uploads into the same pre-frame
|
||||||
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
// submission the old code split into two; flush first so the second
|
||||||
bufferInfo.size = stagingSize;
|
// upload lands in its own later submission, exactly like before.
|
||||||
bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
|
if (m_uploadBatchOpen && WasTouchedThisRecording(outResource) &&
|
||||||
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
std::find(m_uploadBatchImages.begin(), m_uploadBatchImages.end(), outResource.image) !=
|
||||||
VmaAllocationCreateInfo stagingAllocationInfo{};
|
m_uploadBatchImages.end()) {
|
||||||
stagingAllocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST;
|
FlushPendingUploads();
|
||||||
stagingAllocationInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
|
}
|
||||||
stagingAllocationInfo.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
// Bound the staging bytes a single batch can pin before its fence can
|
||||||
VK_VERIFY(vmaCreateBuffer(m_allocator, &bufferInfo, &stagingAllocationInfo, &stagingBuffer, &stagingAllocation, nullptr),
|
// reclaim them.
|
||||||
"vmaCreateBuffer(staging texture)");
|
constexpr VkDeviceSize kMaxBatchStagingBytes = 64u * 1024u * 1024u;
|
||||||
|
if (m_uploadBatchOpen && m_uploadBatchStagingBytes + stagingSize > kMaxBatchStagingBytes) {
|
||||||
void* mapped = nullptr;
|
FlushPendingUploads();
|
||||||
VK_VERIFY(vmaMapMemory(m_allocator, stagingAllocation, &mapped), "vmaMapMemory(staging texture)");
|
|
||||||
for (const auto& item : uploadItems) {
|
|
||||||
std::memcpy(static_cast<Uint8*>(mapped) + item.offset, item.source, item.uploadByteSize);
|
|
||||||
}
|
}
|
||||||
vmaUnmapMemory(m_allocator, stagingAllocation);
|
|
||||||
|
|
||||||
VkCommandBufferAllocateInfo allocInfo{};
|
VkCommandBuffer commandBuffer = EnsureUploadBatchOpen();
|
||||||
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
VkBuffer stagingBuffer = VK_NULL_HANDLE;
|
||||||
allocInfo.commandPool = m_commandPool;
|
VkDeviceSize stagingBase = 0;
|
||||||
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
Uint8* mapped = AcquireUploadStagingSpace(stagingSize, stagingBuffer, stagingBase);
|
||||||
allocInfo.commandBufferCount = 1;
|
for (const auto& item : uploadItems) {
|
||||||
|
Uint8* dst = mapped + item.offset;
|
||||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
if (!item.subRegion) {
|
||||||
VK_VERIFY(vkAllocateCommandBuffers(m_device, &allocInfo, &commandBuffer), "vkAllocateCommandBuffers(texture)");
|
std::memcpy(dst, item.source, item.uploadByteSize);
|
||||||
|
continue;
|
||||||
VkCommandBufferBeginInfo beginInfo{};
|
}
|
||||||
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
// Tight-pack the dirty box(es): the shadow keeps whole-level rows, the
|
||||||
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
// staging slice holds only the region (bufferRowLength stays 0). Multi-
|
||||||
VK_VERIFY(vkBeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer(texture)");
|
// rect items pack their rects back to back in list order; the copy loop
|
||||||
|
// below recomputes the same running offsets.
|
||||||
|
const SizeT levelRowBytes = static_cast<SizeT>(item.texelSize.x()) * item.texelBytes;
|
||||||
|
const SizeT levelSliceBytes = static_cast<SizeT>(item.texelSize.y()) * levelRowBytes;
|
||||||
|
const Uint8* src = static_cast<const Uint8*>(item.source);
|
||||||
|
const auto packBox = [&](Uint8* out, const IntVec3& lo, const IntVec3& boxSize) {
|
||||||
|
const SizeT boxRowBytes = static_cast<SizeT>(boxSize.x()) * item.texelBytes;
|
||||||
|
for (Int z = 0; z < boxSize.z(); ++z) {
|
||||||
|
for (Int y = 0; y < boxSize.y(); ++y) {
|
||||||
|
const Uint8* srcRow = src + static_cast<SizeT>(lo.z() + z) * levelSliceBytes +
|
||||||
|
static_cast<SizeT>(lo.y() + y) * levelRowBytes +
|
||||||
|
static_cast<SizeT>(lo.x()) * item.texelBytes;
|
||||||
|
std::memcpy(out + (static_cast<SizeT>(z) * static_cast<SizeT>(boxSize.y()) + y) *
|
||||||
|
boxRowBytes,
|
||||||
|
srcRow, boxRowBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return static_cast<SizeT>(boxSize.x()) * static_cast<SizeT>(boxSize.y()) *
|
||||||
|
static_cast<SizeT>(boxSize.z()) * item.texelBytes;
|
||||||
|
};
|
||||||
|
if (!item.rects.empty()) {
|
||||||
|
for (const auto& rect : item.rects) {
|
||||||
|
dst += packBox(dst, rect.lo,
|
||||||
|
IntVec3{rect.hi.x() - rect.lo.x(), rect.hi.y() - rect.lo.y(),
|
||||||
|
rect.hi.z() - rect.lo.z()});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
packBox(dst, item.regionLo, item.regionSize);
|
||||||
|
}
|
||||||
|
|
||||||
const VkImageAspectFlags aspectMask = GetAspectMaskForFormat(outResource.format);
|
const VkImageAspectFlags aspectMask = GetAspectMaskForFormat(outResource.format);
|
||||||
VkPipelineStageFlags uploadSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
VkPipelineStageFlags uploadSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
@@ -2196,9 +2611,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
outResource.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY ||
|
outResource.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY ||
|
||||||
outResource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
|
outResource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
|
||||||
for (const auto& item : uploadItems) {
|
for (const auto& item : uploadItems) {
|
||||||
|
if (!item.rects.empty()) {
|
||||||
|
// Multi-rect item: one VkBufferImageCopy per rect, all submitted in a
|
||||||
|
// single vkCmdCopyBufferToImage. The rect list is pairwise disjoint by
|
||||||
|
// construction, so no two copies write the same texels. Multi-rect
|
||||||
|
// implies subRegion, which implies a plain color aspect - the combined
|
||||||
|
// depth-stencil split below can never see one of these.
|
||||||
|
VkBufferImageCopy rectCopies[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
|
||||||
|
Uint32 rectCopyCount = 0;
|
||||||
|
VkDeviceSize runningOffset = item.offset;
|
||||||
|
for (const auto& rect : item.rects) {
|
||||||
|
const IntVec3 rectSize = {rect.hi.x() - rect.lo.x(), rect.hi.y() - rect.lo.y(),
|
||||||
|
rect.hi.z() - rect.lo.z()};
|
||||||
|
const Uint32 rectDepth = static_cast<Uint32>(std::max(rectSize.z(), 1));
|
||||||
|
VkBufferImageCopy rectCopy{};
|
||||||
|
rectCopy.bufferOffset = stagingBase + runningOffset;
|
||||||
|
rectCopy.bufferRowLength = 0;
|
||||||
|
rectCopy.bufferImageHeight = 0;
|
||||||
|
rectCopy.imageSubresource.aspectMask = aspectMask;
|
||||||
|
rectCopy.imageSubresource.mipLevel = item.level;
|
||||||
|
rectCopy.imageSubresource.baseArrayLayer = item.baseArrayLayer;
|
||||||
|
rectCopy.imageSubresource.layerCount = 1;
|
||||||
|
rectCopy.imageOffset = {rect.lo.x(), rect.lo.y(),
|
||||||
|
depthSelectsArrayLayer ? 0 : rect.lo.z()};
|
||||||
|
rectCopy.imageExtent = {static_cast<Uint32>(rectSize.x()),
|
||||||
|
static_cast<Uint32>(rectSize.y()),
|
||||||
|
depthSelectsArrayLayer ? 1u : rectDepth};
|
||||||
|
if (depthSelectsArrayLayer) {
|
||||||
|
// The GL "depth" axis addresses array layers here, so a partial
|
||||||
|
// z-range narrows the layer span rather than the extent.
|
||||||
|
rectCopy.imageSubresource.baseArrayLayer =
|
||||||
|
item.baseArrayLayer + static_cast<Uint32>(rect.lo.z());
|
||||||
|
rectCopy.imageSubresource.layerCount = rectDepth;
|
||||||
|
}
|
||||||
|
rectCopies[rectCopyCount++] = rectCopy;
|
||||||
|
runningOffset += static_cast<VkDeviceSize>(rect.TexelCount() * item.texelBytes);
|
||||||
|
}
|
||||||
|
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image,
|
||||||
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rectCopyCount, rectCopies);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const Uint32 depthOrLayers = item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u;
|
const Uint32 depthOrLayers = item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u;
|
||||||
VkBufferImageCopy copy{};
|
VkBufferImageCopy copy{};
|
||||||
copy.bufferOffset = item.offset;
|
copy.bufferOffset = stagingBase + item.offset;
|
||||||
copy.bufferRowLength = 0;
|
copy.bufferRowLength = 0;
|
||||||
copy.bufferImageHeight = 0;
|
copy.bufferImageHeight = 0;
|
||||||
copy.imageSubresource.aspectMask = aspectMask;
|
copy.imageSubresource.aspectMask = aspectMask;
|
||||||
@@ -2208,6 +2663,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
copy.imageOffset = {0, 0, 0};
|
copy.imageOffset = {0, 0, 0};
|
||||||
copy.imageExtent = {static_cast<Uint32>(item.texelSize.x()), static_cast<Uint32>(item.texelSize.y()),
|
copy.imageExtent = {static_cast<Uint32>(item.texelSize.x()), static_cast<Uint32>(item.texelSize.y()),
|
||||||
depthSelectsArrayLayer ? 1u : depthOrLayers};
|
depthSelectsArrayLayer ? 1u : depthOrLayers};
|
||||||
|
if (item.subRegion) {
|
||||||
|
const Uint32 regionDepth = static_cast<Uint32>(std::max(item.regionSize.z(), 1));
|
||||||
|
copy.imageOffset = {item.regionLo.x(), item.regionLo.y(),
|
||||||
|
depthSelectsArrayLayer ? 0 : item.regionLo.z()};
|
||||||
|
copy.imageExtent = {static_cast<Uint32>(item.regionSize.x()),
|
||||||
|
static_cast<Uint32>(item.regionSize.y()),
|
||||||
|
depthSelectsArrayLayer ? 1u : regionDepth};
|
||||||
|
if (depthSelectsArrayLayer) {
|
||||||
|
// The GL "depth" axis addresses array layers here, so a partial
|
||||||
|
// z-range narrows the layer span rather than the extent.
|
||||||
|
copy.imageSubresource.baseArrayLayer =
|
||||||
|
item.baseArrayLayer + static_cast<Uint32>(item.regionLo.z());
|
||||||
|
copy.imageSubresource.layerCount = regionDepth;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isCombinedDepthStencil) {
|
if (isCombinedDepthStencil) {
|
||||||
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
|
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
|
||||||
static_cast<SizeT>(item.texelSize.y()) *
|
static_cast<SizeT>(item.texelSize.y()) *
|
||||||
@@ -2216,7 +2686,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
depthCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
depthCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||||
VkBufferImageCopy stencilCopy = copy;
|
VkBufferImageCopy stencilCopy = copy;
|
||||||
stencilCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
|
stencilCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||||
stencilCopy.bufferOffset = item.offset + static_cast<VkDeviceSize>(texelCount) * 4;
|
stencilCopy.bufferOffset = stagingBase + item.offset + static_cast<VkDeviceSize>(texelCount) * 4;
|
||||||
const VkBufferImageCopy copies[2] = {depthCopy, stencilCopy};
|
const VkBufferImageCopy copies[2] = {depthCopy, stencilCopy};
|
||||||
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image,
|
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image,
|
||||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, copies);
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, copies);
|
||||||
@@ -2232,43 +2702,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
uploadLayout,
|
uploadLayout,
|
||||||
finalLayout,
|
finalLayout,
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||||
kSampledReadStages,
|
s_sampledReadStages,
|
||||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||||
VK_ACCESS_SHADER_READ_BIT,
|
VK_ACCESS_SHADER_READ_BIT,
|
||||||
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
|
||||||
MOBILEGL_ASSERT(ok, "TransitionImageLayout to sampled read-only layout failed");
|
MOBILEGL_ASSERT(ok, "TransitionImageLayout to sampled read-only layout failed");
|
||||||
outResource.layout = finalLayout;
|
outResource.layout = finalLayout;
|
||||||
|
|
||||||
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture)");
|
// Ordering argument (replaces the old immediate per-texture submit):
|
||||||
|
// this upload is RECORDED into the shared batch command buffer, which
|
||||||
VkSubmitInfo submitInfo{};
|
// FlushPendingUploads submits - with one vkQueueSubmit and one pooled
|
||||||
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
// fence for the whole batch - strictly BEFORE any other submission on
|
||||||
submitInfo.commandBufferCount = 1;
|
// the same queue whose commands could consume the image: the renderer
|
||||||
submitInfo.pCommandBuffers = &commandBuffer;
|
// flushes at every frame-command-buffer submit (mid-frame flush,
|
||||||
|
// readback, Present), and the texture manager flushes before the
|
||||||
VkFenceCreateInfo fenceInfo{};
|
// preserve-on-recreate copy and before deferring an image the batch
|
||||||
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
// references. The frame command buffer therefore still lands behind
|
||||||
VkFence uploadFence = VK_NULL_HANDLE;
|
// the uploads on the queue, so a texture uploaded and then immediately
|
||||||
VK_VERIFY(vkCreateFence(m_device, &fenceInfo, nullptr, &uploadFence), "vkCreateFence(texture upload)");
|
// sampled in the same frame sees its data exactly as it did when each
|
||||||
|
// upload was its own submit. No fence is waited here, for the same
|
||||||
VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, uploadFence), "vkQueueSubmit(texture)");
|
// reason as before: the batch queues behind the previous frame's
|
||||||
// Do NOT wait the fence here: this submit sits behind the previous
|
// rendering, and a synchronous wait would drain the GPU; the staging
|
||||||
// frame's rendering on the queue, so a synchronous wait stalls the CPU
|
// blocks/command buffer are parked on the reclaim list at flush time
|
||||||
// until the GPU drains - a per-frame vkQueueWaitIdle for any workload
|
// and recycled once the batch fence signals.
|
||||||
// with animated textures. Ordering against the current frame's draws is
|
if (std::find(m_uploadBatchImages.begin(), m_uploadBatchImages.end(), outResource.image) ==
|
||||||
// already guaranteed (its command buffer is submitted later, at
|
m_uploadBatchImages.end()) {
|
||||||
// present), so only the transient objects need to survive execution;
|
m_uploadBatchImages.push_back(outResource.image);
|
||||||
// park them until the fence signals.
|
|
||||||
m_pendingUploadReclaims.push_back({uploadFence, commandBuffer, stagingBuffer, stagingAllocation});
|
|
||||||
ReclaimCompletedUploads();
|
|
||||||
// Backstop for pathological upload storms: bound in-flight staging
|
|
||||||
// memory by blocking on the oldest upload only once the list is deep.
|
|
||||||
constexpr SizeT kMaxPendingTextureUploads = 16;
|
|
||||||
if (m_pendingUploadReclaims.size() > kMaxPendingTextureUploads) {
|
|
||||||
VK_VERIFY(vkWaitForFences(m_device, 1, &m_pendingUploadReclaims.front().fence, VK_TRUE, UINT64_MAX),
|
|
||||||
"vkWaitForFences(texture upload backstop)");
|
|
||||||
ReclaimCompletedUploads();
|
|
||||||
}
|
}
|
||||||
|
m_uploadBatchStagingBytes += stagingSize;
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
MGLOG_D("%s: texture upload cmd failed", __func__);
|
MGLOG_D("%s: texture upload cmd failed", __func__);
|
||||||
@@ -2278,6 +2739,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
|
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
|
||||||
}
|
}
|
||||||
outResource.layout = finalLayout;
|
outResource.layout = finalLayout;
|
||||||
|
// Large batches flush right away instead of riding until the frame
|
||||||
|
// submit: a big copy amortizes its own vkQueueSubmit, submitting it
|
||||||
|
// early lets the GPU overlap the copy with the rest of the frame's
|
||||||
|
// CPU recording (measurably faster than a frame-tail burst), and the
|
||||||
|
// frame-tail burst pattern was observed to leave the GPU in a
|
||||||
|
// latency state that taxes whatever runs next. Small uploads keep
|
||||||
|
// accumulating, so a lightmap+sprite frame still costs one submit.
|
||||||
|
constexpr VkDeviceSize kEagerUploadFlushBytes = 128u * 1024u;
|
||||||
|
if (m_uploadBatchStagingBytes >= kEagerUploadFlushBytes) {
|
||||||
|
FlushPendingUploads();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,17 @@ public:
|
|||||||
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
|
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
|
||||||
// formats they will be viewed as, which is what lets a tiler keep them compressed.
|
// formats they will be viewed as, which is what lets a tiler keep them compressed.
|
||||||
Bool imageFormatListSupported = false;
|
Bool imageFormatListSupported = false;
|
||||||
|
// Union of shader stages sampled-read barriers may name on this device; the renderer
|
||||||
|
// builds it from the enabled features because geometry/tessellation stage bits are
|
||||||
|
// invalid in a barrier when their feature is off.
|
||||||
|
VkPipelineStageFlags sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
|
||||||
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||||
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||||
|
// Family of `graphicsQueue`; the manager creates its own command pool
|
||||||
|
// on it for the recycled upload-batch command buffers, so their parked
|
||||||
|
// allocations never sit in (and fragment) the renderer's shared pool
|
||||||
|
// that frame command buffers churn through every frame.
|
||||||
|
Uint32 graphicsQueueFamilyIndex = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TextureResource {
|
struct TextureResource {
|
||||||
@@ -302,6 +313,14 @@ public:
|
|||||||
Bool Initialize(const InitInfo& initInfo);
|
Bool Initialize(const InitInfo& initInfo);
|
||||||
void Shutdown();
|
void Shutdown();
|
||||||
void BeginFrame(Uint32 frameIndex);
|
void BeginFrame(Uint32 frameIndex);
|
||||||
|
// Submits the accumulated texture-upload batch (one command buffer, one
|
||||||
|
// vkQueueSubmit, one pooled fence) if any uploads are pending. MUST run
|
||||||
|
// before any other vkQueueSubmit on the shared graphics queue whose
|
||||||
|
// commands may consume an image the batch writes - the frame command
|
||||||
|
// buffer submit (mid-frame flush, readback, Present) and the
|
||||||
|
// preserve-on-recreate copy are the existing callers. No-op when the
|
||||||
|
// batch is empty.
|
||||||
|
void FlushPendingUploads();
|
||||||
// Drains every frame slot's deferred image/view releases. Only valid when
|
// Drains every frame slot's deferred image/view releases. Only valid when
|
||||||
// the caller has proven every queue submission complete; used by the
|
// the caller has proven every queue submission complete; used by the
|
||||||
// present-less frame-boundary drain.
|
// present-less frame-boundary drain.
|
||||||
@@ -350,6 +369,10 @@ public:
|
|||||||
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
|
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
|
||||||
// submit their pending recording first, so that copy cannot read pre-flush content.
|
// submit their pending recording first, so that copy cannot read pre-flush content.
|
||||||
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
|
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
|
||||||
|
// The same ordering question for the other recreate-and-preserve trigger: true when this
|
||||||
|
// texture's live image carries a shorter mip chain than a full one, so defining the missing
|
||||||
|
// levels recreates it and copies the old contents forward.
|
||||||
|
Bool NeedsMipChainGrowth(MG_State::GLState::ITextureObject& texture) const;
|
||||||
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
|
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
|
||||||
// texture as a storage image may need work that is illegal inside a render pass (resource
|
// texture as a storage image may need work that is illegal inside a render pass (resource
|
||||||
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
|
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
|
||||||
@@ -443,6 +466,9 @@ private:
|
|||||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||||
VmaAllocator m_allocator = nullptr;
|
VmaAllocator m_allocator = nullptr;
|
||||||
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
VkCommandPool m_commandPool = VK_NULL_HANDLE;
|
||||||
|
// Dedicated pool for the recycled upload-batch command buffers (see
|
||||||
|
// InitInfo::graphicsQueueFamilyIndex).
|
||||||
|
VkCommandPool m_uploadCommandPool = VK_NULL_HANDLE;
|
||||||
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
|
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
|
||||||
Bool m_imageFormatListSupported = false;
|
Bool m_imageFormatListSupported = false;
|
||||||
Uint32 m_currentFrameIndex = 0;
|
Uint32 m_currentFrameIndex = 0;
|
||||||
@@ -483,6 +509,10 @@ private:
|
|||||||
// Formats whose mutable-image probe failed on this device; their images are created
|
// Formats whose mutable-image probe failed on this device; their images are created
|
||||||
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
|
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
|
||||||
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
|
||||||
|
// Formats whose 3D images refused VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT. Per format+usage,
|
||||||
|
// exactly like the mutable-format verdict above, so it is answered at image creation and
|
||||||
|
// remembered rather than probed once globally.
|
||||||
|
std::unordered_set<VkFormat> m_2dArrayCompatibleUnsupported;
|
||||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||||
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
|
||||||
@@ -492,15 +522,58 @@ private:
|
|||||||
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
|
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
|
||||||
Vector<Vector<TextureResource>> m_deferredReleases;
|
Vector<Vector<TextureResource>> m_deferredReleases;
|
||||||
Vector<Vector<VkImageView>> m_deferredViewReleases;
|
Vector<Vector<VkImageView>> m_deferredViewReleases;
|
||||||
|
|
||||||
|
// --- Batched upload machinery ---
|
||||||
|
// Uploads within a frame are recorded into ONE shared command buffer and
|
||||||
|
// submitted with ONE vkQueueSubmit at FlushPendingUploads (the renderer
|
||||||
|
// flushes before every frame-command-buffer submit). Staging memory comes
|
||||||
|
// from a pool of persistently-mapped, reusable blocks instead of a
|
||||||
|
// vmaCreateBuffer per upload.
|
||||||
|
struct UploadStagingBlock {
|
||||||
|
VkBuffer buffer = VK_NULL_HANDLE;
|
||||||
|
VmaAllocation allocation = nullptr;
|
||||||
|
Uint8* mapped = nullptr; // persistently mapped for the block's lifetime
|
||||||
|
VkDeviceSize capacity = 0;
|
||||||
|
VkDeviceSize cursor = 0; // bump cursor while the block backs the open batch
|
||||||
|
};
|
||||||
|
// Opens the batch command buffer lazily (allocates/reuses + begins recording).
|
||||||
|
VkCommandBuffer EnsureUploadBatchOpen();
|
||||||
|
// Bump-allocates `size` staging bytes for the open batch, growing onto a
|
||||||
|
// new/pooled block when the current one cannot fit. Returns the write
|
||||||
|
// pointer; outBuffer/outBaseOffset locate the space for copy commands.
|
||||||
|
Uint8* AcquireUploadStagingSpace(VkDeviceSize size, VkBuffer& outBuffer, VkDeviceSize& outBaseOffset);
|
||||||
|
void RecycleUploadStagingBlock(UploadStagingBlock&& block);
|
||||||
|
// Drops a recorded-but-unsubmitted batch on the floor. Shutdown only: the
|
||||||
|
// device is being torn down, so the lost texel data is unobservable.
|
||||||
|
void DiscardPendingUploadBatch();
|
||||||
|
void DestroyUploadPools();
|
||||||
|
|
||||||
|
Vector<UploadStagingBlock> m_freeUploadStagingBlocks;
|
||||||
|
VkDeviceSize m_freeUploadStagingBytes = 0;
|
||||||
|
Vector<VkCommandBuffer> m_freeUploadCommandBuffers;
|
||||||
|
Vector<VkFence> m_freeUploadFences;
|
||||||
|
Bool m_uploadBatchOpen = false;
|
||||||
|
VkCommandBuffer m_uploadBatchCommandBuffer = VK_NULL_HANDLE;
|
||||||
|
// Blocks whose staging bytes the open batch's copies reference (last =
|
||||||
|
// the block the bump cursor is currently allocating from).
|
||||||
|
Vector<UploadStagingBlock> m_uploadBatchBlocks;
|
||||||
|
// Images the open batch writes; consulted for the rare re-upload-after-
|
||||||
|
// draw flush and by DeferResourceRelease (an unsubmitted command buffer
|
||||||
|
// referencing a deferred-released image would escape every fence-based
|
||||||
|
// destruction proof, so the batch is flushed before the image is parked).
|
||||||
|
Vector<VkImage> m_uploadBatchImages;
|
||||||
|
VkDeviceSize m_uploadBatchStagingBytes = 0;
|
||||||
|
|
||||||
// Texture uploads are submitted out-of-band but NOT waited on (waiting
|
// Texture uploads are submitted out-of-band but NOT waited on (waiting
|
||||||
// behind the queue serialized the CPU against the previous frame's GPU
|
// behind the queue serialized the CPU against the previous frame's GPU
|
||||||
// work every time an animated atlas re-uploaded). Their transient objects
|
// work every time an animated atlas re-uploaded). Each flushed batch's
|
||||||
// are parked here and reclaimed once the upload fence signals.
|
// transients are parked here and RECYCLED (fence reset to the fence pool,
|
||||||
|
// command buffer reset to the CB pool, staging blocks back to the block
|
||||||
|
// pool) once the batch fence signals.
|
||||||
struct PendingUploadReclaim {
|
struct PendingUploadReclaim {
|
||||||
VkFence fence = VK_NULL_HANDLE;
|
VkFence fence = VK_NULL_HANDLE;
|
||||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||||
VkBuffer stagingBuffer = VK_NULL_HANDLE;
|
Vector<UploadStagingBlock> stagingBlocks;
|
||||||
VmaAllocation stagingAllocation = nullptr;
|
|
||||||
};
|
};
|
||||||
Vector<PendingUploadReclaim> m_pendingUploadReclaims;
|
Vector<PendingUploadReclaim> m_pendingUploadReclaims;
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -181,6 +181,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
|
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
|
||||||
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||||
GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||||
|
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||||
|
GLenum buffer, GLint drawbuffer, const GLint* value);
|
||||||
|
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||||
|
GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||||
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
|
||||||
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||||
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||||
@@ -323,6 +327,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool RecreateSwapchain();
|
Bool RecreateSwapchain();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Tiered emission for an already-set-up multi-draw batch (state bound, index
|
||||||
|
// buffer bound for the indexed form). Tier 1: VK_EXT_multi_draw. Tier 2: one
|
||||||
|
// vkCmdDraw(Indexed)Indirect over a transient command array. Tier 3: unrolled
|
||||||
|
// vkCmdDraw(Indexed) loop. Tier eligibility is per-batch (uniform instance
|
||||||
|
// state for tier 1, firstInstance/feature legality for tier 2); every tier
|
||||||
|
// consumes the same param span, so contiguous-run merging done by the caller
|
||||||
|
// benefits all of them.
|
||||||
|
void EmitMultiDrawIndexed(VkCommandBuffer commandBuffer, const DrawIndexedCmdParam* pParams, Uint32 drawCount);
|
||||||
|
void EmitMultiDraw(VkCommandBuffer commandBuffer, const DrawCmdParam* pParams, Uint32 drawCount);
|
||||||
|
|
||||||
struct BlitUniformData {
|
struct BlitUniformData {
|
||||||
float srcRect[4] = {0.f, 0.f, 1.f, 1.f};
|
float srcRect[4] = {0.f, 0.f, 1.f, 1.f};
|
||||||
float dstRect[4] = {0.f, 0.f, 1.f, 1.f};
|
float dstRect[4] = {0.f, 0.f, 1.f, 1.f};
|
||||||
@@ -440,6 +454,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// above (rather than being handed one by the caller), so Shutdown() knows it
|
// above (rather than being handed one by the caller), so Shutdown() knows it
|
||||||
// owns that window and must destroy it.
|
// owns that window and must destroy it.
|
||||||
Bool m_ownsFallbackXlibWindow = false;
|
Bool m_ownsFallbackXlibWindow = false;
|
||||||
|
// Android has the same shortfall: no Mali/Adreno driver seen so far exposes
|
||||||
|
// VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an
|
||||||
|
// AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever
|
||||||
|
// displayed - the reader's images are simply never acquired. Owned here, so
|
||||||
|
// Shutdown() deletes it.
|
||||||
|
void* m_fallbackImageReader = nullptr;
|
||||||
VulkanRendererConfig m_config;
|
VulkanRendererConfig m_config;
|
||||||
Bool m_swapchainResizeRequested = false;
|
Bool m_swapchainResizeRequested = false;
|
||||||
// Presentation is suspended while the window is zero-area (minimized): the
|
// Presentation is suspended while the window is zero-area (minimized): the
|
||||||
@@ -467,6 +487,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Bool m_indexTypeUint8ExtensionEnabled = false;
|
Bool m_indexTypeUint8ExtensionEnabled = false;
|
||||||
Bool m_logicOpFeatureEnabled = false;
|
Bool m_logicOpFeatureEnabled = false;
|
||||||
Bool m_multiDrawIndirectFeatureEnabled = false;
|
Bool m_multiDrawIndirectFeatureEnabled = false;
|
||||||
|
// drawIndirectFirstInstance gates indirect commands whose firstInstance != 0;
|
||||||
|
// cached at device creation because the tier-2 multi-draw path (a transient
|
||||||
|
// VkDrawIndexedIndirectCommand array) is illegal for such a sub-draw without it.
|
||||||
|
Bool m_drawIndirectFirstInstanceFeatureEnabled = false;
|
||||||
|
// VK_EXT_multi_draw: native batched submission for the CPU-side glMultiDraw*
|
||||||
|
// families (tier 1 of the multi-draw dispatch).
|
||||||
|
Bool m_multiDrawExtensionEnabled = false;
|
||||||
|
Uint32 m_maxMultiDrawCount = 0;
|
||||||
|
// Multi-draw dispatch tiers, resolved once at device creation from device support
|
||||||
|
// clamped by MOBILEGL_MAGMA_MULTIDRAW_MODE (a preference, never a demand):
|
||||||
|
// tier 1 (ext): one vkCmdDrawMulti(Indexed)EXT - m_multiDrawAllowExt
|
||||||
|
// tier 2 (indirect): one vkCmdDraw(Indexed)Indirect batch - m_multiDrawAllowIndirect
|
||||||
|
// tier 3 (unroll): one vkCmdDraw(Indexed) per sub-draw - always available
|
||||||
|
// m_multiDrawForceUnrollIndirect additionally forces the GPU-parameter
|
||||||
|
// glMultiDraw*Indirect paths onto their per-command loop (mode=unroll only).
|
||||||
|
Bool m_multiDrawAllowExt = false;
|
||||||
|
Bool m_multiDrawAllowIndirect = false;
|
||||||
|
Bool m_multiDrawForceUnrollIndirect = false;
|
||||||
Bool m_samplerAnisotropyFeatureEnabled = false;
|
Bool m_samplerAnisotropyFeatureEnabled = false;
|
||||||
Bool m_shaderDrawParametersExtensionEnabled = false;
|
Bool m_shaderDrawParametersExtensionEnabled = false;
|
||||||
Bool m_shaderDrawParametersFeatureEnabled = false;
|
Bool m_shaderDrawParametersFeatureEnabled = false;
|
||||||
@@ -481,6 +519,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
// needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
|
||||||
Bool m_dualSrcBlendFeatureEnabled = false;
|
Bool m_dualSrcBlendFeatureEnabled = false;
|
||||||
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
|
Bool m_primitiveTopologyListRestartFeatureEnabled = false;
|
||||||
|
// Union of shader stages sampled-read barriers may name; built at device creation
|
||||||
|
// because geometry/tessellation stage bits are invalid in a barrier when their
|
||||||
|
// feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
|
||||||
|
// ALL_GRAPHICS would also serialize against non-shader stages.
|
||||||
|
VkPipelineStageFlags m_sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
|
||||||
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||||
|
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||||
// Cached at device creation from the graphics queue family properties
|
// Cached at device creation from the graphics queue family properties
|
||||||
// and device limits; drives timer-query support.
|
// and device limits; drives timer-query support.
|
||||||
Uint32 m_timestampValidBits = 0;
|
Uint32 m_timestampValidBits = 0;
|
||||||
@@ -491,23 +536,66 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
|
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
|
||||||
Uint32 stride);
|
Uint32 stride);
|
||||||
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
|
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
|
||||||
|
// VK_EXT_multi_draw entry points, loaded at device creation when the extension
|
||||||
|
// (and its multiDraw feature) is enabled; null otherwise.
|
||||||
|
static inline PFN_vkCmdDrawMultiEXT s_vkCmdDrawMultiEXT = nullptr;
|
||||||
|
static inline PFN_vkCmdDrawMultiIndexedEXT s_vkCmdDrawMultiIndexedEXT = nullptr;
|
||||||
|
|
||||||
// VK_EXT_transform_feedback (GL transform feedback capture)
|
// VK_EXT_transform_feedback (GL transform feedback capture)
|
||||||
Bool m_transformFeedbackFeatureEnabled = false;
|
Bool m_transformFeedbackFeatureEnabled = false;
|
||||||
|
// VK_EXT_provoking_vertex. Vulkan's built-in convention is "provoking vertex first"; GL's
|
||||||
|
// default is LAST_VERTEX_CONVENTION, and GL derives BOTH flat shading and the transform
|
||||||
|
// feedback vertex order from it. provokingVertexLast alone fixes flat shading and the
|
||||||
|
// input-assembler capture order and has no dependency on transform feedback; only
|
||||||
|
// transformFeedbackPreservesProvokingVertex does.
|
||||||
|
Bool m_provokingVertexLastEnabled = false;
|
||||||
|
// transformFeedbackPreservesProvokingVertex was actually enabled at device creation. Kept
|
||||||
|
// separate because it is the only thing that arms
|
||||||
|
// VUID-VkGraphicsPipelineCreateInfo-topology-04884, the rule that forbids a TRIANGLE_FAN
|
||||||
|
// pipeline from asking for LAST on a device that cannot preserve a fan's provoking vertex.
|
||||||
|
Bool m_provokingVertexXfbPreserveEnabled = false;
|
||||||
|
// provokingVertexModePerPipeline: when VK_FALSE every pipeline in one render pass instance
|
||||||
|
// must agree on the mode, so glProvokingVertex(GL_FIRST_VERTEX_CONVENTION) cannot be honoured
|
||||||
|
// per draw and every pipeline takes GL's default (LAST) instead.
|
||||||
|
Bool m_provokingVertexModePerPipeline = false;
|
||||||
|
// transformFeedbackPreservesTriangleFanProvokingVertex.
|
||||||
|
Bool m_provokingVertexFanPreserved = false;
|
||||||
|
// Per-pipeline provoking-vertex mode. capturesXfbFromGeometryStage must be a LINK-TIME
|
||||||
|
// property of the program, never the dynamic "is transform feedback active" flag: the
|
||||||
|
// 8-entry m_pipelineMemo and the SetupDrawSnapshot fast path key on programObj.hash and
|
||||||
|
// the pipeline-state value hash, neither of which moves when glBeginTransformFeedback is
|
||||||
|
// called, so a dynamic input here would hand back a stale VkPipeline.
|
||||||
|
VkProvokingVertexModeEXT SelectProvokingVertexMode(VkPrimitiveTopology topology,
|
||||||
|
Bool capturesXfbFromGeometryStage) const;
|
||||||
|
// VK_EXT_vertex_attribute_divisor: without it every non-zero glVertexAttribDivisor
|
||||||
|
// behaves as 1, because that is all Vulkan's instance input rate can express.
|
||||||
|
Bool m_vertexAttributeDivisorEnabled = false;
|
||||||
static inline PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr;
|
static inline PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr;
|
||||||
static inline PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
|
static inline PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
|
||||||
static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
|
static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
|
||||||
// Counter buffers (one 4-byte slot per capture binding) let consecutive
|
// Counter buffers (one 4-byte slot per capture binding) let consecutive
|
||||||
// draws within one glBeginTransformFeedback append GL-style.
|
// draws within one glBeginTransformFeedback append GL-style. Transform feedback
|
||||||
|
// objects can each hold an open, paused span at the same time, so the counters are
|
||||||
|
// per object: one group of four slots each, handed out on first use.
|
||||||
|
static constexpr SizeT kXfbCounterObjectSlots = 16;
|
||||||
VkBufferObject m_xfbCounterBuffer;
|
VkBufferObject m_xfbCounterBuffer;
|
||||||
// Non-zero while inside a GL Begin/End with at least one captured draw
|
UnorderedMap<Uint, Uint32> m_xfbCounterSlotByObject;
|
||||||
// recorded; selects counter-buffer resume on the next captured draw.
|
Uint32 m_xfbNextCounterSlot = 0;
|
||||||
Bool m_xfbCountersValid = false;
|
// Set for a slot once a captured draw has been recorded into its span; selects
|
||||||
Uint64 m_xfbLastSeenGeneration = 0;
|
// counter-buffer resume on the next captured draw of the same span.
|
||||||
|
Array<Bool, kXfbCounterObjectSlots> m_xfbCountersValid{};
|
||||||
|
Array<Uint64, kXfbCounterObjectSlots> m_xfbLastSeenGeneration{};
|
||||||
|
// Counter slot group of the bound transform feedback object.
|
||||||
|
Uint32 CurrentXfbCounterSlot();
|
||||||
// Wraps a recorded draw with BeginTransformFeedbackEXT/EndTransformFeedbackEXT
|
// Wraps a recorded draw with BeginTransformFeedbackEXT/EndTransformFeedbackEXT
|
||||||
// when GL transform feedback is active; binds capture buffers on demand.
|
// when GL transform feedback is active; binds capture buffers on demand.
|
||||||
Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame);
|
Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame);
|
||||||
void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began);
|
void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began);
|
||||||
|
// Makes the captured bytes visible to whatever reads them next. Deferred rather than
|
||||||
|
// recorded next to the capture, because the capturing draw runs inside a render pass
|
||||||
|
// that declares no self-dependency.
|
||||||
|
void MakeXfbWritesVisible();
|
||||||
|
Bool m_xfbWritesPendingVisibility = false;
|
||||||
// Wrap one app draw in an occlusion-query slot while a GL_SAMPLES_PASSED
|
// Wrap one app draw in an occlusion-query slot while a GL_SAMPLES_PASSED
|
||||||
// query is active. Returns whether a slot was begun (End must mirror it).
|
// query is active. Returns whether a slot was begun (End must mirror it).
|
||||||
Bool BeginOcclusionForDraw(VkCommandBuffer commandBuffer);
|
Bool BeginOcclusionForDraw(VkCommandBuffer commandBuffer);
|
||||||
@@ -567,7 +655,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 programHash = 0;
|
Uint64 programHash = 0;
|
||||||
Uint64 vertexInputHash = 0;
|
Uint64 vertexInputHash = 0;
|
||||||
Uint64 renderPassHash = 0;
|
Uint64 renderPassHash = 0;
|
||||||
Uint renderStateVersion = 0;
|
// VALUE hash of the pipeline-relevant fixed-function state (see
|
||||||
|
// ComputePipelineStateHash), not the monotonic pipeline-state version:
|
||||||
|
// the version never repeats, so a per-draw GL_BLEND toggle would miss
|
||||||
|
// all entries forever even though the state alternates between two
|
||||||
|
// values the memo already holds.
|
||||||
|
Uint64 pipelineStateHash = 0;
|
||||||
ProgramFactory::CompileOptionFlags transformFlags = {};
|
ProgramFactory::CompileOptionFlags transformFlags = {};
|
||||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||||
};
|
};
|
||||||
@@ -575,11 +668,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
|
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
|
||||||
Uint32 m_pipelineMemoCount = 0;
|
Uint32 m_pipelineMemoCount = 0;
|
||||||
Uint32 m_pipelineMemoNext = 0;
|
Uint32 m_pipelineMemoNext = 0;
|
||||||
|
// Hash of every fixed-function GL state the pipeline payload reads that the
|
||||||
|
// memo key's other fields (mode / program / vertex input / render pass /
|
||||||
|
// transform flags) do not already pin down. Equal hash under an equal rest
|
||||||
|
// of key => byte-identical PipelineCreatePayload. Cached per pipeline-state
|
||||||
|
// version: the version is monotonic and bumps on every pipeline-state
|
||||||
|
// change, so an unchanged (version, colorAttachmentCount) proves the state
|
||||||
|
// bytes are unchanged and the hash can be reused without re-reading them.
|
||||||
|
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount) const;
|
||||||
|
Uint m_pipelineStateHashVersion = 0;
|
||||||
|
Uint32 m_pipelineStateHashColorCount = 0;
|
||||||
|
Uint64 m_pipelineStateHash = 0;
|
||||||
|
Bool m_pipelineStateHashValid = false;
|
||||||
|
// GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the
|
||||||
|
// function also reads whether the bound DRAW framebuffer is the default one
|
||||||
|
// (only the default framebuffer gets the Y-flip and rotation bits - an FBO
|
||||||
|
// pass renders unflipped). Keyed on BOTH inputs; missing the FBO bit shipped
|
||||||
|
// an upside-down default-framebuffer pass after any render-to-texture
|
||||||
|
// (minecraft-1.17-main-menu retrace, whole frame flipped).
|
||||||
|
VkSurfaceTransformFlagBitsKHR m_baseTransformFlagsPreTransform =
|
||||||
|
VK_SURFACE_TRANSFORM_FLAG_BITS_MAX_ENUM_KHR;
|
||||||
|
Bool m_baseTransformFlagsIsDefaultFbo = false;
|
||||||
|
Bool m_baseTransformFlagsKeyValid = false;
|
||||||
|
Uint32 m_baseTransformFlagsCache = 0;
|
||||||
|
// isDefaultFbo must be the default-ness of the CURRENTLY bound draw framebuffer;
|
||||||
|
// every caller already has it in hand from its own guards.
|
||||||
|
Uint32 GetBaseTransformFlagsRaw(Bool isDefaultFbo);
|
||||||
// Drops every memoized pipeline handle. Required at command-buffer
|
// Drops every memoized pipeline handle. Required at command-buffer
|
||||||
// boundaries and whenever any pipeline may have been destroyed.
|
// boundaries and whenever any pipeline may have been destroyed. Also drops
|
||||||
|
// the cached pipeline-state hash: the same boundaries can retire the GL
|
||||||
|
// context whose monotonic version the cache is keyed on.
|
||||||
void InvalidatePipelineMemo() {
|
void InvalidatePipelineMemo() {
|
||||||
m_pipelineMemoCount = 0;
|
m_pipelineMemoCount = 0;
|
||||||
m_pipelineMemoNext = 0;
|
m_pipelineMemoNext = 0;
|
||||||
|
m_pipelineStateHashValid = false;
|
||||||
}
|
}
|
||||||
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
|
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
|
||||||
UniquePtr<ProgramFactory> m_programFactory;
|
UniquePtr<ProgramFactory> m_programFactory;
|
||||||
@@ -636,6 +758,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 programLifetimeId = 0;
|
Uint64 programLifetimeId = 0;
|
||||||
Uint32 programVersion = 0;
|
Uint32 programVersion = 0;
|
||||||
const void* vao = nullptr;
|
const void* vao = nullptr;
|
||||||
|
// Same rule as VaoDrawMemo::vaoLifetimeId: (address, config version) is not an
|
||||||
|
// identity, because a recycled address can arrive carrying a config version
|
||||||
|
// the dead VAO also had (two mutations to configure one attribute is the
|
||||||
|
// common shape), and "the VAO did not move" would then skip the layout
|
||||||
|
// re-resolve for a different VAO.
|
||||||
|
Uint64 vaoLifetimeId = 0;
|
||||||
Uint32 vaoConfigVersion = 0;
|
Uint32 vaoConfigVersion = 0;
|
||||||
const void* drawFbo = nullptr;
|
const void* drawFbo = nullptr;
|
||||||
Uint16 fboVersion = 0;
|
Uint16 fboVersion = 0;
|
||||||
@@ -651,14 +779,77 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
Uint64 renderbufferImageEpoch = 0;
|
Uint64 renderbufferImageEpoch = 0;
|
||||||
Uint64 sampledContentSum = 0;
|
Uint64 sampledContentSum = 0;
|
||||||
Uint64 sampledParamsSum = 0;
|
Uint64 sampledParamsSum = 0;
|
||||||
|
// Guards the sampler-descriptor reuse hint: bumped by any sampler-object
|
||||||
|
// parameter or texture shape change (see GetSamplingResolutionGeneration),
|
||||||
|
// none of which the sums above cover.
|
||||||
|
Uint64 samplingResolutionGeneration = 0;
|
||||||
|
// Render-pass flavor input (DepthTest || StencilTest at snapshot time).
|
||||||
|
// A pipeline-state change that leaves this equal cannot change which
|
||||||
|
// render pass GetOrCreateRenderPass would pick, so the fast path may
|
||||||
|
// re-resolve just the pipeline against the active pass; a change that
|
||||||
|
// flips it must fall back to the full path's pass selection.
|
||||||
|
Bool drawUsesDepthStencil = false;
|
||||||
IntVec2 renderPassExtent = {0, 0};
|
IntVec2 renderPassExtent = {0, 0};
|
||||||
|
// colorAttachmentCount of the snapshotting draw's render pass: the
|
||||||
|
// pipeline-state hash input, so the fast path can refresh that hash and
|
||||||
|
// probe the pipeline memo after a state change without re-fetching the
|
||||||
|
// render-pass entry (the pass itself is pinned by renderPassHash above).
|
||||||
|
Uint32 renderPassColorCount = 0;
|
||||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||||
|
// layoutHash of the snapshotting draw's vertex-input state. The pipeline and
|
||||||
|
// the vertex-input pre-flight depend on the VAO only through this (plus the
|
||||||
|
// program, pinned separately), so a changed VAO whose aux memo carries the
|
||||||
|
// same layoutHash re-uses the snapshot's pipeline and pre-flight verdict
|
||||||
|
// outright - the VAO-cycling case Minecraft chunk rendering hits every draw.
|
||||||
|
Uint64 vaoLayoutHash = 0;
|
||||||
|
// Memoised ProgramFactory entry of the snapshotting draw, valid while
|
||||||
|
// (programLifetimeId, programVersion, resolvedTransformFlags) match - all
|
||||||
|
// checked above - AND the factory's cache structure epoch is unchanged (the
|
||||||
|
// cache is open-addressing and holds entries by value, so any insert/erase
|
||||||
|
// moves them). The fast path must re-stamp use through StampProgramUse when
|
||||||
|
// it bypasses GetOrCreateProgram, or the idle sweep could evict a live entry.
|
||||||
|
const ProgramFactory::VkProgramObject* programObj = nullptr;
|
||||||
|
Uint64 programFactoryEpoch = 0;
|
||||||
|
// Per-entry copies of the snapshotting draw's sampled set (the scratch
|
||||||
|
// vectors below hold only the LAST full-path draw's set, which with more
|
||||||
|
// than one snapshot entry is not necessarily this entry's program).
|
||||||
|
// sampledTextures/sampledResources carry the same epoch-guarded pointer
|
||||||
|
// lifetime rules as the scratch originals: textureEraseEpoch (checked
|
||||||
|
// every probe) declines the entry before any erased resource pointer
|
||||||
|
// could be dereferenced. sampledLayouts is the layout VALUE each
|
||||||
|
// resource held when this entry's descriptors were built (the
|
||||||
|
// descriptor-reuse hint needs the SAME layout, not just a sampleable
|
||||||
|
// one), and sampledBindingRecords feeds SampledBindingsUnchanged when
|
||||||
|
// the bind generation moved.
|
||||||
|
Vector<MG_State::GLState::ITextureObject*> sampledTextures;
|
||||||
|
Vector<VkTextureManager::TextureResource*> sampledResources;
|
||||||
|
Vector<VkImageLayout> sampledLayouts;
|
||||||
|
Vector<UniformManager::SampledBindingRecord> sampledBindingRecords;
|
||||||
};
|
};
|
||||||
SetupDrawSnapshot m_setupDrawSnapshot;
|
// Program-keyed snapshot entries: program ping-pong (Sodium switches programs
|
||||||
|
// mid-frame every few draws) would otherwise evict the single snapshot on
|
||||||
|
// every switch and send every draw through the full path. Entries are found
|
||||||
|
// by programLifetimeId (MRU-first probe); every other guard stays per-probe,
|
||||||
|
// so a stale entry declines itself exactly like the old single snapshot did.
|
||||||
|
static constexpr Uint32 kSetupDrawSnapshotCount = 4;
|
||||||
|
SetupDrawSnapshot m_setupDrawSnapshots[kSetupDrawSnapshotCount];
|
||||||
|
Uint32 m_setupDrawSnapshotMru = 0; // last entry that hit or was filled
|
||||||
|
Uint32 m_setupDrawSnapshotVictim = 0; // round-robin fill cursor when all entries are live
|
||||||
|
void InvalidateSetupDrawSnapshots() {
|
||||||
|
for (auto& snapshot : m_setupDrawSnapshots) {
|
||||||
|
snapshot.valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
|
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
|
||||||
// draw call and must not allocate.
|
// draw call and must not allocate.
|
||||||
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
|
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
|
||||||
|
// Per-binding (texture, effective sampler) lifetime-id records from the same
|
||||||
|
// CollectSampledTextures walk that filled m_sampledTexturesScratch. The fast
|
||||||
|
// path shadow-compares against them (SampledBindingsUnchanged) when the
|
||||||
|
// texture bind generation moved, so a redundant glBindSampler/glBindTexture
|
||||||
|
// storm that resolves to the same bindings keeps the fast path.
|
||||||
|
Vector<UniformManager::SampledBindingRecord> m_sampledBindingRecordsScratch;
|
||||||
// Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's
|
// Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's
|
||||||
// first sampled-texture loop: the resolved backend resources, so the
|
// first sampled-texture loop: the resolved backend resources, so the
|
||||||
// post-transition loop can skip re-resolving textures whose layout is
|
// post-transition loop can skip re-resolving textures whose layout is
|
||||||
@@ -723,6 +914,138 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
UnorderedMap<ConvertedVertexStreamKey, ConvertedVertexStream, ConvertedVertexStreamKeyHash>
|
UnorderedMap<ConvertedVertexStreamKey, ConvertedVertexStream, ConvertedVertexStreamKeyHash>
|
||||||
m_convertedVertexStreams;
|
m_convertedVertexStreams;
|
||||||
|
|
||||||
|
// One VAO's resolved vkCmdBindVertexBuffers arguments, reusable by a later draw
|
||||||
|
// that would resolve them to the same thing. Consecutive draws in a chunk-renderer
|
||||||
|
// frame keep the program and the vertex layout and only swap the VAO, so a
|
||||||
|
// per-VAO memo turns the second and later draws through each VAO into a validate
|
||||||
|
// plus (usually skipped) rebind.
|
||||||
|
//
|
||||||
|
// Only whole-buffer bindings are memoised. Client-memory and format-converted
|
||||||
|
// streams re-upload from a range that depends on the draw's own vertex/index
|
||||||
|
// range, and synthetic bindings carry glVertexAttrib* values that are not part
|
||||||
|
// of any key here; a layout using any of them is never stored.
|
||||||
|
// Field order is hit-path cache locality, hot to cold: the per-draw validate
|
||||||
|
// reads the scalars and the EBO memo head, then only the first bindingCount
|
||||||
|
// elements of vkBuffers/vkOffsets; the per-binding revalidation arrays at the
|
||||||
|
// tail are touched once per frame at most.
|
||||||
|
struct ResolvedVertexBindings {
|
||||||
|
// Must equal DynamicStateShadow::kMaxShadowedVertexBindings (static_assert in
|
||||||
|
// the .cpp): past that width the bind shadow cannot skip a redundant bind
|
||||||
|
// either, so a wider layout resolves per draw. Minecraft-shaped layouts use four.
|
||||||
|
static constexpr Uint32 kMaxBindings = 8;
|
||||||
|
|
||||||
|
// Frame serial of the last completed resolve OR cross-frame revalidation.
|
||||||
|
// Zero until a resolve completes, and reset to zero before one starts, so a
|
||||||
|
// resolve that bails out midway cannot leave a half-filled entry matchable.
|
||||||
|
// Unlike the original frame-scoped memo, an entry whose buffers are all
|
||||||
|
// resident and unmapped is revalidated across frames (per-binding slice
|
||||||
|
// epoch compares) instead of re-resolved - see TryBindResolvedVertexBindings.
|
||||||
|
Uint64 frameSerial = 0;
|
||||||
|
// Identity of the resolved Vulkan layout: the VAO's content hash
|
||||||
|
// (VertexInputStateFactory::GetOrComputeHash - the same value the factory
|
||||||
|
// keys its entries on) fixes bindings.size(), each binding's base offset,
|
||||||
|
// which bindings are client/converted, and (through the mixed-in buffer
|
||||||
|
// addresses) which buffer each binding reads. Compared against the VAO's
|
||||||
|
// own hash memo on the hit path, so a hit never touches the factory entry.
|
||||||
|
VertexInputStateFactory::HashType vertexInputHash = 0;
|
||||||
|
// The program's vertex input layout: decides the synthetic-binding set and
|
||||||
|
// hence the total binding count.
|
||||||
|
Uint32 activeAttribMask = 0;
|
||||||
|
Uint32 bindingCount = 0;
|
||||||
|
// VkBufferManager::GetSliceEpochCounter() at resolve time. Still equal means
|
||||||
|
// no buffer anywhere changed its slice or was persistently mapped since, which
|
||||||
|
// settles every per-binding question below in one compare.
|
||||||
|
Uint64 sliceEpochCounter = 0;
|
||||||
|
// Any bound buffer already carrying a host map when the slice was resolved.
|
||||||
|
// Such a buffer can mutate its shadow with no API call, so it has to be
|
||||||
|
// re-pushed per draw and the one-compare path above cannot apply.
|
||||||
|
Bool anyBufferMapped = true;
|
||||||
|
|
||||||
|
// Resident element-buffer slice memo (skips the per-draw AcquireResidentSlice
|
||||||
|
// for the VAO's EBO, which cold-chases 500+ distinct resources in a
|
||||||
|
// chunk-cycling frame). Self-validating exactly like the bindings above: a hit
|
||||||
|
// requires the LIVE bound EBO pointer to equal indexBuffer AND either an
|
||||||
|
// unmoved manager-wide slice-epoch counter (nothing anywhere changed slices
|
||||||
|
// or gained a host map, the same one-compare rescue the vertex half uses) or
|
||||||
|
// that buffer's resource still carrying indexSliceEpoch (epochs are minted
|
||||||
|
// from a process-lifetime counter, so a recycled address can never
|
||||||
|
// revalidate). Restart-substituted and streamed EBOs are never stored.
|
||||||
|
// indexFrameSerial tracks the last frame the resource's GPU-use serial was
|
||||||
|
// stamped through this memo; 0 means no index memo. Independent of the
|
||||||
|
// vertex half: both are (pointer, epoch)-validated, so neither can serve
|
||||||
|
// stale state for the other.
|
||||||
|
const MG_State::GLState::BufferObject* indexBuffer = nullptr;
|
||||||
|
Uint64 indexSliceEpoch = 0;
|
||||||
|
// GetSliceEpochCounter() when the resource's epoch was last verified; only
|
||||||
|
// meaningful while indexFrameSerial matches the current frame serial.
|
||||||
|
Uint64 indexSliceEpochCounter = 0;
|
||||||
|
VkBuffer indexVkBuffer = VK_NULL_HANDLE;
|
||||||
|
VkDeviceSize indexSliceOffset = 0;
|
||||||
|
Uint64 indexFrameSerial = 0;
|
||||||
|
|
||||||
|
// Bound per draw (first bindingCount elements).
|
||||||
|
VkBuffer vkBuffers[kMaxBindings] = {};
|
||||||
|
VkDeviceSize vkOffsets[kMaxBindings] = {};
|
||||||
|
// Per binding: the VAO attribute location its buffer comes from, that buffer,
|
||||||
|
// and the buffer's VkBufferManager slice epoch when the slice was resolved.
|
||||||
|
// Only read by the per-frame revalidation and the something-moved fallback.
|
||||||
|
Uint8 attributeLocations[kMaxBindings] = {};
|
||||||
|
const MG_State::GLState::BufferObject* buffers[kMaxBindings] = {};
|
||||||
|
Uint64 sliceEpochs[kMaxBindings] = {};
|
||||||
|
};
|
||||||
|
// One direct-mapped slot of the per-VAO draw-memo table below. A slot belongs to
|
||||||
|
// the object whose (vaoKey, vaoLifetimeId) pair it carries: the address alone
|
||||||
|
// only picks the slot, and the never-reused lifetime id is what proves the slot
|
||||||
|
// is THIS VAO's, so the successor allocated onto a destroyed VAO's address
|
||||||
|
// always misses. That identity check is load-bearing and the content-hash
|
||||||
|
// validations below do NOT stand in for it - a recycled address under a
|
||||||
|
// byte-identical configuration reproduces the content hash exactly, which is
|
||||||
|
// how a destroyed VAO's resolved bindings were once handed to its successor's
|
||||||
|
// draw. The slot is still never dereferenced through vaoKey, and every fact it
|
||||||
|
// carries is still validated against live state before use:
|
||||||
|
// - layoutHash/layoutAuxMasks are valid only while contentHash equals the LIVE
|
||||||
|
// VAO's own hash memo (which the VAO's config version guards), so a config
|
||||||
|
// change or a buffer rebind misses even for the same object.
|
||||||
|
// - bindings revalidates per draw exactly as before (frame serial, content
|
||||||
|
// hash, per-binding live buffer pointers and slice epochs).
|
||||||
|
struct alignas(64) VaoDrawMemo {
|
||||||
|
const MG_State::GLState::VertexArrayObject* vaoKey = nullptr;
|
||||||
|
// The VAO's never-reused lifetime id, checked alongside vaoKey. The pointer
|
||||||
|
// ALONE is not an identity: a deleted VAO's heap address is handed straight
|
||||||
|
// back by the next glGenVertexArrays-shaped allocation, and the successor then
|
||||||
|
// matched this slot and inherited the dead object's memos. Both stated
|
||||||
|
// defences failed with it, because both reduce to the content hash and the
|
||||||
|
// content hash's buffer-identity component was itself a recycled heap address.
|
||||||
|
Uint64 vaoLifetimeId = 0;
|
||||||
|
// The VAO content hash (VertexInputStateFactory::GetOrComputeHash) the two
|
||||||
|
// layout facts below were derived from; 0 while nothing valid is stored.
|
||||||
|
Uint64 contentHash = 0;
|
||||||
|
Bool layoutFactsValid = false;
|
||||||
|
// The resolved layout identity + packed (unsupported, location) masks -
|
||||||
|
// the exact values GetBackendAuxMemo used to serve, moved here so the
|
||||||
|
// per-draw probe stays inside this table's one hot line instead of
|
||||||
|
// touching a second cold line of every cycled VAO object.
|
||||||
|
Uint64 layoutHash = 0;
|
||||||
|
Uint64 layoutAuxMasks = 0;
|
||||||
|
ResolvedVertexBindings bindings;
|
||||||
|
};
|
||||||
|
// Fixed-size, allocated on first use, never rehashed or swept: entries are
|
||||||
|
// recycled in place on slot collisions (two-slot probe, older frame serial
|
||||||
|
// evicted), and stale entries self-invalidate through the compares above. A
|
||||||
|
// fixed table also makes every VaoDrawMemo/ResolvedVertexBindings pointer
|
||||||
|
// stable for the duration of a draw, which the EBO memo handoff
|
||||||
|
// (m_currentDrawResolvedEntry) relies on.
|
||||||
|
static constexpr Uint32 kVaoDrawMemoSlotCount = 2048; // power of two
|
||||||
|
Vector<VaoDrawMemo> m_vaoDrawMemoTable;
|
||||||
|
// Finds the slot holding `vao`, or recycles the older of its two candidate
|
||||||
|
// slots into an empty memo keyed on `vao`. Never returns null.
|
||||||
|
VaoDrawMemo* LookupVaoDrawMemo(const MG_State::GLState::VertexArrayObject* vao);
|
||||||
|
// The current draw's memo entry, set by UploadAndBindVertexBuffers and consumed
|
||||||
|
// by the same draw's UploadAndBindIndexBuffer (the EBO memo lives in the same
|
||||||
|
// entry). Valid ONLY within that window: the next draw's lookup can recycle the
|
||||||
|
// slot. Null when the draw's layout is not memoisable.
|
||||||
|
ResolvedVertexBindings* m_currentDrawResolvedEntry = nullptr;
|
||||||
|
|
||||||
void CreateInstance();
|
void CreateInstance();
|
||||||
VkResult SetupDebugMessenger();
|
VkResult SetupDebugMessenger();
|
||||||
VkResult DestroyDebugMessenger();
|
VkResult DestroyDebugMessenger();
|
||||||
@@ -753,10 +1076,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
const MG_State::GLState::ProgramObject& program,
|
const MG_State::GLState::ProgramObject& program,
|
||||||
const ProgramFactory::VkProgramObject& programObj);
|
const ProgramFactory::VkProgramObject& programObj);
|
||||||
|
|
||||||
|
// The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
|
||||||
|
// bias, line width, stencil), gated behind one render-state-parameters-version
|
||||||
|
// compare per command buffer - see the gate fields in DynamicStateShadow.
|
||||||
|
void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo);
|
||||||
|
|
||||||
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||||
const ProgramFactory::VkProgramObject& programObj,
|
const ProgramFactory::VkProgramObject& programObj,
|
||||||
const DrawCmdParam& drawParams,
|
const DrawCmdParam& drawParams,
|
||||||
const IndexBufferView* pIndexBufferView);
|
const IndexBufferView* pIndexBufferView);
|
||||||
|
// Binds `entry`'s memoised buffers when every input it was resolved from is
|
||||||
|
// still live and unchanged, else returns false and leaves nothing bound.
|
||||||
|
// vaoContentHash is the VAO's memoised content hash (GetBackendHashMemo), which
|
||||||
|
// pins the layout AND the bound buffers without resolving the factory entry.
|
||||||
|
// Non-const entry: a cross-frame revalidation refreshes its serial/epoch stamps.
|
||||||
|
Bool TryBindResolvedVertexBindings(VkCommandBuffer commandBuffer,
|
||||||
|
const MG_State::GLState::VertexArrayObject& vao,
|
||||||
|
ResolvedVertexBindings& entry,
|
||||||
|
Uint64 vaoContentHash,
|
||||||
|
Uint32 activeAttribMask, Uint64 frameSerial);
|
||||||
Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
|
Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
|
||||||
const MG_State::GLState::VertexArrayObject& vao,
|
const MG_State::GLState::VertexArrayObject& vao,
|
||||||
const IndexBufferView* pIndexBufferView = nullptr);
|
const IndexBufferView* pIndexBufferView = nullptr);
|
||||||
@@ -772,6 +1110,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
|
||||||
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
|
||||||
GLenum filter);
|
GLenum filter);
|
||||||
|
// Clears one z slice of a VK_IMAGE_TYPE_3D colour image. See the call site in
|
||||||
|
// MaterializePendingClearForTexture for why a transfer clear cannot do this.
|
||||||
|
Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
|
||||||
|
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||||
|
Uint32 depthSlice, const VkClearValue& clearValue);
|
||||||
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
||||||
MG_State::GLState::ITextureObject& texture);
|
MG_State::GLState::ITextureObject& texture);
|
||||||
Bool MaterializePendingClearForRenderbuffer(
|
Bool MaterializePendingClearForRenderbuffer(
|
||||||
@@ -788,6 +1131,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkImageLayout finalLayout);
|
VkImageLayout finalLayout);
|
||||||
Bool SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame);
|
Bool SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Submits whatever is recorded and waits for it. The CPU is about to read memory
|
||||||
|
// a shader wrote (a mapped shader storage buffer), and coherent host-visible
|
||||||
|
// storage only guarantees visibility once the work that produced it has retired.
|
||||||
|
Bool FinishPendingGpuWork();
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
void ShutdownSwapchain();
|
void ShutdownSwapchain();
|
||||||
|
|
||||||
// Static functions
|
// Static functions
|
||||||
|
|||||||
@@ -41,4 +41,5 @@ add_test(NAME SanityBench COMMAND SanityBench --benchmark_counters_tabular=true)
|
|||||||
set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
|
set_tests_properties(SanityBench PROPERTIES LABELS benchmark)
|
||||||
|
|
||||||
add_subdirectory(Program)
|
add_subdirectory(Program)
|
||||||
add_subdirectory(Buffer)
|
add_subdirectory(Buffer)
|
||||||
|
add_subdirectory(Driver)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.24)
|
||||||
|
|
||||||
|
# A real, headless EGL client, deliberately NOT linked against MobileGL: it
|
||||||
|
# dlopens one EGL provider at runtime ($DRIVERBENCH_EGL_LIB - the system
|
||||||
|
# libEGL.so.1 for the native driver, or a libMobileGL.so path for either
|
||||||
|
# MobileGL backend), so the same binary measures all three stacks.
|
||||||
|
if (NOT UNIX OR APPLE OR ANDROID)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_executable(DriverBench DriverBench.c)
|
||||||
|
target_link_libraries(DriverBench PRIVATE dl)
|
||||||
|
|
||||||
|
add_test(NAME DriverBench COMMAND DriverBench draw_tiny)
|
||||||
|
set_tests_properties(DriverBench PROPERTIES LABELS benchmark)
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
/* MobileGL - MobileGL/MG_Benchmark/Driver/DriverBench.c
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* Headless, EGL-based driver benchmark shaped like Minecraft's GL usage.
|
||||||
|
* Unlike the MobileGL_s microbenches next door this exercises a full GL
|
||||||
|
* stack: it dlopens ONE EGL provider ($DRIVERBENCH_EGL_LIB - the system
|
||||||
|
* libEGL.so.1 for the native driver, or a libMobileGL.so path for either
|
||||||
|
* MobileGL backend selected with MOBILEGL_BACKEND_TYPE), creates a desktop-GL
|
||||||
|
* context on a small pbuffer, renders into its own FBO and paces frames with
|
||||||
|
* glFinish. No window system is required: the default display is tried first
|
||||||
|
* so a desktop run reaches the real driver, and a headless box (CI, a build
|
||||||
|
* server) falls back to EGL_MESA_platform_surfaceless - see
|
||||||
|
* run_driver_bench.sh.
|
||||||
|
*
|
||||||
|
* Every case models one hot pattern from captured Minecraft traces:
|
||||||
|
* draw_tiny back-to-back glDrawElements, shared state (chunk batch)
|
||||||
|
* draw_uniform per-draw vec3 offset uniform + draw (chunk sections)
|
||||||
|
* draw_multi_vao per-draw VAO/VBO switch + draw (per-section buffers)
|
||||||
|
* tex_pingpong per-draw texture bind churn on one unit
|
||||||
|
* program_pingpong alternate two programs + mat4 upload (chunk<->entity)
|
||||||
|
* chunk_upload glBufferData(NULL) orphan + glBufferSubData + draw
|
||||||
|
* atlas_sprite N 16x16 glTexSubImage2D into a 1024x512 atlas + draw
|
||||||
|
* lightmap full 16x16 lightmap respecify per frame + draw
|
||||||
|
* scene_mix composite frame built from the knobs below
|
||||||
|
*
|
||||||
|
* Output: one CSV line per case:
|
||||||
|
* case,frames,ops_per_frame,median_frame_ms,ns_per_op,fps
|
||||||
|
*/
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
/* ---- EGL constants ---- */
|
||||||
|
typedef void* EGLDisplay;
|
||||||
|
typedef void* EGLConfig;
|
||||||
|
typedef void* EGLContext;
|
||||||
|
typedef void* EGLSurface;
|
||||||
|
typedef int EGLint;
|
||||||
|
typedef unsigned int EGLBoolean;
|
||||||
|
typedef unsigned int EGLenum;
|
||||||
|
#define EGL_DEFAULT_DISPLAY ((void*)0)
|
||||||
|
#define EGL_NO_CONTEXT ((EGLContext)0)
|
||||||
|
#define EGL_NO_SURFACE ((EGLSurface)0)
|
||||||
|
#define EGL_FALSE 0
|
||||||
|
#define EGL_SURFACE_TYPE 0x3033
|
||||||
|
#define EGL_PBUFFER_BIT 0x0001
|
||||||
|
#define EGL_RENDERABLE_TYPE 0x3040
|
||||||
|
#define EGL_OPENGL_BIT 0x0008
|
||||||
|
#define EGL_RED_SIZE 0x3024
|
||||||
|
#define EGL_GREEN_SIZE 0x3023
|
||||||
|
#define EGL_BLUE_SIZE 0x3022
|
||||||
|
#define EGL_DEPTH_SIZE 0x3025
|
||||||
|
#define EGL_WIDTH 0x3057
|
||||||
|
#define EGL_HEIGHT 0x3056
|
||||||
|
#define EGL_NONE 0x3038
|
||||||
|
#define EGL_OPENGL_API 0x30A2
|
||||||
|
#define EGL_OPENGL_ES_API 0x30A0
|
||||||
|
#define EGL_OPENGL_ES3_BIT 0x0040
|
||||||
|
#define EGL_CONTEXT_CLIENT_VERSION 0x3098
|
||||||
|
#define EGL_CONTEXT_MAJOR_VERSION 0x3098
|
||||||
|
#define EGL_CONTEXT_MINOR_VERSION 0x30FB
|
||||||
|
#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD
|
||||||
|
#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001
|
||||||
|
#define EGL_PLATFORM_SURFACELESS_MESA 0x31DD
|
||||||
|
|
||||||
|
/* ---- GL constants ---- */
|
||||||
|
#define GL_COLOR_BUFFER_BIT 0x00004000
|
||||||
|
#define GL_DEPTH_BUFFER_BIT 0x00000100
|
||||||
|
#define GL_TRIANGLES 0x0004
|
||||||
|
#define GL_UNSIGNED_INT 0x1405
|
||||||
|
#define GL_SHORT 0x1402
|
||||||
|
#define GL_FLOAT 0x1406
|
||||||
|
#define GL_UNSIGNED_BYTE 0x1401
|
||||||
|
#define GL_ARRAY_BUFFER 0x8892
|
||||||
|
#define GL_ELEMENT_ARRAY_BUFFER 0x8893
|
||||||
|
#define GL_STATIC_DRAW 0x88E4
|
||||||
|
#define GL_TEXTURE_2D 0x0DE1
|
||||||
|
#define GL_TEXTURE0 0x84C0
|
||||||
|
#define GL_RGBA 0x1908
|
||||||
|
#define GL_RGBA8 0x8058
|
||||||
|
#define GL_DEPTH_COMPONENT24 0x81A6
|
||||||
|
#define GL_TEXTURE_MIN_FILTER 0x2801
|
||||||
|
#define GL_TEXTURE_MAG_FILTER 0x2800
|
||||||
|
#define GL_NEAREST 0x2600
|
||||||
|
#define GL_NEAREST_MIPMAP_LINEAR 0x2702
|
||||||
|
#define GL_DEPTH_TEST 0x0B71
|
||||||
|
#define GL_BLEND 0x0BE2
|
||||||
|
#define GL_SRC_ALPHA 0x0302
|
||||||
|
#define GL_ONE_MINUS_SRC_ALPHA 0x0303
|
||||||
|
#define GL_ONE 1
|
||||||
|
#define GL_ZERO 0
|
||||||
|
#define GL_VERTEX_SHADER 0x8B31
|
||||||
|
#define GL_FRAGMENT_SHADER 0x8B30
|
||||||
|
#define GL_COMPILE_STATUS 0x8B81
|
||||||
|
#define GL_LINK_STATUS 0x8B82
|
||||||
|
#define GL_VERSION 0x1F02
|
||||||
|
#define GL_RENDERER 0x1F01
|
||||||
|
#define GL_NO_ERROR 0
|
||||||
|
#define GL_FRAMEBUFFER 0x8D40
|
||||||
|
#define GL_RENDERBUFFER 0x8D41
|
||||||
|
#define GL_COLOR_ATTACHMENT0 0x8CE0
|
||||||
|
#define GL_DEPTH_ATTACHMENT 0x8D00
|
||||||
|
#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
|
||||||
|
#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
|
||||||
|
#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001
|
||||||
|
#define GL_UNIFORM_BUFFER 0x8A11
|
||||||
|
#define GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT 0x8A34
|
||||||
|
#define GL_DYNAMIC_DRAW 0x88E8
|
||||||
|
#define GL_STREAM_DRAW 0x88E0
|
||||||
|
#define GL_UNPACK_ALIGNMENT 0x0CF5
|
||||||
|
#define GL_UNPACK_ROW_LENGTH 0x0CF2
|
||||||
|
#define GL_UNPACK_SKIP_ROWS 0x0CF3
|
||||||
|
#define GL_UNPACK_SKIP_PIXELS 0x0CF4
|
||||||
|
#define GL_TEXTURE_WRAP_S 0x2802
|
||||||
|
#define GL_TEXTURE_WRAP_T 0x2803
|
||||||
|
#define GL_CLAMP_TO_EDGE 0x812F
|
||||||
|
#define GL_REPEAT 0x2901
|
||||||
|
|
||||||
|
typedef unsigned int GLuint;
|
||||||
|
typedef int GLint;
|
||||||
|
typedef int GLsizei;
|
||||||
|
typedef unsigned int GLenum;
|
||||||
|
typedef char GLchar;
|
||||||
|
typedef unsigned char GLboolean;
|
||||||
|
typedef long GLsizeiptr;
|
||||||
|
typedef long GLintptr;
|
||||||
|
|
||||||
|
/* ---- resolved entry points ---- */
|
||||||
|
static void* (*g_eglGetProcAddress)(const char*);
|
||||||
|
static void* g_provider;
|
||||||
|
|
||||||
|
#define GLF(ret, name, args) static ret(*name) args;
|
||||||
|
GLF(void, glClear, (unsigned))
|
||||||
|
GLF(void, glClearColor, (float, float, float, float))
|
||||||
|
GLF(void, glEnable, (GLenum))
|
||||||
|
GLF(void, glDisable, (GLenum))
|
||||||
|
GLF(void, glBlendFuncSeparate, (GLenum, GLenum, GLenum, GLenum))
|
||||||
|
GLF(void, glDrawBuffers, (GLsizei, const GLenum*))
|
||||||
|
GLF(void, glViewport, (GLint, GLint, GLsizei, GLsizei))
|
||||||
|
GLF(const unsigned char*, glGetString, (GLenum))
|
||||||
|
GLF(GLenum, glGetError, (void))
|
||||||
|
GLF(void, glFinish, (void))
|
||||||
|
GLF(void, glFlush, (void))
|
||||||
|
GLF(void, glGenBuffers, (GLsizei, GLuint*))
|
||||||
|
GLF(void, glBindBuffer, (GLenum, GLuint))
|
||||||
|
GLF(void, glBufferData, (GLenum, GLsizeiptr, const void*, GLenum))
|
||||||
|
GLF(void, glBufferSubData, (GLenum, GLintptr, GLsizeiptr, const void*))
|
||||||
|
GLF(void, glGenVertexArrays, (GLsizei, GLuint*))
|
||||||
|
GLF(void, glBindVertexArray, (GLuint))
|
||||||
|
GLF(void, glEnableVertexAttribArray, (GLuint))
|
||||||
|
GLF(void, glVertexAttribPointer, (GLuint, GLint, GLenum, GLboolean, GLsizei, const void*))
|
||||||
|
GLF(void, glGenTextures, (GLsizei, GLuint*))
|
||||||
|
GLF(void, glBindTexture, (GLenum, GLuint))
|
||||||
|
GLF(void, glActiveTexture, (GLenum))
|
||||||
|
GLF(void, glTexImage2D, (GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void*))
|
||||||
|
GLF(void, glTexSubImage2D, (GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const void*))
|
||||||
|
GLF(void, glTexParameteri, (GLenum, GLenum, GLint))
|
||||||
|
GLF(void, glPixelStorei, (GLenum, GLint))
|
||||||
|
GLF(void, glGetIntegerv, (GLenum, GLint*))
|
||||||
|
GLF(void, glGenerateMipmap, (GLenum))
|
||||||
|
GLF(GLuint, glCreateShader, (GLenum))
|
||||||
|
GLF(void, glShaderSource, (GLuint, GLsizei, const GLchar* const*, const GLint*))
|
||||||
|
GLF(void, glCompileShader, (GLuint))
|
||||||
|
GLF(void, glGetShaderiv, (GLuint, GLenum, GLint*))
|
||||||
|
GLF(void, glGetShaderInfoLog, (GLuint, GLsizei, GLsizei*, GLchar*))
|
||||||
|
GLF(GLuint, glCreateProgram, (void))
|
||||||
|
GLF(void, glAttachShader, (GLuint, GLuint))
|
||||||
|
GLF(void, glLinkProgram, (GLuint))
|
||||||
|
GLF(void, glGetProgramiv, (GLuint, GLenum, GLint*))
|
||||||
|
GLF(void, glUseProgram, (GLuint))
|
||||||
|
GLF(GLint, glGetUniformLocation, (GLuint, const GLchar*))
|
||||||
|
GLF(void, glUniform1i, (GLint, GLint))
|
||||||
|
GLF(void, glUniform3f, (GLint, float, float, float))
|
||||||
|
GLF(void, glUniformMatrix4fv, (GLint, GLsizei, GLboolean, const float*))
|
||||||
|
GLF(void, glDrawElements, (GLenum, GLsizei, GLenum, const void*))
|
||||||
|
GLF(void, glBindAttribLocation, (GLuint, GLuint, const GLchar*))
|
||||||
|
GLF(void, glUniform3fv, (GLint, GLsizei, const float*))
|
||||||
|
GLF(void, glDrawArrays, (GLenum, GLint, GLsizei))
|
||||||
|
GLF(void, glDrawElementsBaseVertex, (GLenum, GLsizei, GLenum, const void*, GLint))
|
||||||
|
GLF(void, glMultiDrawElementsBaseVertex,
|
||||||
|
(GLenum, const GLsizei*, GLenum, const void* const*, GLsizei, const GLint*))
|
||||||
|
GLF(void, glBindBufferRange, (GLenum, GLuint, GLuint, GLintptr, GLsizeiptr))
|
||||||
|
GLF(void, glBindBufferBase, (GLenum, GLuint, GLuint))
|
||||||
|
GLF(GLuint, glGetUniformBlockIndex, (GLuint, const GLchar*))
|
||||||
|
GLF(void, glUniformBlockBinding, (GLuint, GLuint, GLuint))
|
||||||
|
GLF(void, glGenSamplers, (GLsizei, GLuint*))
|
||||||
|
GLF(void, glBindSampler, (GLuint, GLuint))
|
||||||
|
GLF(void, glSamplerParameteri, (GLuint, GLenum, GLint))
|
||||||
|
GLF(void, glGenFramebuffers, (GLsizei, GLuint*))
|
||||||
|
GLF(void, glBindFramebuffer, (GLenum, GLuint))
|
||||||
|
GLF(void, glGenRenderbuffers, (GLsizei, GLuint*))
|
||||||
|
GLF(void, glBindRenderbuffer, (GLenum, GLuint))
|
||||||
|
GLF(void, glRenderbufferStorage, (GLenum, GLenum, GLsizei, GLsizei))
|
||||||
|
GLF(void, glFramebufferRenderbuffer, (GLenum, GLenum, GLenum, GLuint))
|
||||||
|
GLF(GLenum, glCheckFramebufferStatus, (GLenum))
|
||||||
|
GLF(void*, glFenceSync, (GLenum, unsigned))
|
||||||
|
GLF(GLenum, glClientWaitSync, (void*, unsigned, unsigned long long))
|
||||||
|
GLF(void, glDeleteSync, (void*))
|
||||||
|
|
||||||
|
static uint64_t now_ns(void) {
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmp_u64(const void* a, const void* b) {
|
||||||
|
uint64_t x = *(const uint64_t*)a, y = *(const uint64_t*)b;
|
||||||
|
return x < y ? -1 : x > y;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Scene, cases and the case table live next door so the Android plugin's
|
||||||
|
* in-process benchmark runs byte-identical bodies. */
|
||||||
|
static void bench_gl_failed(const char* what, const char* detail) {
|
||||||
|
fprintf(stderr, "FAIL: %s %s\n", what, detail ? detail : "");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GLES has glDrawElementsBaseVertex (3.2 core) but no multi-draw form of it, so
|
||||||
|
* against a native mobile driver the multi-draw case issues the same sub-draws
|
||||||
|
* one at a time - which is what the extension folds up, and what an application
|
||||||
|
* without it would have to write. Desktop GL and MobileGL take the real call. */
|
||||||
|
static void bench_multi_draw_elements_base_vertex(GLenum mode, const GLsizei* counts, GLenum type,
|
||||||
|
const void* const* offsets, GLsizei drawCount,
|
||||||
|
const GLint* baseVertices) {
|
||||||
|
if (glMultiDrawElementsBaseVertex) {
|
||||||
|
glMultiDrawElementsBaseVertex(mode, counts, type, offsets, drawCount, baseVertices);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (GLsizei i = 0; i < drawCount; ++i) {
|
||||||
|
glDrawElementsBaseVertex(mode, counts[i], type, offsets[i], baseVertices[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "DriverBenchCases.inc"
|
||||||
|
|
||||||
|
/* ---- bench driver: fence-paced frames on the offscreen FBO ----------------
|
||||||
|
* Frames are closed with a real fence wait, not glFinish: MobileGL implements
|
||||||
|
* glFinish and glFlush as no-ops (MG_Impl/GLImpl/Exporting/Definitions.cpp),
|
||||||
|
* so a glFinish-paced loop would time only the CPU-side submit on a MobileGL
|
||||||
|
* backend while timing submit-plus-GPU on the native driver - the two numbers
|
||||||
|
* would not describe the same work. A sync object is honoured by every stack
|
||||||
|
* measured here.
|
||||||
|
*/
|
||||||
|
typedef void (*case_fn)(int frame, long a, long b);
|
||||||
|
static int g_warmup = 30, g_frames = 120;
|
||||||
|
|
||||||
|
static void end_frame_wait(void) {
|
||||||
|
if (glFenceSync && glClientWaitSync && glDeleteSync) {
|
||||||
|
void* sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||||
|
if (sync) {
|
||||||
|
glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000ull);
|
||||||
|
glDeleteSync(sync);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glFinish();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void run_case(const char* name, case_fn body, long a, long b, long opsPerFrame) {
|
||||||
|
static uint64_t samples[4096];
|
||||||
|
if (g_frames > 4096) g_frames = 4096;
|
||||||
|
end_frame_wait();
|
||||||
|
for (int i = 0; i < g_warmup; ++i) {
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
body(i, a, b);
|
||||||
|
end_frame_wait();
|
||||||
|
}
|
||||||
|
for (int i = 0; i < g_frames; ++i) {
|
||||||
|
uint64_t t0 = now_ns();
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
body(i, a, b);
|
||||||
|
end_frame_wait();
|
||||||
|
samples[i] = now_ns() - t0;
|
||||||
|
}
|
||||||
|
qsort(samples, g_frames, sizeof(uint64_t), cmp_u64);
|
||||||
|
uint64_t med = samples[g_frames / 2];
|
||||||
|
double frameMs = med / 1e6;
|
||||||
|
double nsPerOp = opsPerFrame > 0 ? (double)med / (double)opsPerFrame : 0.0;
|
||||||
|
printf("%s,%d,%ld,%.3f,%.1f,%.1f\n", name, g_frames, opsPerFrame, frameMs, nsPerOp,
|
||||||
|
1e9 / (double)med);
|
||||||
|
fflush(stdout);
|
||||||
|
if (glGetError() != GL_NO_ERROR) fprintf(stderr, "WARN: GL error after %s\n", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A display that needs no window system. eglGetPlatformDisplay is EGL 1.5
|
||||||
|
* core and eglGetPlatformDisplayEXT is the EGL_EXT_platform_base spelling
|
||||||
|
* older loaders ship; both are client entry points, so they resolve before
|
||||||
|
* any display exists. Only the attribute-list types differ between the two
|
||||||
|
* and this passes none, so one cast covers both. */
|
||||||
|
static EGLDisplay surfaceless_display(void) {
|
||||||
|
void* fn = dlsym(g_provider, "eglGetPlatformDisplay");
|
||||||
|
if (!fn) fn = g_eglGetProcAddress("eglGetPlatformDisplay");
|
||||||
|
if (!fn) fn = dlsym(g_provider, "eglGetPlatformDisplayEXT");
|
||||||
|
if (!fn) fn = g_eglGetProcAddress("eglGetPlatformDisplayEXT");
|
||||||
|
if (!fn) return NULL;
|
||||||
|
return ((EGLDisplay(*)(EGLenum, void*, const void*))fn)(EGL_PLATFORM_SURFACELESS_MESA,
|
||||||
|
EGL_DEFAULT_DISPLAY, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- EGL bootstrap: one provider library, pbuffer, desktop-GL context ---- */
|
||||||
|
static int boot_egl(void) {
|
||||||
|
const char* libpath = getenv("DRIVERBENCH_EGL_LIB");
|
||||||
|
if (!libpath) libpath = "libEGL.so.1";
|
||||||
|
g_provider = dlopen(libpath, RTLD_LAZY | RTLD_LOCAL);
|
||||||
|
if (!g_provider) {
|
||||||
|
fprintf(stderr, "FAIL: dlopen %s: %s\n", libpath, dlerror());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
#define ESYM(name) \
|
||||||
|
void* p_##name = dlsym(g_provider, #name); \
|
||||||
|
if (!p_##name) { fprintf(stderr, "FAIL: dlsym %s\n", #name); return 1; }
|
||||||
|
ESYM(eglGetDisplay)
|
||||||
|
ESYM(eglInitialize)
|
||||||
|
ESYM(eglChooseConfig)
|
||||||
|
ESYM(eglBindAPI)
|
||||||
|
ESYM(eglCreateContext)
|
||||||
|
ESYM(eglCreatePbufferSurface)
|
||||||
|
ESYM(eglMakeCurrent)
|
||||||
|
ESYM(eglGetProcAddress)
|
||||||
|
ESYM(eglGetError)
|
||||||
|
g_eglGetProcAddress = (void* (*)(const char*))p_eglGetProcAddress;
|
||||||
|
|
||||||
|
EGLint (*getError)(void) = (EGLint(*)(void))p_eglGetError;
|
||||||
|
EGLBoolean (*initialize)(EGLDisplay, EGLint*, EGLint*) =
|
||||||
|
(EGLBoolean(*)(EGLDisplay, EGLint*, EGLint*))p_eglInitialize;
|
||||||
|
|
||||||
|
/* The default display first: it is the one a windowed app would get, and
|
||||||
|
* on a desktop it is the one that reaches the real GPU - which is the
|
||||||
|
* driver this bench exists to measure. It does need a window system,
|
||||||
|
* though; Mesa's default platform is X11, so with no $DISPLAY (CI, a
|
||||||
|
* build server, ssh without forwarding) eglInitialize fails. Fall back to
|
||||||
|
* EGL_MESA_platform_surfaceless rather than give up: every case draws into
|
||||||
|
* the FBO built by build_resources(), so no window is needed for any of
|
||||||
|
* the work being timed. */
|
||||||
|
EGLint maj = 0, min = 0;
|
||||||
|
const char* how = "default display";
|
||||||
|
EGLDisplay dpy = ((EGLDisplay(*)(void*))p_eglGetDisplay)(EGL_DEFAULT_DISPLAY);
|
||||||
|
if (!dpy || !initialize(dpy, &maj, &min)) {
|
||||||
|
dpy = surfaceless_display();
|
||||||
|
how = "surfaceless display";
|
||||||
|
if (!dpy || !initialize(dpy, &maj, &min)) {
|
||||||
|
fprintf(stderr, "FAIL: eglInitialize (0x%x)\n", getError());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fprintf(stderr, "EGL %d.%d via %s (%s)\n", maj, min, libpath, how);
|
||||||
|
|
||||||
|
// Desktop GL first (that is what MobileGL exposes and what the cases are
|
||||||
|
// written against), GLES 3 second so the same binary can measure a device's
|
||||||
|
// native driver as the baseline. The .inc picks ESSL shader sources when the
|
||||||
|
// context turns out to be ES.
|
||||||
|
EGLBoolean (*chooseConfig)(EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*) =
|
||||||
|
(EGLBoolean(*)(EGLDisplay, const EGLint*, EGLConfig*, EGLint, EGLint*))p_eglChooseConfig;
|
||||||
|
EGLContext (*createContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint*) =
|
||||||
|
(EGLContext(*)(EGLDisplay, EGLConfig, EGLContext, const EGLint*))p_eglCreateContext;
|
||||||
|
EGLBoolean (*bindApi)(EGLenum) = (EGLBoolean(*)(EGLenum))p_eglBindAPI;
|
||||||
|
|
||||||
|
EGLConfig cfg = NULL;
|
||||||
|
EGLint ncfg = 0;
|
||||||
|
EGLContext ctx = EGL_NO_CONTEXT;
|
||||||
|
|
||||||
|
if (bindApi(EGL_OPENGL_API)) {
|
||||||
|
const EGLint cfgAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8,
|
||||||
|
EGL_DEPTH_SIZE, 24, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE};
|
||||||
|
if (chooseConfig(dpy, cfgAttribs, &cfg, 1, &ncfg) && ncfg >= 1) {
|
||||||
|
const EGLint ctxAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 2,
|
||||||
|
EGL_CONTEXT_OPENGL_PROFILE_MASK,
|
||||||
|
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE};
|
||||||
|
ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, ctxAttribs);
|
||||||
|
if (ctx == EGL_NO_CONTEXT) ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctx == EGL_NO_CONTEXT) {
|
||||||
|
if (!bindApi(EGL_OPENGL_ES_API)) {
|
||||||
|
fprintf(stderr, "FAIL: neither OpenGL nor OpenGL ES is bindable on this provider\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const EGLint esCfgAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RED_SIZE, 8,
|
||||||
|
EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_DEPTH_SIZE, 24,
|
||||||
|
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_NONE};
|
||||||
|
ncfg = 0;
|
||||||
|
if (!chooseConfig(dpy, esCfgAttribs, &cfg, 1, &ncfg) || ncfg < 1) {
|
||||||
|
// EGL_SURFACE_TYPE 0 matches any config: a stack that offers no
|
||||||
|
// pbuffer at all is still usable through the surfaceless context
|
||||||
|
// path below.
|
||||||
|
const EGLint relaxed[] = {EGL_SURFACE_TYPE, 0, EGL_RED_SIZE, 8, EGL_NONE};
|
||||||
|
if (!chooseConfig(dpy, relaxed, &cfg, 1, &ncfg) || ncfg < 1) {
|
||||||
|
fprintf(stderr, "FAIL: eglChooseConfig\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const EGLint esCtxAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
|
||||||
|
ctx = createContext(dpy, cfg, EGL_NO_CONTEXT, esCtxAttribs);
|
||||||
|
}
|
||||||
|
if (ctx == EGL_NO_CONTEXT) {
|
||||||
|
fprintf(stderr, "FAIL: eglCreateContext (0x%x)\n", getError());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The pbuffer only exists to have something to make current - nothing is
|
||||||
|
* ever drawn to it. Where there is no pbuffer config, EGL_NO_SURFACE is
|
||||||
|
* exactly what EGL_KHR_surfaceless_context takes, so the same call covers
|
||||||
|
* both. */
|
||||||
|
const EGLint pbAttribs[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE};
|
||||||
|
EGLSurface surf = ((EGLSurface(*)(EGLDisplay, EGLConfig, const EGLint*))p_eglCreatePbufferSurface)(
|
||||||
|
dpy, cfg, pbAttribs);
|
||||||
|
if (surf == EGL_NO_SURFACE)
|
||||||
|
fprintf(stderr, "no pbuffer (0x%x), using a surfaceless context\n", getError());
|
||||||
|
if (!((EGLBoolean(*)(EGLDisplay, EGLSurface, EGLSurface, EGLContext))p_eglMakeCurrent)(dpy, surf,
|
||||||
|
surf, ctx)) {
|
||||||
|
fprintf(stderr, "FAIL: eglMakeCurrent (0x%x)\n", getError());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Core GL entry points: eglGetProcAddress first (EGL 1.5 serves core
|
||||||
|
* functions), provider dlsym as fallback (both glvnd and MobileGL export
|
||||||
|
* the gl* symbols directly). */
|
||||||
|
#define RESOLVE(name) \
|
||||||
|
do { \
|
||||||
|
*(void**)&name = g_eglGetProcAddress(#name); \
|
||||||
|
if (!name) *(void**)&name = dlsym(g_provider, #name); \
|
||||||
|
if (!name) { fprintf(stderr, "FAIL: resolve %s\n", #name); return 1; } \
|
||||||
|
} while (0)
|
||||||
|
RESOLVE(glClear); RESOLVE(glClearColor); RESOLVE(glEnable); RESOLVE(glViewport);
|
||||||
|
RESOLVE(glDisable); RESOLVE(glBlendFuncSeparate); RESOLVE(glDrawBuffers);
|
||||||
|
RESOLVE(glGetString); RESOLVE(glGetError); RESOLVE(glFinish); RESOLVE(glFlush);
|
||||||
|
RESOLVE(glGenBuffers); RESOLVE(glBindBuffer); RESOLVE(glBufferData); RESOLVE(glBufferSubData);
|
||||||
|
RESOLVE(glGenVertexArrays); RESOLVE(glBindVertexArray); RESOLVE(glEnableVertexAttribArray);
|
||||||
|
RESOLVE(glVertexAttribPointer); RESOLVE(glGenTextures); RESOLVE(glBindTexture);
|
||||||
|
RESOLVE(glActiveTexture); RESOLVE(glTexImage2D); RESOLVE(glTexSubImage2D);
|
||||||
|
RESOLVE(glTexParameteri); RESOLVE(glGenerateMipmap); RESOLVE(glCreateShader);
|
||||||
|
RESOLVE(glPixelStorei); RESOLVE(glGetIntegerv);
|
||||||
|
RESOLVE(glShaderSource); RESOLVE(glCompileShader); RESOLVE(glGetShaderiv);
|
||||||
|
RESOLVE(glGetShaderInfoLog); RESOLVE(glCreateProgram); RESOLVE(glAttachShader);
|
||||||
|
RESOLVE(glLinkProgram); RESOLVE(glGetProgramiv); RESOLVE(glUseProgram);
|
||||||
|
RESOLVE(glGetUniformLocation); RESOLVE(glUniform1i); RESOLVE(glUniform3f);
|
||||||
|
RESOLVE(glUniformMatrix4fv); RESOLVE(glDrawElements); RESOLVE(glBindAttribLocation);
|
||||||
|
RESOLVE(glUniform3fv); RESOLVE(glDrawArrays); RESOLVE(glDrawElementsBaseVertex);
|
||||||
|
RESOLVE(glBindBufferRange); RESOLVE(glBindBufferBase);
|
||||||
|
RESOLVE(glGetUniformBlockIndex); RESOLVE(glUniformBlockBinding);
|
||||||
|
RESOLVE(glGenSamplers); RESOLVE(glBindSampler); RESOLVE(glSamplerParameteri);
|
||||||
|
RESOLVE(glGenFramebuffers); RESOLVE(glBindFramebuffer); RESOLVE(glGenRenderbuffers);
|
||||||
|
RESOLVE(glBindRenderbuffer); RESOLVE(glRenderbufferStorage); RESOLVE(glFramebufferRenderbuffer);
|
||||||
|
RESOLVE(glCheckFramebufferStatus);
|
||||||
|
// Optional: end_frame_wait() falls back to glFinish when a stack has no
|
||||||
|
// sync objects, so resolve without failing the run.
|
||||||
|
*(void**)&glFenceSync = g_eglGetProcAddress("glFenceSync");
|
||||||
|
if (!glFenceSync) *(void**)&glFenceSync = dlsym(g_provider, "glFenceSync");
|
||||||
|
*(void**)&glClientWaitSync = g_eglGetProcAddress("glClientWaitSync");
|
||||||
|
if (!glClientWaitSync) *(void**)&glClientWaitSync = dlsym(g_provider, "glClientWaitSync");
|
||||||
|
*(void**)&glDeleteSync = g_eglGetProcAddress("glDeleteSync");
|
||||||
|
if (!glDeleteSync) *(void**)&glDeleteSync = dlsym(g_provider, "glDeleteSync");
|
||||||
|
// Desktop-only: GLES 3.2 has DrawElementsBaseVertex but no multi-draw form,
|
||||||
|
// so bench_multi_draw_elements_base_vertex() emulates it when this is null.
|
||||||
|
*(void**)&glMultiDrawElementsBaseVertex = g_eglGetProcAddress("glMultiDrawElementsBaseVertex");
|
||||||
|
if (!glMultiDrawElementsBaseVertex)
|
||||||
|
*(void**)&glMultiDrawElementsBaseVertex = dlsym(g_provider, "glMultiDrawElementsBaseVertex");
|
||||||
|
|
||||||
|
fprintf(stderr, "renderer: %s\n", glGetString(GL_RENDERER));
|
||||||
|
fprintf(stderr, "version: %s\n", glGetString(GL_VERSION));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
long draws = 2048;
|
||||||
|
if (getenv("DRIVERBENCH_DRAWS")) draws = atol(getenv("DRIVERBENCH_DRAWS"));
|
||||||
|
if (getenv("DRIVERBENCH_FRAMES")) g_frames = atoi(getenv("DRIVERBENCH_FRAMES"));
|
||||||
|
if (getenv("DRIVERBENCH_SPRITES")) g_mixSprites = atol(getenv("DRIVERBENCH_SPRITES"));
|
||||||
|
|
||||||
|
if (boot_egl()) return 1;
|
||||||
|
build_resources();
|
||||||
|
|
||||||
|
printf("case,frames,ops_per_frame,median_frame_ms,ns_per_op,fps\n");
|
||||||
|
for (int i = 0; i < kBenchCaseCount; ++i) {
|
||||||
|
const BenchCaseDesc* c = &kBenchCases[i];
|
||||||
|
if (argc > 1) {
|
||||||
|
int wanted = 0;
|
||||||
|
for (int j = 1; j < argc; ++j)
|
||||||
|
if (strcmp(argv[j], c->name) == 0) wanted = 1;
|
||||||
|
if (!wanted) continue;
|
||||||
|
}
|
||||||
|
// The generic cases scale with DRIVERBENCH_DRAWS; the mc_* rates are
|
||||||
|
// measured and must not move, or the numbers stop being comparable.
|
||||||
|
long a = c->a, ops = c->opsPerFrame;
|
||||||
|
if (strncmp(c->name, "mc_", 3) != 0 && a > 100) {
|
||||||
|
a = draws * a / 2048;
|
||||||
|
ops = c->opsPerFrame * draws / 2048;
|
||||||
|
}
|
||||||
|
run_case(c->name, c->fn, a, c->b, ops);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,640 @@
|
|||||||
|
/* MobileGL - MobileGL/MG_Benchmark/Driver/DriverBenchCases.inc
|
||||||
|
* Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
* Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
* https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
* https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
* SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
* End of Source File Header
|
||||||
|
*
|
||||||
|
* The benchmark scene and its cases, with no harness and no GL loader: the
|
||||||
|
* includer supplies both. DriverBench.c drives it through function pointers
|
||||||
|
* resolved from one EGL provider; MG_Util/SelfTest/DriverBenchJni.cpp drives
|
||||||
|
* it through MobileGL's own frontend entry points inside the Android plugin.
|
||||||
|
* Sharing the bodies is the point - a number from the phone and a number from
|
||||||
|
* the desktop have to describe the same work.
|
||||||
|
*
|
||||||
|
* The includer must have declared, before including this file: the GL types
|
||||||
|
* and enums used below, and callable gl* entry points with the standard
|
||||||
|
* signatures. bench_gl_failed() is called (and must be defined) when shader
|
||||||
|
* compilation or linking fails, so a caller can report the failure instead of
|
||||||
|
* dying inside a benchmark.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ---- shared scene resources (Minecraft-shaped) ---- */
|
||||||
|
#define MAX_SECTIONS 512
|
||||||
|
static GLuint g_progChunk, g_progEntity;
|
||||||
|
static GLint g_uOffsetChunk, g_uMvpChunk, g_uMvpEntity;
|
||||||
|
static GLuint g_vao[MAX_SECTIONS], g_vbo[MAX_SECTIONS];
|
||||||
|
static GLuint g_sharedIbo;
|
||||||
|
static GLuint g_texAtlas, g_texLight, g_texEntity;
|
||||||
|
static int g_quadsPerSection = 128; /* 128 quads = 512 verts, 768 indices */
|
||||||
|
static unsigned char* g_scratch;
|
||||||
|
/* Uniform ring + sampler for the 26.2-shaped cases (see the case block below). */
|
||||||
|
static GLuint g_uboRing;
|
||||||
|
static GLint g_uboAlign = 256;
|
||||||
|
static size_t g_uboSlot = 256;
|
||||||
|
static GLuint g_sampler;
|
||||||
|
/* Two small offscreen targets for the 26.2-style render-pass churn case. */
|
||||||
|
static GLuint g_passFbo[2];
|
||||||
|
static GLuint g_passColor[2];
|
||||||
|
static float g_mvp[16] = {0.002f, 0, 0, 0, 0, 0.002f, 0, 0, 0, 0, -0.001f, 0, -1.f, -1.f, 0.f, 1.f};
|
||||||
|
|
||||||
|
/* Minecraft chunk vertex: pos 3f, color 4ub, uv 2f, packed light 2s -> 32 B */
|
||||||
|
#define VERT_STRIDE 32
|
||||||
|
static void fill_section_vertices(unsigned char* dst, int quads, unsigned seed) {
|
||||||
|
for (int q = 0; q < quads * 4; ++q) {
|
||||||
|
float* f = (float*)(dst + q * VERT_STRIDE);
|
||||||
|
unsigned r = seed = seed * 1664525u + 1013904223u;
|
||||||
|
f[0] = (float)(q & 31) * 8.0f + (float)(r & 7);
|
||||||
|
f[1] = (float)((q >> 5) & 31) * 8.0f;
|
||||||
|
f[2] = (float)(q % 7) * 0.1f;
|
||||||
|
dst[q * VERT_STRIDE + 12] = (unsigned char)r;
|
||||||
|
dst[q * VERT_STRIDE + 13] = (unsigned char)(r >> 8);
|
||||||
|
dst[q * VERT_STRIDE + 14] = (unsigned char)(r >> 16);
|
||||||
|
dst[q * VERT_STRIDE + 15] = 255;
|
||||||
|
f[4] = (float)(r & 1023) / 1024.0f;
|
||||||
|
f[5] = (float)((r >> 10) & 511) / 512.0f;
|
||||||
|
((short*)(dst + q * VERT_STRIDE + 24))[0] = 15 << 4;
|
||||||
|
((short*)(dst + q * VERT_STRIDE + 24))[1] = 15 << 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static GLuint make_shader(GLenum kind, const char* src) {
|
||||||
|
GLuint sh = glCreateShader(kind);
|
||||||
|
glShaderSource(sh, 1, &src, NULL);
|
||||||
|
glCompileShader(sh);
|
||||||
|
GLint ok = 0;
|
||||||
|
glGetShaderiv(sh, GL_COMPILE_STATUS, &ok);
|
||||||
|
if (!ok) {
|
||||||
|
char log[1024];
|
||||||
|
glGetShaderInfoLog(sh, sizeof log, NULL, log);
|
||||||
|
bench_gl_failed("shader compile", log);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return sh;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GLuint make_program(const char* vs_src, const char* fs_src) {
|
||||||
|
GLuint prog = glCreateProgram();
|
||||||
|
glAttachShader(prog, make_shader(GL_VERTEX_SHADER, vs_src));
|
||||||
|
glAttachShader(prog, make_shader(GL_FRAGMENT_SHADER, fs_src));
|
||||||
|
glBindAttribLocation(prog, 0, "aPos");
|
||||||
|
glBindAttribLocation(prog, 1, "aColor");
|
||||||
|
glBindAttribLocation(prog, 2, "aUv");
|
||||||
|
glBindAttribLocation(prog, 3, "aLight");
|
||||||
|
glLinkProgram(prog);
|
||||||
|
GLint ok = 0;
|
||||||
|
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
|
||||||
|
if (!ok) {
|
||||||
|
bench_gl_failed("program link", "");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prog;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* kChunkVs =
|
||||||
|
"#version 150 core\n"
|
||||||
|
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
|
||||||
|
"uniform mat4 uMvp; uniform vec3 uOffset;\n"
|
||||||
|
"out vec4 vColor; out vec2 vUv; out vec2 vLight;\n"
|
||||||
|
"void main(){ gl_Position = uMvp * vec4(aPos + uOffset, 1.0);\n"
|
||||||
|
" vColor = aColor; vUv = aUv; vLight = aLight * (1.0/256.0); }\n";
|
||||||
|
static const char* kChunkFs =
|
||||||
|
"#version 150 core\n"
|
||||||
|
"in vec4 vColor; in vec2 vUv; in vec2 vLight; out vec4 o;\n"
|
||||||
|
"uniform sampler2D uAtlas; uniform sampler2D uLight;\n"
|
||||||
|
"void main(){ o = texture(uAtlas, vUv) * vColor * texture(uLight, vLight); }\n";
|
||||||
|
static const char* kEntityVs =
|
||||||
|
"#version 150 core\n"
|
||||||
|
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
|
||||||
|
"uniform mat4 uMvp; uniform mat4 uModel;\n"
|
||||||
|
"out vec4 vColor; out vec2 vUv;\n"
|
||||||
|
"void main(){ gl_Position = uMvp * uModel * vec4(aPos, 1.0); vColor = aColor; vUv = aUv; }\n";
|
||||||
|
static const char* kEntityFs =
|
||||||
|
"#version 150 core\n"
|
||||||
|
"in vec4 vColor; in vec2 vUv; out vec4 o; uniform sampler2D uTex;\n"
|
||||||
|
"void main(){ o = texture(uTex, vUv) * vColor; }\n";
|
||||||
|
|
||||||
|
// ESSL 3.20 twins of the four shaders above. The bodies are identical; only the
|
||||||
|
// version line and the precision qualifiers differ, so the two paths compile the
|
||||||
|
// same work. Needed because this bench also runs against a device's native GLES
|
||||||
|
// driver as the baseline MobileGL is measured against, and that driver rejects
|
||||||
|
// desktop GLSL - while MobileGL is fed desktop GLSL on purpose, since translating
|
||||||
|
// it is the thing under test.
|
||||||
|
static const char* kChunkVsEs =
|
||||||
|
"#version 320 es\n"
|
||||||
|
"precision highp float;\n"
|
||||||
|
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
|
||||||
|
"uniform mat4 uMvp; uniform vec3 uOffset;\n"
|
||||||
|
"out vec4 vColor; out vec2 vUv; out vec2 vLight;\n"
|
||||||
|
"void main(){ gl_Position = uMvp * vec4(aPos + uOffset, 1.0);\n"
|
||||||
|
" vColor = aColor; vUv = aUv; vLight = aLight * (1.0/256.0); }\n";
|
||||||
|
static const char* kChunkFsEs =
|
||||||
|
"#version 320 es\n"
|
||||||
|
"precision mediump float;\n"
|
||||||
|
"in vec4 vColor; in vec2 vUv; in vec2 vLight; out vec4 o;\n"
|
||||||
|
"uniform sampler2D uAtlas; uniform sampler2D uLight;\n"
|
||||||
|
"void main(){ o = texture(uAtlas, vUv) * vColor * texture(uLight, vLight); }\n";
|
||||||
|
static const char* kEntityVsEs =
|
||||||
|
"#version 320 es\n"
|
||||||
|
"precision highp float;\n"
|
||||||
|
"in vec3 aPos; in vec4 aColor; in vec2 aUv; in vec2 aLight;\n"
|
||||||
|
"uniform mat4 uMvp; uniform mat4 uModel;\n"
|
||||||
|
"out vec4 vColor; out vec2 vUv;\n"
|
||||||
|
"void main(){ gl_Position = uMvp * uModel * vec4(aPos, 1.0); vColor = aColor; vUv = aUv; }\n";
|
||||||
|
static const char* kEntityFsEs =
|
||||||
|
"#version 320 es\n"
|
||||||
|
"precision mediump float;\n"
|
||||||
|
"in vec4 vColor; in vec2 vUv; out vec4 o; uniform sampler2D uTex;\n"
|
||||||
|
"void main(){ o = texture(uTex, vUv) * vColor; }\n";
|
||||||
|
|
||||||
|
// True once build_resources() has seen a GL_VERSION beginning with "OpenGL ES".
|
||||||
|
static int g_isGlesContext = 0;
|
||||||
|
|
||||||
|
static void setup_vao(GLuint vao, GLuint vbo, GLuint ibo) {
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
glEnableVertexAttribArray(2);
|
||||||
|
glEnableVertexAttribArray(3);
|
||||||
|
glVertexAttribPointer(0, 3, GL_FLOAT, 0, VERT_STRIDE, (void*)0);
|
||||||
|
glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, 1, VERT_STRIDE, (void*)12);
|
||||||
|
glVertexAttribPointer(2, 2, GL_FLOAT, 0, VERT_STRIDE, (void*)16);
|
||||||
|
glVertexAttribPointer(3, 2, GL_SHORT, 0, VERT_STRIDE, (void*)24);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GLuint g_mainFbo;
|
||||||
|
|
||||||
|
static void build_resources(void) {
|
||||||
|
/* offscreen render target: 1280x720 RBO FBO, like CTS fbo surface mode */
|
||||||
|
GLuint fbo, rboColor, rboDepth;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
g_mainFbo = fbo;
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glGenRenderbuffers(1, &rboColor);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, rboColor);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1280, 720);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rboColor);
|
||||||
|
glGenRenderbuffers(1, &rboDepth);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 1280, 720);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
bench_gl_failed("FBO incomplete", "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* versionString = (const char*)glGetString(GL_VERSION);
|
||||||
|
g_isGlesContext = versionString != NULL && strncmp(versionString, "OpenGL ES", 9) == 0;
|
||||||
|
g_progChunk = g_isGlesContext ? make_program(kChunkVsEs, kChunkFsEs) : make_program(kChunkVs, kChunkFs);
|
||||||
|
g_progEntity = g_isGlesContext ? make_program(kEntityVsEs, kEntityFsEs) : make_program(kEntityVs, kEntityFs);
|
||||||
|
glUseProgram(g_progChunk);
|
||||||
|
g_uMvpChunk = glGetUniformLocation(g_progChunk, "uMvp");
|
||||||
|
g_uOffsetChunk = glGetUniformLocation(g_progChunk, "uOffset");
|
||||||
|
glUniform1i(glGetUniformLocation(g_progChunk, "uAtlas"), 0);
|
||||||
|
glUniform1i(glGetUniformLocation(g_progChunk, "uLight"), 2);
|
||||||
|
glUniformMatrix4fv(g_uMvpChunk, 1, 0, g_mvp);
|
||||||
|
glUseProgram(g_progEntity);
|
||||||
|
g_uMvpEntity = glGetUniformLocation(g_progEntity, "uMvp");
|
||||||
|
glUniform1i(glGetUniformLocation(g_progEntity, "uTex"), 0);
|
||||||
|
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
|
||||||
|
glUseProgram(g_progChunk);
|
||||||
|
|
||||||
|
/* shared quad index buffer, like Blaze3D's RenderSystem shared sequences */
|
||||||
|
int maxQuads = 4096;
|
||||||
|
unsigned* idx = (unsigned*)malloc((size_t)maxQuads * 6 * 4);
|
||||||
|
for (int q = 0; q < maxQuads; ++q) {
|
||||||
|
unsigned base = q * 4;
|
||||||
|
unsigned* p = idx + q * 6;
|
||||||
|
p[0] = base; p[1] = base + 1; p[2] = base + 2;
|
||||||
|
p[3] = base + 2; p[4] = base + 3; p[5] = base;
|
||||||
|
}
|
||||||
|
glGenBuffers(1, &g_sharedIbo);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_sharedIbo);
|
||||||
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, maxQuads * 6 * 4, idx, GL_STATIC_DRAW);
|
||||||
|
free(idx);
|
||||||
|
|
||||||
|
g_scratch = (unsigned char*)malloc(4 * 1024 * 1024);
|
||||||
|
memset(g_scratch, 0x5a, 4 * 1024 * 1024);
|
||||||
|
|
||||||
|
glGenVertexArrays(MAX_SECTIONS, g_vao);
|
||||||
|
glGenBuffers(MAX_SECTIONS, g_vbo);
|
||||||
|
int bytes = g_quadsPerSection * 4 * VERT_STRIDE;
|
||||||
|
for (int i = 0; i < MAX_SECTIONS; ++i) {
|
||||||
|
fill_section_vertices(g_scratch, g_quadsPerSection, i * 7919u + 1);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, g_vbo[i]);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, bytes, g_scratch, GL_STATIC_DRAW);
|
||||||
|
setup_vao(g_vao[i], g_vbo[i], g_sharedIbo);
|
||||||
|
}
|
||||||
|
|
||||||
|
glGenTextures(1, &g_texAtlas);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1024, 512, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glGenerateMipmap(GL_TEXTURE_2D);
|
||||||
|
|
||||||
|
glGenTextures(1, &g_texLight);
|
||||||
|
glActiveTexture(GL_TEXTURE0 + 2);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texLight);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
|
||||||
|
glGenTextures(1, &g_texEntity);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texEntity);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
|
||||||
|
// Uniform ring the 26.2-style case sub-ranges into, sized like a real
|
||||||
|
// frame's worth of per-draw uniform slots.
|
||||||
|
GLint align = 256;
|
||||||
|
glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &align);
|
||||||
|
g_uboAlign = align > 0 ? align : 256;
|
||||||
|
g_uboSlot = (size_t)g_uboAlign;
|
||||||
|
glGenBuffers(1, &g_uboRing);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, g_uboRing);
|
||||||
|
glBufferData(GL_UNIFORM_BUFFER, 4 * 1024 * 1024, g_scratch, GL_DYNAMIC_DRAW);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < 2; ++i) {
|
||||||
|
glGenFramebuffers(1, &g_passFbo[i]);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, g_passFbo[i]);
|
||||||
|
glGenRenderbuffers(1, &g_passColor[i]);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, g_passColor[i]);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 256, 256);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, g_passColor[i]);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
bench_gl_failed("pass FBO incomplete", "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* back to the main offscreen target the harness set up */
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, g_mainFbo);
|
||||||
|
|
||||||
|
glGenSamplers(1, &g_sampler);
|
||||||
|
glSamplerParameteri(g_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glSamplerParameteri(g_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
|
glClearColor(0.3f, 0.5f, 0.9f, 1.0f);
|
||||||
|
glViewport(0, 0, 1280, 720);
|
||||||
|
const GLenum setupError = glGetError();
|
||||||
|
if (setupError != GL_NO_ERROR) {
|
||||||
|
char message[64];
|
||||||
|
snprintf(message, sizeof message, "0x%04x", setupError);
|
||||||
|
bench_gl_failed("GL error during resource setup", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void case_draw_tiny(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void case_draw_uniform(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void case_draw_multi_vao(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||||
|
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void case_tex_pingpong(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glBindTexture(GL_TEXTURE_2D, (i & 1) ? g_texEntity : g_texAtlas);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void case_program_pingpong(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
if (i & 1) {
|
||||||
|
glUseProgram(g_progEntity);
|
||||||
|
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
|
||||||
|
} else {
|
||||||
|
glUseProgram(g_progChunk);
|
||||||
|
glUniform3f(g_uOffsetChunk, (float)(i & 15), 0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
glUseProgram(g_progChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* a = uploads per frame, b = bytes per upload (0 => section size) */
|
||||||
|
static void case_chunk_upload(int frame, long a, long b) {
|
||||||
|
if (b <= 0) b = g_quadsPerSection * 4 * VERT_STRIDE;
|
||||||
|
if (b > 4 * 1024 * 1024) b = 4 * 1024 * 1024;
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
int slot = (int)(((long)frame * a + i) % MAX_SECTIONS);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, g_vbo[slot]);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, b, NULL, GL_STATIC_DRAW); /* orphan */
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, 0, b, g_scratch);
|
||||||
|
glBindVertexArray(g_vao[slot]);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* a = sprite updates per frame */
|
||||||
|
static void case_atlas_sprite(int frame, long a, long b) {
|
||||||
|
(void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
int x = (int)((frame * 13 + i * 17) % (1024 - 16));
|
||||||
|
int y = (int)((frame * 7 + i * 29) % (512 - 16));
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
}
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* a = lightmap updates (+draw) per frame */
|
||||||
|
static void case_lightmap(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glActiveTexture(GL_TEXTURE0 + 2);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texLight);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Composite: a = total draws, b = uploads per frame. Mix modeled on trace
|
||||||
|
* analysis: chunk draws with per-draw offset uniform across sections, 10%
|
||||||
|
* entity-style program flips, per-frame lightmap + sprite updates, b chunk
|
||||||
|
* re-uploads. */
|
||||||
|
static long g_mixSprites = 8;
|
||||||
|
static void case_scene_mix(int frame, long a, long b) {
|
||||||
|
glActiveTexture(GL_TEXTURE0 + 2);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texLight);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
for (long i = 0; i < g_mixSprites; ++i) {
|
||||||
|
int x = (int)((frame * 13 + i * 17) % (1024 - 16));
|
||||||
|
int y = (int)((frame * 7 + i * 29) % (512 - 16));
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
}
|
||||||
|
for (long i = 0; i < b; ++i) {
|
||||||
|
int slot = (int)(((long)frame * b + i) % MAX_SECTIONS);
|
||||||
|
long bytes = g_quadsPerSection * 4 * VERT_STRIDE;
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, g_vbo[slot]);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, bytes, NULL, GL_STATIC_DRAW);
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, 0, bytes, g_scratch);
|
||||||
|
}
|
||||||
|
long entityEvery = 10;
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
if (i % entityEvery == entityEvery - 1) {
|
||||||
|
glUseProgram(g_progEntity);
|
||||||
|
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texEntity);
|
||||||
|
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
glUseProgram(g_progChunk);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
} else {
|
||||||
|
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||||
|
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Trace-derived cases -------------------------------------------------
|
||||||
|
* Per-frame call mixes measured from the three captured Minecraft traces
|
||||||
|
* (render distance 32, 1280x720, hovering in-world). Each case reproduces one
|
||||||
|
* renderer's dominant per-draw sequence at its measured rate, so the number a
|
||||||
|
* backend posts here is directly comparable to what that game version asks of
|
||||||
|
* the driver every frame.
|
||||||
|
*
|
||||||
|
* vanilla 1.21.1 : 5495 glDrawElements, 5490 glBindVertexArray,
|
||||||
|
* 5487 glUniform3fv, 95 glTexSubImage2D (+382 glPixelStorei,
|
||||||
|
* 247 glTexParameteri), 23 glBufferData per frame
|
||||||
|
* fabric+sodium : 132 glMultiDrawElementsBaseVertex, 279 glBindVertexArray,
|
||||||
|
* 132 glUniform3f, 32 glBufferData per frame
|
||||||
|
* 26.2 snapshot : 3401 glDrawElementsBaseVertex, each preceded by
|
||||||
|
* glBindBufferRange + glBindBuffer (3639/3412 per frame)
|
||||||
|
*/
|
||||||
|
/* vanilla: bind VAO, push the chunk offset, draw. a = draws per frame. */
|
||||||
|
static void case_mc_vanilla_draw(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
float offset[3];
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||||
|
offset[0] = (float)(i & 15);
|
||||||
|
offset[1] = (float)((i >> 4) & 15);
|
||||||
|
offset[2] = 0.0f;
|
||||||
|
glUniform3fv(g_uOffsetChunk, 1, offset);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sodium: one multi-draw covers many chunk sections out of a shared buffer.
|
||||||
|
* a = multi-draws per frame, b = sub-draws inside each. */
|
||||||
|
static void case_mc_sodium_multidraw(int frame, long a, long b) {
|
||||||
|
(void)frame;
|
||||||
|
enum { kMaxSub = 64 };
|
||||||
|
if (b <= 0 || b > kMaxSub) b = 32;
|
||||||
|
GLsizei counts[kMaxSub];
|
||||||
|
const void* offsets[kMaxSub];
|
||||||
|
GLint baseVertices[kMaxSub];
|
||||||
|
for (long s = 0; s < b; ++s) {
|
||||||
|
counts[s] = (GLsizei)(g_quadsPerSection * 6 / b);
|
||||||
|
offsets[s] = (const void*)(uintptr_t)(s * (g_quadsPerSection * 6 / b) * 4);
|
||||||
|
baseVertices[s] = 0;
|
||||||
|
}
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glBindVertexArray(g_vao[i % MAX_SECTIONS]);
|
||||||
|
glBindVertexArray(g_vao[i % MAX_SECTIONS]); /* sodium rebinds ~2x per draw */
|
||||||
|
glUniform3f(g_uOffsetChunk, (float)(i & 15), (float)((i >> 4) & 15), 0.0f);
|
||||||
|
// Routed through the includer: GLES has no multi-draw-with-base-vertex, so
|
||||||
|
// a native-driver harness emulates it with the loop the extension folds up.
|
||||||
|
bench_multi_draw_elements_base_vertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets,
|
||||||
|
(GLsizei)b, baseVertices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 26.2: every draw rebinds a fresh uniform-buffer range out of a ring.
|
||||||
|
* a = draws per frame. */
|
||||||
|
static void case_mc_ubo_range(int frame, long a, long b) {
|
||||||
|
(void)b;
|
||||||
|
const size_t slots = (4u * 1024u * 1024u) / g_uboSlot;
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
const size_t slot = (size_t)(((long)frame * a + i) % (long)slots);
|
||||||
|
glBindBufferRange(GL_UNIFORM_BUFFER, 0, g_uboRing, (GLintptr)(slot * g_uboSlot),
|
||||||
|
(GLsizeiptr)g_uboSlot);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, g_uboRing);
|
||||||
|
glDrawElementsBaseVertex(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vanilla's animated-sprite path: every upload is wrapped in the pixel-store
|
||||||
|
* and filter state Blaze3D re-sets around it. a = uploads per frame. */
|
||||||
|
static void case_mc_tex_stream(int frame, long a, long b) {
|
||||||
|
(void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||||
|
glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
|
||||||
|
glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
int x = (int)((frame * 13 + i * 17) % (1024 - 16));
|
||||||
|
int y = (int)((frame * 7 + i * 29) % (512 - 16));
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, g_scratch);
|
||||||
|
}
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blaze3D re-resolves uniform locations by name every frame. a = lookups. */
|
||||||
|
static void case_mc_uniform_lookup(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
static const char* names[4] = {"uMvp", "uOffset", "uAtlas", "uLight"};
|
||||||
|
volatile GLint sink = 0;
|
||||||
|
for (long i = 0; i < a; ++i) sink += glGetUniformLocation(g_progChunk, names[i & 3]);
|
||||||
|
(void)sink;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 26.2 rebinds a sampler object per texture unit switch. a = switches. */
|
||||||
|
static void case_mc_sampler_churn(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glActiveTexture(GL_TEXTURE0 + (GLenum)(i & 3));
|
||||||
|
glBindTexture(GL_TEXTURE_2D, (i & 1) ? g_texEntity : g_texAtlas);
|
||||||
|
glBindSampler((GLuint)(i & 3), g_sampler);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 26.2 switches render targets constantly: 132 glBindFramebuffer and 198
|
||||||
|
* glDrawBuffers per frame. Pass switching is where a Vulkan backend pays for
|
||||||
|
* render-pass breaks, so this case is the one to watch on Magma. a = passes. */
|
||||||
|
static void case_mc_pass_switch(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
static const GLenum kColor0[1] = {GL_COLOR_ATTACHMENT0};
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, g_passFbo[i & 1]);
|
||||||
|
glDrawBuffers(1, kColor0);
|
||||||
|
glViewport(0, 0, 256, 256);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, g_mainFbo);
|
||||||
|
glViewport(0, 0, 1280, 720);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blaze3D toggles blend around batches: 46 glEnable/glDisable pairs and 28
|
||||||
|
* glBlendFuncSeparate per vanilla frame. a = toggle pairs. */
|
||||||
|
static void case_mc_state_toggle(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 26.2 re-sets texture parameters relentlessly - 612 glTexParameteri per frame,
|
||||||
|
* almost always to the value already in place. Measures redundant-param
|
||||||
|
* filtering. a = parameter writes. */
|
||||||
|
static void case_mc_tex_param(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, g_texAtlas);
|
||||||
|
for (long i = 0; i < a; i += 4) {
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
}
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sodium switches programs mid-frame far more than vanilla: 62 glUseProgram and
|
||||||
|
* 60 mat4 uploads per frame. a = program switches. */
|
||||||
|
static void case_mc_use_program(int frame, long a, long b) {
|
||||||
|
(void)frame; (void)b;
|
||||||
|
glBindVertexArray(g_vao[0]);
|
||||||
|
for (long i = 0; i < a; ++i) {
|
||||||
|
if (i & 1) {
|
||||||
|
glUseProgram(g_progEntity);
|
||||||
|
glUniformMatrix4fv(g_uMvpEntity, 1, 0, g_mvp);
|
||||||
|
} else {
|
||||||
|
glUseProgram(g_progChunk);
|
||||||
|
glUniformMatrix4fv(g_uMvpChunk, 1, 0, g_mvp);
|
||||||
|
}
|
||||||
|
glDrawElements(GL_TRIANGLES, g_quadsPerSection * 6, GL_UNSIGNED_INT, 0);
|
||||||
|
}
|
||||||
|
glUseProgram(g_progChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- the case table both harnesses iterate --------------------------------
|
||||||
|
* a/b are the case's own knobs; opsPerFrame is what one bench frame is
|
||||||
|
* normalised by, so ns_per_op compares across renderers. The mc_* rates are
|
||||||
|
* the per-frame call counts measured from the captured traces.
|
||||||
|
*/
|
||||||
|
typedef void (*bench_case_fn)(int frame, long a, long b);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char* name;
|
||||||
|
bench_case_fn fn;
|
||||||
|
long a, b, opsPerFrame;
|
||||||
|
} BenchCaseDesc;
|
||||||
|
|
||||||
|
static const BenchCaseDesc kBenchCases[] = {
|
||||||
|
{"mc_vanilla_draw", case_mc_vanilla_draw, 5495, 0, 5495},
|
||||||
|
{"mc_sodium_multidraw", case_mc_sodium_multidraw, 132, 32, 132},
|
||||||
|
{"mc_ubo_range", case_mc_ubo_range, 3401, 0, 3401},
|
||||||
|
{"mc_tex_stream", case_mc_tex_stream, 95, 0, 95},
|
||||||
|
{"mc_uniform_lookup", case_mc_uniform_lookup, 41, 0, 41},
|
||||||
|
{"mc_sampler_churn", case_mc_sampler_churn, 306, 0, 306},
|
||||||
|
{"mc_pass_switch", case_mc_pass_switch, 132, 0, 132},
|
||||||
|
{"mc_state_toggle", case_mc_state_toggle, 46, 0, 46},
|
||||||
|
{"mc_tex_param", case_mc_tex_param, 612, 0, 612},
|
||||||
|
{"mc_use_program", case_mc_use_program, 62, 0, 62},
|
||||||
|
{"draw_tiny", case_draw_tiny, 2048, 0, 2048},
|
||||||
|
{"draw_uniform", case_draw_uniform, 2048, 0, 2048},
|
||||||
|
{"draw_multi_vao", case_draw_multi_vao, 2048, 0, 2048},
|
||||||
|
{"tex_pingpong", case_tex_pingpong, 1024, 0, 1024},
|
||||||
|
{"program_pingpong", case_program_pingpong, 512, 0, 512},
|
||||||
|
{"chunk_upload", case_chunk_upload, 24, 0, 24},
|
||||||
|
{"atlas_sprite", case_atlas_sprite, 32, 0, 32},
|
||||||
|
{"lightmap", case_lightmap, 4, 0, 4},
|
||||||
|
{"scene_mix", case_scene_mix, 2048, 12, 2048},
|
||||||
|
};
|
||||||
|
static const int kBenchCaseCount = (int)(sizeof kBenchCases / sizeof kBenchCases[0]);
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Run the headless EGL DriverBench on one renderer:
|
||||||
|
# ./run_driver_bench.sh native [bench args...]
|
||||||
|
# ./run_driver_bench.sh espryt <libMobileGL.so> [bench args...]
|
||||||
|
# ./run_driver_bench.sh magma <libMobileGL.so> [bench args...]
|
||||||
|
# The bench dlopens exactly one EGL provider (DRIVERBENCH_EGL_LIB): the system
|
||||||
|
# libEGL.so.1 for native, or the given libMobileGL.so for a MobileGL backend -
|
||||||
|
# no LD_LIBRARY_PATH shadowing, so MobileGL's own loader still finds the real
|
||||||
|
# driver underneath.
|
||||||
|
#
|
||||||
|
# Pin the vendor libraries explicitly. A bare libEGL.so.1 on a glvnd system
|
||||||
|
# picks whatever vendor eglGetDisplay(EGL_DEFAULT_DISPLAY) resolves first,
|
||||||
|
# which is Mesa/llvmpipe here - a software rasteriser silently replacing the
|
||||||
|
# GPU under a benchmark. Override MGL_EGL_VENDOR / MGL_VK_ICD to test another
|
||||||
|
# driver.
|
||||||
|
set -eu
|
||||||
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
BENCH=${DRIVERBENCH_BIN:-$HERE/DriverBench}
|
||||||
|
EGL_VENDOR=${MGL_EGL_VENDOR:-/usr/share/glvnd/egl_vendor.d/10_nvidia.json}
|
||||||
|
VK_ICD=${MGL_VK_ICD:-/usr/share/vulkan/icd.d/nvidia_icd.x86_64.json}
|
||||||
|
MODE=$1; shift
|
||||||
|
|
||||||
|
export __EGL_VENDOR_LIBRARY_FILENAMES=$EGL_VENDOR
|
||||||
|
export EGL_PLATFORM=${EGL_PLATFORM:-x11}
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
native)
|
||||||
|
export DRIVERBENCH_EGL_LIB=${DRIVERBENCH_EGL_LIB:-libEGL.so.1}
|
||||||
|
;;
|
||||||
|
espryt)
|
||||||
|
export DRIVERBENCH_EGL_LIB=$(readlink -f "$1"); shift
|
||||||
|
export MOBILEGL_BACKEND_TYPE=DirectGLES
|
||||||
|
;;
|
||||||
|
magma)
|
||||||
|
export DRIVERBENCH_EGL_LIB=$(readlink -f "$1"); shift
|
||||||
|
export MOBILEGL_BACKEND_TYPE=DirectVulkan
|
||||||
|
export VK_ICD_FILENAMES=$VK_ICD
|
||||||
|
;;
|
||||||
|
*) echo "unknown mode: $MODE (native|espryt|magma)"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
exec "$BENCH" "$@"
|
||||||
@@ -8,6 +8,10 @@
|
|||||||
|
|
||||||
#include "GL_Buffer.h"
|
#include "GL_Buffer.h"
|
||||||
#include "Validators.h"
|
#include "Validators.h"
|
||||||
|
#include "../Texture/GL_Texture.h"
|
||||||
|
#include "../Getter/GL_Getter.h"
|
||||||
|
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||||
|
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_State/GLState/ErrorState/Error.h>
|
#include <MG_State/GLState/ErrorState/Error.h>
|
||||||
@@ -38,6 +42,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GetNamedBufferParameteriv,
|
GetNamedBufferParameteriv,
|
||||||
GetNamedBufferParameteri64v,
|
GetNamedBufferParameteri64v,
|
||||||
GetNamedBufferPointerv,
|
GetNamedBufferPointerv,
|
||||||
|
GetNamedBufferSubData,
|
||||||
};
|
};
|
||||||
|
|
||||||
const char* GetBufferOpName(BufferOp op) {
|
const char* GetBufferOpName(BufferOp op) {
|
||||||
@@ -76,6 +81,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return "UnmapNamedBuffer";
|
return "UnmapNamedBuffer";
|
||||||
case BufferOp::FlushMappedNamedBufferRange:
|
case BufferOp::FlushMappedNamedBufferRange:
|
||||||
return "FlushMappedNamedBufferRange";
|
return "FlushMappedNamedBufferRange";
|
||||||
|
case BufferOp::GetNamedBufferSubData:
|
||||||
|
return "GetNamedBufferSubData";
|
||||||
case BufferOp::GetNamedBufferParameteriv:
|
case BufferOp::GetNamedBufferParameteriv:
|
||||||
return "GetNamedBufferParameteriv";
|
return "GetNamedBufferParameteriv";
|
||||||
case BufferOp::GetNamedBufferParameteri64v:
|
case BufferOp::GetNamedBufferParameteri64v:
|
||||||
@@ -89,25 +96,64 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
SharedPtr<MG_State::GLState::BufferObject> GetNamedBufferObject(GLuint buffer, BufferOp op);
|
SharedPtr<MG_State::GLState::BufferObject> GetNamedBufferObject(GLuint buffer, BufferOp op);
|
||||||
|
|
||||||
|
// The size of one cleared element, which is what offset and size must be multiples of
|
||||||
|
// (GL 4.6 core 6.3). `internalformat` is restricted to the buffer-texture format table, and
|
||||||
|
// `format`/`type` describe the client-side pattern, so both are validated here and the
|
||||||
|
// caller only has to know how wide an element is.
|
||||||
SizeT GetClearPatternSize(GLenum internalformat, GLenum format, GLenum type, BufferOp op) {
|
SizeT GetClearPatternSize(GLenum internalformat, GLenum format, GLenum type, BufferOp op) {
|
||||||
if (format != GL_RED_INTEGER) {
|
if (!IsBufferTextureInternalFormat(internalformat)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
|
MakeUnique<GenericErrorInfo>(
|
||||||
"Only GL_RED_INTEGER buffer clears are currently supported."));
|
"MG_Impl/GLImpl", GetBufferOpName(op),
|
||||||
|
std::format("internalformat 0x{:X} is not one of the sized formats a buffer clear accepts.",
|
||||||
|
internalformat)));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (internalformat == GL_R8UI && type == GL_UNSIGNED_BYTE) return sizeof(GLubyte);
|
// Unlike internalformat, a bad format or type here is INVALID_VALUE rather than
|
||||||
if (internalformat == GL_R32UI && type == GL_UNSIGNED_INT) return sizeof(GLuint);
|
// INVALID_ENUM (GL 4.6 core 6.3) - the odd one out among the enum arguments.
|
||||||
|
const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
|
||||||
|
if (inputFormat == TextureInputFormat::Unknown) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
|
||||||
|
std::format("format 0x{:X} is not a pixel format.", format)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
MG_State::pGLContext->RecordError(
|
const TexturePixelDataType pixelType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
|
||||||
ErrorCode::InvalidEnum,
|
if (pixelType == TexturePixelDataType::Unknown) {
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
|
MG_State::pGLContext->RecordError(
|
||||||
std::format("Unsupported clear format tuple: internalformat=0x{:X}, "
|
ErrorCode::InvalidValue,
|
||||||
"format=0x{:X}, type=0x{:X}",
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
|
||||||
internalformat, format, type)));
|
std::format("type 0x{:X} is not a pixel type.", type)));
|
||||||
return 0;
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextureInternalFormat internal =
|
||||||
|
MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
|
||||||
|
const SizeT elementSize = MG_Util::GetSizedInternalFormatSizeInBytes(internal);
|
||||||
|
if (elementSize == 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
|
||||||
|
std::format("internalformat 0x{:X} has no known element size.",
|
||||||
|
internalformat)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The pattern is replicated verbatim, which is only the whole story while the client
|
||||||
|
// layout already matches the internal format - the case every entry point in practice
|
||||||
|
// uses, and the only one the conversion machinery here can express. Say so rather than
|
||||||
|
// quietly writing a differently-sized pattern.
|
||||||
|
const SizeT sourceSize = MG_Util::GetInputBytesPerPixel(inputFormat, pixelType);
|
||||||
|
if (sourceSize != elementSize) {
|
||||||
|
MGLOG_W("%s: clear pattern is %zu bytes but internalformat 0x%X stores %zu; "
|
||||||
|
"converting between them is not implemented",
|
||||||
|
GetBufferOpName(op), sourceSize, internalformat, elementSize);
|
||||||
|
}
|
||||||
|
return elementSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateBufferClearRange(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject, GLintptr offset,
|
Bool ValidateBufferClearRange(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject, GLintptr offset,
|
||||||
@@ -330,12 +376,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
} else if (access & BufferMappingAccessBit::Write) {
|
} else if (access & BufferMappingAccessBit::Write) {
|
||||||
*params = GL_WRITE_ONLY;
|
*params = GL_WRITE_ONLY;
|
||||||
} else {
|
} else {
|
||||||
*params = 0;
|
*params = GL_READ_WRITE;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
*params = 0;
|
// Initial value, and what glUnmapBuffer restores (GL 4.6 core table 6.2).
|
||||||
|
*params = GL_READ_WRITE;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case GL_BUFFER_ACCESS_FLAGS:
|
||||||
|
// The MapBufferRange flags verbatim; glMapBuffer's access enum has already been
|
||||||
|
// normalised into the same bits. Zero while the buffer is not mapped.
|
||||||
|
*params = bufferObject->IsMapped()
|
||||||
|
? static_cast<GLint>(
|
||||||
|
MG_Util::ConvertBufferMappingAccessToGLEnum(bufferObject->GetMappingAccess()))
|
||||||
|
: 0;
|
||||||
|
break;
|
||||||
case GL_BUFFER_MAPPED:
|
case GL_BUFFER_MAPPED:
|
||||||
*params = bufferObject->IsMapped() ? GL_TRUE : GL_FALSE;
|
*params = bufferObject->IsMapped() ? GL_TRUE : GL_FALSE;
|
||||||
break;
|
break;
|
||||||
@@ -807,6 +862,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
Range1D mappedRange = bufferObject->GetMappedRange();
|
Range1D mappedRange = bufferObject->GetMappedRange();
|
||||||
auto mappingAccess = bufferObject->GetMappingAccess();
|
auto mappingAccess = bufferObject->GetMappingAccess();
|
||||||
|
// GL 4.6 6.5: the error is on OVERLAP with the mapped range, i.e. a half-open
|
||||||
|
// intersection test. There used to be a second test below this one asking only
|
||||||
|
// `offset + size >= mappedRange.start`, which rejects every write that starts
|
||||||
|
// before a mapped tail as well - it made a legal disjoint glBufferSubData fail.
|
||||||
if (bufferObject->IsMapped() && !(mappingAccess & BufferMappingAccessBit::Persistent) &&
|
if (bufferObject->IsMapped() && !(mappingAccess & BufferMappingAccessBit::Persistent) &&
|
||||||
(offset < mappedRange.end) && (offset + size > mappedRange.start)) {
|
(offset < mappedRange.end) && (offset + size > mappedRange.start)) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -817,18 +876,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bufferObject->IsMapped() && !(mappingAccess & BufferMappingAccessBit::Persistent)) {
|
|
||||||
Range1D mappedRange = bufferObject->GetMappedRange();
|
|
||||||
if (offset + size >= mappedRange.start) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
|
||||||
ErrorCode::InvalidOperation,
|
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BufferSubData_State",
|
|
||||||
"Cannot modify a mapped buffer object unless it was "
|
|
||||||
"mapped with GL_MAP_PERSISTENT_BIT."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bufferObject->UploadSubData({(void*)data, (SizeT)size}, offset);
|
bufferObject->UploadSubData({(void*)data, (SizeT)size}, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,6 +925,45 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bufferObject->SyncGpuWrites();
|
||||||
|
bufferObject->DownloadSubData(data, static_cast<SizeT>(offset), static_cast<SizeT>(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetNamedBufferSubData_State(GLuint buffer, GLintptr offset, GLsizeiptr size, void* data) {
|
||||||
|
if (!data) {
|
||||||
|
// Match GetBufferSubData_State: a null pointer is a caller bug, not a GL-specified error.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size < 0 || offset < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetNamedBufferSubData_State",
|
||||||
|
"Offset and size must be non-negative."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::GetNamedBufferSubData);
|
||||||
|
if (!bufferObject) return;
|
||||||
|
|
||||||
|
if (static_cast<SizeT>(offset) + static_cast<SizeT>(size) > bufferObject->GetSize()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetNamedBufferSubData_State",
|
||||||
|
"Offset and size exceed buffer size."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferObject->IsMapped() &&
|
||||||
|
!(bufferObject->GetMappingAccess() & BufferMappingAccessBit::Persistent)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetNamedBufferSubData_State",
|
||||||
|
"Cannot read from a buffer object mapped without GL_MAP_PERSISTENT_BIT."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferObject->SyncGpuWrites();
|
||||||
bufferObject->DownloadSubData(data, static_cast<SizeT>(offset), static_cast<SizeT>(size));
|
bufferObject->DownloadSubData(data, static_cast<SizeT>(offset), static_cast<SizeT>(size));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,6 +1006,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BufferStorage_State(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) {
|
void BufferStorage_State(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) {
|
||||||
|
// Error precedence: "no buffer is bound to target" outranks a bad size or bad
|
||||||
|
// flags, so the binding has to be resolved before either is validated.
|
||||||
|
auto bufferObject = GetBoundBufferObject(target, BufferOp::BufferStorage);
|
||||||
|
if (!bufferObject) return;
|
||||||
|
|
||||||
if (size <= 0) {
|
if (size <= 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
@@ -928,8 +1019,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
if (!ValidateStorageFlags(flags, BufferOp::BufferStorage)) return;
|
if (!ValidateStorageFlags(flags, BufferOp::BufferStorage)) return;
|
||||||
|
|
||||||
auto bufferObject = GetBoundBufferObject(target, BufferOp::BufferStorage);
|
|
||||||
if (!bufferObject) return;
|
|
||||||
if (bufferObject->IsImmutableStorage()) {
|
if (bufferObject->IsImmutableStorage()) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -964,6 +1053,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NamedBufferStorage_State(GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) {
|
void NamedBufferStorage_State(GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) {
|
||||||
|
// Same precedence as BufferStorage_State: the buffer-name error comes first.
|
||||||
|
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::NamedBufferStorage);
|
||||||
|
if (!bufferObject) return;
|
||||||
|
|
||||||
if (size <= 0) {
|
if (size <= 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
@@ -972,8 +1065,6 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
if (!ValidateStorageFlags(flags, BufferOp::NamedBufferStorage)) return;
|
if (!ValidateStorageFlags(flags, BufferOp::NamedBufferStorage)) return;
|
||||||
|
|
||||||
auto bufferObject = GetNamedBufferObject(buffer, BufferOp::NamedBufferStorage);
|
|
||||||
if (!bufferObject) return;
|
|
||||||
if (bufferObject->IsImmutableStorage()) {
|
if (bufferObject->IsImmutableStorage()) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -1366,6 +1457,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (buffer == 0) {
|
if (buffer == 0) {
|
||||||
point.Bind(nullptr);
|
point.Bind(nullptr);
|
||||||
point.SetRange(Range1D(0, 0));
|
point.SetRange(Range1D(0, 0));
|
||||||
|
GetBufferBindingSlot(bufferTarget).Bind(nullptr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1384,6 +1476,70 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
} else {
|
} else {
|
||||||
point.ClearRange();
|
point.ClearRange();
|
||||||
}
|
}
|
||||||
|
// The indexed bind also binds to the generic binding point of the same target
|
||||||
|
// (GL 4.6 core 6.1.1). Callers rely on it: the texture_gather tests set up their
|
||||||
|
// SSBO with BindBufferBase and then size it through glBufferData on the generic
|
||||||
|
// target alone, which would otherwise raise GL_INVALID_OPERATION and leave the
|
||||||
|
// buffer with no storage.
|
||||||
|
GetBufferBindingSlot(bufferTarget).Bind(bufferObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core 6.1.1: the constraints glBindBufferRange puts on the (offset, size) pair.
|
||||||
|
// Every one of them is INVALID_VALUE, and all of them are checked before a single piece
|
||||||
|
// of state is written - a rejected bind must leave the binding point exactly as it was.
|
||||||
|
// They apply only to a non-zero buffer: buffer 0 detaches the binding point and ignores
|
||||||
|
// offset and size, which is also how glBindBuffersRange spells "reset this element"
|
||||||
|
// (a NULL buffers array, or a zero entry inside one).
|
||||||
|
static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size,
|
||||||
|
const char* funcName) {
|
||||||
|
if (size <= 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
|
std::format("size ({}) must be greater than zero.", size)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (offset < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
|
std::format("offset ({}) must not be negative.", offset)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// GL_UNIFORM_BUFFER and GL_SHADER_STORAGE_BUFFER each constrain the offset to their own
|
||||||
|
// implementation-defined alignment, which glGetIntegerv already answers.
|
||||||
|
GLenum alignmentQuery = GL_NONE;
|
||||||
|
if (target == GL_SHADER_STORAGE_BUFFER) {
|
||||||
|
alignmentQuery = GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT;
|
||||||
|
} else if (target == GL_UNIFORM_BUFFER) {
|
||||||
|
alignmentQuery = GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT;
|
||||||
|
}
|
||||||
|
if (alignmentQuery != GL_NONE) {
|
||||||
|
GLint alignment = 0;
|
||||||
|
GetIntegerv(alignmentQuery, &alignment);
|
||||||
|
if (alignment > 0 && (offset % static_cast<GLintptr>(alignment)) != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", funcName,
|
||||||
|
std::format("offset ({}) must be a multiple of {} ({}).", offset,
|
||||||
|
MG_Util::ConvertGLEnumToString(alignmentQuery), alignment)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A transform feedback capture binding is addressed in 32-bit components, so BOTH the
|
||||||
|
// offset and the size must be multiples of 4.
|
||||||
|
if (target == GL_TRANSFORM_FEEDBACK_BUFFER && ((offset % 4) != 0 || (size % 4) != 0)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", funcName,
|
||||||
|
std::format("offset ({}) and size ({}) must both be multiples of 4 for "
|
||||||
|
"GL_TRANSFORM_FEEDBACK_BUFFER.",
|
||||||
|
offset, size)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BindBufferRange_State(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
|
void BindBufferRange_State(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
|
||||||
@@ -1392,6 +1548,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||||
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
|
||||||
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
|
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
|
||||||
|
if (buffer != 0 && !ValidateBufferRangeOffsetAndSize(target, offset, size, __func__)) return;
|
||||||
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
|
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -1407,6 +1564,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (buffer == 0) {
|
if (buffer == 0) {
|
||||||
point.Bind(nullptr);
|
point.Bind(nullptr);
|
||||||
point.SetRange(Range1D(0, 0));
|
point.SetRange(Range1D(0, 0));
|
||||||
|
GetBufferBindingSlot(bufferTarget).Bind(nullptr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1424,6 +1582,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
} else {
|
} else {
|
||||||
point.ClearRange();
|
point.ClearRange();
|
||||||
}
|
}
|
||||||
|
// Also the generic binding point, exactly as BindBufferBase (GL 4.6 core 6.1.1).
|
||||||
|
GetBufferBindingSlot(bufferTarget).Bind(bufferObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
|
||||||
@@ -1533,6 +1693,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
BufferSubData_State(target, offset, size, data);
|
BufferSubData_State(target, offset, size, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GetNamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, void* data) {
|
||||||
|
GetNamedBufferSubData_State(buffer, offset, size, data);
|
||||||
|
}
|
||||||
|
|
||||||
void GetBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, void* data) {
|
void GetBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, void* data) {
|
||||||
GetBufferSubData_State(target, offset, size, data);
|
GetBufferSubData_State(target, offset, size, data);
|
||||||
}
|
}
|
||||||
@@ -1558,15 +1722,32 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ARB_multi_bind: defined by the spec as equivalent to a loop over the single-bind entry
|
// ARB_multi_bind: defined by the spec as equivalent to a loop over the single-bind entry
|
||||||
// points (with buffer 0 resetting the binding point).
|
// points (with buffer 0 resetting the binding point) - but only AFTER an up-front check
|
||||||
|
// of the whole [first, first + count) range. Looping straight into the single-bind entry
|
||||||
|
// points reports the single-bind INVALID_VALUE for an out-of-range index instead of the
|
||||||
|
// multi-bind INVALID_OPERATION, and binds the in-range prefix before failing.
|
||||||
|
static Bool ValidateMultiBindBufferRange(GLenum target, GLuint first, GLsizei count, const char* funcName) {
|
||||||
|
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
|
||||||
|
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return false;
|
||||||
|
return BufferImpl::ValidateBufferBindingPointRange(bufferTarget, first, count, funcName);
|
||||||
|
}
|
||||||
|
|
||||||
void BindBuffersBase(GLenum target, GLuint first, GLsizei count, const GLuint* buffers) {
|
void BindBuffersBase(GLenum target, GLuint first, GLsizei count, const GLuint* buffers) {
|
||||||
|
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
BindBufferBase_State(target, first + i, buffers ? buffers[i] : 0);
|
BindBufferBase_State(target, first + i, buffers ? buffers[i] : 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The (offset, size) constraints are the one part of glBindBuffersRange that stays
|
||||||
|
// per-element: ARB_multi_bind checks them separately for each binding point, leaves that
|
||||||
|
// point unchanged on failure, and still applies the remaining elements - which is exactly
|
||||||
|
// what looping into BindBufferRange_State does. Only the [first, first + count) range is
|
||||||
|
// an up-front, all-or-nothing check. Elements that name buffer 0 (or a NULL buffers array)
|
||||||
|
// reset the binding point through BindBufferBase_State and carry no offset/size to check.
|
||||||
void BindBuffersRange(GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
|
void BindBuffersRange(GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
|
||||||
const GLsizeiptr* sizes) {
|
const GLsizeiptr* sizes) {
|
||||||
|
if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
if (!buffers || buffers[i] == 0) {
|
if (!buffers || buffers[i] == 0) {
|
||||||
BindBufferBase_State(target, first + i, 0);
|
BindBufferBase_State(target, first + i, 0);
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLsizeiptr size);
|
GLsizeiptr size);
|
||||||
void BufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const void* data);
|
void BufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const void* data);
|
||||||
void GetBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, void* data);
|
void GetBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, void* data);
|
||||||
|
void GetNamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, void* data);
|
||||||
void BufferData(GLenum target, GLsizeiptr size, const void* data, GLenum usage);
|
void BufferData(GLenum target, GLsizeiptr size, const void* data, GLenum usage);
|
||||||
void BindBuffer(GLenum target, GLuint buffer);
|
void BindBuffer(GLenum target, GLuint buffer);
|
||||||
void GenBuffers(GLsizei n, GLuint* buffers);
|
void GenBuffers(GLsizei n, GLuint* buffers);
|
||||||
|
|||||||
@@ -53,18 +53,46 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// The GL-visible number of indexed binding points for `target`.
|
||||||
|
SizeT GetBufferBindingPointLimit(BufferTarget target) {
|
||||||
|
SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(target);
|
||||||
|
if (target == BufferTarget::ShaderStorage && MG_Backend::pActiveBackendObject) {
|
||||||
|
const Int backendCount =
|
||||||
|
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
||||||
|
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
||||||
|
}
|
||||||
|
if (target == BufferTarget::TransformFeedback) {
|
||||||
|
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture
|
||||||
|
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
||||||
|
pointCount = std::min<SizeT>(pointCount, 4);
|
||||||
|
}
|
||||||
|
return pointCount;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Bool ValidateBufferBindingPointRange(BufferTarget target, Uint first, GLsizei count, const char* funcName) {
|
||||||
|
if (count < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl", funcName,
|
||||||
|
"count must be non-negative."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const SizeT pointCount = GetBufferBindingPointLimit(target);
|
||||||
|
if (static_cast<Uint64>(first) + static_cast<Uint64>(count) > static_cast<Uint64>(pointCount)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl/BufferImpl", funcName,
|
||||||
|
std::format("first + count ({} + {}) exceeds the {} indexed binding points of target {}.", first,
|
||||||
|
count, pointCount, MG_Util::ConvertBufferTargetToString(target))));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index) {
|
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index) {
|
||||||
SizeT pointCount = MG_State::pGLContext->GetBufferBindingPointCount(target);
|
const SizeT pointCount = GetBufferBindingPointLimit(target);
|
||||||
if (target == BufferTarget::ShaderStorage && MG_Backend::pActiveBackendObject) {
|
|
||||||
const Int backendCount =
|
|
||||||
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
|
|
||||||
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
|
|
||||||
}
|
|
||||||
if (target == BufferTarget::TransformFeedback) {
|
|
||||||
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture
|
|
||||||
// binding points in GL 3.3 (no ARB_transform_feedback3).
|
|
||||||
pointCount = std::min<SizeT>(pointCount, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < pointCount) {
|
if (index < pointCount) {
|
||||||
return true;
|
return true;
|
||||||
@@ -112,14 +140,10 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateBufferMappingAccess(Flags<BufferMappingAccessBit> accessBits) {
|
Bool ValidateBufferMappingAccess(Flags<BufferMappingAccessBit> accessBits) {
|
||||||
if (accessBits == BufferMappingAccessBit::Null) {
|
// An empty mask is a legal value for a bitfield - it just fails the rule that a mapping
|
||||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
// must ask for read or write access, which is INVALID_OPERATION and belongs to the callers
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/BufferImpl",
|
// (both of them check it immediately after this). Rejecting it here as INVALID_ENUM
|
||||||
"ValidateBufferMappingAccess",
|
// reported the wrong error and hid theirs.
|
||||||
"Access bits cannot be null."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto validBits = BufferMappingAccessBit::Read | BufferMappingAccessBit::Write |
|
const auto validBits = BufferMappingAccessBit::Read | BufferMappingAccessBit::Write |
|
||||||
BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer |
|
BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer |
|
||||||
BufferMappingAccessBit::FlushExplicit | BufferMappingAccessBit::Unsynchronized |
|
BufferMappingAccessBit::FlushExplicit | BufferMappingAccessBit::Unsynchronized |
|
||||||
|
|||||||
@@ -17,4 +17,8 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
|
|||||||
Bool ValidateBufferMappingAccess(Flags<BufferMappingAccessBit> accessBits);
|
Bool ValidateBufferMappingAccess(Flags<BufferMappingAccessBit> accessBits);
|
||||||
Bool ValidateBufferBindingPointTarget(BufferTarget target);
|
Bool ValidateBufferBindingPointTarget(BufferTarget target);
|
||||||
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index);
|
Bool ValidateBufferBindingPointIndex(BufferTarget target, Uint index);
|
||||||
|
// ARB_multi_bind: glBindBuffersBase/Range validate the whole [first, first + count) range
|
||||||
|
// up front and report INVALID_OPERATION, where a single out-of-range index would be
|
||||||
|
// INVALID_VALUE. Naively looping the single-bind entry points reports the wrong class.
|
||||||
|
Bool ValidateBufferBindingPointRange(BufferTarget target, Uint first, GLsizei count, const char* funcName);
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl::BufferImpl
|
} // namespace MobileGL::MG_Impl::GLImpl::BufferImpl
|
||||||
|
|||||||
@@ -11,10 +11,11 @@
|
|||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_State/EGLState/Core.h>
|
#include <MG_State/EGLState/Core.h>
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
#include "../Getter/GL_Getter.h"
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
|
static Bool ValidateCurrentProgramForExecution(const char* functionName) {
|
||||||
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||||
if (!currentProgram) {
|
if (!currentProgram) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -36,7 +37,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
static Bool ValidateCurrentProgramForCompute(const char* functionName) {
|
static Bool ValidateCurrentProgramForCompute(const char* functionName) {
|
||||||
if (!ValidateCurrentProgramForExecution(functionName)) return false;
|
if (!ValidateCurrentProgramForExecution(functionName)) return false;
|
||||||
|
|
||||||
const auto& currentProgram = MG_State::pGLContext->GetCurrentProgram();
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||||
if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -72,6 +73,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// Geometry amplification is not modelled here.
|
// Geometry amplification is not modelled here.
|
||||||
static void AccountTransformFeedbackPrimitives(GLenum mode, GLsizei count) {
|
static void AccountTransformFeedbackPrimitives(GLenum mode, GLsizei count) {
|
||||||
if (!MG_State::pGLContext->IsTransformFeedbackActive()) return;
|
if (!MG_State::pGLContext->IsTransformFeedbackActive()) return;
|
||||||
|
// A paused span captures nothing, so a draw made while paused contributes to
|
||||||
|
// PRIMITIVES_GENERATED but not to TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN.
|
||||||
|
if (MG_State::pGLContext->IsTransformFeedbackPaused()) {
|
||||||
|
MG_State::pGLContext->AddTransformFeedbackPausedPrimitives(CountPrimitivesForDraw(mode, count));
|
||||||
|
return;
|
||||||
|
}
|
||||||
Uint64 primitives = CountPrimitivesForDraw(mode, count);
|
Uint64 primitives = CountPrimitivesForDraw(mode, count);
|
||||||
if (primitives == 0) return;
|
if (primitives == 0) return;
|
||||||
MG_State::pGLContext->AddTransformFeedbackInputPrimitives(primitives);
|
MG_State::pGLContext->AddTransformFeedbackInputPrimitives(primitives);
|
||||||
@@ -115,7 +122,36 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
|
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every primitive mode a draw command accepts (GL 4.6 core table 10.1, plus
|
||||||
|
// GL_PATCHES for the tessellation pipeline). Anything else is GL_INVALID_ENUM.
|
||||||
|
static Bool IsAcceptedPrimitiveMode(GLenum mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case GL_POINTS:
|
||||||
|
case GL_LINES:
|
||||||
|
case GL_LINE_LOOP:
|
||||||
|
case GL_LINE_STRIP:
|
||||||
|
case GL_LINES_ADJACENCY:
|
||||||
|
case GL_LINE_STRIP_ADJACENCY:
|
||||||
|
case GL_TRIANGLES:
|
||||||
|
case GL_TRIANGLE_STRIP:
|
||||||
|
case GL_TRIANGLE_FAN:
|
||||||
|
case GL_TRIANGLES_ADJACENCY:
|
||||||
|
case GL_TRIANGLE_STRIP_ADJACENCY:
|
||||||
|
case GL_PATCHES:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
|
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum 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;
|
||||||
|
}
|
||||||
|
|
||||||
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
|
||||||
if (!activeBackendObject) {
|
if (!activeBackendObject) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -133,11 +169,51 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// is the tessellation pipeline's input and reaches the geometry stage already
|
||||||
|
// converted, so it is not constrained here.
|
||||||
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||||
|
const GLenum gsInput = currentProgram ? currentProgram->GetGeometryInputType() : GL_NONE;
|
||||||
|
if (gsInput != GL_NONE && mode != GL_PATCHES) {
|
||||||
|
Bool compatible = false;
|
||||||
|
switch (gsInput) {
|
||||||
|
case GL_POINTS:
|
||||||
|
compatible = mode == GL_POINTS;
|
||||||
|
break;
|
||||||
|
case GL_LINES:
|
||||||
|
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP;
|
||||||
|
break;
|
||||||
|
case GL_LINES_ADJACENCY:
|
||||||
|
compatible = mode == GL_LINES_ADJACENCY || mode == GL_LINE_STRIP_ADJACENCY;
|
||||||
|
break;
|
||||||
|
case GL_TRIANGLES:
|
||||||
|
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN;
|
||||||
|
break;
|
||||||
|
case GL_TRIANGLES_ADJACENCY:
|
||||||
|
compatible = mode == GL_TRIANGLES_ADJACENCY || mode == GL_TRIANGLE_STRIP_ADJACENCY;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!compatible) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", functionName,
|
||||||
|
"Primitive mode is incompatible with the geometry shader's input primitive type."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
// the draw mode itself is unconstrained here. 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).
|
||||||
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||||
|
!MG_State::pGLContext->IsTransformFeedbackPaused() &&
|
||||||
!(MG_State::pGLContext->GetTransformFeedbackProgram() &&
|
!(MG_State::pGLContext->GetTransformFeedbackProgram() &&
|
||||||
MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) {
|
MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) {
|
||||||
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
|
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
|
||||||
@@ -168,6 +244,58 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Byte size of the command structures the indirect draws read (GL 4.6 core 10.3.10).
|
||||||
|
constexpr SizeT kDrawArraysIndirectCommandBytes = 4 * sizeof(Uint32);
|
||||||
|
constexpr SizeT kDrawElementsIndirectCommandBytes = 5 * sizeof(Uint32);
|
||||||
|
|
||||||
|
// Shared preconditions of every *Indirect draw: `indirect` is a byte offset into the
|
||||||
|
// buffer bound to GL_DRAW_INDIRECT_BUFFER, must be 4-byte aligned, and the whole
|
||||||
|
// command has to lie inside that buffer.
|
||||||
|
static Bool ValidateIndirectDrawSource(const char* functionName, const void* indirect, SizeT commandBytes) {
|
||||||
|
const auto offset = reinterpret_cast<uintptr_t>(indirect);
|
||||||
|
if (offset % 4 != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"indirect offset must be a multiple of 4."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& buffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||||
|
if (!buffer) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"No buffer is bound to GL_DRAW_INDIRECT_BUFFER."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset + commandBytes > buffer->GetSize()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"The indirect command extends past the end of the bound "
|
||||||
|
"GL_DRAW_INDIRECT_BUFFER."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index type accepted by the DrawElements family (GL 4.6 core 10.3.9).
|
||||||
|
static Bool ValidateDrawElementsIndexType(const char* functionName, GLenum type) {
|
||||||
|
switch (type) {
|
||||||
|
case GL_UNSIGNED_BYTE:
|
||||||
|
case GL_UNSIGNED_SHORT:
|
||||||
|
case GL_UNSIGNED_INT:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "type is not an accepted index type."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Clear_Backend(GLbitfield mask) {
|
void Clear_Backend(GLbitfield mask) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
@@ -346,6 +474,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||||
|
// GL 4.6 core 19: each num_groups_* must be within GL_MAX_COMPUTE_WORK_GROUP_COUNT
|
||||||
|
// for its dimension. GetIntegeri_v already floors that at the spec minimum.
|
||||||
|
const GLuint numGroups[3] = {numGroupsX, numGroupsY, numGroupsZ};
|
||||||
|
for (GLuint dimension = 0; dimension < 3; ++dimension) {
|
||||||
|
GLint maxGroups = 0;
|
||||||
|
GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, dimension, &maxGroups);
|
||||||
|
if (numGroups[dimension] > static_cast<GLuint>(std::max(maxGroups, 0))) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"num_groups exceeds GL_MAX_COMPUTE_WORK_GROUP_COUNT for dimension " +
|
||||||
|
std::to_string(dimension) + "."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,9 +502,49 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
if (!ValidateCurrentProgramForCompute(__func__)) return;
|
||||||
|
// GL 4.6 core 19: `indirect` is a byte offset into GL_DISPATCH_INDIRECT_BUFFER -
|
||||||
|
// negative or misaligned is INVALID_VALUE, nothing bound is INVALID_OPERATION.
|
||||||
|
if (indirect < 0 || (indirect % 4) != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"indirect must be non-negative and a multiple of 4."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& indirectBuffer =
|
||||||
|
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DispatchIndirect).GetBoundObject();
|
||||||
|
if (!indirectBuffer) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"No buffer is bound to GL_DISPATCH_INDIRECT_BUFFER."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
dispatchComputeIndirect(indirect);
|
dispatchComputeIndirect(indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PatchParameteri(GLenum pname, GLint value) {
|
||||||
|
if (pname != GL_PATCH_VERTICES) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pname must be GL_PATCH_VERTICES."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GLint maxPatchVertices = 32;
|
||||||
|
GetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);
|
||||||
|
if (value <= 0 || value > maxPatchVertices) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"value must be in [1, GL_MAX_PATCH_VERTICES]."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->SetPatchVertices(static_cast<Uint>(value));
|
||||||
|
if (const auto patchParameteri = MG_Backend::gBackendFunctionsTable.GL.PatchParameteri) {
|
||||||
|
patchParameteri(pname, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MemoryBarrier(GLbitfield barriers) {
|
void MemoryBarrier(GLbitfield barriers) {
|
||||||
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
|
auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
|
||||||
if (!memoryBarrier) {
|
if (!memoryBarrier) {
|
||||||
@@ -467,6 +650,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
|
if (!ValidateDrawElementsIndexType(__func__, type)) return;
|
||||||
|
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawElementsIndirectCommandBytes)) return;
|
||||||
DrawElementsIndirect_Backend(mode, type, indirect);
|
DrawElementsIndirect_Backend(mode, type, indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,6 +671,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
void DrawArraysIndirect(GLenum mode, const void* indirect) {
|
||||||
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
if (!ValidateCurrentProgramForExecution(__func__)) return;
|
||||||
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
|
||||||
|
if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
|
||||||
DrawArraysIndirect_Backend(mode, indirect);
|
DrawArraysIndirect_Backend(mode, indirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,7 +740,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is already active."));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is already active."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto& program = MG_State::pGLContext->GetCurrentProgram();
|
const auto& program = MG_State::pGLContext->GetProgramForDraw();
|
||||||
if (!program || !program->GetLinkStatus() || program->GetTransformFeedbackVaryingCount() == 0) {
|
if (!program || !program->GetLinkStatus() || program->GetTransformFeedbackVaryingCount() == 0) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -563,9 +749,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"No program with transform feedback varyings is active."));
|
"No program with transform feedback varyings is active."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Every capture buffer slot the program's mode uses must have a buffer bound.
|
// Every capture buffer slot the program's mode uses must have a buffer bound. A slot
|
||||||
|
// of stride 0 - two consecutive gl_NextBuffer entries - captures nothing and so needs
|
||||||
|
// no binding.
|
||||||
const SizeT usedBufferCount = program->GetTransformFeedbackBufferCount();
|
const SizeT usedBufferCount = program->GetTransformFeedbackBufferCount();
|
||||||
for (SizeT i = 0; i < usedBufferCount; ++i) {
|
for (SizeT i = 0; i < usedBufferCount; ++i) {
|
||||||
|
if (program->GetTransformFeedbackStride(static_cast<Uint32>(i)) == 0) continue;
|
||||||
const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
|
||||||
static_cast<Uint>(i));
|
static_cast<Uint>(i));
|
||||||
if (point.GetBoundObject() == nullptr) {
|
if (point.GetBoundObject() == nullptr) {
|
||||||
@@ -578,6 +767,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program);
|
MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program);
|
||||||
|
if (const auto beginXfb = MG_Backend::gBackendFunctionsTable.GL.BeginTransformFeedback) {
|
||||||
|
beginXfb(primitiveMode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vulkan transform feedback captures triangle strips in plain (i, i+1, i+2)
|
// Vulkan transform feedback captures triangle strips in plain (i, i+1, i+2)
|
||||||
@@ -587,6 +779,12 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
// vertex records of every odd triangle within each emitted strip.
|
// vertex records of every odd triangle within each emitted strip.
|
||||||
static void FixupGsStripCaptureOrder(const SharedPtr<MG_State::GLState::ProgramObject>& program,
|
static void FixupGsStripCaptureOrder(const SharedPtr<MG_State::GLState::ProgramObject>& program,
|
||||||
Uint64 inputPrimitives) {
|
Uint64 inputPrimitives) {
|
||||||
|
// Only Vulkan-order captures need this. A backend that runs the capture on its
|
||||||
|
// own GL/ES driver (it owns the span, hence the EndTransformFeedback entry) has
|
||||||
|
// already produced GL's vertex order, and reordering it again would corrupt it.
|
||||||
|
if (MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback != nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) {
|
if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -649,6 +847,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
const auto capturedProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
|
const auto capturedProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
|
||||||
const Uint64 inputPrimitives = MG_State::pGLContext->GetTransformFeedbackInputPrimitives();
|
const Uint64 inputPrimitives = MG_State::pGLContext->GetTransformFeedbackInputPrimitives();
|
||||||
|
// Closed while the capture state is still active: a backend that captures
|
||||||
|
// through its own driver reads the capture program and buffer bindings here.
|
||||||
|
if (const auto endXfb = MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback) {
|
||||||
|
endXfb();
|
||||||
|
}
|
||||||
MG_State::pGLContext->EndTransformFeedback();
|
MG_State::pGLContext->EndTransformFeedback();
|
||||||
// Captured results must be visible to MapBuffer/GetBufferSubData after
|
// Captured results must be visible to MapBuffer/GetBufferSubData after
|
||||||
// End; the capture targets are host-coherent GPU memory, so completing
|
// End; the capture targets are host-coherent GPU memory, so completing
|
||||||
@@ -665,4 +868,341 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
FixupGsStripCaptureOrder(capturedProgram, inputPrimitives);
|
FixupGsStripCaptureOrder(capturedProgram, inputPrimitives);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PauseTransformFeedback(void) {
|
||||||
|
if (!MG_State::pGLContext->IsTransformFeedbackActive() ||
|
||||||
|
MG_State::pGLContext->IsTransformFeedbackPaused()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"Transform feedback is not active, or is already paused."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->SetTransformFeedbackPaused(true);
|
||||||
|
if (const auto pauseXfb = MG_Backend::gBackendFunctionsTable.GL.PauseTransformFeedback) {
|
||||||
|
pauseXfb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResumeTransformFeedback(void) {
|
||||||
|
if (!MG_State::pGLContext->IsTransformFeedbackActive() ||
|
||||||
|
!MG_State::pGLContext->IsTransformFeedbackPaused()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is not paused."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->SetTransformFeedbackPaused(false);
|
||||||
|
if (const auto resumeXfb = MG_Backend::gBackendFunctionsTable.GL.ResumeTransformFeedback) {
|
||||||
|
resumeXfb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GenTransformFeedbacks(GLsizei n, GLuint* ids) {
|
||||||
|
if (n < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "n must be non-negative."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (n == 0 || ids == nullptr) return;
|
||||||
|
Vector<Uint> names;
|
||||||
|
MG_State::pGLContext->GenTransformFeedbackNames(static_cast<Uint>(n), names);
|
||||||
|
Memcpy(ids, names.data(), static_cast<SizeT>(n) * sizeof(GLuint));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CreateTransformFeedbacks(GLsizei n, GLuint* ids) {
|
||||||
|
if (n < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "n must be non-negative."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (n == 0 || ids == nullptr) return;
|
||||||
|
Vector<Uint> names;
|
||||||
|
MG_State::pGLContext->GenTransformFeedbackNames(static_cast<Uint>(n), names);
|
||||||
|
// Unlike glGenTransformFeedbacks, the names are objects immediately: there is no bind step
|
||||||
|
// to create them from (GL 4.6 core 13.2.1).
|
||||||
|
for (const Uint name : names) {
|
||||||
|
MG_State::pGLContext->CreateTransformFeedbackObject(name);
|
||||||
|
}
|
||||||
|
Memcpy(ids, names.data(), static_cast<SizeT>(n) * sizeof(GLuint));
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Shared front half of the by-name transform feedback entry points: the object has to exist
|
||||||
|
// (INVALID_OPERATION otherwise) before anything else about the call is looked at.
|
||||||
|
Bool ValidateNamedTransformFeedback(GLuint xfb, const char* functionName) {
|
||||||
|
if (!MG_State::pGLContext->IsTransformFeedbackObject(xfb)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
std::to_string(xfb) + " is not a transform feedback object."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ValidateTransformFeedbackBufferIndex(GLuint index, const char* functionName) {
|
||||||
|
if (index >= MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"index exceeds GL_MAX_TRANSFORM_FEEDBACK_BUFFERS."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A capture binding may not be changed while the object is capturing (GL 4.6 core 13.2.2).
|
||||||
|
Bool ValidateNamedTransformFeedbackNotActive(GLuint xfb, const char* functionName) {
|
||||||
|
if (MG_State::pGLContext->IsNamedTransformFeedbackActive(xfb)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"The transform feedback object is capturing."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SharedPtr<MG_State::GLState::BufferObject> ResolveTransformFeedbackBuffer(GLuint buffer,
|
||||||
|
const char* functionName) {
|
||||||
|
if (buffer == 0) return nullptr;
|
||||||
|
if (!MG_State::pGLContext->ValidateBufferName(buffer)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
std::to_string(buffer) + " is not a buffer object."));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return MG_State::pGLContext->GetBufferObject(buffer);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void TransformFeedbackBufferBase(GLuint xfb, GLuint index, GLuint buffer) {
|
||||||
|
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
|
||||||
|
if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
|
||||||
|
if (!ValidateNamedTransformFeedbackNotActive(xfb, __func__)) return;
|
||||||
|
if (buffer != 0 && !MG_State::pGLContext->ValidateBufferName(buffer)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
std::to_string(buffer) + " is not a buffer object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->SetNamedTransformFeedbackBinding(xfb, index,
|
||||||
|
ResolveTransformFeedbackBuffer(buffer, __func__), {},
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TransformFeedbackBufferRange(GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
|
||||||
|
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
|
||||||
|
if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
|
||||||
|
if (!ValidateNamedTransformFeedbackNotActive(xfb, __func__)) return;
|
||||||
|
if (offset < 0 || size <= 0 || (offset % 4) != 0 || (size % 4) != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"offset and size must be non-negative multiples of 4."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (buffer != 0 && !MG_State::pGLContext->ValidateBufferName(buffer)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
std::to_string(buffer) + " is not a buffer object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto bufferObject = ResolveTransformFeedbackBuffer(buffer, __func__);
|
||||||
|
const Range1D range{static_cast<SizeT>(offset), static_cast<SizeT>(offset) + static_cast<SizeT>(size)};
|
||||||
|
MG_State::pGLContext->SetNamedTransformFeedbackBinding(xfb, index, bufferObject, range,
|
||||||
|
bufferObject != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetTransformFeedbackiv(GLuint xfb, GLenum pname, GLint* param) {
|
||||||
|
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
|
||||||
|
if (!param) return;
|
||||||
|
switch (pname) {
|
||||||
|
case GL_TRANSFORM_FEEDBACK_ACTIVE:
|
||||||
|
*param = MG_State::pGLContext->IsNamedTransformFeedbackActive(xfb) ? GL_TRUE : GL_FALSE;
|
||||||
|
return;
|
||||||
|
case GL_TRANSFORM_FEEDBACK_PAUSED:
|
||||||
|
*param = MG_State::pGLContext->IsNamedTransformFeedbackPaused(xfb) ? GL_TRUE : GL_FALSE;
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"pname must be GL_TRANSFORM_FEEDBACK_ACTIVE or _PAUSED."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetTransformFeedbacki_v(GLuint xfb, GLenum pname, GLuint index, GLint* param) {
|
||||||
|
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
|
||||||
|
if (pname != GL_TRANSFORM_FEEDBACK_BUFFER_BINDING) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"pname must be GL_TRANSFORM_FEEDBACK_BUFFER_BINDING."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
|
||||||
|
if (!param) return;
|
||||||
|
const auto binding = MG_State::pGLContext->GetNamedTransformFeedbackBinding(xfb, index);
|
||||||
|
*param = binding.Buffer ? static_cast<GLint>(binding.Buffer->GetExternalIndex()) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetTransformFeedbacki64_v(GLuint xfb, GLenum pname, GLuint index, GLint64* param) {
|
||||||
|
if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
|
||||||
|
if (pname != GL_TRANSFORM_FEEDBACK_BUFFER_START && pname != GL_TRANSFORM_FEEDBACK_BUFFER_SIZE) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"pname must be GL_TRANSFORM_FEEDBACK_BUFFER_START or _SIZE."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
|
||||||
|
if (!param) return;
|
||||||
|
const auto binding = MG_State::pGLContext->GetNamedTransformFeedbackBinding(xfb, index);
|
||||||
|
// glTransformFeedbackBufferBase leaves both at zero; only the range form sets them
|
||||||
|
// (GL 4.6 core table 23.48).
|
||||||
|
if (!binding.Buffer || !binding.HasExplicitRange) {
|
||||||
|
*param = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*param = (pname == GL_TRANSFORM_FEEDBACK_BUFFER_START)
|
||||||
|
? static_cast<GLint64>(binding.Range.start)
|
||||||
|
: static_cast<GLint64>(binding.Range.end - binding.Range.start);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids) {
|
||||||
|
if (n < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "n must be non-negative."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ids == nullptr) return;
|
||||||
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
|
const GLuint id = ids[i];
|
||||||
|
// Unknown names and 0 are silently ignored; an object whose capture span is
|
||||||
|
// still open is not (GL 4.6 core 13.2.1).
|
||||||
|
if (id == 0 || !MG_State::pGLContext->ValidateTransformFeedbackName(id)) continue;
|
||||||
|
if (id == MG_State::pGLContext->GetBoundTransformFeedbackName() &&
|
||||||
|
MG_State::pGLContext->IsTransformFeedbackActive()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"Cannot delete a transform feedback object whose capture is active."));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (const auto deleteXfb = MG_Backend::gBackendFunctionsTable.GL.DeleteTransformFeedback) {
|
||||||
|
deleteXfb(id);
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->MarkTransformFeedbackObjectForDeletion(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BindTransformFeedback(GLenum target, GLuint id) {
|
||||||
|
if (target != GL_TRANSFORM_FEEDBACK) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "target must be GL_TRANSFORM_FEEDBACK."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A running capture pins its object; only a paused one may be swapped out.
|
||||||
|
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
|
||||||
|
!MG_State::pGLContext->IsTransformFeedbackPaused()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"Transform feedback is active and not paused."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
std::to_string(id) + " is not a transform feedback object name."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->BindTransformFeedbackObject(id);
|
||||||
|
if (const auto bindXfb = MG_Backend::gBackendFunctionsTable.GL.BindTransformFeedback) {
|
||||||
|
bindXfb(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLboolean IsTransformFeedback(GLuint id) {
|
||||||
|
// Name 0 is the default object, and a name glGenTransformFeedbacks handed out only
|
||||||
|
// becomes the name of an object once it has been bound.
|
||||||
|
return MG_State::pGLContext->IsTransformFeedbackObject(id) ? GL_TRUE : GL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// glDrawTransformFeedback[Stream][Instanced]: replays the vertices the named object
|
||||||
|
// captured in its last completed span, as if by glDrawArraysInstanced with that count
|
||||||
|
// (GL 4.6 core 10.3.7).
|
||||||
|
static void DrawTransformFeedbackImpl(const char* functionName, GLenum mode, GLuint id, GLuint stream,
|
||||||
|
GLsizei instancecount) {
|
||||||
|
if (!ValidateCurrentProgramForExecution(functionName)) return;
|
||||||
|
if (!ValidatePrimitiveModeForBackend(functionName, mode)) return;
|
||||||
|
if (instancecount < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "instancecount must be non-negative."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
std::to_string(id) + " is not a transform feedback object name."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// GL_MAX_VERTEX_STREAMS is 1, so stream 0 is the only one that exists.
|
||||||
|
if (stream != 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"stream must be less than GL_MAX_VERTEX_STREAMS."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Drawing from an object whose capture is currently open is legal and deliberate:
|
||||||
|
// it is how a transform feedback result is fed straight back into the next span
|
||||||
|
// (ARB_transform_feedback2 lists no such restriction).
|
||||||
|
if (!MG_State::pGLContext->HasTransformFeedbackCompletedSpan(id)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
|
||||||
|
"glEndTransformFeedback has never been called for this object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uint64 vertices = MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id);
|
||||||
|
if (vertices == 0) return;
|
||||||
|
const auto count = static_cast<GLsizei>(vertices);
|
||||||
|
AccountTransformFeedbackPrimitives(mode, count);
|
||||||
|
if (instancecount == 1) {
|
||||||
|
DrawArrays_Backend(mode, 0, count);
|
||||||
|
} else {
|
||||||
|
DrawArraysInstanced_Backend(mode, 0, count, instancecount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawTransformFeedback(GLenum mode, GLuint id) {
|
||||||
|
DrawTransformFeedbackImpl(__func__, mode, id, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawTransformFeedbackInstanced(GLenum mode, GLuint id, GLsizei instancecount) {
|
||||||
|
DrawTransformFeedbackImpl(__func__, mode, id, 0, instancecount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawTransformFeedbackStream(GLenum mode, GLuint id, GLuint stream) {
|
||||||
|
DrawTransformFeedbackImpl(__func__, mode, id, stream, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawTransformFeedbackStreamInstanced(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) {
|
||||||
|
DrawTransformFeedbackImpl(__func__, mode, id, stream, instancecount);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -13,8 +13,25 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
||||||
void BeginTransformFeedback(GLenum primitiveMode);
|
void BeginTransformFeedback(GLenum primitiveMode);
|
||||||
void EndTransformFeedback(void);
|
void EndTransformFeedback(void);
|
||||||
|
void PauseTransformFeedback(void);
|
||||||
|
void ResumeTransformFeedback(void);
|
||||||
|
void GenTransformFeedbacks(GLsizei n, GLuint* ids);
|
||||||
|
void CreateTransformFeedbacks(GLsizei n, GLuint* ids);
|
||||||
|
void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids);
|
||||||
|
void TransformFeedbackBufferBase(GLuint xfb, GLuint index, GLuint buffer);
|
||||||
|
void TransformFeedbackBufferRange(GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
|
||||||
|
void GetTransformFeedbackiv(GLuint xfb, GLenum pname, GLint* param);
|
||||||
|
void GetTransformFeedbacki_v(GLuint xfb, GLenum pname, GLuint index, GLint* param);
|
||||||
|
void GetTransformFeedbacki64_v(GLuint xfb, GLenum pname, GLuint index, GLint64* param);
|
||||||
|
void BindTransformFeedback(GLenum target, GLuint id);
|
||||||
|
GLboolean IsTransformFeedback(GLuint id);
|
||||||
|
void DrawTransformFeedback(GLenum mode, GLuint id);
|
||||||
|
void DrawTransformFeedbackInstanced(GLenum mode, GLuint id, GLsizei instancecount);
|
||||||
|
void DrawTransformFeedbackStream(GLenum mode, GLuint id, GLuint stream);
|
||||||
|
void DrawTransformFeedbackStreamInstanced(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount);
|
||||||
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
|
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
|
||||||
void DispatchComputeIndirect(GLintptr indirect);
|
void DispatchComputeIndirect(GLintptr indirect);
|
||||||
|
void PatchParameteri(GLenum pname, GLint value);
|
||||||
void MemoryBarrier(GLbitfield barriers);
|
void MemoryBarrier(GLbitfield barriers);
|
||||||
void MemoryBarrierByRegion(GLbitfield barriers);
|
void MemoryBarrierByRegion(GLbitfield barriers);
|
||||||
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);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "../Texture/GL_Texture.h"
|
#include "../Texture/GL_Texture.h"
|
||||||
#include "../Drawing/GL_Drawing.h"
|
#include "../Drawing/GL_Drawing.h"
|
||||||
#include "../Program/GL_Program.h"
|
#include "../Program/GL_Program.h"
|
||||||
|
#include "../Program/GL_ProgramPipeline.h"
|
||||||
#include "../RenderState/GL_RenderState.h"
|
#include "../RenderState/GL_RenderState.h"
|
||||||
#include "../Framebuffer/GL_Framebuffer.h"
|
#include "../Framebuffer/GL_Framebuffer.h"
|
||||||
#include "../VertexArray/GL_VertexArray.h"
|
#include "../VertexArray/GL_VertexArray.h"
|
||||||
@@ -292,23 +293,17 @@ DECLARE_GL_FUNCTION_HEAD(void, SamplerParameterfv, GLuint sampler, GLenum pname,
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameteriv, GLuint sampler, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameteriv, sampler, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameteriv, GLuint sampler, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameteriv, sampler, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterfv, GLuint sampler, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterfv, sampler, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterfv, GLuint sampler, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterfv, sampler, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribDivisor, GLuint index, GLuint divisor) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribDivisor, index, divisor)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribDivisor, GLuint index, GLuint divisor) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribDivisor, index, divisor)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedback, target, id)
|
DECLARE_GL_FUNCTION_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindTransformFeedback, target, id)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids)
|
DECLARE_GL_FUNCTION_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacks, n, ids)
|
DECLARE_GL_FUNCTION_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenTransformFeedbacks, n, ids)
|
||||||
// Transform feedback objects are not implemented, so no name is ever a live object. The shared
|
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsTransformFeedback, GLuint id) DECLARE_GL_FUNCTION_END(GLboolean, IsTransformFeedback, id)
|
||||||
// stub returns (type)1, telling a probing caller that every id it invents already exists; GL_FALSE
|
DECLARE_GL_FUNCTION_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PauseTransformFeedback)
|
||||||
// is both truthful and what the spec requires for a name that was never generated.
|
DECLARE_GL_FUNCTION_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ResumeTransformFeedback)
|
||||||
MOBILEGL_GL_API GLboolean glIsTransformFeedback(GLuint id) {
|
DECLARE_GL_FUNCTION_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary)
|
||||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramBinary, GLuint program, GLenum binaryFormat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramBinary, program, binaryFormat, binary, length)
|
||||||
return GL_FALSE;
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramParameteri, GLuint program, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramParameteri, program, pname, value)
|
||||||
}
|
DECLARE_GL_FUNCTION_HEAD(void, InvalidateFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments) DECLARE_GL_FUNCTION_END_NO_RETURN(void, InvalidateFramebuffer, target, numAttachments, attachments)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedback)
|
DECLARE_GL_FUNCTION_HEAD(void, InvalidateSubFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, InvalidateSubFramebuffer, target, numAttachments, attachments, x, y, width, height)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedback)
|
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary)
|
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramBinary, GLuint program, GLenum binaryFormat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramBinary, program, binaryFormat, binary, length)
|
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramParameteri, GLuint program, GLenum pname, GLint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramParameteri, program, pname, value)
|
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateFramebuffer, target, numAttachments, attachments)
|
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateSubFramebuffer, GLenum target, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateSubFramebuffer, target, numAttachments, attachments, x, y, width, height)
|
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TexStorage2D, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage2D, target, levels, internalformat, width, height)
|
DECLARE_GL_FUNCTION_HEAD(void, TexStorage2D, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage2D, target, levels, internalformat, width, height)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TexStorage3D, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage3D, target, levels, internalformat, width, height, depth)
|
DECLARE_GL_FUNCTION_HEAD(void, TexStorage3D, GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage3D, target, levels, internalformat, width, height, depth)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetInternalformativ, GLenum target, GLenum internalformat, GLenum pname, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetInternalformativ, target, internalformat, pname, bufSize, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetInternalformativ, GLenum target, GLenum internalformat, GLenum pname, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetInternalformativ, target, internalformat, pname, bufSize, params)
|
||||||
@@ -316,21 +311,21 @@ DECLARE_GL_FUNCTION_HEAD(void, DispatchCompute, GLuint num_groups_x, GLuint num_
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, DispatchComputeIndirect, GLintptr indirect) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DispatchComputeIndirect, indirect)
|
DECLARE_GL_FUNCTION_HEAD(void, DispatchComputeIndirect, GLintptr indirect) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DispatchComputeIndirect, indirect)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysIndirect, GLenum mode, const void* indirect) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysIndirect, mode, indirect)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawArraysIndirect, GLenum mode, const void* indirect) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawArraysIndirect, mode, indirect)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsIndirect, GLenum mode, GLenum type, const void* indirect) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsIndirect, mode, type, indirect)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsIndirect, GLenum mode, GLenum type, const void* indirect) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsIndirect, mode, type, indirect)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferParameteri, GLenum target, GLenum pname, GLint param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferParameteri, target, pname, param)
|
DECLARE_GL_FUNCTION_HEAD(void, FramebufferParameteri, GLenum target, GLenum pname, GLint param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferParameteri, target, pname, param)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetFramebufferParameteriv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetFramebufferParameteriv, target, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetFramebufferParameteriv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFramebufferParameteriv, target, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetProgramInterfaceiv, GLuint program, GLenum programInterface, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramInterfaceiv, program, programInterface, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetProgramInterfaceiv, GLuint program, GLenum programInterface, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramInterfaceiv, program, programInterface, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(GLuint, GetProgramResourceIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLuint, GetProgramResourceIndex, program, programInterface, name)
|
DECLARE_GL_FUNCTION_HEAD(GLuint, GetProgramResourceIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLuint, GetProgramResourceIndex, program, programInterface, name)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetProgramResourceName, GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length, GLchar* name) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramResourceName, program, programInterface, index, bufSize, length, name)
|
DECLARE_GL_FUNCTION_HEAD(void, GetProgramResourceName, GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length, GLchar* name) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramResourceName, program, programInterface, index, bufSize, length, name)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetProgramResourceiv, GLuint program, GLenum programInterface, GLuint index, GLsizei propCount, const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramResourceiv, program, programInterface, index, propCount, props, bufSize, length, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetProgramResourceiv, GLuint program, GLenum programInterface, GLuint index, GLsizei propCount, const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramResourceiv, program, programInterface, index, propCount, props, bufSize, length, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocation, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocation, program, programInterface, name)
|
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocation, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocation, program, programInterface, name)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UseProgramStages, GLuint pipeline, GLbitfield stages, GLuint program) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UseProgramStages, pipeline, stages, program)
|
DECLARE_GL_FUNCTION_HEAD(void, UseProgramStages, GLuint pipeline, GLbitfield stages, GLuint program) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UseProgramStages, pipeline, stages, program)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ActiveShaderProgram, GLuint pipeline, GLuint program) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ActiveShaderProgram, pipeline, program)
|
DECLARE_GL_FUNCTION_HEAD(void, ActiveShaderProgram, GLuint pipeline, GLuint program) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ActiveShaderProgram, pipeline, program)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, CreateShaderProgramv, GLenum type, GLsizei count, const GLchar* const* strings) DECLARE_GL_FUNCTION_STUB_END(GLuint, CreateShaderProgramv, type, count, strings)
|
DECLARE_GL_FUNCTION_HEAD(GLuint, CreateShaderProgramv, GLenum type, GLsizei count, const GLchar* const* strings) DECLARE_GL_FUNCTION_END(GLuint, CreateShaderProgramv, type, count, strings)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindProgramPipeline, pipeline)
|
DECLARE_GL_FUNCTION_HEAD(void, BindProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindProgramPipeline, pipeline)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteProgramPipelines, GLsizei n, const GLuint* pipelines) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteProgramPipelines, n, pipelines)
|
DECLARE_GL_FUNCTION_HEAD(void, DeleteProgramPipelines, GLsizei n, const GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DeleteProgramPipelines, n, pipelines)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenProgramPipelines, n, pipelines)
|
DECLARE_GL_FUNCTION_HEAD(void, GenProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenProgramPipelines, n, pipelines)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsProgramPipeline, pipeline)
|
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_END(GLboolean, IsProgramPipeline, pipeline)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramPipelineiv, GLuint pipeline, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramPipelineiv, pipeline, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetProgramPipelineiv, GLuint pipeline, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramPipelineiv, pipeline, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform1i, GLuint program, GLint location, GLint v0) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform1i, program, location, v0)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform1i, GLuint program, GLint location, GLint v0) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform1i, program, location, v0)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform2i, GLuint program, GLint location, GLint v0, GLint v1) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform2i, program, location, v0, v1)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform2i, GLuint program, GLint location, GLint v0, GLint v1) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform2i, program, location, v0, v1)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform3i, GLuint program, GLint location, GLint v0, GLint v1, GLint v2) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform3i, program, location, v0, v1, v2)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform3i, GLuint program, GLint location, GLint v0, GLint v1, GLint v2) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform3i, program, location, v0, v1, v2)
|
||||||
@@ -364,8 +359,8 @@ DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2x4fv, GLuint program, GLint
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x2fv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x2fv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x2fv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x2fv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3x4fv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3x4fv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3x4fv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3x4fv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x3fv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x3fv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x3fv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x3fv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ValidateProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ValidateProgramPipeline, pipeline)
|
DECLARE_GL_FUNCTION_HEAD(void, ValidateProgramPipeline, GLuint pipeline) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ValidateProgramPipeline, pipeline)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramPipelineInfoLog, GLuint pipeline, GLsizei bufSize, GLsizei* length, GLchar* infoLog) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramPipelineInfoLog, pipeline, bufSize, length, infoLog)
|
DECLARE_GL_FUNCTION_HEAD(void, GetProgramPipelineInfoLog, GLuint pipeline, GLsizei bufSize, GLsizei* length, GLchar* infoLog) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetProgramPipelineInfoLog, pipeline, bufSize, length, infoLog)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, BindImageTexture, GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindImageTexture, unit, texture, level, layered, layer, access, format)
|
DECLARE_GL_FUNCTION_HEAD(void, BindImageTexture, GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindImageTexture, unit, texture, level, layered, layer, access, format)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetBooleani_v, GLenum target, GLuint index, GLboolean* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetBooleani_v, target, index, data)
|
DECLARE_GL_FUNCTION_HEAD(void, GetBooleani_v, GLenum target, GLuint index, GLboolean* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetBooleani_v, target, index, data)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, MemoryBarrier, GLbitfield barriers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MemoryBarrier, barriers)
|
DECLARE_GL_FUNCTION_HEAD(void, MemoryBarrier, GLbitfield barriers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MemoryBarrier, barriers)
|
||||||
@@ -425,12 +420,12 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertex, GLenum mode, GLs
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture, GLenum target, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture, target, attachment, texture, level)
|
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture, GLenum target, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture, target, attachment, texture, level)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBox, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrimitiveBoundingBox, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBox, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrimitiveBoundingBox, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
|
||||||
DECLARE_GL_FUNCTION_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_END(GLenum, GetGraphicsResetStatus)
|
DECLARE_GL_FUNCTION_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_END(GLenum, GetGraphicsResetStatus)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data)
|
DECLARE_GL_FUNCTION_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformuiv, GLuint program, GLint location, GLsizei bufSize, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformuiv, program, location, bufSize, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MinSampleShading, value)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, MinSampleShading, GLfloat value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MinSampleShading, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameteri, pname, value)
|
DECLARE_GL_FUNCTION_HEAD(void, PatchParameteri, GLenum pname, GLint value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PatchParameteri, pname, value)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIiv, GLenum target, GLenum pname, const GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIiv, target, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIuiv, GLenum target, GLenum pname, const GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIuiv, target, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, TexParameterIuiv, GLenum target, GLenum pname, const GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterIuiv, target, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetTexParameterIiv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTexParameterIiv, target, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetTexParameterIiv, GLenum target, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTexParameterIiv, target, pname, params)
|
||||||
@@ -440,7 +435,7 @@ DECLARE_GL_FUNCTION_HEAD(void, SamplerParameterIuiv, GLuint sampler, GLenum pnam
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterIiv, GLuint sampler, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterIiv, sampler, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterIiv, GLuint sampler, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterIiv, sampler, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterIuiv, GLuint sampler, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterIuiv, sampler, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetSamplerParameterIuiv, GLuint sampler, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetSamplerParameterIuiv, sampler, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TexBuffer, GLenum target, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexBuffer, target, internalformat, buffer)
|
DECLARE_GL_FUNCTION_HEAD(void, TexBuffer, GLenum target, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexBuffer, target, internalformat, buffer)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TexBufferRange, GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TexBufferRange, target, internalformat, buffer, offset, size)
|
DECLARE_GL_FUNCTION_HEAD(void, TexBufferRange, GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexBufferRange, target, internalformat, buffer, offset, size)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TexStorage3DMultisample, GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage3DMultisample, target, samples, internalformat, width, height, depth, fixedsamplelocations)
|
DECLARE_GL_FUNCTION_HEAD(void, TexStorage3DMultisample, GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexStorage3DMultisample, target, samples, internalformat, width, height, depth, fixedsamplelocations)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void*, MapBufferRange, GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access) DECLARE_GL_FUNCTION_END(void*, MapBufferRange, target, offset, length, access)
|
DECLARE_GL_FUNCTION_HEAD(void*, MapBufferRange, GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access) DECLARE_GL_FUNCTION_END(void*, MapBufferRange, target, offset, length, access)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearIndex, GLfloat c) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearIndex, c)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearIndex, GLfloat c) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearIndex, c)
|
||||||
@@ -915,24 +910,24 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ColorP4ui, GLenum type, GLuint color) DECLAR
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ColorP4uiv, GLenum type, const GLuint* color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ColorP4uiv, type, color)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, ColorP4uiv, GLenum type, const GLuint* color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ColorP4uiv, type, color)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColorP3ui, GLenum type, GLuint color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColorP3ui, type, color)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColorP3ui, GLenum type, GLuint color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColorP3ui, type, color)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColorP3uiv, GLenum type, const GLuint* color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColorP3uiv, type, color)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, SecondaryColorP3uiv, GLenum type, const GLuint* color) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SecondaryColorP3uiv, type, color)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1d, GLint location, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1d, location, x)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform1d, GLint location, GLdouble x) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform1d, location, x)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2d, GLint location, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2d, location, x, y)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform2d, GLint location, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform2d, location, x, y)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3d, GLint location, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3d, location, x, y, z)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform3d, GLint location, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform3d, location, x, y, z)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform4d, GLint location, GLdouble x, GLdouble y, GLdouble z, GLdouble w) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform4d, location, x, y, z, w)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform4d, GLint location, GLdouble x, GLdouble y, GLdouble z, GLdouble w) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform4d, location, x, y, z, w)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1dv, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform1dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform1dv, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2dv, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform2dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform2dv, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3dv, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform3dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform3dv, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform4dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform4dv, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, Uniform4dv, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Uniform4dv, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix2dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix2dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix3dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix3dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix4dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix4dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix2x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix2x3dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix2x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix2x3dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix2x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix2x4dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix2x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix2x4dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix3x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix3x2dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix3x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix3x2dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix3x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix3x4dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix3x4dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix3x4dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix4x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix4x2dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix4x2dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix4x2dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformMatrix4x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformMatrix4x3dv, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, UniformMatrix4x3dv, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, UniformMatrix4x3dv, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformdv, GLuint program, GLint location, GLdouble* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformdv, program, location, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetUniformdv, GLuint program, GLint location, GLdouble* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetUniformdv, program, location, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLint, GetSubroutineUniformLocation, GLuint program, GLenum shadertype, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLint, GetSubroutineUniformLocation, program, shadertype, name)
|
DECLARE_GL_FUNCTION_STUB_HEAD(GLint, GetSubroutineUniformLocation, GLuint program, GLenum shadertype, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLint, GetSubroutineUniformLocation, program, shadertype, name)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, GetSubroutineIndex, GLuint program, GLenum shadertype, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLuint, GetSubroutineIndex, program, shadertype, name)
|
DECLARE_GL_FUNCTION_STUB_HEAD(GLuint, GetSubroutineIndex, GLuint program, GLenum shadertype, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END(GLuint, GetSubroutineIndex, program, shadertype, name)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveSubroutineUniformiv, GLuint program, GLenum shadertype, GLuint index, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveSubroutineUniformiv, program, shadertype, index, pname, values)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveSubroutineUniformiv, GLuint program, GLenum shadertype, GLuint index, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveSubroutineUniformiv, program, shadertype, index, pname, values)
|
||||||
@@ -942,28 +937,28 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformSubroutinesuiv, GLenum shadertype, GL
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformSubroutineuiv, GLenum shadertype, GLint location, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformSubroutineuiv, shadertype, location, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetUniformSubroutineuiv, GLenum shadertype, GLint location, GLuint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetUniformSubroutineuiv, shadertype, location, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStageiv, GLuint program, GLenum shadertype, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStageiv, program, shadertype, pname, values)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStageiv, GLuint program, GLenum shadertype, GLenum pname, GLint* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStageiv, program, shadertype, pname, values)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameterfv, pname, values)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PatchParameterfv, GLenum pname, const GLfloat* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PatchParameterfv, pname, values)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedback, mode, id)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedback, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedback, mode, id)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream)
|
DECLARE_GL_FUNCTION_HEAD(void, DrawTransformFeedbackStream, GLenum mode, GLuint id, GLuint stream) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawTransformFeedbackStream, mode, id, stream)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginQueryIndexed, target, index, id)
|
DECLARE_GL_FUNCTION_HEAD(void, BeginQueryIndexed, GLenum target, GLuint index, GLuint id) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginQueryIndexed, target, index, id)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndQueryIndexed, GLenum target, GLuint index) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndQueryIndexed, target, index)
|
DECLARE_GL_FUNCTION_HEAD(void, EndQueryIndexed, GLenum target, GLuint index) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndQueryIndexed, target, index)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryIndexediv, GLenum target, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryIndexediv, target, index, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetQueryIndexediv, GLenum target, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryIndexediv, target, index, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform1d, GLuint program, GLint location, GLdouble v0) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform1d, program, location, v0)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform1d, GLuint program, GLint location, GLdouble v0) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform1d, program, location, v0)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform1dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform1dv, program, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform1dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform1dv, program, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform2d, GLuint program, GLint location, GLdouble v0, GLdouble v1) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform2d, program, location, v0, v1)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform2d, GLuint program, GLint location, GLdouble v0, GLdouble v1) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform2d, program, location, v0, v1)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform2dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform2dv, program, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform2dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform2dv, program, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform3d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform3d, program, location, v0, v1, v2)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform3d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform3d, program, location, v0, v1, v2)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform3dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform3dv, program, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform3dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform3dv, program, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform4d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform4d, program, location, v0, v1, v2, v3)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform4d, GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform4d, program, location, v0, v1, v2, v3)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniform4dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniform4dv, program, location, count, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniform4dv, GLuint program, GLint location, GLsizei count, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniform4dv, program, location, count, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix2dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix2dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix3dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix4dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix2x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix2x3dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix2x3dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix3x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix3x2dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3x2dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix2x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix2x4dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix2x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix2x4dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix4x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix4x2dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x2dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x2dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix3x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix3x4dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix3x4dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix3x4dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ProgramUniformMatrix4x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ProgramUniformMatrix4x3dv, program, location, count, transpose, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ProgramUniformMatrix4x3dv, GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ProgramUniformMatrix4x3dv, program, location, count, transpose, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL1d, GLuint index, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL1d, index, x)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL1d, GLuint index, GLdouble x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL1d, index, x)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL2d, GLuint index, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL2d, index, x, y)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL2d, GLuint index, GLdouble x, GLdouble y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL2d, index, x, y)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3d, GLuint index, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL3d, index, x, y, z)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribL3d, GLuint index, GLdouble x, GLdouble y, GLdouble z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribL3d, index, x, y, z)
|
||||||
@@ -982,14 +977,14 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexed, GLuint index, GLint left, GL
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, ScissorIndexedv, GLuint index, const GLint* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ScissorIndexedv, index, v)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeArrayv, GLuint first, GLsizei count, const GLdouble* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeArrayv, first, count, v)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, DepthRangeIndexed, GLuint index, GLdouble n, GLdouble f) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DepthRangeIndexed, index, n, f)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetFloati_v, target, index, data)
|
DECLARE_GL_FUNCTION_HEAD(void, GetFloati_v, GLenum target, GLuint index, GLfloat* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetFloati_v, target, index, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetDoublei_v, target, index, data)
|
DECLARE_GL_FUNCTION_HEAD(void, GetDoublei_v, GLenum target, GLuint index, GLdouble* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetDoublei_v, target, index, data)
|
||||||
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_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_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_STUB_HEAD(void, DrawTransformFeedbackInstanced, GLenum mode, GLuint id, GLsizei instancecount) DECLARE_GL_FUNCTION_STUB_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_STUB_HEAD(void, DrawTransformFeedbackStreamInstanced, GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) DECLARE_GL_FUNCTION_STUB_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_STUB_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferData, GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferData, target, internalformat, format, type, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearBufferSubData, GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearBufferSubData, target, internalformat, offset, size, format, type, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInternalformati64v, GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInternalformati64v, target, internalformat, pname, count, params)
|
||||||
@@ -1002,7 +997,7 @@ DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirect, GLenum mode, GLenum ty
|
|||||||
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocationIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocationIndex, program, programInterface, name)
|
DECLARE_GL_FUNCTION_HEAD(GLint, GetProgramResourceLocationIndex, GLuint program, GLenum programInterface, const GLchar* name) DECLARE_GL_FUNCTION_END(GLint, GetProgramResourceLocationIndex, program, programInterface, name)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderStorageBlockBinding, program, storageBlockIndex, storageBlockBinding)
|
DECLARE_GL_FUNCTION_HEAD(void, ShaderStorageBlockBinding, GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderStorageBlockBinding, program, storageBlockIndex, storageBlockBinding)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureView, GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureView, texture, target, origtexture, internalformat, minlevel, numlevels, minlayer, numlayers)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribLFormat, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribLFormat, attribindex, size, type, relativeoffset)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
|
DECLARE_GL_FUNCTION_HEAD(void, BufferStorage, GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BufferStorage, target, size, data, flags)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
|
DECLARE_GL_FUNCTION_HEAD(void, ClearTexImage, GLuint texture, GLint level, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexImage, texture, level, format, type, data)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data)
|
DECLARE_GL_FUNCTION_HEAD(void, ClearTexSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearTexSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, data)
|
||||||
@@ -1013,12 +1008,12 @@ DECLARE_GL_FUNCTION_HEAD(void, BindSamplers, GLuint first, GLsizei count, const
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindImageTextures, first, count, textures)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindImageTextures, GLuint first, GLsizei count, const GLuint* textures) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindImageTextures, first, count, textures)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides)
|
DECLARE_GL_FUNCTION_HEAD(void, BindVertexBuffers, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets, const GLsizei* strides) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindVertexBuffers, first, count, buffers, offsets, strides)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClipControl, origin, depth)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClipControl, GLenum origin, GLenum depth) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClipControl, origin, depth)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
|
DECLARE_GL_FUNCTION_HEAD(void, CreateTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTransformFeedbacks, n, ids)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer)
|
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferBase, GLuint xfb, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferBase, xfb, index, buffer)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size)
|
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackBufferRange, GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackBufferRange, xfb, index, buffer, offset, size)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbackiv, GLuint xfb, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbackiv, xfb, pname, param)
|
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbackiv, GLuint xfb, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbackiv, xfb, pname, param)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbacki_v, GLuint xfb, GLenum pname, GLuint index, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbacki_v, xfb, pname, index, param)
|
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbacki_v, GLuint xfb, GLenum pname, GLuint index, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbacki_v, xfb, pname, index, param)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbacki64_v, GLuint xfb, GLenum pname, GLuint index, GLint64* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbacki64_v, xfb, pname, index, param)
|
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbacki64_v, GLuint xfb, GLenum pname, GLuint index, GLint64* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbacki64_v, xfb, pname, index, param)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, CreateBuffers, GLsizei n, GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateBuffers, n, buffers)
|
DECLARE_GL_FUNCTION_HEAD(void, CreateBuffers, GLsizei n, GLuint* buffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateBuffers, n, buffers)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, NamedBufferStorage, GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedBufferStorage, buffer, size, data, flags)
|
DECLARE_GL_FUNCTION_HEAD(void, NamedBufferStorage, GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedBufferStorage, buffer, size, data, flags)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, NamedBufferData, GLuint buffer, GLsizeiptr size, const void* data, GLenum usage) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedBufferData, buffer, size, data, usage)
|
DECLARE_GL_FUNCTION_HEAD(void, NamedBufferData, GLuint buffer, GLsizeiptr size, const void* data, GLenum usage) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedBufferData, buffer, size, data, usage)
|
||||||
@@ -1031,32 +1026,32 @@ DECLARE_GL_FUNCTION_HEAD(void, FlushMappedNamedBufferRange, GLuint buffer, GLint
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferParameteriv, GLuint buffer, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferParameteriv, buffer, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferParameteriv, GLuint buffer, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferParameteriv, buffer, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferParameteri64v, GLuint buffer, GLenum pname, GLint64* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferParameteri64v, buffer, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferParameteri64v, GLuint buffer, GLenum pname, GLint64* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferParameteri64v, buffer, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferPointerv, GLuint buffer, GLenum pname, void** params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferPointerv, buffer, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferPointerv, GLuint buffer, GLenum pname, void** params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferPointerv, buffer, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetNamedBufferSubData, GLuint buffer, GLintptr offset, GLsizeiptr size, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetNamedBufferSubData, buffer, offset, size, data)
|
DECLARE_GL_FUNCTION_HEAD(void, GetNamedBufferSubData, GLuint buffer, GLintptr offset, GLsizeiptr size, void* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedBufferSubData, buffer, offset, size, data)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, CreateFramebuffers, GLsizei n, GLuint* framebuffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateFramebuffers, n, framebuffers)
|
DECLARE_GL_FUNCTION_HEAD(void, CreateFramebuffers, GLsizei n, GLuint* framebuffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateFramebuffers, n, framebuffers)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferRenderbuffer, GLuint framebuffer, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferRenderbuffer, framebuffer, attachment, renderbuffertarget, renderbuffer)
|
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferRenderbuffer, GLuint framebuffer, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferRenderbuffer, framebuffer, attachment, renderbuffertarget, renderbuffer)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, NamedFramebufferParameteri, GLuint framebuffer, GLenum pname, GLint param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, NamedFramebufferParameteri, framebuffer, pname, param)
|
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferParameteri, GLuint framebuffer, GLenum pname, GLint param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferParameteri, framebuffer, pname, param)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferTexture, GLuint framebuffer, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferTexture, framebuffer, attachment, texture, level)
|
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferTexture, GLuint framebuffer, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferTexture, framebuffer, attachment, texture, level)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferTextureLayer, GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLint layer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferTextureLayer, framebuffer, attachment, texture, level, layer)
|
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferTextureLayer, GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLint layer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferTextureLayer, framebuffer, attachment, texture, level, layer)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferDrawBuffer, GLuint framebuffer, GLenum buf) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferDrawBuffer, framebuffer, buf)
|
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferDrawBuffer, GLuint framebuffer, GLenum buf) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferDrawBuffer, framebuffer, buf)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferDrawBuffers, GLuint framebuffer, GLsizei n, const GLenum* bufs) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferDrawBuffers, framebuffer, n, bufs)
|
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferDrawBuffers, GLuint framebuffer, GLsizei n, const GLenum* bufs) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferDrawBuffers, framebuffer, n, bufs)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferReadBuffer, GLuint framebuffer, GLenum src) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferReadBuffer, framebuffer, src)
|
DECLARE_GL_FUNCTION_HEAD(void, NamedFramebufferReadBuffer, GLuint framebuffer, GLenum src) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedFramebufferReadBuffer, framebuffer, src)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateNamedFramebufferData, GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateNamedFramebufferData, framebuffer, numAttachments, attachments)
|
DECLARE_GL_FUNCTION_HEAD(void, InvalidateNamedFramebufferData, GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments) DECLARE_GL_FUNCTION_END_NO_RETURN(void, InvalidateNamedFramebufferData, framebuffer, numAttachments, attachments)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, InvalidateNamedFramebufferSubData, GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, InvalidateNamedFramebufferSubData, framebuffer, numAttachments, attachments, x, y, width, height)
|
DECLARE_GL_FUNCTION_HEAD(void, InvalidateNamedFramebufferSubData, GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, InvalidateNamedFramebufferSubData, framebuffer, numAttachments, attachments, x, y, width, height)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearNamedFramebufferiv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLint* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearNamedFramebufferiv, framebuffer, buffer, drawbuffer, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ClearNamedFramebufferiv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLint* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearNamedFramebufferiv, framebuffer, buffer, drawbuffer, value)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ClearNamedFramebufferuiv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLuint* value) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ClearNamedFramebufferuiv, framebuffer, buffer, drawbuffer, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ClearNamedFramebufferuiv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLuint* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearNamedFramebufferuiv, framebuffer, buffer, drawbuffer, value)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ClearNamedFramebufferfv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearNamedFramebufferfv, framebuffer, buffer, drawbuffer, value)
|
DECLARE_GL_FUNCTION_HEAD(void, ClearNamedFramebufferfv, GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLfloat* value) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearNamedFramebufferfv, framebuffer, buffer, drawbuffer, value)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, ClearNamedFramebufferfi, GLuint framebuffer, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearNamedFramebufferfi, framebuffer, buffer, drawbuffer, depth, stencil)
|
DECLARE_GL_FUNCTION_HEAD(void, ClearNamedFramebufferfi, GLuint framebuffer, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearNamedFramebufferfi, framebuffer, buffer, drawbuffer, depth, stencil)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, BlitNamedFramebuffer, GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BlitNamedFramebuffer, readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter)
|
DECLARE_GL_FUNCTION_HEAD(void, BlitNamedFramebuffer, GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BlitNamedFramebuffer, readFramebuffer, drawFramebuffer, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter)
|
||||||
DECLARE_GL_FUNCTION_HEAD(GLenum, CheckNamedFramebufferStatus, GLuint framebuffer, GLenum target) DECLARE_GL_FUNCTION_END(GLenum, CheckNamedFramebufferStatus, framebuffer, target)
|
DECLARE_GL_FUNCTION_HEAD(GLenum, CheckNamedFramebufferStatus, GLuint framebuffer, GLenum target) DECLARE_GL_FUNCTION_END(GLenum, CheckNamedFramebufferStatus, framebuffer, target)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetNamedFramebufferParameteriv, GLuint framebuffer, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetNamedFramebufferParameteriv, framebuffer, pname, param)
|
DECLARE_GL_FUNCTION_HEAD(void, GetNamedFramebufferParameteriv, GLuint framebuffer, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedFramebufferParameteriv, framebuffer, pname, param)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetNamedFramebufferAttachmentParameteriv, GLuint framebuffer, GLenum attachment, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedFramebufferAttachmentParameteriv, framebuffer, attachment, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetNamedFramebufferAttachmentParameteriv, GLuint framebuffer, GLenum attachment, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedFramebufferAttachmentParameteriv, framebuffer, attachment, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, CreateRenderbuffers, GLsizei n, GLuint* renderbuffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateRenderbuffers, n, renderbuffers)
|
DECLARE_GL_FUNCTION_HEAD(void, CreateRenderbuffers, GLsizei n, GLuint* renderbuffers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateRenderbuffers, n, renderbuffers)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, NamedRenderbufferStorage, GLuint renderbuffer, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedRenderbufferStorage, renderbuffer, internalformat, width, height)
|
DECLARE_GL_FUNCTION_HEAD(void, NamedRenderbufferStorage, GLuint renderbuffer, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedRenderbufferStorage, renderbuffer, internalformat, width, height)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, NamedRenderbufferStorageMultisample, GLuint renderbuffer, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedRenderbufferStorageMultisample, renderbuffer, samples, internalformat, width, height)
|
DECLARE_GL_FUNCTION_HEAD(void, NamedRenderbufferStorageMultisample, GLuint renderbuffer, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, NamedRenderbufferStorageMultisample, renderbuffer, samples, internalformat, width, height)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetNamedRenderbufferParameteriv, GLuint renderbuffer, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedRenderbufferParameteriv, renderbuffer, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetNamedRenderbufferParameteriv, GLuint renderbuffer, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetNamedRenderbufferParameteriv, renderbuffer, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, CreateTextures, GLenum target, GLsizei n, GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTextures, target, n, textures)
|
DECLARE_GL_FUNCTION_HEAD(void, CreateTextures, GLenum target, GLsizei n, GLuint* textures) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateTextures, target, n, textures)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBuffer, GLuint texture, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBuffer, texture, internalformat, buffer)
|
DECLARE_GL_FUNCTION_HEAD(void, TextureBuffer, GLuint texture, GLenum internalformat, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBuffer, texture, internalformat, buffer)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, TextureBufferRange, GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TextureBufferRange, texture, internalformat, buffer, offset, size)
|
DECLARE_GL_FUNCTION_HEAD(void, TextureBufferRange, GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBufferRange, texture, internalformat, buffer, offset, size)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage1D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage1D, texture, levels, internalformat, width)
|
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage1D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage1D, texture, levels, internalformat, width)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage2D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage2D, texture, levels, internalformat, width, height)
|
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage2D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage2D, texture, levels, internalformat, width, height)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage3D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage3D, texture, levels, internalformat, width, height, depth)
|
DECLARE_GL_FUNCTION_HEAD(void, TextureStorage3D, GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureStorage3D, texture, levels, internalformat, width, height, depth)
|
||||||
@@ -1068,9 +1063,9 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureSubImage3D, GLuint texture, GLint level, G
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage1D, texture, level, xoffset, width, format, imageSize, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage2D, texture, level, xoffset, yoffset, width, height, format, imageSize, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompressedTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompressedTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
|
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage1D, GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage1D, texture, level, xoffset, x, y, width)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
|
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage2D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage2D, texture, level, xoffset, yoffset, x, y, width, height)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
|
DECLARE_GL_FUNCTION_HEAD(void, CopyTextureSubImage3D, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyTextureSubImage3D, texture, level, xoffset, yoffset, zoffset, x, y, width, height)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterf, GLuint texture, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterf, texture, pname, param)
|
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterf, GLuint texture, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterf, texture, pname, param)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterfv, GLuint texture, GLenum pname, const GLfloat* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterfv, texture, pname, param)
|
DECLARE_GL_FUNCTION_HEAD(void, TextureParameterfv, GLuint texture, GLenum pname, const GLfloat* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameterfv, texture, pname, param)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, TextureParameteri, GLuint texture, GLenum pname, GLint param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameteri, texture, pname, param)
|
DECLARE_GL_FUNCTION_HEAD(void, TextureParameteri, GLuint texture, GLenum pname, GLint param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureParameteri, texture, pname, param)
|
||||||
@@ -1080,7 +1075,7 @@ DECLARE_GL_FUNCTION_HEAD(void, TextureParameteriv, GLuint texture, GLenum pname,
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, GenerateTextureMipmap, GLuint texture) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenerateTextureMipmap, texture)
|
DECLARE_GL_FUNCTION_HEAD(void, GenerateTextureMipmap, GLuint texture) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenerateTextureMipmap, texture)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, BindTextureUnit, GLuint unit, GLuint texture) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindTextureUnit, unit, texture)
|
DECLARE_GL_FUNCTION_HEAD(void, BindTextureUnit, GLuint unit, GLuint texture) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindTextureUnit, unit, texture)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetTextureImage, GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureImage, texture, level, format, type, bufSize, pixels)
|
DECLARE_GL_FUNCTION_HEAD(void, GetTextureImage, GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureImage, texture, level, format, type, bufSize, pixels)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureImage, GLuint texture, GLint level, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureImage, texture, level, bufSize, pixels)
|
DECLARE_GL_FUNCTION_HEAD(void, GetCompressedTextureImage, GLuint texture, GLint level, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetCompressedTextureImage, texture, level, bufSize, pixels)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetTextureLevelParameterfv, GLuint texture, GLint level, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureLevelParameterfv, texture, level, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetTextureLevelParameterfv, GLuint texture, GLint level, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureLevelParameterfv, texture, level, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetTextureLevelParameteriv, GLuint texture, GLint level, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureLevelParameteriv, texture, level, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetTextureLevelParameteriv, GLuint texture, GLint level, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureLevelParameteriv, texture, level, pname, params)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetTextureParameterfv, GLuint texture, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureParameterfv, texture, pname, params)
|
DECLARE_GL_FUNCTION_HEAD(void, GetTextureParameterfv, GLuint texture, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureParameterfv, texture, pname, params)
|
||||||
@@ -1096,18 +1091,18 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexArrayVertexBuffers, GLuint vaobj, GLuint fi
|
|||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribBinding, GLuint vaobj, GLuint attribindex, GLuint bindingindex) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribBinding, vaobj, attribindex, bindingindex)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribBinding, GLuint vaobj, GLuint attribindex, GLuint bindingindex) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribBinding, vaobj, attribindex, bindingindex)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribFormat, vaobj, attribindex, size, type, normalized, relativeoffset)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribFormat, vaobj, attribindex, size, type, normalized, relativeoffset)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribIFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribIFormat, vaobj, attribindex, size, type, relativeoffset)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribIFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribIFormat, vaobj, attribindex, size, type, relativeoffset)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexArrayAttribLFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexArrayAttribLFormat, vaobj, attribindex, size, type, relativeoffset)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayAttribLFormat, GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayAttribLFormat, vaobj, attribindex, size, type, relativeoffset)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayBindingDivisor, GLuint vaobj, GLuint bindingindex, GLuint divisor) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayBindingDivisor, vaobj, bindingindex, divisor)
|
DECLARE_GL_FUNCTION_HEAD(void, VertexArrayBindingDivisor, GLuint vaobj, GLuint bindingindex, GLuint divisor) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexArrayBindingDivisor, vaobj, bindingindex, divisor)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexArrayiv, GLuint vaobj, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexArrayiv, vaobj, pname, param)
|
DECLARE_GL_FUNCTION_HEAD(void, GetVertexArrayiv, GLuint vaobj, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexArrayiv, vaobj, pname, param)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexArrayIndexediv, GLuint vaobj, GLuint index, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexArrayIndexediv, vaobj, index, pname, param)
|
DECLARE_GL_FUNCTION_HEAD(void, GetVertexArrayIndexediv, GLuint vaobj, GLuint index, GLenum pname, GLint* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexArrayIndexediv, vaobj, index, pname, param)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetVertexArrayIndexed64iv, GLuint vaobj, GLuint index, GLenum pname, GLint64* param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetVertexArrayIndexed64iv, vaobj, index, pname, param)
|
DECLARE_GL_FUNCTION_HEAD(void, GetVertexArrayIndexed64iv, GLuint vaobj, GLuint index, GLenum pname, GLint64* param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexArrayIndexed64iv, vaobj, index, pname, param)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, CreateSamplers, GLsizei n, GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateSamplers, n, samplers)
|
DECLARE_GL_FUNCTION_HEAD(void, CreateSamplers, GLsizei n, GLuint* samplers) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateSamplers, n, samplers)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CreateProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CreateProgramPipelines, n, pipelines)
|
DECLARE_GL_FUNCTION_HEAD(void, CreateProgramPipelines, GLsizei n, GLuint* pipelines) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateProgramPipelines, n, pipelines)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, CreateQueries, GLenum target, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CreateQueries, target, n, ids)
|
DECLARE_GL_FUNCTION_HEAD(void, CreateQueries, GLenum target, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CreateQueries, target, n, ids)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjecti64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjecti64v, id, buffer, pname, offset)
|
DECLARE_GL_FUNCTION_HEAD(void, GetQueryBufferObjecti64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryBufferObjecti64v, id, buffer, pname, offset)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjectiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjectiv, id, buffer, pname, offset)
|
DECLARE_GL_FUNCTION_HEAD(void, GetQueryBufferObjectiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryBufferObjectiv, id, buffer, pname, offset)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjectui64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjectui64v, id, buffer, pname, offset)
|
DECLARE_GL_FUNCTION_HEAD(void, GetQueryBufferObjectui64v, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryBufferObjectui64v, id, buffer, pname, offset)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryBufferObjectuiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryBufferObjectuiv, id, buffer, pname, offset)
|
DECLARE_GL_FUNCTION_HEAD(void, GetQueryBufferObjectuiv, GLuint id, GLuint buffer, GLenum pname, GLintptr offset) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetQueryBufferObjectuiv, id, buffer, pname, offset)
|
||||||
DECLARE_GL_FUNCTION_HEAD(void, GetTextureSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, bufSize, pixels)
|
DECLARE_GL_FUNCTION_HEAD(void, GetTextureSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTextureSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, format, type, bufSize, pixels)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, bufSize, pixels)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetCompressedTextureSubImage, GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetCompressedTextureSubImage, texture, level, xoffset, yoffset, zoffset, width, height, depth, bufSize, pixels)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnCompressedTexImage, GLenum target, GLint lod, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnCompressedTexImage, target, lod, bufSize, pixels)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnCompressedTexImage, GLenum target, GLint lod, GLsizei bufSize, void* pixels) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnCompressedTexImage, target, lod, bufSize, pixels)
|
||||||
@@ -1278,7 +1273,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4ivARB, GLenum target, const GL
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4sARB, GLenum target, GLshort s, GLshort t, GLshort r, GLshort q) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord4sARB, target, s, t, r, q)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4sARB, GLenum target, GLshort s, GLshort t, GLshort r, GLshort q) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord4sARB, target, s, t, r, q)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4svARB, GLenum target, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord4svARB, target, v)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord4svARB, GLenum target, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord4svARB, target, v)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjectivARB, GLuint id, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjectivARB, id, pname, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetQueryObjectivARB, GLuint id, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetQueryObjectivARB, id, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MaxShaderCompilerThreadsARB, GLuint count) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MaxShaderCompilerThreadsARB, count)
|
DECLARE_GL_FUNCTION_HEAD(void, MaxShaderCompilerThreadsARB, GLuint count) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MaxShaderCompilerThreadsARB, count)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfARB, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfARB, pname, param)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfARB, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfARB, pname, param)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvARB, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvARB, pname, params)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, PointParameterfvARB, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PointParameterfvARB, pname, params)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnTexImageARB, GLenum target, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnTexImageARB, target, level, format, type, bufSize, img)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnTexImageARB, GLenum target, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* img) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnTexImageARB, target, level, format, type, bufSize, img)
|
||||||
@@ -1386,7 +1381,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3ivARB, const GLint* v) DECLARE_GL_
|
|||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3sARB, GLshort x, GLshort y, GLshort z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3sARB, x, y, z)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3sARB, GLshort x, GLshort y, GLshort z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3sARB, x, y, z)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3svARB, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3svARB, v)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, WindowPos3svARB, const GLshort* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, WindowPos3svARB, v)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BlendBarrierKHR, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BlendBarrierKHR, )
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, BlendBarrierKHR, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BlendBarrierKHR, )
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MaxShaderCompilerThreadsKHR, GLuint count) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MaxShaderCompilerThreadsKHR, count)
|
DECLARE_GL_FUNCTION_HEAD(void, MaxShaderCompilerThreadsKHR, GLuint count) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MaxShaderCompilerThreadsKHR, count)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord1bOES, GLenum texture, GLbyte s) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord1bOES, texture, s)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord1bOES, GLenum texture, GLbyte s) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord1bOES, texture, s)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord1bvOES, GLenum texture, const GLbyte* coords) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord1bvOES, texture, coords)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord1bvOES, GLenum texture, const GLbyte* coords) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord1bvOES, texture, coords)
|
||||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord2bOES, GLenum texture, GLbyte s, GLbyte t) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord2bOES, texture, s, t)
|
DECLARE_GL_FUNCTION_STUB_HEAD(void, MultiTexCoord2bOES, GLenum texture, GLbyte s, GLbyte t) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, MultiTexCoord2bOES, texture, s, t)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,8 @@
|
|||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
||||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||||
|
void ReadnPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize,
|
||||||
|
void* data);
|
||||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||||
@@ -56,7 +58,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void NamedFramebufferReadBuffer(GLuint framebuffer, GLenum src);
|
void NamedFramebufferReadBuffer(GLuint framebuffer, GLenum src);
|
||||||
void ClearNamedFramebufferfv(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
void ClearNamedFramebufferfv(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||||
void ClearNamedFramebufferfi(GLuint framebuffer, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
void ClearNamedFramebufferfi(GLuint framebuffer, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||||
|
void InvalidateNamedFramebufferData(GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments);
|
||||||
|
void InvalidateNamedFramebufferSubData(GLuint framebuffer, GLsizei numAttachments, const GLenum* attachments,
|
||||||
|
GLint x, GLint y, GLsizei width, GLsizei height);
|
||||||
|
void InvalidateFramebuffer(GLenum target, GLsizei numAttachments, const GLenum* attachments);
|
||||||
|
void InvalidateSubFramebuffer(GLenum target, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y,
|
||||||
|
GLsizei width, GLsizei height);
|
||||||
|
void ClearNamedFramebufferiv(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLint* value);
|
||||||
|
void ClearNamedFramebufferuiv(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||||
GLenum CheckNamedFramebufferStatus(GLuint framebuffer, GLenum target);
|
GLenum CheckNamedFramebufferStatus(GLuint framebuffer, GLenum target);
|
||||||
|
void GetFramebufferParameteriv(GLenum target, GLenum pname, GLint* params);
|
||||||
|
void FramebufferParameteri(GLenum target, GLenum pname, GLint param);
|
||||||
|
void GetNamedFramebufferParameteriv(GLuint framebuffer, GLenum pname, GLint* params);
|
||||||
|
void NamedFramebufferParameteri(GLuint framebuffer, GLenum pname, GLint param);
|
||||||
void GetNamedFramebufferAttachmentParameteriv(GLuint framebuffer, GLenum attachment, GLenum pname, GLint* params);
|
void GetNamedFramebufferAttachmentParameteriv(GLuint framebuffer, GLenum attachment, GLenum pname, GLint* params);
|
||||||
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,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "Validators.h"
|
#include "Validators.h"
|
||||||
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.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>
|
||||||
@@ -60,6 +61,26 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool ValidateColorAttachmentInRange(FramebufferAttachmentType attachment, const char* caller) {
|
||||||
|
const auto first = static_cast<SizeT>(FramebufferAttachmentType::Color0);
|
||||||
|
const auto index = static_cast<SizeT>(attachment);
|
||||||
|
if (index < first) return true;
|
||||||
|
const auto colorIndex = index - first;
|
||||||
|
const auto limit = static_cast<SizeT>(
|
||||||
|
MG_Backend::pActiveBackendObject ? MG_Backend::pActiveBackendObject->GetDynamicParameters()
|
||||||
|
.MaxColorAttachments
|
||||||
|
: static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS));
|
||||||
|
if (colorIndex >= limit) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl/FramebufferImpl", caller,
|
||||||
|
std::format("Colour attachment {} is beyond GL_MAX_COLOR_ATTACHMENTS ({}).", colorIndex, limit)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Bool ValidateRenderbufferTarget(RenderbufferTarget target) {
|
Bool ValidateRenderbufferTarget(RenderbufferTarget target) {
|
||||||
if (target == RenderbufferTarget::Unknown) {
|
if (target == RenderbufferTarget::Unknown) {
|
||||||
using namespace MG_Util;
|
using namespace MG_Util;
|
||||||
@@ -97,4 +118,100 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
|
|||||||
std::format("Renderbuffer name {} is not valid.", index)));
|
std::format("Renderbuffer name {} is not valid.", index)));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool ValidateFramebufferParameterPname(GLenum pname, Bool isDefaultFramebuffer, Bool forSetter,
|
||||||
|
const char* caller) {
|
||||||
|
Bool isDefaultParameter = false;
|
||||||
|
switch (pname) {
|
||||||
|
case GL_FRAMEBUFFER_DEFAULT_WIDTH:
|
||||||
|
case GL_FRAMEBUFFER_DEFAULT_HEIGHT:
|
||||||
|
case GL_FRAMEBUFFER_DEFAULT_LAYERS:
|
||||||
|
case GL_FRAMEBUFFER_DEFAULT_SAMPLES:
|
||||||
|
case GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS:
|
||||||
|
isDefaultParameter = true;
|
||||||
|
break;
|
||||||
|
case GL_DOUBLEBUFFER:
|
||||||
|
case GL_IMPLEMENTATION_COLOR_READ_FORMAT:
|
||||||
|
case GL_IMPLEMENTATION_COLOR_READ_TYPE:
|
||||||
|
case GL_SAMPLES:
|
||||||
|
case GL_SAMPLE_BUFFERS:
|
||||||
|
case GL_STEREO:
|
||||||
|
// Queryable only; glFramebufferParameteri sets none of these.
|
||||||
|
if (forSetter) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl/FramebufferImpl", caller,
|
||||||
|
std::format("pname {} is not settable on a framebuffer.",
|
||||||
|
MG_Util::ConvertGLEnumToString(pname))));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl/FramebufferImpl", caller,
|
||||||
|
std::format("pname {} is not a framebuffer parameter.",
|
||||||
|
MG_Util::ConvertGLEnumToString(pname))));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The default framebuffer has no DEFAULT_* state of its own - its shape comes from the
|
||||||
|
// surface - so those names are accepted enums it simply cannot answer or accept.
|
||||||
|
if (isDefaultFramebuffer && isDefaultParameter) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl/FramebufferImpl", caller,
|
||||||
|
std::format("pname {} does not apply to the default framebuffer.",
|
||||||
|
MG_Util::ConvertGLEnumToString(pname))));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ValidateReadFramebufferForCopy(const char* caller) {
|
||||||
|
auto& framebufferObject =
|
||||||
|
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
|
||||||
|
if (!framebufferObject || !framebufferObject->CheckCompleteness()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidFramebufferOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", caller,
|
||||||
|
"Read framebuffer is not framebuffer complete."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FramebufferAttachmentType readBuffer = framebufferObject->GetReadBuffer();
|
||||||
|
if (readBuffer == FramebufferAttachmentType::None ||
|
||||||
|
!framebufferObject->GetAttachment(readBuffer).IsValid()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", caller,
|
||||||
|
"Read buffer names no attachment of the read framebuffer."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAMPLE_BUFFERS is one whenever the read buffer resolves to multisample storage. A
|
||||||
|
// multisample texture says so by its target - its sample count can legally be one - while a
|
||||||
|
// renderbuffer says so by having been given a non-zero sample count.
|
||||||
|
const auto& readAttachment = framebufferObject->GetAttachment(readBuffer);
|
||||||
|
Bool isMultisampled = false;
|
||||||
|
if (readAttachment.IsRenderbuffer() && readAttachment.GetRenderbuffer()) {
|
||||||
|
isMultisampled = readAttachment.GetRenderbuffer()->GetSamples() > 0;
|
||||||
|
} else if (readAttachment.IsTexture() && readAttachment.GetTexture()) {
|
||||||
|
const auto target = readAttachment.GetTexture()->GetTarget();
|
||||||
|
isMultisampled = target == TextureTarget::Texture2DMultisample ||
|
||||||
|
target == TextureTarget::Texture2DMultisampleArray;
|
||||||
|
}
|
||||||
|
if (isMultisampled) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl/FramebufferImpl", caller,
|
||||||
|
"Cannot copy from a multisampled read framebuffer."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl
|
} // namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl
|
||||||
|
|||||||
@@ -14,6 +14,21 @@ namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl {
|
|||||||
Bool ValidateFramebufferTarget(FramebufferTarget target);
|
Bool ValidateFramebufferTarget(FramebufferTarget target);
|
||||||
Bool ValidateFramebufferName(Uint index, Bool allowZero = true);
|
Bool ValidateFramebufferName(Uint index, Bool allowZero = true);
|
||||||
Bool ValidateFramebufferAttachmentType(FramebufferAttachmentType attachment);
|
Bool ValidateFramebufferAttachmentType(FramebufferAttachmentType attachment);
|
||||||
|
// GL_COLOR_ATTACHMENTn is a token per n up to 31, but only the first GL_MAX_COLOR_ATTACHMENTS of
|
||||||
|
// them name an attachment point of a framebuffer object; the rest are INVALID_OPERATION for the
|
||||||
|
// attaching entry points (GL 4.6 core 9.2.7). Non-colour attachments pass through unchanged.
|
||||||
|
Bool ValidateColorAttachmentInRange(FramebufferAttachmentType attachment, const char* caller);
|
||||||
Bool ValidateRenderbufferTarget(RenderbufferTarget target);
|
Bool ValidateRenderbufferTarget(RenderbufferTarget target);
|
||||||
Bool ValidateRenderbufferName(Uint index, Bool allowZero = true);
|
Bool ValidateRenderbufferName(Uint index, Bool allowZero = true);
|
||||||
|
// The read-framebuffer preconditions the CopyTexSubImage family shares (GL 4.6 core 8.6): the
|
||||||
|
// read framebuffer must be complete, its read buffer must name a real attachment, and it must
|
||||||
|
// not be multisampled. Incompleteness is INVALID_FRAMEBUFFER_OPERATION, the other two are
|
||||||
|
// INVALID_OPERATION.
|
||||||
|
Bool ValidateReadFramebufferForCopy(const char* caller);
|
||||||
|
// The pname sets of glGet/FramebufferParameteri (GL 4.6 core 9.2.3). Order matters and is part
|
||||||
|
// of the contract: a name outside the table is INVALID_ENUM, and only then is a name that the
|
||||||
|
// DEFAULT framebuffer does not answer INVALID_OPERATION. Testing the framebuffer kind first
|
||||||
|
// would turn GL_FRAMEBUFFER_DEFAULT_WIDTH on framebuffer zero into the wrong error.
|
||||||
|
Bool ValidateFramebufferParameterPname(GLenum pname, Bool isDefaultFramebuffer, Bool forSetter,
|
||||||
|
const char* caller);
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl
|
} // namespace MobileGL::MG_Impl::GLImpl::FramebufferImpl
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include <MG_Util/Converters/MGToGL/RenderStateEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/RenderStateEnumConverter.h>
|
||||||
#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_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
@@ -50,6 +51,14 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
|
constexpr GLint kFrontendMaxTessControlAtomicCounters = 0;
|
||||||
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
|
constexpr GLint kFrontendMaxTessEvaluationAtomicCounters = 0;
|
||||||
constexpr GLint kFrontendMaxVertexAtomicCounters = 0;
|
constexpr GLint kFrontendMaxVertexAtomicCounters = 0;
|
||||||
|
// One atomic counter is a uint, and a buffer never has to hold more counters than the
|
||||||
|
// combined limit the frontend advertises. GL 4.6 table 23.63 floors this at 32 bytes.
|
||||||
|
constexpr GLint kFrontendMaxAtomicCounterBufferSize =
|
||||||
|
kFrontendMaxCombinedAtomicCounters * static_cast<GLint>(sizeof(GLuint));
|
||||||
|
// 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.
|
||||||
|
constexpr GLint kFrontendMaxDebugGroupStackDepth = 64;
|
||||||
|
constexpr GLint kFrontendMaxDebugLoggedMessages = 1;
|
||||||
constexpr GLint kFrontendMaxVertexUniformComponents = 4096;
|
constexpr GLint kFrontendMaxVertexUniformComponents = 4096;
|
||||||
constexpr GLint kFrontendMaxVertexUniformVectors = 128;
|
constexpr GLint kFrontendMaxVertexUniformVectors = 128;
|
||||||
constexpr GLint kFrontendMaxVertexUniformBlocks = 14;
|
constexpr GLint kFrontendMaxVertexUniformBlocks = 14;
|
||||||
@@ -263,6 +272,60 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL_TEXTURE_BINDING_* is per-texture-unit state: glGetIntegerv answers for the
|
||||||
|
// active unit, glGetIntegeri_v answers for unit `index`. Both need the same
|
||||||
|
// pname -> target decode, so it lives here instead of being spelled out twice.
|
||||||
|
bool TryDecodeTextureUnitBindingPname(GLenum pname, TextureTarget& outTarget) {
|
||||||
|
switch (pname) {
|
||||||
|
case GL_TEXTURE_BINDING_1D: outTarget = TextureTarget::Texture1D; return true;
|
||||||
|
case GL_TEXTURE_BINDING_1D_ARRAY: outTarget = TextureTarget::Texture1DArray; return true;
|
||||||
|
case GL_TEXTURE_BINDING_2D: outTarget = TextureTarget::Texture2D; return true;
|
||||||
|
case GL_TEXTURE_BINDING_2D_ARRAY: outTarget = TextureTarget::Texture2DArray; return true;
|
||||||
|
case GL_TEXTURE_BINDING_2D_MULTISAMPLE: outTarget = TextureTarget::Texture2DMultisample; return true;
|
||||||
|
case GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY:
|
||||||
|
outTarget = TextureTarget::Texture2DMultisampleArray;
|
||||||
|
return true;
|
||||||
|
case GL_TEXTURE_BINDING_3D: outTarget = TextureTarget::Texture3D; return true;
|
||||||
|
case GL_TEXTURE_BINDING_BUFFER: outTarget = TextureTarget::TextureBuffer; return true;
|
||||||
|
case GL_TEXTURE_BINDING_CUBE_MAP: outTarget = TextureTarget::TextureCubeMap; return true;
|
||||||
|
case GL_TEXTURE_BINDING_CUBE_MAP_ARRAY: outTarget = TextureTarget::TextureCubeMapArray; return true;
|
||||||
|
case GL_TEXTURE_BINDING_RECTANGLE: outTarget = TextureTarget::TextureRectangle; return true;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint QueryTextureBindingOnUnit(Int unit, TextureTarget target) {
|
||||||
|
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||||
|
const auto& obj = textureUnit.GetBindingSlot(target).GetBoundObject();
|
||||||
|
return obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint QuerySamplerBindingOnUnit(Int unit) {
|
||||||
|
const auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||||
|
const auto& sampler = textureUnit.GetSamplerObject();
|
||||||
|
return sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ARB_viewport_array indexed rectangles. MobileGL keeps exactly one viewport, one
|
||||||
|
// scissor box and one depth range, so every in-range index answers with that single
|
||||||
|
// value - but it has to come from the frontend state the non-indexed getters read.
|
||||||
|
// The generic path at the bottom of GetIntegeri_v is a raw backend passthrough that
|
||||||
|
// has no case for these, so routing them through it returned zeros.
|
||||||
|
Bool IsIndexedViewportQuery(GLenum target) {
|
||||||
|
return target == GL_VIEWPORT || target == GL_SCISSOR_BOX || target == GL_DEPTH_RANGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ARB_viewport_array: `index` selects a viewport and MAX_VIEWPORTS bounds it.
|
||||||
|
Bool ValidateViewportQueryIndex(GLuint index, const char* caller) {
|
||||||
|
GLint maxViewports = 0;
|
||||||
|
GetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
|
||||||
|
if (index < static_cast<GLuint>(std::max(maxViewports, 1))) return true;
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Viewport index is out of range."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) {
|
void CopyIntsToBooleans(const GLint* src, SizeT count, GLboolean* dst) {
|
||||||
for (SizeT i = 0; i < count; ++i) {
|
for (SizeT i = 0; i < count; ++i) {
|
||||||
dst[i] = src[i] ? GL_TRUE : GL_FALSE;
|
dst[i] = src[i] ? GL_TRUE : GL_FALSE;
|
||||||
@@ -670,7 +733,71 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-texture-unit bindings: GL 4.6 core table 23.19 makes every GL_TEXTURE_BINDING_*
|
||||||
|
// and GL_SAMPLER_BINDING indexed by texture unit. Without this they fell through to
|
||||||
|
// the raw backend passthrough at the bottom, which knows nothing about the
|
||||||
|
// frontend's binding state.
|
||||||
|
if (TextureTarget textureBindingTarget = TextureTarget::Unknown;
|
||||||
|
TryDecodeTextureUnitBindingPname(target, textureBindingTarget) || target == GL_SAMPLER_BINDING) {
|
||||||
|
GLint maxUnits = 0;
|
||||||
|
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxUnits);
|
||||||
|
maxUnits = std::min<GLint>(maxUnits, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||||
|
if (index >= static_cast<GLuint>(std::max(maxUnits, 0))) {
|
||||||
|
*data = 0;
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture unit index is out of range."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*data = target == GL_SAMPLER_BINDING
|
||||||
|
? QuerySamplerBindingOnUnit(static_cast<Int>(index))
|
||||||
|
: QueryTextureBindingOnUnit(static_cast<Int>(index), textureBindingTarget);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (target) {
|
switch (target) {
|
||||||
|
// ARB_viewport_array queries the indexed rectangles through glGetIntegeri_v as well
|
||||||
|
// (gl4cMultiBindTests and the viewport_array group both do). The frontend keeps one
|
||||||
|
// viewport and one scissor box, so every in-range index reports that one.
|
||||||
|
case GL_VIEWPORT:
|
||||||
|
case GL_SCISSOR_BOX:
|
||||||
|
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||||
|
GetIntegerv(target, data);
|
||||||
|
return;
|
||||||
|
// The vertex buffer binding points of the vertex array object that is bound. Indexed by
|
||||||
|
// binding point, not by attribute (GL 4.6 core 10.3.1).
|
||||||
|
case GL_VERTEX_BINDING_BUFFER:
|
||||||
|
case GL_VERTEX_BINDING_DIVISOR:
|
||||||
|
case GL_VERTEX_BINDING_OFFSET:
|
||||||
|
case GL_VERTEX_BINDING_STRIDE: {
|
||||||
|
if (index >= VertexArrayImpl::GetMaxVertexAttribBindings()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"Vertex buffer binding index is out of range."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||||
|
if (!vao) {
|
||||||
|
*data = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& binding = vao->GetBindingPoint(index);
|
||||||
|
switch (target) {
|
||||||
|
case GL_VERTEX_BINDING_BUFFER:
|
||||||
|
*data = binding.Buffer ? static_cast<GLint>(binding.Buffer->GetExternalIndex()) : 0;
|
||||||
|
return;
|
||||||
|
case GL_VERTEX_BINDING_DIVISOR:
|
||||||
|
*data = static_cast<GLint>(binding.Divisor);
|
||||||
|
return;
|
||||||
|
case GL_VERTEX_BINDING_OFFSET:
|
||||||
|
*data = static_cast<GLint>(binding.Offset);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
*data = static_cast<GLint>(binding.Stride);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
case GL_IMAGE_BINDING_NAME:
|
case GL_IMAGE_BINDING_NAME:
|
||||||
case GL_IMAGE_BINDING_LEVEL:
|
case GL_IMAGE_BINDING_LEVEL:
|
||||||
case GL_IMAGE_BINDING_LAYERED:
|
case GL_IMAGE_BINDING_LAYERED:
|
||||||
@@ -748,6 +875,46 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
getIntegeri(target, index, data);
|
getIntegeri(target, index, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL_ARB_viewport_array's typed indexed getters. They were no-op stubs, which left the
|
||||||
|
// caller's output buffer holding whatever was on the stack. The multi-component indexed
|
||||||
|
// rectangles are answered from the frontend's own viewport/scissor/depth-range state, via
|
||||||
|
// the non-indexed getter of the matching type - GL_DEPTH_RANGE is float state, so putting
|
||||||
|
// it through the integer query would round it to 0/1. Everything else MobileGL answers
|
||||||
|
// indexed is scalar integer-domain state, where converting the integer query is exact.
|
||||||
|
void GetFloati_v(GLenum target, GLuint index, GLfloat* data) {
|
||||||
|
if (!data) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "data pointer cannot be null"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (IsIndexedViewportQuery(target)) {
|
||||||
|
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||||
|
GetFloatv(target, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GLint ints[4] = {};
|
||||||
|
GetIntegeri_v(target, index, ints);
|
||||||
|
data[0] = static_cast<GLfloat>(ints[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetDoublei_v(GLenum target, GLuint index, GLdouble* data) {
|
||||||
|
if (!data) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "data pointer cannot be null"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (IsIndexedViewportQuery(target)) {
|
||||||
|
if (!ValidateViewportQueryIndex(index, __func__)) return;
|
||||||
|
GetDoublev(target, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GLint ints[4] = {};
|
||||||
|
GetIntegeri_v(target, index, ints);
|
||||||
|
data[0] = static_cast<GLdouble>(ints[0]);
|
||||||
|
}
|
||||||
|
|
||||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data) {
|
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -924,6 +1091,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-texture-unit bindings: the non-indexed query reports the active unit.
|
||||||
|
if (TextureTarget textureBindingTarget = TextureTarget::Unknown;
|
||||||
|
TryDecodeTextureUnitBindingPname(pname, textureBindingTarget)) {
|
||||||
|
*params = QueryTextureBindingOnUnit(MG_State::pGLContext->GetActiveTextureUnit(), textureBindingTarget);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (pname) {
|
switch (pname) {
|
||||||
case GL_ACTIVE_TEXTURE:
|
case GL_ACTIVE_TEXTURE:
|
||||||
*params = MG_State::pGLContext->GetActiveTextureUnit() + GL_TEXTURE0;
|
*params = MG_State::pGLContext->GetActiveTextureUnit() + GL_TEXTURE0;
|
||||||
@@ -1030,12 +1204,37 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case GL_DRAW_INDIRECT_BUFFER_BINDING: {
|
||||||
|
auto& obj = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||||
|
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case GL_MAX_SHADER_COMPILER_THREADS_KHR:
|
||||||
|
// GL_KHR_parallel_shader_compile (GL_MAX_SHADER_COMPILER_THREADS_ARB is the same
|
||||||
|
// 0x91B0). The number of threads MobileGL's compile pool would actually use, so
|
||||||
|
// an application sizing its own submission batches gets a real answer.
|
||||||
|
//
|
||||||
|
// Zero when asynchronous compilation is off, which is the honest reply and the
|
||||||
|
// one the extension defines for an implementation with no compiler threads: the
|
||||||
|
// extension string is withdrawn in that configuration too, so a conforming
|
||||||
|
// application never reaches this query, and one that asks anyway is told there
|
||||||
|
// are none rather than being handed a thread count nothing will use.
|
||||||
|
*params = MG_Util::Async::AsyncShaderCompileEnabled()
|
||||||
|
? static_cast<GLint>(MG_Util::Async::ShaderCompilePool::Get().GetThreadCount())
|
||||||
|
: 0;
|
||||||
|
return;
|
||||||
case GL_MAX_DEBUG_GROUP_STACK_DEPTH:
|
case GL_MAX_DEBUG_GROUP_STACK_DEPTH:
|
||||||
*params = 0; // debug-group entrypoints are stubbed
|
// KHR_debug floors this at 64 even when the group entry points are stubs: the
|
||||||
|
// limit describes how deep glPushDebugGroup may nest, and 0 is not a legal answer.
|
||||||
|
*params = kFrontendMaxDebugGroupStackDepth;
|
||||||
return;
|
return;
|
||||||
case GL_MAX_DEBUG_MESSAGE_LENGTH:
|
case GL_MAX_DEBUG_MESSAGE_LENGTH:
|
||||||
*params = 1024; // debug-message entrypoints are stubbed, but KHR_debug requires a valid limit
|
*params = 1024; // debug-message entrypoints are stubbed, but KHR_debug requires a valid limit
|
||||||
return;
|
return;
|
||||||
|
case GL_MAX_DEBUG_LOGGED_MESSAGES:
|
||||||
|
// Size of the message log ring; KHR_debug requires at least 1.
|
||||||
|
*params = kFrontendMaxDebugLoggedMessages;
|
||||||
|
return;
|
||||||
case GL_DEBUG_GROUP_STACK_DEPTH:
|
case GL_DEBUG_GROUP_STACK_DEPTH:
|
||||||
*params = 0; // debug-group entrypoints are stubbed
|
*params = 0; // debug-group entrypoints are stubbed
|
||||||
return;
|
return;
|
||||||
@@ -1393,7 +1592,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = 0; // program-binary entrypoints are stubbed
|
*params = 0; // program-binary entrypoints are stubbed
|
||||||
return;
|
return;
|
||||||
case GL_PROGRAM_PIPELINE_BINDING:
|
case GL_PROGRAM_PIPELINE_BINDING:
|
||||||
*params = 0; // program-pipeline entrypoints are stubbed
|
*params = static_cast<GLint>(MG_State::pGLContext->GetBoundProgramPipelineName());
|
||||||
return;
|
return;
|
||||||
case GL_PROGRAM_POINT_SIZE:
|
case GL_PROGRAM_POINT_SIZE:
|
||||||
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ProgramPointSize) ? GL_TRUE : GL_FALSE;
|
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ProgramPointSize) ? GL_TRUE : GL_FALSE;
|
||||||
@@ -1477,13 +1676,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_SAMPLE_MASK_VALUE:
|
case GL_SAMPLE_MASK_VALUE:
|
||||||
*params = static_cast<GLint>(MG_State::pGLContext->GetSampleMaskValue());
|
*params = static_cast<GLint>(MG_State::pGLContext->GetSampleMaskValue());
|
||||||
return;
|
return;
|
||||||
case GL_SAMPLER_BINDING: {
|
case GL_SAMPLER_BINDING:
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
*params = QuerySamplerBindingOnUnit(MG_State::pGLContext->GetActiveTextureUnit());
|
||||||
const auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& sampler = tu.GetSamplerObject();
|
|
||||||
*params = sampler ? static_cast<GLint>(sampler->GetExternalIndex()) : 0;
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
case GL_SAMPLES:
|
case GL_SAMPLES:
|
||||||
*params = ResolveDrawFramebufferSampleCount();
|
*params = ResolveDrawFramebufferSampleCount();
|
||||||
return;
|
return;
|
||||||
@@ -1579,92 +1774,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_STEREO:
|
case GL_STEREO:
|
||||||
*params = 0; // stereo surfaces are not exposed
|
*params = 0; // stereo surfaces are not exposed
|
||||||
return;
|
return;
|
||||||
case GL_TEXTURE_BINDING_1D: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture1D);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_1D_ARRAY: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture1DArray);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_2D: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2D);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
MGLOG_D("Get GL_TEXTURE_BINDING_2D: %d", *params);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_2D_ARRAY: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2DArray);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_2D_MULTISAMPLE: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2DMultisample);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture2DMultisampleArray);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_3D: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::Texture3D);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_BUFFER: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::TextureBuffer);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_CUBE_MAP: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::TextureCubeMap);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_BINDING_RECTANGLE: {
|
|
||||||
Int unit = MG_State::pGLContext->GetActiveTextureUnit();
|
|
||||||
auto& tu = MG_State::pGLContext->GetTextureUnitObject(unit);
|
|
||||||
const auto& slot = tu.GetBindingSlot(TextureTarget::TextureRectangle);
|
|
||||||
const auto& obj = slot.GetBoundObject();
|
|
||||||
*params = obj ? static_cast<GLint>(obj->GetExternalIndex()) : 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case GL_TEXTURE_COMPRESSION_HINT:
|
case GL_TEXTURE_COMPRESSION_HINT:
|
||||||
*params = static_cast<GLint>(MG_State::pGLContext->GetHint(pname));
|
*params = static_cast<GLint>(MG_State::pGLContext->GetHint(pname));
|
||||||
return;
|
return;
|
||||||
case GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT:
|
case GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT:
|
||||||
*params = 0; // texture-buffer range entrypoints are stubbed
|
*params = MG_Backend::pActiveBackendObject->GetDynamicParameters().TextureBufferOffsetAlignment;
|
||||||
return;
|
return;
|
||||||
case GL_TIMESTAMP: {
|
case GL_TIMESTAMP: {
|
||||||
Int64 timestamp = 0;
|
Int64 timestamp = 0;
|
||||||
@@ -1733,20 +1847,22 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = vao ? static_cast<GLint>(vao->GetExternalIndex()) : 0;
|
*params = vao ? static_cast<GLint>(vao->GetExternalIndex()) : 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// The vertex buffer binding points are per-binding-index state, so the non-indexed getter
|
||||||
|
// has nothing to answer with (GL 4.6 core table 23.4).
|
||||||
|
case GL_VERTEX_BINDING_BUFFER:
|
||||||
case GL_VERTEX_BINDING_DIVISOR:
|
case GL_VERTEX_BINDING_DIVISOR:
|
||||||
*params = 0; // vertex-binding entrypoints are stubbed
|
|
||||||
return;
|
|
||||||
case GL_VERTEX_BINDING_OFFSET:
|
case GL_VERTEX_BINDING_OFFSET:
|
||||||
*params = 0; // vertex-binding entrypoints are stubbed
|
|
||||||
return;
|
|
||||||
case GL_VERTEX_BINDING_STRIDE:
|
case GL_VERTEX_BINDING_STRIDE:
|
||||||
*params = 0; // vertex-binding entrypoints are stubbed
|
RecordIndexedOnlyGetterError(__func__, pname);
|
||||||
return;
|
return;
|
||||||
case GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
case GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
||||||
*params = 0; // vertex-binding entrypoints are stubbed
|
*params = static_cast<GLint>(VertexArrayImpl::GetMaxVertexAttribRelativeOffset());
|
||||||
return;
|
return;
|
||||||
case GL_MAX_VERTEX_ATTRIB_BINDINGS:
|
case GL_MAX_VERTEX_ATTRIB_BINDINGS:
|
||||||
*params = 0; // vertex-binding entrypoints are stubbed
|
*params = static_cast<GLint>(VertexArrayImpl::GetMaxVertexAttribBindings());
|
||||||
|
return;
|
||||||
|
case GL_MAX_VERTEX_ATTRIB_STRIDE:
|
||||||
|
*params = static_cast<GLint>(VertexArrayImpl::GetMaxVertexAttribStride());
|
||||||
return;
|
return;
|
||||||
case GL_VIEWPORT: {
|
case GL_VIEWPORT: {
|
||||||
const auto& vp = MG_State::pGLContext->GetViewport();
|
const auto& vp = MG_State::pGLContext->GetViewport();
|
||||||
@@ -1912,9 +2028,36 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_MAX_SAMPLE_MASK_WORDS:
|
case GL_MAX_SAMPLE_MASK_WORDS:
|
||||||
*params = dynamicParameters.MaxSampleMaskWords;
|
*params = dynamicParameters.MaxSampleMaskWords;
|
||||||
break;
|
break;
|
||||||
|
case GL_PATCH_VERTICES:
|
||||||
|
*params = static_cast<GLint>(MG_State::pGLContext->GetPatchVertices());
|
||||||
|
break;
|
||||||
|
case GL_MAX_PATCH_VERTICES:
|
||||||
|
*params = dynamicParameters.MaxPatchVertices;
|
||||||
|
break;
|
||||||
|
case GL_MAX_TESS_GEN_LEVEL:
|
||||||
|
*params = dynamicParameters.MaxTessGenLevel;
|
||||||
|
break;
|
||||||
|
case GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET:
|
||||||
|
*params = dynamicParameters.MinProgramTextureGatherOffset;
|
||||||
|
break;
|
||||||
|
case GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET:
|
||||||
|
*params = dynamicParameters.MaxProgramTextureGatherOffset;
|
||||||
|
break;
|
||||||
case GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS:
|
case GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS:
|
||||||
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::ShaderStorage));
|
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::ShaderStorage));
|
||||||
break;
|
break;
|
||||||
|
case GL_MAX_SHADER_STORAGE_BLOCK_SIZE:
|
||||||
|
// 64-bit state (see GetInteger64v); the 32-bit query saturates, per the GL
|
||||||
|
// state-query conversion rules.
|
||||||
|
*params = static_cast<GLint>(std::min<Uint64>(dynamicParameters.MaxShaderStorageBlockSize,
|
||||||
|
static_cast<Uint64>(INT32_MAX)));
|
||||||
|
break;
|
||||||
|
case GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS:
|
||||||
|
*params = static_cast<GLint>(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter));
|
||||||
|
break;
|
||||||
|
case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE:
|
||||||
|
*params = kFrontendMaxAtomicCounterBufferSize;
|
||||||
|
break;
|
||||||
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
case GL_MAX_TEXTURE_BUFFER_SIZE:
|
||||||
*params = dynamicParameters.MaxTextureBufferSize;
|
*params = dynamicParameters.MaxTextureBufferSize;
|
||||||
break;
|
break;
|
||||||
@@ -1941,7 +2084,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
|
*params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
|
||||||
break;
|
break;
|
||||||
case GL_TRANSFORM_FEEDBACK_PAUSED:
|
case GL_TRANSFORM_FEEDBACK_PAUSED:
|
||||||
*params = 0;
|
*params = MG_State::pGLContext->IsTransformFeedbackPaused() ? 1 : 0;
|
||||||
|
break;
|
||||||
|
case GL_TRANSFORM_FEEDBACK_BINDING:
|
||||||
|
*params = static_cast<GLint>(MG_State::pGLContext->GetBoundTransformFeedbackName());
|
||||||
break;
|
break;
|
||||||
case GL_MAX_TEXTURE_IMAGE_UNITS:
|
case GL_MAX_TEXTURE_IMAGE_UNITS:
|
||||||
*params = dynamicParameters.MaxTextureImageUnits;
|
*params = dynamicParameters.MaxTextureImageUnits;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void GetIntegerv(GLenum pname, GLint* params);
|
void GetIntegerv(GLenum pname, GLint* params);
|
||||||
void GetInteger64v(GLenum pname, GLint64* params);
|
void GetInteger64v(GLenum pname, GLint64* params);
|
||||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||||
|
void GetFloati_v(GLenum target, GLuint index, GLfloat* data);
|
||||||
|
void GetDoublei_v(GLenum target, GLuint index, GLdouble* data);
|
||||||
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();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLboolean IsProgram(GLuint program);
|
GLboolean IsProgram(GLuint program);
|
||||||
GLboolean IsShader(GLuint shader);
|
GLboolean IsShader(GLuint shader);
|
||||||
void LinkProgram(GLuint program);
|
void LinkProgram(GLuint program);
|
||||||
|
// GL_KHR_parallel_shader_compile / GL_ARB_parallel_shader_compile. Both names are the
|
||||||
|
// same entry point; see MaxShaderCompilerThreadsKHR_State for the semantics of count.
|
||||||
|
void MaxShaderCompilerThreadsKHR(GLuint count);
|
||||||
|
void MaxShaderCompilerThreadsARB(GLuint count);
|
||||||
void ShaderSource(GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length);
|
void ShaderSource(GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length);
|
||||||
void UseProgram(GLuint program);
|
void UseProgram(GLuint program);
|
||||||
void Uniform1f(GLint location, GLfloat v0);
|
void Uniform1f(GLint location, GLfloat v0);
|
||||||
@@ -137,7 +141,46 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
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 ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
|
||||||
|
void Uniform1d(GLint location, GLdouble v0);
|
||||||
|
void Uniform1dv(GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void ProgramUniform1d(GLuint program, GLint location, GLdouble v0);
|
||||||
|
void ProgramUniform1dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void Uniform2d(GLint location, GLdouble v0, GLdouble v1);
|
||||||
|
void Uniform2dv(GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void ProgramUniform2d(GLuint program, GLint location, GLdouble v0, GLdouble v1);
|
||||||
|
void ProgramUniform2dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void Uniform3d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2);
|
||||||
|
void Uniform3dv(GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void ProgramUniform3d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2);
|
||||||
|
void ProgramUniform3dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void Uniform4d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3);
|
||||||
|
void Uniform4dv(GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void ProgramUniform4d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3);
|
||||||
|
void ProgramUniform4dv(GLuint program, GLint location, GLsizei count, const GLdouble* value);
|
||||||
|
void UniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix2x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix2x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix2x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix2x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix3x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix3x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix3x4dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix3x4dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix4x2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix4x2dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void UniformMatrix4x3dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void ProgramUniformMatrix4x3dv(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble* value);
|
||||||
|
void GetUniformdv(GLuint program, GLint location, GLdouble* params);
|
||||||
void ValidateProgram(GLuint program);
|
void ValidateProgram(GLuint program);
|
||||||
|
void ProgramParameteri(GLuint program, GLenum pname, GLint value);
|
||||||
|
GLuint CreateShaderProgramv(GLenum type, GLsizei count, const GLchar* const* strings);
|
||||||
|
void GetProgramBinary(GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary);
|
||||||
|
void ProgramBinary(GLuint program, GLenum binaryFormat, const void* binary, GLsizei length);
|
||||||
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode);
|
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode);
|
||||||
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
|
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
|
||||||
GLenum* type, GLchar* name);
|
GLenum* type, GLchar* name);
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "GL_ProgramPipeline.h"
|
||||||
|
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
||||||
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
|
namespace {
|
||||||
|
void RecordPipelineError(ErrorCode code, const char* function, String message) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function, Move(message)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pipeline name only names an object once it has been bound or created; querying a
|
||||||
|
// reserved-but-unmaterialised name is INVALID_OPERATION (GL 4.6 core 7.4).
|
||||||
|
const SharedPtr<MG_State::GLState::ProgramPipelineObject>* TryGetPipeline(GLuint pipeline,
|
||||||
|
const char* function) {
|
||||||
|
if (!MG_State::pGLContext->IsProgramPipelineObject(pipeline)) {
|
||||||
|
RecordPipelineError(ErrorCode::InvalidOperation, function,
|
||||||
|
std::format("Program pipeline {} does not exist.", pipeline));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return &MG_State::pGLContext->GetProgramPipelineObject(pipeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ValidatePipelineCount(GLsizei n, const char* function) {
|
||||||
|
if (n < 0) {
|
||||||
|
RecordPipelineError(ErrorCode::InvalidValue, function, "n must be non-negative.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL 4.6 core table 7.1 maps each stage bit onto a shader stage.
|
||||||
|
Bool TryResolveStageBit(GLbitfield bit, ShaderStage& outStage) {
|
||||||
|
switch (bit) {
|
||||||
|
case GL_VERTEX_SHADER_BIT: outStage = ShaderStage::Vertex; return true;
|
||||||
|
case GL_TESS_CONTROL_SHADER_BIT: outStage = ShaderStage::TessControl; return true;
|
||||||
|
case GL_TESS_EVALUATION_SHADER_BIT: outStage = ShaderStage::TessEval; return true;
|
||||||
|
case GL_GEOMETRY_SHADER_BIT: outStage = ShaderStage::Geometry; return true;
|
||||||
|
case GL_FRAGMENT_SHADER_BIT: outStage = ShaderStage::Fragment; return true;
|
||||||
|
case GL_COMPUTE_SHADER_BIT: outStage = ShaderStage::Compute; return true;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr GLbitfield kAllStageBits = GL_VERTEX_SHADER_BIT | GL_TESS_CONTROL_SHADER_BIT |
|
||||||
|
GL_TESS_EVALUATION_SHADER_BIT | GL_GEOMETRY_SHADER_BIT |
|
||||||
|
GL_FRAGMENT_SHADER_BIT | GL_COMPUTE_SHADER_BIT;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void GenProgramPipelines(GLsizei n, GLuint* pipelines) {
|
||||||
|
if (!ValidatePipelineCount(n, __func__)) return;
|
||||||
|
if (n == 0 || !pipelines) return;
|
||||||
|
|
||||||
|
static thread_local Vector<GLuint> names;
|
||||||
|
MG_State::pGLContext->GenProgramPipelineNames(static_cast<Uint>(n), names);
|
||||||
|
Memcpy(pipelines, names.data(), static_cast<SizeT>(n) * sizeof(GLuint));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CreateProgramPipelines(GLsizei n, GLuint* pipelines) {
|
||||||
|
if (!ValidatePipelineCount(n, __func__)) return;
|
||||||
|
if (n == 0 || !pipelines) return;
|
||||||
|
|
||||||
|
static thread_local Vector<GLuint> names;
|
||||||
|
MG_State::pGLContext->GenProgramPipelineNames(static_cast<Uint>(n), names);
|
||||||
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
|
pipelines[i] = names[static_cast<SizeT>(i)];
|
||||||
|
MG_State::pGLContext->CreateProgramPipelineObject(names[static_cast<SizeT>(i)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeleteProgramPipelines(GLsizei n, const GLuint* pipelines) {
|
||||||
|
if (!ValidatePipelineCount(n, __func__)) return;
|
||||||
|
if (!pipelines) return;
|
||||||
|
|
||||||
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
|
// Deleting zero, an unknown name, or a name that was only reserved is silently ignored.
|
||||||
|
MG_State::pGLContext->MarkProgramPipelineForDeletion(pipelines[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BindProgramPipeline(GLuint pipeline) {
|
||||||
|
if (pipeline != 0 && !MG_State::pGLContext->ValidateProgramPipelineName(pipeline)) {
|
||||||
|
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
|
||||||
|
std::format("Program pipeline name {} is not valid.", pipeline));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MG_State::pGLContext->BindProgramPipelineObject(pipeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
GLboolean IsProgramPipeline(GLuint pipeline) {
|
||||||
|
return MG_State::pGLContext->IsProgramPipelineObject(pipeline) ? GL_TRUE : GL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetProgramPipelineiv(GLuint pipeline, GLenum pname, GLint* params) {
|
||||||
|
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
|
||||||
|
if (!pipelineObject || !params) return;
|
||||||
|
|
||||||
|
const auto stageProgramName = [&](ShaderStage stage) -> GLint {
|
||||||
|
const auto& program = (*pipelineObject)->GetStageProgram(stage);
|
||||||
|
return program ? static_cast<GLint>(program->GetExternalIndex()) : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (pname) {
|
||||||
|
case GL_ACTIVE_PROGRAM: {
|
||||||
|
const auto& active = (*pipelineObject)->GetActiveProgram();
|
||||||
|
*params = active ? static_cast<GLint>(active->GetExternalIndex()) : 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GL_VERTEX_SHADER: *params = stageProgramName(ShaderStage::Vertex); break;
|
||||||
|
case GL_TESS_CONTROL_SHADER: *params = stageProgramName(ShaderStage::TessControl); break;
|
||||||
|
case GL_TESS_EVALUATION_SHADER: *params = stageProgramName(ShaderStage::TessEval); break;
|
||||||
|
case GL_GEOMETRY_SHADER: *params = stageProgramName(ShaderStage::Geometry); break;
|
||||||
|
case GL_FRAGMENT_SHADER: *params = stageProgramName(ShaderStage::Fragment); break;
|
||||||
|
case GL_COMPUTE_SHADER: *params = stageProgramName(ShaderStage::Compute); break;
|
||||||
|
case GL_VALIDATE_STATUS: *params = (*pipelineObject)->GetValidateStatus() ? GL_TRUE : GL_FALSE; break;
|
||||||
|
case GL_INFO_LOG_LENGTH: {
|
||||||
|
// GL counts the null terminator, and reports 0 rather than 1 for an empty log.
|
||||||
|
const auto& log = (*pipelineObject)->GetInfoLog();
|
||||||
|
*params = log.empty() ? 0 : static_cast<GLint>(log.length()) + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
RecordPipelineError(ErrorCode::InvalidEnum, __func__,
|
||||||
|
std::format("pname {} is not a program pipeline parameter.",
|
||||||
|
MG_Util::ConvertGLEnumToString(pname)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetProgramPipelineInfoLog(GLuint pipeline, GLsizei bufSize, GLsizei* length, GLchar* infoLog) {
|
||||||
|
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
|
||||||
|
if (!pipelineObject) return;
|
||||||
|
if (bufSize < 0) {
|
||||||
|
RecordPipelineError(ErrorCode::InvalidValue, __func__, "bufSize must be non-negative.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bufSize == 0 || !infoLog) {
|
||||||
|
if (length) *length = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& log = (*pipelineObject)->GetInfoLog();
|
||||||
|
const auto copied = std::min<GLsizei>(bufSize - 1, static_cast<GLsizei>(log.length()));
|
||||||
|
if (copied > 0) Memcpy(infoLog, log.data(), static_cast<SizeT>(copied));
|
||||||
|
infoLog[copied] = '\0';
|
||||||
|
if (length) *length = copied;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UseProgramStages(GLuint pipeline, GLbitfield stages, GLuint program) {
|
||||||
|
if (stages != GL_ALL_SHADER_BITS && (stages & ~kAllStageBits) != 0) {
|
||||||
|
RecordPipelineError(ErrorCode::InvalidValue, __func__, "stages names a bit that is not a shader stage.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
|
||||||
|
if (!pipelineObject) return;
|
||||||
|
|
||||||
|
SharedPtr<MG_State::GLState::ProgramObject> programObject;
|
||||||
|
if (program != 0) {
|
||||||
|
if (!MG_State::pGLContext->ValidateProgramName(program)) {
|
||||||
|
RecordPipelineError(ErrorCode::InvalidValue, __func__,
|
||||||
|
std::format("{} is not the name of a program object.", program));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||||
|
if (!programObject) {
|
||||||
|
RecordPipelineError(ErrorCode::InvalidValue, __func__,
|
||||||
|
std::format("{} is not the name of a program object.", program));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!programObject->GetLinkStatus()) {
|
||||||
|
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
|
||||||
|
std::format("Program {} has not been linked successfully.", program));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLbitfield selected = stages == GL_ALL_SHADER_BITS ? kAllStageBits : stages;
|
||||||
|
for (GLbitfield bit = 1; bit != 0 && bit <= kAllStageBits; bit <<= 1) {
|
||||||
|
if ((selected & bit) == 0) continue;
|
||||||
|
ShaderStage stage = ShaderStage::Unknown;
|
||||||
|
if (!TryResolveStageBit(bit, stage)) continue;
|
||||||
|
// program == 0 clears the stage, which is what a null program reference means here.
|
||||||
|
(*pipelineObject)->SetStageProgram(stage, programObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActiveShaderProgram(GLuint pipeline, GLuint program) {
|
||||||
|
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
|
||||||
|
if (!pipelineObject) return;
|
||||||
|
|
||||||
|
if (program == 0) {
|
||||||
|
(*pipelineObject)->SetActiveProgram(nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!MG_State::pGLContext->ValidateProgramName(program)) {
|
||||||
|
RecordPipelineError(ErrorCode::InvalidValue, __func__,
|
||||||
|
std::format("{} is not the name of a program object.", program));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto programObject = MG_State::pGLContext->GetProgramObject(program);
|
||||||
|
if (!programObject) {
|
||||||
|
RecordPipelineError(ErrorCode::InvalidValue, __func__,
|
||||||
|
std::format("{} is not the name of a program object.", program));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!programObject->GetLinkStatus()) {
|
||||||
|
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
|
||||||
|
std::format("Program {} has not been linked successfully.", program));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(*pipelineObject)->SetActiveProgram(programObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ValidateProgramPipeline(GLuint pipeline) {
|
||||||
|
const auto* pipelineObject = TryGetPipeline(pipeline, __func__);
|
||||||
|
if (!pipelineObject) return;
|
||||||
|
// Nothing here can fail today: MobileGL links each stage program on its own, so there is no
|
||||||
|
// cross-stage interface to re-check at validation time. The log stays empty, which GL allows.
|
||||||
|
(*pipelineObject)->SetValidateStatus(true);
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
|
void GenProgramPipelines(GLsizei n, GLuint* pipelines);
|
||||||
|
void CreateProgramPipelines(GLsizei n, GLuint* pipelines);
|
||||||
|
void DeleteProgramPipelines(GLsizei n, const GLuint* pipelines);
|
||||||
|
void BindProgramPipeline(GLuint pipeline);
|
||||||
|
GLboolean IsProgramPipeline(GLuint pipeline);
|
||||||
|
void GetProgramPipelineiv(GLuint pipeline, GLenum pname, GLint* params);
|
||||||
|
void GetProgramPipelineInfoLog(GLuint pipeline, GLsizei bufSize, GLsizei* length, GLchar* infoLog);
|
||||||
|
void UseProgramStages(GLuint pipeline, GLbitfield stages, GLuint program);
|
||||||
|
void ActiveShaderProgram(GLuint pipeline, GLuint program);
|
||||||
|
void ValidateProgramPipeline(GLuint pipeline);
|
||||||
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
@@ -0,0 +1,841 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "ProgramInterface.h"
|
||||||
|
|
||||||
|
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||||
|
namespace {
|
||||||
|
// glslang folds atomic counters into synthesized blocks named
|
||||||
|
// "<getAtomicCounterBlockName()>_<binding>" (ParseContextBase.cpp), one per GL
|
||||||
|
// 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.
|
||||||
|
constexpr const char* kAtomicCounterBlockPrefix = "gl_AtomicCounterBlock";
|
||||||
|
|
||||||
|
enum class BlockKind {
|
||||||
|
Uniform, // a real GL uniform block
|
||||||
|
GlobalUbo, // the synthesized MGL_GLOBAL_UBO: GL sees its members as default-block
|
||||||
|
AtomicCounter, // gl_AtomicCounterBlock_<binding>
|
||||||
|
Storage, // a shader storage block
|
||||||
|
};
|
||||||
|
|
||||||
|
// One row of any interface. Fields a given interface does not have keep the
|
||||||
|
// spec-mandated "not applicable" value, so a prop read never has to special-case
|
||||||
|
// the interface a second time.
|
||||||
|
struct Resource {
|
||||||
|
String name;
|
||||||
|
GLenum type = GL_NONE;
|
||||||
|
GLint arraySize = 1;
|
||||||
|
GLint location = -1;
|
||||||
|
GLint locationIndex = -1;
|
||||||
|
GLint blockIndex = -1;
|
||||||
|
GLint offset = -1;
|
||||||
|
GLint arrayStride = -1;
|
||||||
|
GLint matrixStride = -1;
|
||||||
|
GLint isRowMajor = 0;
|
||||||
|
GLint atomicCounterBufferIndex = -1;
|
||||||
|
GLint topLevelArraySize = 0;
|
||||||
|
GLint topLevelArrayStride = 0;
|
||||||
|
GLint bufferBinding = 0;
|
||||||
|
GLint bufferDataSize = 0;
|
||||||
|
GLint isPerPatch = 0;
|
||||||
|
GLint xfbBufferIndex = 0;
|
||||||
|
Uint32 stages = 0; // EShLanguageMask
|
||||||
|
Vector<GLuint> activeVariables;
|
||||||
|
};
|
||||||
|
|
||||||
|
using ResourceList = Vector<Resource>;
|
||||||
|
|
||||||
|
struct Model {
|
||||||
|
ResourceList uniforms;
|
||||||
|
ResourceList uniformBlocks;
|
||||||
|
ResourceList atomicCounterBuffers;
|
||||||
|
ResourceList bufferVariables;
|
||||||
|
ResourceList storageBlocks;
|
||||||
|
ResourceList programInputs;
|
||||||
|
ResourceList programOutputs;
|
||||||
|
ResourceList xfbVaryings;
|
||||||
|
Bool valid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResourceList& EmptyList() {
|
||||||
|
static const ResourceList empty;
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- name spelling (cluster 6) -------------------------------------------------
|
||||||
|
|
||||||
|
Bool EndsWithZeroSubscript(const String& name) {
|
||||||
|
return name.length() >= 3 && name.compare(name.length() - 3, 3, "[0]") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The enumerated spelling of an array resource is "name[0]". glslang already applies
|
||||||
|
// that to uniforms and buffer variables (EShReflectionBasicArraySuffix), but never to
|
||||||
|
// stage inputs/outputs, so those get it here.
|
||||||
|
String WithArraySuffix(const String& name, const glslang::TType* type) {
|
||||||
|
if (type == nullptr || !type->isArray() || EndsWithZeroSubscript(name)) return name;
|
||||||
|
return name + "[0]";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
GLint ArraySizeOf(const glslang::TType* type, GLint reflectedSize) {
|
||||||
|
if (type != nullptr && type->isArray()) {
|
||||||
|
if (!type->isSizedArray()) return 0;
|
||||||
|
return type->getOuterArraySize();
|
||||||
|
}
|
||||||
|
return reflectedSize < 1 ? 1 : reflectedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two spellings name the same resource when they are equal, or differ only by the
|
||||||
|
// "[0]" the enumeration appends to an array.
|
||||||
|
Bool NamesMatch(const String& resourceName, const String& query) {
|
||||||
|
if (resourceName == query) return true;
|
||||||
|
if (EndsWithZeroSubscript(resourceName) &&
|
||||||
|
resourceName.compare(0, resourceName.length() - 3, query) == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return EndsWithZeroSubscript(query) && query.compare(0, query.length() - 3, resourceName) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits "base[k]" into ("base", k). GL 4.6 §7.3.1.1 requires the subscript to be a
|
||||||
|
// decimal integer with no white space and no leading zeros, which is exactly what
|
||||||
|
// separates array-names' "a[1]" (resolves) from "a[01]", "a[0 + 0]" and "a[ 0]" (do
|
||||||
|
// not). Returns false when there is no trailing subscript at all; sets `malformed`
|
||||||
|
// when there is one but it is not a strict decimal.
|
||||||
|
Bool SplitTrailingSubscript(const String& name, String& outBase, Uint& outElement, Bool& outMalformed) {
|
||||||
|
outMalformed = false;
|
||||||
|
if (name.empty() || name.back() != ']') return false;
|
||||||
|
const SizeT bracket = name.rfind('[');
|
||||||
|
if (bracket == String::npos) return false;
|
||||||
|
const SizeT first = bracket + 1;
|
||||||
|
const SizeT last = name.length() - 1; // one past the digits
|
||||||
|
if (first >= last) {
|
||||||
|
outMalformed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// No leading zeros: "0" is the only spelling that may start with '0'.
|
||||||
|
if (name[first] == '0' && last - first > 1) {
|
||||||
|
outMalformed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Uint element = 0;
|
||||||
|
for (SizeT i = first; i < last; ++i) {
|
||||||
|
if (name[i] < '0' || name[i] > '9') {
|
||||||
|
outMalformed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
element = element * 10 + static_cast<Uint>(name[i] - '0');
|
||||||
|
if (element > 0x0FFFFFFFu) {
|
||||||
|
outMalformed = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outBase = name.substr(0, bracket);
|
||||||
|
outElement = element;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- block classification ------------------------------------------------------
|
||||||
|
|
||||||
|
Bool IsAtomicCounterBlockName(const String& name) {
|
||||||
|
return name.compare(0, std::strlen(kAtomicCounterBlockPrefix), kAtomicCounterBlockPrefix) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "gl_AtomicCounterBlock_5" -> 5. The suffix is the GL binding the counters were
|
||||||
|
// declared with, which glslang does NOT keep in the block's own layout qualifier
|
||||||
|
// (that one is remapped to a plain buffer binding).
|
||||||
|
GLint AtomicCounterBlockBinding(const String& name) {
|
||||||
|
const SizeT underscore = name.rfind('_');
|
||||||
|
if (underscore == String::npos || underscore + 1 >= name.length()) return 0;
|
||||||
|
GLint binding = 0;
|
||||||
|
for (SizeT i = underscore + 1; i < name.length(); ++i) {
|
||||||
|
if (name[i] < '0' || name[i] > '9') return 0;
|
||||||
|
binding = binding * 10 + (name[i] - '0');
|
||||||
|
}
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Element index of an arrayed block instance ("TrickyBuffer[1]" -> 1).
|
||||||
|
GLint BlockArrayElement(const String& name) {
|
||||||
|
String base;
|
||||||
|
Uint element = 0;
|
||||||
|
Bool malformed = false;
|
||||||
|
if (!SplitTrailingSubscript(name, base, element, malformed)) return 0;
|
||||||
|
return static_cast<GLint>(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockKind ClassifyBlock(const glslang::TObjectReflection& block) {
|
||||||
|
if (std::strstr(block.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) {
|
||||||
|
return BlockKind::GlobalUbo;
|
||||||
|
}
|
||||||
|
if (IsAtomicCounterBlockName(block.name)) return BlockKind::AtomicCounter;
|
||||||
|
const glslang::TType* type = block.getType();
|
||||||
|
if (type != nullptr && type->getQualifier().storage == glslang::EvqBuffer) return BlockKind::Storage;
|
||||||
|
return BlockKind::Uniform;
|
||||||
|
}
|
||||||
|
|
||||||
|
// std140/std430 column stride, the same vec4-rounded rule ProgramObject applies to
|
||||||
|
// uniform matrices. 0 for a non-matrix.
|
||||||
|
GLint MatrixStrideOf(const glslang::TType* type) {
|
||||||
|
if (type == nullptr || !type->isMatrix()) return 0;
|
||||||
|
const bool rowMajor = type->getQualifier().layoutMatrix == glslang::ElmRowMajor;
|
||||||
|
const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows();
|
||||||
|
constexpr int scalarSize = 4;
|
||||||
|
const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize
|
||||||
|
: (strideVectorComponents == 2) ? 2 * scalarSize
|
||||||
|
: 4 * scalarSize;
|
||||||
|
return (vectorAlignment + 15) & ~15;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint IsRowMajorOf(const glslang::TType* type) {
|
||||||
|
if (type == nullptr || !type->isMatrix()) return 0;
|
||||||
|
return type->getQualifier().layoutMatrix == glslang::ElmRowMajor ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint MappedLocation(Int rawLocation) {
|
||||||
|
// glslang parks "no location" at layoutLocationEnd; GL spells it -1.
|
||||||
|
if (rawLocation < 0 || rawLocation >= static_cast<Int>(glslang::TQualifier::layoutLocationEnd)) return -1;
|
||||||
|
return rawLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- model construction --------------------------------------------------------
|
||||||
|
|
||||||
|
void BuildBlocks(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
||||||
|
Vector<BlockKind>& blockKind, Vector<Int>& blockInterfaceIndex) {
|
||||||
|
const Int blockCount = const_cast<glslang::TProgram&>(reflection).getNumUniformBlocks();
|
||||||
|
blockKind.assign(blockCount, BlockKind::Uniform);
|
||||||
|
blockInterfaceIndex.assign(blockCount, -1);
|
||||||
|
|
||||||
|
for (Int tIndex = 0; tIndex < blockCount; ++tIndex) {
|
||||||
|
const auto& block = const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex);
|
||||||
|
const BlockKind kind = ClassifyBlock(block);
|
||||||
|
blockKind[tIndex] = kind;
|
||||||
|
if (kind == BlockKind::AtomicCounter) {
|
||||||
|
Resource resource;
|
||||||
|
// GL_ATOMIC_COUNTER_BUFFER resources have no name (and GetProgramResource
|
||||||
|
// Index/Name reject the interface outright, which is why this stays empty).
|
||||||
|
resource.bufferBinding = AtomicCounterBlockBinding(block.name);
|
||||||
|
resource.bufferDataSize = block.size;
|
||||||
|
resource.stages = static_cast<Uint32>(block.stages);
|
||||||
|
blockInterfaceIndex[tIndex] = static_cast<Int>(model.atomicCounterBuffers.size());
|
||||||
|
model.atomicCounterBuffers.push_back(Move(resource));
|
||||||
|
} else if (kind == BlockKind::Storage) {
|
||||||
|
Resource resource;
|
||||||
|
resource.name = block.name;
|
||||||
|
// glslang reports the DECLARED binding for every instance of an arrayed
|
||||||
|
// block; GL gives element k the binding base + k. That is only the initial
|
||||||
|
// value: GL_BUFFER_BINDING must report the CURRENT binding, so a later
|
||||||
|
// glShaderStorageBlockBinding wins over the declaration (GL 4.6 §7.6.2 -
|
||||||
|
// exactly the same rule GL_UNIFORM_BLOCK follows through
|
||||||
|
// GetUniformBlockBinding below).
|
||||||
|
const GLint declared = block.getBinding();
|
||||||
|
resource.bufferBinding = declared < 0 ? 0 : declared + BlockArrayElement(block.name);
|
||||||
|
const Int rebound = program.GetShaderStorageBlockBindingOverride(block.name);
|
||||||
|
if (rebound >= 0) resource.bufferBinding = static_cast<GLint>(rebound);
|
||||||
|
resource.bufferDataSize = block.size;
|
||||||
|
resource.stages = static_cast<Uint32>(block.stages);
|
||||||
|
blockInterfaceIndex[tIndex] = static_cast<Int>(model.storageBlocks.size());
|
||||||
|
model.storageBlocks.push_back(Move(resource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL_UNIFORM_BLOCK keeps the index space glUniformBlockBinding and
|
||||||
|
// glGetActiveUniformBlockiv already use, so an index handed out here is usable
|
||||||
|
// with them (which is exactly what the CTS does).
|
||||||
|
const Int glBlockCount = program.GetActiveUniformBlocksCount();
|
||||||
|
for (Int glIndex = 0; glIndex < glBlockCount; ++glIndex) {
|
||||||
|
Resource resource;
|
||||||
|
resource.name = program.GetUniformBlockName(glIndex);
|
||||||
|
resource.bufferBinding = static_cast<GLint>(program.GetUniformBlockBinding(glIndex));
|
||||||
|
resource.bufferDataSize = static_cast<GLint>(program.GetUBOSizeAt(glIndex));
|
||||||
|
const Int tIndex = program.TProgramBlockIndex(static_cast<Uint>(glIndex));
|
||||||
|
if (tIndex >= 0 && tIndex < blockCount) {
|
||||||
|
resource.stages =
|
||||||
|
static_cast<Uint32>(const_cast<glslang::TProgram&>(reflection).getUniformBlock(tIndex).stages);
|
||||||
|
}
|
||||||
|
model.uniformBlocks.push_back(Move(resource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildUniformsAndBufferVariables(ProgramObject& program, const glslang::TProgram& reflection, Model& model,
|
||||||
|
const Vector<BlockKind>& blockKind,
|
||||||
|
const Vector<Int>& blockInterfaceIndex) {
|
||||||
|
const Uint uniformCount = program.GetUniformCount();
|
||||||
|
for (Uint glIndex = 0; glIndex < uniformCount; ++glIndex) {
|
||||||
|
const Int tIndex = program.TProgramUniformIndex(glIndex);
|
||||||
|
const auto& refl = const_cast<glslang::TProgram&>(reflection).getUniform(tIndex);
|
||||||
|
const glslang::TType* type = refl.getType();
|
||||||
|
const Int owner = refl.index;
|
||||||
|
const BlockKind kind = (owner >= 0 && owner < static_cast<Int>(blockKind.size()))
|
||||||
|
? blockKind[owner]
|
||||||
|
: BlockKind::GlobalUbo;
|
||||||
|
|
||||||
|
Resource resource;
|
||||||
|
resource.name = refl.name;
|
||||||
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
|
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||||
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
|
|
||||||
|
if (kind == BlockKind::Storage) {
|
||||||
|
resource.blockIndex = blockInterfaceIndex[owner];
|
||||||
|
resource.offset = refl.offset;
|
||||||
|
resource.arrayStride = refl.arrayStride;
|
||||||
|
resource.matrixStride = MatrixStrideOf(type);
|
||||||
|
resource.isRowMajor = IsRowMajorOf(type);
|
||||||
|
// GL requires 1 for a member that is not inside a top-level array (and for
|
||||||
|
// the top-level array itself); glslang leaves 0/-1 there.
|
||||||
|
resource.topLevelArraySize = refl.topLevelArraySize > 0 ? refl.topLevelArraySize : 1;
|
||||||
|
resource.topLevelArrayStride = refl.topLevelArrayStride;
|
||||||
|
model.bufferVariables.push_back(Move(resource));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind == BlockKind::AtomicCounter) {
|
||||||
|
// An atomic counter is a default-block uniform with no location and no
|
||||||
|
// owning uniform block; what it does have is a buffer to point at.
|
||||||
|
resource.type = GL_UNSIGNED_INT_ATOMIC_COUNTER;
|
||||||
|
resource.blockIndex = -1;
|
||||||
|
resource.offset = refl.offset;
|
||||||
|
resource.arrayStride = refl.arrayStride;
|
||||||
|
resource.matrixStride = 0;
|
||||||
|
resource.atomicCounterBufferIndex = blockInterfaceIndex[owner];
|
||||||
|
resource.location = -1;
|
||||||
|
} else {
|
||||||
|
resource.blockIndex = program.GetActiveUniformBlockIndex(glIndex);
|
||||||
|
resource.offset = program.GetActiveUniformOffset(glIndex);
|
||||||
|
resource.arrayStride = program.GetActiveUniformArrayStride(glIndex);
|
||||||
|
resource.matrixStride = program.GetActiveUniformMatrixStride(glIndex);
|
||||||
|
resource.isRowMajor = program.GetActiveUniformIsRowMajor(glIndex);
|
||||||
|
// 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
|
||||||
|
// so glUniform* can address block members through the global UBO).
|
||||||
|
resource.location =
|
||||||
|
resource.blockIndex >= 0 ? -1 : program.GetUniformLocation(refl.name);
|
||||||
|
}
|
||||||
|
model.uniforms.push_back(Move(resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL_ACTIVE_VARIABLES, both directions.
|
||||||
|
for (SizeT i = 0; i < model.uniforms.size(); ++i) {
|
||||||
|
const Resource& uniform = model.uniforms[i];
|
||||||
|
if (uniform.atomicCounterBufferIndex >= 0 &&
|
||||||
|
uniform.atomicCounterBufferIndex < static_cast<GLint>(model.atomicCounterBuffers.size())) {
|
||||||
|
model.atomicCounterBuffers[uniform.atomicCounterBufferIndex].activeVariables.push_back(
|
||||||
|
static_cast<GLuint>(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (SizeT blockIndex = 0; blockIndex < model.uniformBlocks.size(); ++blockIndex) {
|
||||||
|
// Members of an arrayed block are reflected once, against instance [0].
|
||||||
|
const Int owner = static_cast<Int>(program.GetUniformBlockMemberOwnerIndex(static_cast<Uint>(blockIndex)));
|
||||||
|
for (SizeT i = 0; i < model.uniforms.size(); ++i) {
|
||||||
|
if (model.uniforms[i].blockIndex == owner) {
|
||||||
|
model.uniformBlocks[blockIndex].activeVariables.push_back(static_cast<GLuint>(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (SizeT blockIndex = 0; blockIndex < model.storageBlocks.size(); ++blockIndex) {
|
||||||
|
for (SizeT i = 0; i < model.bufferVariables.size(); ++i) {
|
||||||
|
if (model.bufferVariables[i].blockIndex == static_cast<GLint>(blockIndex)) {
|
||||||
|
model.storageBlocks[blockIndex].activeVariables.push_back(static_cast<GLuint>(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildStageIO(ProgramObject& program, const glslang::TProgram& reflection, Model& model) {
|
||||||
|
auto& mutableReflection = const_cast<glslang::TProgram&>(reflection);
|
||||||
|
|
||||||
|
const Int inputCount = mutableReflection.getNumPipeInputs();
|
||||||
|
for (Int index = 0; index < inputCount; ++index) {
|
||||||
|
const auto& refl = mutableReflection.getPipeInput(index);
|
||||||
|
const glslang::TType* type = refl.getType();
|
||||||
|
Resource resource;
|
||||||
|
// The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V
|
||||||
|
// names; GL enumerates the GL spellings.
|
||||||
|
const String& glName = ProgramObject::NormalizeBuiltinPipeInputName(refl.name);
|
||||||
|
resource.name = WithArraySuffix(glName, type);
|
||||||
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
|
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||||
|
resource.location = program.GetAttributeLocation(refl.name);
|
||||||
|
if (resource.location < 0) resource.location = MappedLocation(static_cast<Int>(refl.layoutLocation()));
|
||||||
|
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
|
||||||
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
|
model.programInputs.push_back(Move(resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
const Int outputCount = mutableReflection.getNumPipeOutputs();
|
||||||
|
for (Int index = 0; index < outputCount; ++index) {
|
||||||
|
const auto& refl = mutableReflection.getPipeOutput(index);
|
||||||
|
const glslang::TType* type = refl.getType();
|
||||||
|
Resource resource;
|
||||||
|
resource.name = WithArraySuffix(refl.name, type);
|
||||||
|
resource.type = static_cast<GLenum>(refl.glDefineType);
|
||||||
|
resource.arraySize = ArraySizeOf(type, refl.size);
|
||||||
|
resource.location = MappedLocation(program.GetFragmentDataLocation(refl.name.c_str()));
|
||||||
|
if (resource.location < 0) {
|
||||||
|
// A built-in output (gl_FragDepth, gl_SampleMask) and a non-fragment stage
|
||||||
|
// output both have no location, and therefore no color index either.
|
||||||
|
resource.locationIndex = -1;
|
||||||
|
} else {
|
||||||
|
resource.locationIndex = program.GetFragmentDataIndex(refl.name.c_str());
|
||||||
|
// glBindFragDataLocationIndexed wins; otherwise the shader's
|
||||||
|
// layout(index = N), which the frag-data maps never saw.
|
||||||
|
if (resource.locationIndex == 0 && type != nullptr && type->getQualifier().hasIndex()) {
|
||||||
|
resource.locationIndex = static_cast<GLint>(type->getQualifier().layoutIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resource.isPerPatch = (type != nullptr && type->getQualifier().patch) ? 1 : 0;
|
||||||
|
resource.stages = static_cast<Uint32>(refl.stages);
|
||||||
|
model.programOutputs.push_back(Move(resource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildXfb(ProgramObject& program, Model& model) {
|
||||||
|
const auto& requested = program.GetTransformFeedbackInterfaceNames();
|
||||||
|
const auto& captured = program.GetTransformFeedbackVaryings();
|
||||||
|
for (const String& name : requested) {
|
||||||
|
Resource resource;
|
||||||
|
resource.name = name;
|
||||||
|
// ARB_transform_feedback3's layout controls are enumerated as resources of
|
||||||
|
// type NONE: gl_NextBuffer with array size 0, gl_SkipComponentsN with N.
|
||||||
|
if (name == "gl_NextBuffer") {
|
||||||
|
resource.type = GL_NONE;
|
||||||
|
resource.arraySize = 0;
|
||||||
|
} else if (name.size() == 18 && name.compare(0, 17, "gl_SkipComponents") == 0 && name[17] >= '1' &&
|
||||||
|
name[17] <= '4') {
|
||||||
|
resource.type = GL_NONE;
|
||||||
|
resource.arraySize = name[17] - '0';
|
||||||
|
} else {
|
||||||
|
resource.type = GL_NONE;
|
||||||
|
resource.arraySize = 1;
|
||||||
|
for (const auto& varying : captured) {
|
||||||
|
if (varying.name != name) continue;
|
||||||
|
resource.type = varying.type;
|
||||||
|
resource.arraySize = varying.size < 1 ? 1 : varying.size;
|
||||||
|
resource.offset = static_cast<GLint>(varying.offsetBytes);
|
||||||
|
resource.xfbBufferIndex = static_cast<GLint>(varying.bufferIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.xfbVaryings.push_back(Move(resource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Model BuildModel(ProgramObject& program) {
|
||||||
|
Model model;
|
||||||
|
if (!program.GetLinkStatus()) return model;
|
||||||
|
const glslang::TProgram* reflection = program.GetReflection();
|
||||||
|
if (reflection == nullptr) return model;
|
||||||
|
model.valid = true;
|
||||||
|
|
||||||
|
Vector<BlockKind> blockKind;
|
||||||
|
Vector<Int> blockInterfaceIndex;
|
||||||
|
BuildBlocks(program, *reflection, model, blockKind, blockInterfaceIndex);
|
||||||
|
BuildUniformsAndBufferVariables(program, *reflection, model, blockKind, blockInterfaceIndex);
|
||||||
|
BuildStageIO(program, *reflection, model);
|
||||||
|
BuildXfb(program, model);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResourceList& Select(const Model& model, GLenum programInterface) {
|
||||||
|
switch (programInterface) {
|
||||||
|
case GL_UNIFORM:
|
||||||
|
return model.uniforms;
|
||||||
|
case GL_UNIFORM_BLOCK:
|
||||||
|
return model.uniformBlocks;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER:
|
||||||
|
return model.atomicCounterBuffers;
|
||||||
|
case GL_BUFFER_VARIABLE:
|
||||||
|
return model.bufferVariables;
|
||||||
|
case GL_SHADER_STORAGE_BLOCK:
|
||||||
|
return model.storageBlocks;
|
||||||
|
case GL_PROGRAM_INPUT:
|
||||||
|
return model.programInputs;
|
||||||
|
case GL_PROGRAM_OUTPUT:
|
||||||
|
return model.programOutputs;
|
||||||
|
case GL_TRANSFORM_FEEDBACK_VARYING:
|
||||||
|
return model.xfbVaryings;
|
||||||
|
default:
|
||||||
|
// The subroutine interfaces are accepted by the API but nothing can populate
|
||||||
|
// them: glslang refuses `subroutine` when generating SPIR-V, so a program
|
||||||
|
// using one never links. Zero active resources is the honest answer.
|
||||||
|
return EmptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Bool IsInterfaceEnum(GLenum programInterface) {
|
||||||
|
switch (programInterface) {
|
||||||
|
case GL_UNIFORM:
|
||||||
|
case GL_UNIFORM_BLOCK:
|
||||||
|
case GL_PROGRAM_INPUT:
|
||||||
|
case GL_PROGRAM_OUTPUT:
|
||||||
|
case GL_BUFFER_VARIABLE:
|
||||||
|
case GL_SHADER_STORAGE_BLOCK:
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER:
|
||||||
|
case GL_TRANSFORM_FEEDBACK_VARYING:
|
||||||
|
case GL_TRANSFORM_FEEDBACK_BUFFER:
|
||||||
|
case GL_VERTEX_SUBROUTINE:
|
||||||
|
case GL_TESS_CONTROL_SUBROUTINE:
|
||||||
|
case GL_TESS_EVALUATION_SUBROUTINE:
|
||||||
|
case GL_GEOMETRY_SUBROUTINE:
|
||||||
|
case GL_FRAGMENT_SUBROUTINE:
|
||||||
|
case GL_COMPUTE_SUBROUTINE:
|
||||||
|
case GL_VERTEX_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_TESS_CONTROL_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_TESS_EVALUATION_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_GEOMETRY_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_FRAGMENT_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_COMPUTE_SUBROUTINE_UNIFORM:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool IsNamedInterface(GLenum programInterface) {
|
||||||
|
// GL 4.6 §7.3.1.2: the two buffer interfaces have no resource names, and asking for
|
||||||
|
// one is INVALID_ENUM (deliberately asymmetric with GetProgramInterfaceiv, which
|
||||||
|
// does count them).
|
||||||
|
return IsInterfaceEnum(programInterface) && programInterface != GL_ATOMIC_COUNTER_BUFFER &&
|
||||||
|
programInterface != GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool InterfaceHasLocations(GLenum programInterface) {
|
||||||
|
switch (programInterface) {
|
||||||
|
case GL_UNIFORM:
|
||||||
|
case GL_PROGRAM_INPUT:
|
||||||
|
case GL_PROGRAM_OUTPUT:
|
||||||
|
case GL_VERTEX_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_TESS_CONTROL_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_TESS_EVALUATION_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_GEOMETRY_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_FRAGMENT_SUBROUTINE_UNIFORM:
|
||||||
|
case GL_COMPUTE_SUBROUTINE_UNIFORM:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool IsResourceProp(GLenum prop) {
|
||||||
|
switch (prop) {
|
||||||
|
case GL_NAME_LENGTH:
|
||||||
|
case GL_TYPE:
|
||||||
|
case GL_ARRAY_SIZE:
|
||||||
|
case GL_OFFSET:
|
||||||
|
case GL_BLOCK_INDEX:
|
||||||
|
case GL_ARRAY_STRIDE:
|
||||||
|
case GL_MATRIX_STRIDE:
|
||||||
|
case GL_IS_ROW_MAJOR:
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_INDEX:
|
||||||
|
case GL_BUFFER_BINDING:
|
||||||
|
case GL_BUFFER_DATA_SIZE:
|
||||||
|
case GL_NUM_ACTIVE_VARIABLES:
|
||||||
|
case GL_ACTIVE_VARIABLES:
|
||||||
|
case GL_REFERENCED_BY_VERTEX_SHADER:
|
||||||
|
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||||
|
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||||
|
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
||||||
|
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
||||||
|
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
||||||
|
case GL_TOP_LEVEL_ARRAY_SIZE:
|
||||||
|
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
||||||
|
case GL_LOCATION:
|
||||||
|
case GL_LOCATION_INDEX:
|
||||||
|
case GL_IS_PER_PATCH:
|
||||||
|
case GL_LOCATION_COMPONENT:
|
||||||
|
case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
|
||||||
|
case GL_TRANSFORM_FEEDBACK_BUFFER_STRIDE:
|
||||||
|
case GL_NUM_COMPATIBLE_SUBROUTINES:
|
||||||
|
case GL_COMPATIBLE_SUBROUTINES:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL 4.6 Table 7.2, transcribed row by row: which interfaces each property applies to.
|
||||||
|
// Too tight a table turns a currently-answered prop into a fresh INVALID_OPERATION, so
|
||||||
|
// the rows below are deliberately no narrower than the spec's.
|
||||||
|
Bool InterfaceSupportsProp(GLenum programInterface, GLenum prop) {
|
||||||
|
const Bool isSubroutine =
|
||||||
|
programInterface == GL_VERTEX_SUBROUTINE || programInterface == GL_TESS_CONTROL_SUBROUTINE ||
|
||||||
|
programInterface == GL_TESS_EVALUATION_SUBROUTINE || programInterface == GL_GEOMETRY_SUBROUTINE ||
|
||||||
|
programInterface == GL_FRAGMENT_SUBROUTINE || programInterface == GL_COMPUTE_SUBROUTINE;
|
||||||
|
const Bool isSubroutineUniform =
|
||||||
|
programInterface == GL_VERTEX_SUBROUTINE_UNIFORM ||
|
||||||
|
programInterface == GL_TESS_CONTROL_SUBROUTINE_UNIFORM ||
|
||||||
|
programInterface == GL_TESS_EVALUATION_SUBROUTINE_UNIFORM ||
|
||||||
|
programInterface == GL_GEOMETRY_SUBROUTINE_UNIFORM ||
|
||||||
|
programInterface == GL_FRAGMENT_SUBROUTINE_UNIFORM || programInterface == GL_COMPUTE_SUBROUTINE_UNIFORM;
|
||||||
|
|
||||||
|
switch (prop) {
|
||||||
|
case GL_NAME_LENGTH:
|
||||||
|
return programInterface != GL_ATOMIC_COUNTER_BUFFER && programInterface != GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||||
|
case GL_TYPE:
|
||||||
|
case GL_ARRAY_SIZE:
|
||||||
|
return programInterface == GL_UNIFORM || programInterface == GL_PROGRAM_INPUT ||
|
||||||
|
programInterface == GL_PROGRAM_OUTPUT || programInterface == GL_BUFFER_VARIABLE ||
|
||||||
|
programInterface == GL_TRANSFORM_FEEDBACK_VARYING ||
|
||||||
|
(prop == GL_ARRAY_SIZE && isSubroutineUniform);
|
||||||
|
case GL_OFFSET:
|
||||||
|
return programInterface == GL_UNIFORM || programInterface == GL_BUFFER_VARIABLE ||
|
||||||
|
programInterface == GL_TRANSFORM_FEEDBACK_VARYING;
|
||||||
|
case GL_BLOCK_INDEX:
|
||||||
|
case GL_ARRAY_STRIDE:
|
||||||
|
case GL_MATRIX_STRIDE:
|
||||||
|
case GL_IS_ROW_MAJOR:
|
||||||
|
return programInterface == GL_UNIFORM || programInterface == GL_BUFFER_VARIABLE;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_INDEX:
|
||||||
|
return programInterface == GL_UNIFORM;
|
||||||
|
case GL_BUFFER_BINDING:
|
||||||
|
case GL_NUM_ACTIVE_VARIABLES:
|
||||||
|
case GL_ACTIVE_VARIABLES:
|
||||||
|
// Table 7.2 lists GL_TRANSFORM_FEEDBACK_BUFFER on these three rows too. This
|
||||||
|
// implementation enumerates no resources on that interface, so the query still
|
||||||
|
// ends in an error - but INVALID_VALUE for the out-of-range index, not the
|
||||||
|
// INVALID_OPERATION a narrower table would invent.
|
||||||
|
return programInterface == GL_UNIFORM_BLOCK || programInterface == GL_ATOMIC_COUNTER_BUFFER ||
|
||||||
|
programInterface == GL_SHADER_STORAGE_BLOCK ||
|
||||||
|
programInterface == GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||||
|
case GL_BUFFER_DATA_SIZE:
|
||||||
|
return programInterface == GL_UNIFORM_BLOCK || programInterface == GL_ATOMIC_COUNTER_BUFFER ||
|
||||||
|
programInterface == GL_SHADER_STORAGE_BLOCK;
|
||||||
|
case GL_REFERENCED_BY_VERTEX_SHADER:
|
||||||
|
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||||
|
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||||
|
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
||||||
|
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
||||||
|
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
||||||
|
return programInterface == GL_UNIFORM || programInterface == GL_UNIFORM_BLOCK ||
|
||||||
|
programInterface == GL_ATOMIC_COUNTER_BUFFER || programInterface == GL_BUFFER_VARIABLE ||
|
||||||
|
programInterface == GL_SHADER_STORAGE_BLOCK || programInterface == GL_PROGRAM_INPUT ||
|
||||||
|
programInterface == GL_PROGRAM_OUTPUT || isSubroutineUniform;
|
||||||
|
case GL_TOP_LEVEL_ARRAY_SIZE:
|
||||||
|
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
||||||
|
return programInterface == GL_BUFFER_VARIABLE;
|
||||||
|
case GL_LOCATION:
|
||||||
|
return InterfaceHasLocations(programInterface);
|
||||||
|
case GL_LOCATION_INDEX:
|
||||||
|
return programInterface == GL_PROGRAM_OUTPUT;
|
||||||
|
case GL_IS_PER_PATCH:
|
||||||
|
case GL_LOCATION_COMPONENT:
|
||||||
|
return programInterface == GL_PROGRAM_INPUT || programInterface == GL_PROGRAM_OUTPUT;
|
||||||
|
case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
|
||||||
|
return programInterface == GL_TRANSFORM_FEEDBACK_VARYING;
|
||||||
|
case GL_TRANSFORM_FEEDBACK_BUFFER_STRIDE:
|
||||||
|
return programInterface == GL_TRANSFORM_FEEDBACK_BUFFER;
|
||||||
|
case GL_NUM_COMPATIBLE_SUBROUTINES:
|
||||||
|
case GL_COMPATIBLE_SUBROUTINES:
|
||||||
|
return isSubroutineUniform;
|
||||||
|
default:
|
||||||
|
(void)isSubroutine;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Int GetActiveResourceCount(ProgramObject& program, GLenum programInterface) {
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
return static_cast<Int>(Select(model, programInterface).size());
|
||||||
|
}
|
||||||
|
|
||||||
|
Int GetMaxNameLength(ProgramObject& program, GLenum programInterface) {
|
||||||
|
if (!IsNamedInterface(programInterface)) return 0;
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
SizeT longest = 0;
|
||||||
|
for (const Resource& resource : Select(model, programInterface)) {
|
||||||
|
longest = std::max(longest, resource.name.length() + 1);
|
||||||
|
}
|
||||||
|
return static_cast<Int>(longest);
|
||||||
|
}
|
||||||
|
|
||||||
|
Int GetMaxNumActiveVariables(ProgramObject& program, GLenum programInterface) {
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
SizeT longest = 0;
|
||||||
|
for (const Resource& resource : Select(model, programInterface)) {
|
||||||
|
longest = std::max(longest, resource.activeVariables.size());
|
||||||
|
}
|
||||||
|
return static_cast<Int>(longest);
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint GetResourceIndex(ProgramObject& program, GLenum programInterface, const char* name) {
|
||||||
|
if (name == nullptr || name[0] == '\0') return GL_INVALID_INDEX;
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
const ResourceList& resources = Select(model, programInterface);
|
||||||
|
const String query = name;
|
||||||
|
// The layout controls of an interleaved capture are enumerable but not addressable
|
||||||
|
// by name (GL 4.6 §7.3.1.1).
|
||||||
|
if (programInterface == GL_TRANSFORM_FEEDBACK_VARYING &&
|
||||||
|
(query == "gl_NextBuffer" ||
|
||||||
|
(query.size() == 18 && query.compare(0, 17, "gl_SkipComponents") == 0))) {
|
||||||
|
return GL_INVALID_INDEX;
|
||||||
|
}
|
||||||
|
for (SizeT i = 0; i < resources.size(); ++i) {
|
||||||
|
if (NamesMatch(resources[i].name, query)) return static_cast<GLuint>(i);
|
||||||
|
}
|
||||||
|
return GL_INVALID_INDEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool GetResourceName(ProgramObject& program, GLenum programInterface, GLuint index, String& outName) {
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
const ResourceList& resources = Select(model, programInterface);
|
||||||
|
if (index >= resources.size()) return false;
|
||||||
|
outName = resources[index].name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool GetResourceProp(ProgramObject& program, GLenum programInterface, GLuint index, GLenum prop,
|
||||||
|
Vector<GLint>& outValues) {
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
const ResourceList& resources = Select(model, programInterface);
|
||||||
|
if (index >= resources.size()) return false;
|
||||||
|
const Resource& resource = resources[index];
|
||||||
|
|
||||||
|
const auto referencedBy = [&resource](EShLanguage stage) {
|
||||||
|
return (resource.stages & static_cast<Uint32>(1u << stage)) != 0 ? GL_TRUE : GL_FALSE;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (prop) {
|
||||||
|
case GL_NAME_LENGTH:
|
||||||
|
outValues.push_back(static_cast<GLint>(resource.name.length() + 1));
|
||||||
|
break;
|
||||||
|
case GL_TYPE:
|
||||||
|
outValues.push_back(static_cast<GLint>(resource.type));
|
||||||
|
break;
|
||||||
|
case GL_ARRAY_SIZE:
|
||||||
|
outValues.push_back(resource.arraySize);
|
||||||
|
break;
|
||||||
|
case GL_OFFSET:
|
||||||
|
outValues.push_back(resource.offset);
|
||||||
|
break;
|
||||||
|
case GL_BLOCK_INDEX:
|
||||||
|
outValues.push_back(resource.blockIndex);
|
||||||
|
break;
|
||||||
|
case GL_ARRAY_STRIDE:
|
||||||
|
outValues.push_back(resource.arrayStride);
|
||||||
|
break;
|
||||||
|
case GL_MATRIX_STRIDE:
|
||||||
|
outValues.push_back(resource.matrixStride);
|
||||||
|
break;
|
||||||
|
case GL_IS_ROW_MAJOR:
|
||||||
|
outValues.push_back(resource.isRowMajor);
|
||||||
|
break;
|
||||||
|
case GL_ATOMIC_COUNTER_BUFFER_INDEX:
|
||||||
|
outValues.push_back(resource.atomicCounterBufferIndex);
|
||||||
|
break;
|
||||||
|
case GL_BUFFER_BINDING:
|
||||||
|
outValues.push_back(resource.bufferBinding);
|
||||||
|
break;
|
||||||
|
case GL_BUFFER_DATA_SIZE:
|
||||||
|
outValues.push_back(resource.bufferDataSize);
|
||||||
|
break;
|
||||||
|
case GL_NUM_ACTIVE_VARIABLES:
|
||||||
|
outValues.push_back(static_cast<GLint>(resource.activeVariables.size()));
|
||||||
|
break;
|
||||||
|
case GL_ACTIVE_VARIABLES:
|
||||||
|
for (const GLuint variable : resource.activeVariables) outValues.push_back(static_cast<GLint>(variable));
|
||||||
|
break;
|
||||||
|
case GL_REFERENCED_BY_VERTEX_SHADER:
|
||||||
|
outValues.push_back(referencedBy(EShLangVertex));
|
||||||
|
break;
|
||||||
|
case GL_REFERENCED_BY_TESS_CONTROL_SHADER:
|
||||||
|
outValues.push_back(referencedBy(EShLangTessControl));
|
||||||
|
break;
|
||||||
|
case GL_REFERENCED_BY_TESS_EVALUATION_SHADER:
|
||||||
|
outValues.push_back(referencedBy(EShLangTessEvaluation));
|
||||||
|
break;
|
||||||
|
case GL_REFERENCED_BY_GEOMETRY_SHADER:
|
||||||
|
outValues.push_back(referencedBy(EShLangGeometry));
|
||||||
|
break;
|
||||||
|
case GL_REFERENCED_BY_FRAGMENT_SHADER:
|
||||||
|
outValues.push_back(referencedBy(EShLangFragment));
|
||||||
|
break;
|
||||||
|
case GL_REFERENCED_BY_COMPUTE_SHADER:
|
||||||
|
outValues.push_back(referencedBy(EShLangCompute));
|
||||||
|
break;
|
||||||
|
case GL_TOP_LEVEL_ARRAY_SIZE:
|
||||||
|
outValues.push_back(resource.topLevelArraySize);
|
||||||
|
break;
|
||||||
|
case GL_TOP_LEVEL_ARRAY_STRIDE:
|
||||||
|
outValues.push_back(resource.topLevelArrayStride);
|
||||||
|
break;
|
||||||
|
case GL_LOCATION:
|
||||||
|
outValues.push_back(resource.location);
|
||||||
|
break;
|
||||||
|
case GL_LOCATION_INDEX:
|
||||||
|
outValues.push_back(resource.locationIndex);
|
||||||
|
break;
|
||||||
|
case GL_IS_PER_PATCH:
|
||||||
|
outValues.push_back(resource.isPerPatch);
|
||||||
|
break;
|
||||||
|
case GL_LOCATION_COMPONENT:
|
||||||
|
outValues.push_back(0);
|
||||||
|
break;
|
||||||
|
case GL_TRANSFORM_FEEDBACK_BUFFER_INDEX:
|
||||||
|
outValues.push_back(resource.xfbBufferIndex);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
outValues.push_back(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint GetResourceLocation(ProgramObject& program, GLenum programInterface, const char* name) {
|
||||||
|
if (name == nullptr || name[0] == '\0') return -1;
|
||||||
|
const String query = name;
|
||||||
|
|
||||||
|
String base;
|
||||||
|
Uint element = 0;
|
||||||
|
Bool malformed = false;
|
||||||
|
const Bool subscripted = SplitTrailingSubscript(query, base, element, malformed);
|
||||||
|
if (malformed) return -1;
|
||||||
|
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
const ResourceList& resources = Select(model, programInterface);
|
||||||
|
for (const Resource& resource : resources) {
|
||||||
|
if (NamesMatch(resource.name, query)) return resource.location;
|
||||||
|
}
|
||||||
|
if (!subscripted || element == 0) return -1;
|
||||||
|
// "d[1]" addresses the second element of an array resource enumerated as "d[0]".
|
||||||
|
for (const Resource& resource : resources) {
|
||||||
|
if (!NamesMatch(resource.name, base)) continue;
|
||||||
|
if (resource.location < 0 || static_cast<GLint>(element) >= resource.arraySize) return -1;
|
||||||
|
return resource.location + static_cast<GLint>(element);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint GetResourceLocationIndex(ProgramObject& program, GLenum programInterface, const char* name) {
|
||||||
|
if (programInterface != GL_PROGRAM_OUTPUT || name == nullptr || name[0] == '\0') return -1;
|
||||||
|
const String query = name;
|
||||||
|
String base;
|
||||||
|
Uint element = 0;
|
||||||
|
Bool malformed = false;
|
||||||
|
const Bool subscripted = SplitTrailingSubscript(query, base, element, malformed);
|
||||||
|
if (malformed) return -1;
|
||||||
|
|
||||||
|
const Model model = BuildModel(program);
|
||||||
|
for (const Resource& resource : model.programOutputs) {
|
||||||
|
if (NamesMatch(resource.name, query)) return resource.locationIndex;
|
||||||
|
}
|
||||||
|
if (!subscripted) return -1;
|
||||||
|
for (const Resource& resource : model.programOutputs) {
|
||||||
|
if (!NamesMatch(resource.name, base)) continue;
|
||||||
|
if (resource.location < 0 || static_cast<GLint>(element) >= resource.arraySize) return -1;
|
||||||
|
return resource.locationIndex;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Impl::GLImpl::ProgramInterface
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
class ProgramObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The GL program interface (ARB_program_interface_query / GL 4.3 §7.3.1) as a frontend
|
||||||
|
// resource model.
|
||||||
|
//
|
||||||
|
// WHY IT IS HERE AND NOT IN A BACKEND. glGetProgramResource* describes the program the
|
||||||
|
// APPLICATION wrote, in the application's namespace. Neither backend program is in that
|
||||||
|
// namespace: DirectGLES compiles SPIRV-Cross-generated ESSL where default-block uniforms
|
||||||
|
// live inside the synthesized MGL_GLOBAL_UBO (so a GL_UNIFORM location query against it is
|
||||||
|
// structurally -1) and stage in/out names are rewritten; DirectVulkan has no GL-level
|
||||||
|
// reflection at all and can only re-derive a partial, diverging copy. The one authoritative
|
||||||
|
// source is the frontend glslang reflection a link already produced, which is the same
|
||||||
|
// place glGetActiveUniform answers from. This layer generalizes that rule to every
|
||||||
|
// interface, so the six entry points never consult gBackendFunctionsTable.
|
||||||
|
//
|
||||||
|
// NAMING RULES LIVE HERE, NOT IN ProgramObject. The interface query spells resources
|
||||||
|
// differently from glGetActiveUniform / glGetActiveAttrib (an array is "name[0]", a lookup
|
||||||
|
// accepts both "name" and "name[0]", a subscript must be a strict decimal). Those two
|
||||||
|
// getters are what GL30-33 exercises and they must not move, so every normalization is
|
||||||
|
// applied on the way in and out of THIS file.
|
||||||
|
namespace MobileGL::MG_Impl::GLImpl::ProgramInterface {
|
||||||
|
using ProgramObject = MG_State::GLState::ProgramObject;
|
||||||
|
|
||||||
|
// <programInterface> is one of the GL 4.6 Table 7.1 interfaces.
|
||||||
|
Bool IsInterfaceEnum(GLenum programInterface);
|
||||||
|
// Interfaces whose resources have names (everything except GL_ATOMIC_COUNTER_BUFFER).
|
||||||
|
Bool IsNamedInterface(GLenum programInterface);
|
||||||
|
// <prop> is a property token GetProgramResourceiv knows at all (else GL_INVALID_ENUM).
|
||||||
|
Bool IsResourceProp(GLenum prop);
|
||||||
|
// <prop> applies to <programInterface> (else GL_INVALID_OPERATION).
|
||||||
|
Bool InterfaceSupportsProp(GLenum programInterface, GLenum prop);
|
||||||
|
// Interfaces GetProgramResourceLocation accepts (else GL_INVALID_ENUM).
|
||||||
|
Bool InterfaceHasLocations(GLenum programInterface);
|
||||||
|
|
||||||
|
// GL_ACTIVE_RESOURCES / GL_MAX_NAME_LENGTH / GL_MAX_NUM_ACTIVE_VARIABLES. All three
|
||||||
|
// report zero for an interface this implementation cannot enumerate and for a program
|
||||||
|
// that has not linked successfully - which is what the spec requires of a program with
|
||||||
|
// no active resources.
|
||||||
|
Int GetActiveResourceCount(ProgramObject& program, GLenum programInterface);
|
||||||
|
Int GetMaxNameLength(ProgramObject& program, GLenum programInterface);
|
||||||
|
Int GetMaxNumActiveVariables(ProgramObject& program, GLenum programInterface);
|
||||||
|
|
||||||
|
// GL_INVALID_INDEX when <name> names no active resource of the interface.
|
||||||
|
GLuint GetResourceIndex(ProgramObject& program, GLenum programInterface, const char* name);
|
||||||
|
// False when <index> is out of range for the interface (the caller raises INVALID_VALUE).
|
||||||
|
Bool GetResourceName(ProgramObject& program, GLenum programInterface, GLuint index, String& outName);
|
||||||
|
// Appends the value(s) of <prop> for the resource; GL_ACTIVE_VARIABLES appends several.
|
||||||
|
// False when <index> is out of range.
|
||||||
|
Bool GetResourceProp(ProgramObject& program, GLenum programInterface, GLuint index, GLenum prop,
|
||||||
|
Vector<GLint>& outValues);
|
||||||
|
GLint GetResourceLocation(ProgramObject& program, GLenum programInterface, const char* name);
|
||||||
|
GLint GetResourceLocationIndex(ProgramObject& program, GLenum programInterface, const char* name);
|
||||||
|
} // namespace MobileGL::MG_Impl::GLImpl::ProgramInterface
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
// End of Source File Header
|
// End of Source File Header
|
||||||
|
|
||||||
#include "GL_Query.h"
|
#include "GL_Query.h"
|
||||||
|
#include "../Getter/GL_Getter.h"
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MG_Backend/BackendObjects.h>
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
@@ -22,6 +23,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
struct QueryObject {
|
struct QueryObject {
|
||||||
GLuint id = 0;
|
GLuint id = 0;
|
||||||
GLenum target = 0; // 0 = gen'd but never used with BeginQuery/QueryCounter
|
GLenum target = 0; // 0 = gen'd but never used with BeginQuery/QueryCounter
|
||||||
|
// glCreateQueries makes the object outright; glGenQueries only reserves the name,
|
||||||
|
// and the object appears when the name is first used (GL 4.6 core 4.2.1).
|
||||||
|
Bool created = false;
|
||||||
MG_Backend::BackendQueryHandle backendHandle = nullptr;
|
MG_Backend::BackendQueryHandle backendHandle = nullptr;
|
||||||
Bool active = false;
|
Bool active = false;
|
||||||
Bool ended = false;
|
Bool ended = false;
|
||||||
@@ -58,6 +62,34 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function, message));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function, message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The by-buffer query getters write the result into a buffer object instead of client
|
||||||
|
// memory. Everything about the query itself - the name, whether it is still active, the
|
||||||
|
// parameter - is checked by GetQueryObjectValue; what is left is the destination, so this
|
||||||
|
// resolves the buffer and confirms the write lands inside it (GL 4.6 core 4.2.1).
|
||||||
|
Bool ResolveQueryResultDestination(GLuint buffer, GLintptr offset, SizeT writeSize, const char* function,
|
||||||
|
SharedPtr<MG_State::GLState::BufferObject>& outBuffer) {
|
||||||
|
if (offset < 0) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidValue, function, "Offset cannot be negative.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!MG_State::pGLContext->ValidateBufferObject(buffer)) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, function, "Buffer object does not exist.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
|
||||||
|
if (!bufferObject) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, function, "Buffer object does not exist.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (static_cast<SizeT>(offset) + writeSize > bufferObject->GetSize()) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidOperation, function,
|
||||||
|
"The query result does not fit in the buffer object at this offset.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
outBuffer = bufferObject;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Callers must hold g_queryObjectsMutex.
|
// Callers must hold g_queryObjectsMutex.
|
||||||
QueryObject* FindQueryObjectLocked(GLuint id) {
|
QueryObject* FindQueryObjectLocked(GLuint id) {
|
||||||
const auto it = g_liveQueryObjects.find(id);
|
const auto it = g_liveQueryObjects.find(id);
|
||||||
@@ -91,8 +123,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
// was recorded and no value should be written back. `outValueProduced`, when given,
|
||||||
Bool GetQueryObjectValue(GLuint id, GLenum pname, const char* function, Uint64& outValue) {
|
// additionally distinguishes "succeeded with a value" from "succeeded but the result is not
|
||||||
|
// ready" - the GL_QUERY_RESULT_NO_WAIT case, where GL_ARB_query_buffer_object says the
|
||||||
|
// destination is left alone rather than written with a placeholder.
|
||||||
|
Bool GetQueryObjectValue(GLuint id, GLenum pname, const char* function, Uint64& outValue,
|
||||||
|
Bool* outValueProduced = nullptr) {
|
||||||
|
if (outValueProduced) *outValueProduced = true;
|
||||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
auto* queryObject = FindQueryObjectLocked(id);
|
auto* queryObject = FindQueryObjectLocked(id);
|
||||||
if (!queryObject) {
|
if (!queryObject) {
|
||||||
@@ -105,6 +142,41 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (pname) {
|
switch (pname) {
|
||||||
|
case GL_QUERY_TARGET:
|
||||||
|
// The target a query was begun with (or created with, for glCreateQueries) - state
|
||||||
|
// the object has carried all along, GL 4.6 core table 23.35.
|
||||||
|
outValue = queryObject->target;
|
||||||
|
return true;
|
||||||
|
case GL_QUERY_RESULT_NO_WAIT: {
|
||||||
|
if (queryObject->resultCached) {
|
||||||
|
outValue = queryObject->cachedResult;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Uint64 result = 0;
|
||||||
|
const auto getQueryResult64 = MG_Backend::gBackendFunctionsTable.GL.GetQueryResult64;
|
||||||
|
if (queryObject->backendHandle && getQueryResult64 &&
|
||||||
|
!getQueryResult64(queryObject->backendHandle, /*wait=*/false, &result)) {
|
||||||
|
// Not ready. The whole point of the no-wait form is that the caller's
|
||||||
|
// destination keeps whatever it already held.
|
||||||
|
if (outValueProduced) *outValueProduced = false;
|
||||||
|
outValue = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (queryObject->target == GL_ANY_SAMPLES_PASSED ||
|
||||||
|
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
|
||||||
|
result = result != 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (queryObject->backendHandle) {
|
||||||
|
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
|
||||||
|
deleteBackendQuery(queryObject->backendHandle);
|
||||||
|
}
|
||||||
|
queryObject->backendHandle = nullptr;
|
||||||
|
}
|
||||||
|
queryObject->cachedResult = result;
|
||||||
|
queryObject->resultCached = true;
|
||||||
|
outValue = result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
case GL_QUERY_RESULT_AVAILABLE: {
|
case GL_QUERY_RESULT_AVAILABLE: {
|
||||||
if (queryObject->resultCached || !queryObject->backendHandle) {
|
if (queryObject->resultCached || !queryObject->backendHandle) {
|
||||||
outValue = 1;
|
outValue = 1;
|
||||||
@@ -156,6 +228,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void GetQueryBufferObject(GLuint id, GLuint buffer, GLenum pname, GLintptr offset, const char* function) {
|
||||||
|
SharedPtr<MG_State::GLState::BufferObject> bufferObject;
|
||||||
|
if (!ResolveQueryResultDestination(buffer, offset, sizeof(T), function, bufferObject)) return;
|
||||||
|
|
||||||
|
Uint64 value = 0;
|
||||||
|
Bool valueProduced = false;
|
||||||
|
if (!GetQueryObjectValue(id, pname, function, value, &valueProduced)) return;
|
||||||
|
// GL_QUERY_RESULT_NO_WAIT on a result that has not landed writes nothing at all.
|
||||||
|
if (!valueProduced) return;
|
||||||
|
|
||||||
|
const T narrowed = static_cast<T>(value);
|
||||||
|
bufferObject->UploadSubData({const_cast<T*>(&narrowed), sizeof(T)}, static_cast<SizeT>(offset));
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void GenQueries(GLsizei n, GLuint* ids) {
|
void GenQueries(GLsizei n, GLuint* ids) {
|
||||||
@@ -176,6 +263,41 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// glCreateQueries differs from glGenQueries in creating the objects outright, with their
|
||||||
|
// target already fixed and the rest of their state at the defaults (GL 4.6 core 4.2.1).
|
||||||
|
void CreateQueries(GLenum target, GLsizei n, GLuint* ids) {
|
||||||
|
switch (target) {
|
||||||
|
case GL_SAMPLES_PASSED:
|
||||||
|
case GL_ANY_SAMPLES_PASSED:
|
||||||
|
case GL_ANY_SAMPLES_PASSED_CONSERVATIVE:
|
||||||
|
case GL_TIME_ELAPSED:
|
||||||
|
case GL_TIMESTAMP:
|
||||||
|
case GL_PRIMITIVES_GENERATED:
|
||||||
|
case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not accepted.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (n < 0) {
|
||||||
|
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ids) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
|
for (GLsizei i = 0; i < n; ++i) {
|
||||||
|
const GLuint id = g_nextQueryId++;
|
||||||
|
auto* queryObject = new QueryObject;
|
||||||
|
queryObject->id = id;
|
||||||
|
queryObject->target = target;
|
||||||
|
queryObject->created = true;
|
||||||
|
g_liveQueryObjects[id] = queryObject;
|
||||||
|
ids[i] = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DeleteQueries(GLsizei n, const GLuint* ids) {
|
void DeleteQueries(GLsizei n, const GLuint* ids) {
|
||||||
if (n < 0) {
|
if (n < 0) {
|
||||||
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
|
RecordQueryError(ErrorCode::InvalidValue, __FUNCTION__, "n cannot be negative.");
|
||||||
@@ -227,9 +349,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return GL_FALSE;
|
return GL_FALSE;
|
||||||
}
|
}
|
||||||
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
|
||||||
// Gen'd ids count as query objects here: the registry creates live
|
// A name from glGenQueries is not yet a query object: it becomes one when it is first
|
||||||
// objects at GenQueries time.
|
// used with BeginQuery/QueryCounter (which is what a non-zero target records), or
|
||||||
return FindQueryObjectLocked(id) != nullptr ? GL_TRUE : GL_FALSE;
|
// immediately if it came from glCreateQueries.
|
||||||
|
const auto* queryObject = FindQueryObjectLocked(id);
|
||||||
|
return (queryObject != nullptr && (queryObject->created || queryObject->target != 0)) ? GL_TRUE : GL_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BeginQuery(GLenum target, GLuint id) {
|
void BeginQuery(GLenum target, GLuint id) {
|
||||||
@@ -434,9 +558,26 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GetQueryBufferObjectiv(GLuint id, GLuint buffer, GLenum pname, GLintptr offset) {
|
||||||
|
GetQueryBufferObject<GLint>(id, buffer, pname, offset, __FUNCTION__);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetQueryBufferObjectuiv(GLuint id, GLuint buffer, GLenum pname, GLintptr offset) {
|
||||||
|
GetQueryBufferObject<GLuint>(id, buffer, pname, offset, __FUNCTION__);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetQueryBufferObjecti64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset) {
|
||||||
|
GetQueryBufferObject<GLint64>(id, buffer, pname, offset, __FUNCTION__);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetQueryBufferObjectui64v(GLuint id, GLuint buffer, GLenum pname, GLintptr offset) {
|
||||||
|
GetQueryBufferObject<GLuint64>(id, buffer, pname, offset, __FUNCTION__);
|
||||||
|
}
|
||||||
|
|
||||||
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params) {
|
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params) {
|
||||||
Uint64 value = 0;
|
Uint64 value = 0;
|
||||||
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
|
Bool valueProduced = false;
|
||||||
|
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value, &valueProduced) || !valueProduced || !params) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
constexpr Uint64 kMaxInt = static_cast<Uint64>(INT_MAX);
|
constexpr Uint64 kMaxInt = static_cast<Uint64>(INT_MAX);
|
||||||
@@ -445,7 +586,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params) {
|
void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params) {
|
||||||
Uint64 value = 0;
|
Uint64 value = 0;
|
||||||
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
|
Bool valueProduced = false;
|
||||||
|
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value, &valueProduced) || !valueProduced || !params) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
*params = static_cast<GLuint>(value & 0xFFFFFFFFull);
|
*params = static_cast<GLuint>(value & 0xFFFFFFFFull);
|
||||||
@@ -453,7 +595,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params) {
|
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params) {
|
||||||
Uint64 value = 0;
|
Uint64 value = 0;
|
||||||
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
|
Bool valueProduced = false;
|
||||||
|
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value, &valueProduced) || !valueProduced || !params) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
*params = static_cast<GLint64>(value);
|
*params = static_cast<GLint64>(value);
|
||||||
@@ -461,9 +604,48 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params) {
|
void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params) {
|
||||||
Uint64 value = 0;
|
Uint64 value = 0;
|
||||||
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value) || !params) {
|
Bool valueProduced = false;
|
||||||
|
if (!GetQueryObjectValue(id, pname, __FUNCTION__, value, &valueProduced) || !valueProduced || !params) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
*params = static_cast<GLuint64>(value);
|
*params = static_cast<GLuint64>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// The indexed query entry points differ from the plain ones only in the vertex
|
||||||
|
// stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS
|
||||||
|
// for the two transform feedback targets and zero for every other target. With a
|
||||||
|
// single vertex stream both bounds are 1, so a valid call is always index 0 and
|
||||||
|
// forwards to the unindexed implementation.
|
||||||
|
Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) {
|
||||||
|
const Bool perStreamTarget =
|
||||||
|
target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN;
|
||||||
|
GLint maxVertexStreams = 1;
|
||||||
|
if (perStreamTarget) {
|
||||||
|
GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
|
||||||
|
}
|
||||||
|
if (index < static_cast<GLuint>(std::max(maxVertexStreams, 1))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
RecordQueryError(ErrorCode::InvalidValue, function,
|
||||||
|
perStreamTarget ? "index is not less than GL_MAX_VERTEX_STREAMS."
|
||||||
|
: "index must be zero for this query target.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) {
|
||||||
|
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
|
||||||
|
BeginQuery(target, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndQueryIndexed(GLenum target, GLuint index) {
|
||||||
|
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
|
||||||
|
EndQuery(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetQueryIndexediv(GLenum target, GLuint index, GLenum pname, GLint* params) {
|
||||||
|
if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return;
|
||||||
|
GetQueryiv(target, pname, params);
|
||||||
|
}
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -11,14 +11,22 @@
|
|||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
void GenQueries(GLsizei n, GLuint* ids);
|
void GenQueries(GLsizei n, GLuint* ids);
|
||||||
|
void CreateQueries(GLenum target, GLsizei n, GLuint* ids);
|
||||||
void DeleteQueries(GLsizei n, const GLuint* ids);
|
void DeleteQueries(GLsizei n, const GLuint* ids);
|
||||||
GLboolean IsQuery(GLuint id);
|
GLboolean IsQuery(GLuint id);
|
||||||
void BeginQuery(GLenum target, GLuint id);
|
void BeginQuery(GLenum target, GLuint id);
|
||||||
void EndQuery(GLenum target);
|
void EndQuery(GLenum target);
|
||||||
void GetQueryiv(GLenum target, GLenum pname, GLint* params);
|
void GetQueryiv(GLenum target, GLenum pname, GLint* params);
|
||||||
|
void BeginQueryIndexed(GLenum target, GLuint index, GLuint id);
|
||||||
|
void EndQueryIndexed(GLenum target, GLuint index);
|
||||||
|
void GetQueryIndexediv(GLenum target, GLuint index, GLenum pname, GLint* params);
|
||||||
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params);
|
void GetQueryObjectiv(GLuint id, GLenum pname, GLint* params);
|
||||||
void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params);
|
void GetQueryObjectuiv(GLuint id, GLenum pname, GLuint* params);
|
||||||
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params);
|
void GetQueryObjecti64v(GLuint id, GLenum pname, GLint64* params);
|
||||||
void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params);
|
void GetQueryObjectui64v(GLuint id, GLenum pname, GLuint64* params);
|
||||||
|
void GetQueryBufferObjectiv(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);
|
||||||
|
void GetQueryBufferObjectuiv(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 QueryCounter(GLuint id, GLenum target);
|
void QueryCounter(GLuint id, GLenum target);
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl
|
} // namespace MobileGL::MG_Impl::GLImpl
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "GL_Sampler.h"
|
#include "GL_Sampler.h"
|
||||||
#include "Validators.h"
|
#include "Validators.h"
|
||||||
|
#include "../Getter/GL_Getter.h"
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||||
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
|
||||||
@@ -28,6 +29,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_TEXTURE_MAX_LOD:
|
case GL_TEXTURE_MAX_LOD:
|
||||||
case GL_TEXTURE_LOD_BIAS:
|
case GL_TEXTURE_LOD_BIAS:
|
||||||
return true;
|
return true;
|
||||||
|
// Four components, and GL puts no range on them - a border colour outside [0,1] is
|
||||||
|
// clamped when a fixed-point format is sampled, not rejected here. The scalar readers
|
||||||
|
// below would look at one component and invent an error.
|
||||||
|
case GL_TEXTURE_BORDER_COLOR:
|
||||||
|
return true;
|
||||||
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
case GL_TEXTURE_MAX_ANISOTROPY_EXT:
|
||||||
if (ReadSamplerScalar(param, isFloat, isUnsignedInteger) >= 1.0f) return true;
|
if (ReadSamplerScalar(param, isFloat, isUnsignedInteger) >= 1.0f) return true;
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -99,6 +105,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_TEXTURE_COMPARE_FUNC:
|
case GL_TEXTURE_COMPARE_FUNC:
|
||||||
samplerObj->SetSamplerCompareFunc(MG_Util::ConvertGLEnumToSamplerCompareFunc(*(const GLint*)param));
|
samplerObj->SetSamplerCompareFunc(MG_Util::ConvertGLEnumToSamplerCompareFunc(*(const GLint*)param));
|
||||||
break;
|
break;
|
||||||
|
case GL_TEXTURE_BORDER_COLOR:
|
||||||
|
// The only four-component sampler parameter: the caller's form decides which
|
||||||
|
// representation is authoritative, and SamplerObject keeps the other two in step.
|
||||||
|
if (isFloat) {
|
||||||
|
const auto* values = (const GLfloat*)param;
|
||||||
|
samplerObj->SetBorderColor(FloatVec4(values[0], values[1], values[2], values[3]));
|
||||||
|
} else if (isUnsignedInteger) {
|
||||||
|
const auto* values = (const GLuint*)param;
|
||||||
|
samplerObj->SetBorderColorUI(UintVec4(values[0], values[1], values[2], values[3]));
|
||||||
|
} else {
|
||||||
|
const auto* values = (const GLint*)param;
|
||||||
|
samplerObj->SetBorderColorI(IntVec4(values[0], values[1], values[2], values[3]));
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SetSamplerParam_State",
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "SetSamplerParam_State",
|
||||||
@@ -162,6 +182,31 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_TEXTURE_COMPARE_FUNC:
|
case GL_TEXTURE_COMPARE_FUNC:
|
||||||
*(GLuint*)params = MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc());
|
*(GLuint*)params = MG_Util::ConvertSamplerCompareFuncToGLEnum(samplerObj->GetSamplerCompareFunc());
|
||||||
break;
|
break;
|
||||||
|
case GL_TEXTURE_BORDER_COLOR: {
|
||||||
|
if (isFloat) {
|
||||||
|
const auto& color = samplerObj->GetBorderColor();
|
||||||
|
auto* out = (GLfloat*)params;
|
||||||
|
out[0] = color.x();
|
||||||
|
out[1] = color.y();
|
||||||
|
out[2] = color.z();
|
||||||
|
out[3] = color.w();
|
||||||
|
} else if (isUnsignedInteger) {
|
||||||
|
const auto& color = samplerObj->GetBorderColorUI();
|
||||||
|
auto* out = (GLuint*)params;
|
||||||
|
out[0] = color.x();
|
||||||
|
out[1] = color.y();
|
||||||
|
out[2] = color.z();
|
||||||
|
out[3] = color.w();
|
||||||
|
} else {
|
||||||
|
const auto& color = samplerObj->GetBorderColorI();
|
||||||
|
auto* out = (GLint*)params;
|
||||||
|
out[0] = color.x();
|
||||||
|
out[1] = color.y();
|
||||||
|
out[2] = color.z();
|
||||||
|
out[3] = color.w();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetSamplerParam_State",
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetSamplerParam_State",
|
||||||
@@ -224,9 +269,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The number of texture units a sampler may be bound to. GL 3.3 core 3.8.2 names
|
||||||
|
// GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, which is what the backend advertises; the frontend's
|
||||||
|
// MAX_TEXTURE_IMAGE_UNITS is only the capacity of the unit array, so it is a clamp on the
|
||||||
|
// answer and never the answer itself - gating on it alone accepts every unit up to 192 no
|
||||||
|
// matter what the driver reports.
|
||||||
|
static GLint GetSamplerBindableTextureUnitCount() {
|
||||||
|
GLint maxTextureUnits = 0;
|
||||||
|
GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
|
||||||
|
return std::min<GLint>(std::max(maxTextureUnits, 0), MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
|
||||||
|
}
|
||||||
|
|
||||||
void BindSampler_State(GLuint unit, GLuint sampler) {
|
void BindSampler_State(GLuint unit, GLuint sampler) {
|
||||||
MGLOG_D("BindSampler_State: unit = %u, sampler = %u", unit, sampler);
|
MGLOG_D("BindSampler_State: unit = %u, sampler = %u", unit, sampler);
|
||||||
if (unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
|
if (static_cast<Uint64>(unit) >= static_cast<Uint64>(GetSamplerBindableTextureUnitCount())) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler", "texture unit out of range"));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSampler", "texture unit out of range"));
|
||||||
@@ -265,6 +321,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSamplers", "count must be non-negative"));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSamplers", "count must be non-negative"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// ARB_multi_bind: the whole [first, first + count) range is checked up front and a
|
||||||
|
// range that runs past the last texture unit is INVALID_OPERATION - not the
|
||||||
|
// INVALID_VALUE the single-bind BindSampler_State reports per element, and nothing is
|
||||||
|
// bound when it fails. Both gates read the same limit (see
|
||||||
|
// GetSamplerBindableTextureUnitCount), so an out-of-range multi-bind can no longer slip
|
||||||
|
// past this check and be caught one element at a time with the wrong error class.
|
||||||
|
const GLint maxTextureUnits = GetSamplerBindableTextureUnitCount();
|
||||||
|
if (static_cast<Uint64>(first) + static_cast<Uint64>(count) > static_cast<Uint64>(maxTextureUnits)) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindSamplers",
|
||||||
|
"first + count exceeds the number of texture units."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
BindSampler_State(first + i, samplers ? samplers[i] : 0);
|
BindSampler_State(first + i, samplers ? samplers[i] : 0);
|
||||||
|
|||||||
@@ -75,7 +75,11 @@ namespace MobileGL::MG_Impl::GLImpl::SamplerImpl {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case GL_TEXTURE_COMPARE_FUNC:
|
case GL_TEXTURE_COMPARE_FUNC:
|
||||||
if (param < GL_LEQUAL || param > GL_ALWAYS) {
|
// The eight depth-compare functions are contiguous from GL_NEVER (0x0200) to
|
||||||
|
// GL_ALWAYS (0x0207); GL_LEQUAL sits in the middle of that block, so starting
|
||||||
|
// the range there rejected NEVER/LESS/EQUAL and let GREATER/NOTEQUAL/GEQUAL
|
||||||
|
// through only by accident of them being above LEQUAL.
|
||||||
|
if (param < GL_NEVER || param > GL_ALWAYS) {
|
||||||
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerParam",
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "ValidateSamplerParam",
|
||||||
"Invalid compare function parameter"));
|
"Invalid compare function parameter"));
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,10 @@
|
|||||||
|
|
||||||
namespace MobileGL::MG_Impl::GLImpl {
|
namespace MobileGL::MG_Impl::GLImpl {
|
||||||
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
|
||||||
|
// The sized internal formats a buffer texture accepts (GL 4.6 core table 8.16). The buffer
|
||||||
|
// clears take the same list, so it is shared rather than written out twice.
|
||||||
|
Bool IsBufferTextureInternalFormat(GLenum internalformat);
|
||||||
|
|
||||||
void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data);
|
void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data);
|
||||||
void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
||||||
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data);
|
GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* data);
|
||||||
@@ -42,6 +46,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void GenerateTextureMipmap(GLuint texture);
|
void GenerateTextureMipmap(GLuint texture);
|
||||||
void BindTextureUnit(GLuint unit, GLuint texture);
|
void BindTextureUnit(GLuint unit, GLuint texture);
|
||||||
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels);
|
void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels);
|
||||||
|
void GetCompressedTextureImage(GLuint texture, GLint level, GLsizei bufSize, void* pixels);
|
||||||
|
void TexBufferRange(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size);
|
||||||
|
void TextureBuffer(GLuint texture, GLenum internalformat, GLuint buffer);
|
||||||
|
void TextureBufferRange(GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size);
|
||||||
void GetTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
void GetTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
|
||||||
GLsizei height, GLsizei depth, GLenum format, GLenum type, GLsizei bufSize, void* pixels);
|
GLsizei height, GLsizei depth, GLenum format, GLenum type, GLsizei bufSize, void* pixels);
|
||||||
void GetTextureParameterfv(GLuint texture, GLenum pname, GLfloat* params);
|
void GetTextureParameterfv(GLuint texture, GLenum pname, GLfloat* params);
|
||||||
@@ -98,6 +106,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
GLsizei width, GLsizei height);
|
GLsizei width, GLsizei height);
|
||||||
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 CopyTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
|
||||||
|
void CopyTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
|
||||||
|
GLint y, GLsizei width, GLsizei height);
|
||||||
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||||
GLsizei width, GLsizei height);
|
GLsizei width, GLsizei height);
|
||||||
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
|
void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
|
||||||
|
|||||||
@@ -226,6 +226,9 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
case TextureInternalFormat::Depth24Stencil8:
|
case TextureInternalFormat::Depth24Stencil8:
|
||||||
case TextureInternalFormat::Depth32FStencil8:
|
case TextureInternalFormat::Depth32FStencil8:
|
||||||
case TextureInternalFormat::DepthStencil:
|
case TextureInternalFormat::DepthStencil:
|
||||||
|
// Stencil-only is not a colour format either: a colour client format read against a
|
||||||
|
// STENCIL_INDEX8 texture has to be the same INVALID_OPERATION as against a depth one.
|
||||||
|
case TextureInternalFormat::StencilIndex8:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -350,7 +353,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateTextureObject(SharedPtr<MG_State::GLState::ITextureObject> textureObject) {
|
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
|
||||||
if (!textureObject) {
|
if (!textureObject) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidOperation,
|
ErrorCode::InvalidOperation,
|
||||||
@@ -373,7 +376,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
|
Bool ValidateTextureTargetUniformity(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||||
TextureTarget target) {
|
TextureTarget target) {
|
||||||
if (!textureObject) return true; // should be created later
|
if (!textureObject) return true; // should be created later
|
||||||
TextureTarget prevTarget = textureObject->GetTarget();
|
TextureTarget prevTarget = textureObject->GetTarget();
|
||||||
@@ -387,7 +390,7 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateTextureSubImageOffsets(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Int xoffset,
|
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
||||||
Int width, Int yoffset, Int height, Int zoffset, Int depth) {
|
Int width, Int yoffset, Int height, Int zoffset, Int depth) {
|
||||||
auto baseSize = textureObject->GetBaseSize();
|
auto baseSize = textureObject->GetBaseSize();
|
||||||
if (xoffset < 0 || (xoffset + width) > baseSize.x()) {
|
if (xoffset < 0 || (xoffset + width) > baseSize.x()) {
|
||||||
|
|||||||
@@ -30,15 +30,15 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
|
|||||||
TextureInternalFormat internalFormat,
|
TextureInternalFormat internalFormat,
|
||||||
TexturePixelDataType type);
|
TexturePixelDataType type);
|
||||||
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
|
Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level);
|
||||||
Bool ValidateTextureObject(SharedPtr<MG_State::GLState::ITextureObject> textureObject);
|
Bool ValidateTextureObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
|
||||||
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
|
// Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry
|
||||||
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
|
// points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION
|
||||||
// error is generated if zero is bound to target", ARB_texture_storage).
|
// error is generated if zero is bound to target", ARB_texture_storage).
|
||||||
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
Bool ValidateTextureNotDefault(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||||
const char* caller);
|
const char* caller);
|
||||||
Bool ValidateTextureTargetUniformity(SharedPtr<MG_State::GLState::ITextureObject> textureObject,
|
Bool ValidateTextureTargetUniformity(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
|
||||||
TextureTarget target);
|
TextureTarget target);
|
||||||
Bool ValidateTextureSubImageOffsets(SharedPtr<MG_State::GLState::ITextureObject> textureObject, Int xoffset,
|
Bool ValidateTextureSubImageOffsets(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Int xoffset,
|
||||||
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
|
Int width, Int yoffset = 0, Int height = 0, Int zoffset = 0, Int depth = 0);
|
||||||
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
|
Bool ValidateBaseInternalFormatMatch(TextureInternalFormat format1, TextureInternalFormat format2);
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "GL_VertexArray.h"
|
#include "GL_VertexArray.h"
|
||||||
#include "Validators.h"
|
#include "Validators.h"
|
||||||
|
#include <MG_Backend/BackendObjects.h>
|
||||||
#include <MG_Impl/GLImpl/Buffer/Validators.h>
|
#include <MG_Impl/GLImpl/Buffer/Validators.h>
|
||||||
#include <MG_State/GLState/Core.h>
|
#include <MG_State/GLState/Core.h>
|
||||||
#include <MG_State/GLState/ErrorState/Error.h>
|
#include <MG_State/GLState/ErrorState/Error.h>
|
||||||
@@ -105,12 +106,60 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
return pname == GL_CURRENT_VERTEX_ATTRIB;
|
return pname == GL_CURRENT_VERTEX_ATTRIB;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The two ARB_vertex_attrib_binding per-attribute queries. They do not live on the
|
||||||
|
// resolved VertexAttribute (which is the flat, already-combined view) but on the VAO's
|
||||||
|
// binding-point mapping, so they need the object, not the attribute.
|
||||||
|
static bool TryGetVertexAttribBindingQuery(GLuint index, GLenum pname, GLint& out) {
|
||||||
|
if (pname != GL_VERTEX_ATTRIB_BINDING && pname != GL_VERTEX_ATTRIB_RELATIVE_OFFSET) return false;
|
||||||
|
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||||
|
if (!vao) {
|
||||||
|
out = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
out = pname == GL_VERTEX_ATTRIB_BINDING ? static_cast<GLint>(vao->GetAttributeBindingIndex(index))
|
||||||
|
: static_cast<GLint>(vao->GetAttributeRelativeOffset(index));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The stride a pointer-style call gives its binding point: the argument when it is non-zero,
|
||||||
|
// otherwise the tightly packed element size (GL 4.6 core 10.3.2). A packed 2_10_10_10 or
|
||||||
|
// 10F_11F_11F attribute is one 32-bit word regardless of its component count.
|
||||||
|
static int EffectiveVertexStride(GLsizei stride, GLint size, GLenum type) {
|
||||||
|
if (stride != 0) return static_cast<int>(stride);
|
||||||
|
switch (type) {
|
||||||
|
case GL_INT_2_10_10_10_REV:
|
||||||
|
case GL_UNSIGNED_INT_2_10_10_10_REV:
|
||||||
|
case GL_UNSIGNED_INT_10F_11F_11F_REV:
|
||||||
|
return 4;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return static_cast<int>(size * MG_Util::GetGLTypeSize(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
// glBindVertexBuffers / glVertexArrayVertexBuffers take a range of binding points, and a
|
||||||
|
// range that runs past the last one is INVALID_OPERATION rather than the INVALID_VALUE a
|
||||||
|
// single out-of-range index gets (GL 4.6 core 10.3.1).
|
||||||
|
static bool ValidateVertexBindingRange(GLuint first, GLsizei count, const char* funcName) {
|
||||||
|
if (count < 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName, "count must be non-negative."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (static_cast<Uint64>(first) + static_cast<Uint64>(count) >
|
||||||
|
VertexArrayImpl::GetMaxVertexAttribBindings()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
|
"first + count exceeds GL_MAX_VERTEX_ATTRIB_BINDINGS."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool ValidateVertexBindingIndex(GLuint bindingindex, const char* funcName) {
|
static bool ValidateVertexBindingIndex(GLuint bindingindex, const char* funcName) {
|
||||||
// Bound by the same dynamic limit as attribute indices: the default attribute -> binding
|
if (bindingindex >= VertexArrayImpl::GetMaxVertexAttribBindings()) {
|
||||||
// mapping is the identity, so a binding point the backend cannot address as an attribute
|
|
||||||
// would resolve into an attribute the backend must then reject on every draw. Real drivers
|
|
||||||
// likewise report MAX_VERTEX_ATTRIB_BINDINGS == MAX_VERTEX_ATTRIBS.
|
|
||||||
if (bindingindex >= VertexArrayImpl::GetMaxVertexAttribs()) {
|
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidValue,
|
ErrorCode::InvalidValue,
|
||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
|
||||||
@@ -140,8 +189,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_CURRENT_VERTEX_ATTRIB:
|
case GL_CURRENT_VERTEX_ATTRIB:
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING:
|
case GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING:
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
||||||
|
// Core since GL 4.1 (ARB_vertex_attrib_64bit). It was rejected while no attribute could
|
||||||
|
// ever be long; now that IsLong is real state the pname has to be accepted.
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_POINTER:
|
case GL_VERTEX_ATTRIB_ARRAY_POINTER:
|
||||||
|
// ARB_vertex_attrib_binding (core since GL 4.3). The binding-point view is real
|
||||||
|
// state on the VAO (GetAttributeBindingIndex / GetAttributeRelativeOffset), so
|
||||||
|
// both of its per-attribute queries are answerable.
|
||||||
|
case GL_VERTEX_ATTRIB_BINDING:
|
||||||
|
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
@@ -155,6 +212,17 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
SharedPtr<MG_State::GLState::VertexArrayObject> GetNamedVertexArrayObject_State(GLuint vaobj,
|
SharedPtr<MG_State::GLState::VertexArrayObject> GetNamedVertexArrayObject_State(GLuint vaobj,
|
||||||
const char* caller) {
|
const char* caller) {
|
||||||
|
// Name zero is not a vertex array object in a core profile: it names the default vertex
|
||||||
|
// array, which the by-name (direct state access) entry points never accept. MobileGL keeps a
|
||||||
|
// real object at index 0 for the compatibility paths, so the generic name validation below
|
||||||
|
// would otherwise let it through (GL 4.6 core 10.3.1).
|
||||||
|
if (vaobj == 0) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
|
"Vertex array name 0 is not a vertex array object."));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
if (!VertexArrayImpl::ValidateVertexArrayName(vaobj)) return nullptr;
|
if (!VertexArrayImpl::ValidateVertexArrayName(vaobj)) return nullptr;
|
||||||
if (!VertexArrayImpl::ValidateVertexArrayObject(vaobj)) return nullptr;
|
if (!VertexArrayImpl::ValidateVertexArrayObject(vaobj)) return nullptr;
|
||||||
return MG_State::pGLContext->GetVertexArrayObject(vaobj);
|
return MG_State::pGLContext->GetVertexArrayObject(vaobj);
|
||||||
@@ -210,7 +278,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
DataType dataType = MG_Util::ConvertGLEnumToDataType(type);
|
DataType dataType = MG_Util::ConvertGLEnumToDataType(type);
|
||||||
// Integer path: never normalized, never BGRA/packed (the validator rejects those).
|
// Integer path: never normalized, never BGRA/packed (the validator rejects those).
|
||||||
if (!VertexArrayImpl::ValidateVertexAttribFormat(index, size, dataType, false, stride, true)) return;
|
if (!VertexArrayImpl::ValidateVertexAttribFormat(index, size, type, dataType, false, stride, true)) return;
|
||||||
|
|
||||||
auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||||
if (!vao) {
|
if (!vao) {
|
||||||
@@ -227,6 +295,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
|
|
||||||
vao->SetAttributeFormat(index, size, dataType, false, stride, offset, true, false);
|
vao->SetAttributeFormat(index, size, dataType, false, stride, offset, true, false);
|
||||||
vao->BindAttributeBuffer(index, vbo);
|
vao->BindAttributeBuffer(index, vbo);
|
||||||
|
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, size, type));
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexAttribPointer_State(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,
|
void VertexAttribPointer_State(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,
|
||||||
@@ -234,7 +303,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return;
|
if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return;
|
||||||
|
|
||||||
DataType dataType = MG_Util::ConvertGLEnumToDataType(type);
|
DataType dataType = MG_Util::ConvertGLEnumToDataType(type);
|
||||||
if (!VertexArrayImpl::ValidateVertexAttribFormat(index, size, dataType, normalized == GL_TRUE, stride, false))
|
if (!VertexArrayImpl::ValidateVertexAttribFormat(index, size, type, dataType, normalized == GL_TRUE, stride, false))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||||
@@ -256,6 +325,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const int effectiveSize = isBgra ? 4 : size;
|
const int effectiveSize = isBgra ? 4 : size;
|
||||||
vao->SetAttributeFormat(index, effectiveSize, dataType, normalized, stride, offset, false, isBgra);
|
vao->SetAttributeFormat(index, effectiveSize, dataType, normalized, stride, offset, false, isBgra);
|
||||||
vao->BindAttributeBuffer(index, vbo);
|
vao->BindAttributeBuffer(index, vbo);
|
||||||
|
vao->MirrorPointerIntoBinding(index, vbo, offset, EffectiveVertexStride(stride, effectiveSize, type));
|
||||||
}
|
}
|
||||||
|
|
||||||
void BindVertexArray_State(GLuint array) {
|
void BindVertexArray_State(GLuint array) {
|
||||||
@@ -359,6 +429,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "offset and stride must be non-negative."));
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "offset and stride must be non-negative."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (static_cast<Uint>(stride) > VertexArrayImpl::GetMaxVertexAttribStride()) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||||
|
"stride exceeds GL_MAX_VERTEX_ATTRIB_STRIDE."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
auto bufferObject = GetVertexArrayBufferObject_State(buffer, caller);
|
auto bufferObject = GetVertexArrayBufferObject_State(buffer, caller);
|
||||||
if (buffer != 0 && !bufferObject) return;
|
if (buffer != 0 && !bufferObject) return;
|
||||||
|
|
||||||
@@ -376,6 +453,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const GLintptr* offsets, const GLsizei* strides) {
|
const GLintptr* offsets, const GLsizei* strides) {
|
||||||
auto vao = GetNamedVertexArrayObject_State(vaobj, "VertexArrayVertexBuffers_State");
|
auto vao = GetNamedVertexArrayObject_State(vaobj, "VertexArrayVertexBuffers_State");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
|
if (!ValidateVertexBindingRange(first, count, "VertexArrayVertexBuffers_State")) return;
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
if (!buffers) {
|
if (!buffers) {
|
||||||
VertexBufferBinding_State(vao, first + i, 0, 0, 16, "VertexArrayVertexBuffers_State");
|
VertexBufferBinding_State(vao, first + i, 0, 0, 16, "VertexArrayVertexBuffers_State");
|
||||||
@@ -389,12 +467,55 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
static void VertexAttribFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
|
static void VertexAttribFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
|
||||||
GLuint attribindex, GLint size, GLenum type, GLboolean normalized,
|
GLuint attribindex, GLint size, GLenum type, GLboolean normalized,
|
||||||
GLuint relativeoffset, Bool isInteger, const char* caller) {
|
GLuint relativeoffset, Bool isInteger, const char* caller) {
|
||||||
|
static_cast<void>(caller);
|
||||||
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
||||||
|
|
||||||
DataType dataType = MG_Util::ConvertGLEnumToDataType(type);
|
DataType dataType = MG_Util::ConvertGLEnumToDataType(type);
|
||||||
if (!VertexArrayImpl::ValidateVertexAttribPointerParams(attribindex, size, dataType, 0)) return;
|
// The separate-format entry points take the same size/type rules as the pointer ones,
|
||||||
|
// GL_BGRA included, so they need the full format validation rather than the pointer-only
|
||||||
|
// subset - that one reports GL_BGRA as an out-of-range size.
|
||||||
|
if (!VertexArrayImpl::ValidateVertexAttribFormat(attribindex, size, type, dataType, normalized == GL_TRUE, 0,
|
||||||
|
isInteger))
|
||||||
|
return;
|
||||||
|
if (!VertexArrayImpl::ValidateVertexAttribRelativeOffset(relativeoffset)) return;
|
||||||
|
|
||||||
vao->SetAttributeFormatSeparate(attribindex, size, dataType, normalized, isInteger, relativeoffset);
|
const Bool isBgra = (size == static_cast<GLint>(GL_BGRA));
|
||||||
|
vao->SetAttributeFormatSeparate(attribindex, isBgra ? 4 : size, dataType, normalized, isInteger,
|
||||||
|
relativeoffset, isBgra);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The long (64-bit) attribute format: the values reach the shader as doubles, unconverted
|
||||||
|
// (GL 4.6 core 10.3.2). ValidateVertexAttribLFormat has already pinned type to GL_DOUBLE, so the
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Whether the backend can feed it is detected, not assumed: DirectVulkan needs shaderFloat64,
|
||||||
|
// and DirectGLES can never have it at all. A backend without it declines here, loudly - GL error
|
||||||
|
// plus a log line naming the reason - rather than accepting state no draw could honour and
|
||||||
|
// rendering garbage. The matching startup POST row is in MG_Util/SelfTest/DriverPost.cpp.
|
||||||
|
static void VertexAttribLFormatSeparate_State(const SharedPtr<MG_State::GLState::VertexArrayObject>& vao,
|
||||||
|
GLuint attribindex, GLint size, GLenum type,
|
||||||
|
GLuint relativeoffset) {
|
||||||
|
if (!VertexArrayImpl::ValidateVertexAttributeIndex(attribindex)) return;
|
||||||
|
if (!VertexArrayImpl::ValidateVertexAttribLFormat(attribindex, size, type)) return;
|
||||||
|
if (!VertexArrayImpl::ValidateVertexAttribRelativeOffset(relativeoffset)) return;
|
||||||
|
|
||||||
|
if (!MG_Backend::pActiveBackendObject ||
|
||||||
|
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||||
|
MGLOG_I("VertexAttribLFormat: attribute %u asked for a 64-bit (GL_DOUBLE) format, but this "
|
||||||
|
"backend has no double-precision vertex attribute support - see the "
|
||||||
|
"\"64-bit vertex attributes\" / \"shaderFloat64\" POST row for what that costs",
|
||||||
|
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),
|
||||||
|
/*normalized: */ false, /*isInteger: */ false, relativeoffset,
|
||||||
|
/*isBgra: */ false, /*isLong: */ true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VertexArrayAttribFormat_State(GLuint vaobj, GLuint attribindex, GLint size, GLenum type,
|
void VertexArrayAttribFormat_State(GLuint vaobj, GLuint attribindex, GLint size, GLenum type,
|
||||||
@@ -837,9 +958,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
||||||
params[0] = attr->IsInteger ? 1.0f : 0.0f;
|
params[0] = attr->IsInteger ? 1.0f : 0.0f;
|
||||||
return;
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||||
|
params[0] = attr->IsLong ? 1.0f : 0.0f;
|
||||||
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||||
params[0] = static_cast<GLfloat>(attr->Divisor);
|
params[0] = static_cast<GLfloat>(attr->Divisor);
|
||||||
return;
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_BINDING:
|
||||||
|
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET: {
|
||||||
|
GLint value = 0;
|
||||||
|
TryGetVertexAttribBindingQuery(index, pname, value);
|
||||||
|
params[0] = static_cast<GLfloat>(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
@@ -897,9 +1028,19 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
||||||
params[0] = attr->IsInteger ? 1.0 : 0.0;
|
params[0] = attr->IsInteger ? 1.0 : 0.0;
|
||||||
return;
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||||
|
params[0] = attr->IsLong ? 1.0 : 0.0;
|
||||||
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||||
params[0] = static_cast<GLdouble>(attr->Divisor);
|
params[0] = static_cast<GLdouble>(attr->Divisor);
|
||||||
return;
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_BINDING:
|
||||||
|
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET: {
|
||||||
|
GLint value = 0;
|
||||||
|
TryGetVertexAttribBindingQuery(index, pname, value);
|
||||||
|
params[0] = static_cast<GLdouble>(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
@@ -953,9 +1094,16 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
||||||
params[0] = attr->IsInteger ? GL_TRUE : GL_FALSE;
|
params[0] = attr->IsInteger ? GL_TRUE : GL_FALSE;
|
||||||
return;
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||||
|
params[0] = attr->IsLong ? GL_TRUE : GL_FALSE;
|
||||||
|
return;
|
||||||
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||||
params[0] = static_cast<GLint>(attr->Divisor);
|
params[0] = static_cast<GLint>(attr->Divisor);
|
||||||
return;
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_BINDING:
|
||||||
|
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
||||||
|
TryGetVertexAttribBindingQuery(index, pname, params[0]);
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
@@ -1044,6 +1192,85 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
VertexArrayVertexBuffer_State(vaobj, bindingindex, buffer, offset, stride);
|
VertexArrayVertexBuffer_State(vaobj, bindingindex, buffer, offset, stride);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// glGetVertexArrayiv reports exactly one thing (GL 4.6 core table 23.4): which buffer the
|
||||||
|
// named vertex array takes its indices from. Everything else about a vertex array is
|
||||||
|
// per-attribute and belongs to the indexed queries below.
|
||||||
|
void GetVertexArrayiv(GLuint vaobj, GLenum pname, GLint* param) {
|
||||||
|
auto vao = GetNamedVertexArrayObject_State(vaobj, __func__);
|
||||||
|
if (!vao || !param) return;
|
||||||
|
if (pname != GL_ELEMENT_ARRAY_BUFFER_BINDING) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"pname must be GL_ELEMENT_ARRAY_BUFFER_BINDING."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& indexBuffer = vao->GetIndexBufferBindingSlot().GetBoundObject();
|
||||||
|
*param = indexBuffer ? static_cast<GLint>(indexBuffer->GetExternalIndex()) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetVertexArrayIndexediv(GLuint vaobj, GLuint index, GLenum pname, GLint* param) {
|
||||||
|
auto vao = GetNamedVertexArrayObject_State(vaobj, __func__);
|
||||||
|
if (!vao || !param) return;
|
||||||
|
if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return;
|
||||||
|
const auto& attr = vao->GetAttribute(index);
|
||||||
|
switch (pname) {
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_ENABLED:
|
||||||
|
*param = attr.Enabled ? GL_TRUE : GL_FALSE;
|
||||||
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_SIZE:
|
||||||
|
*param = static_cast<GLint>(attr.Size);
|
||||||
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_STRIDE:
|
||||||
|
*param = static_cast<GLint>(attr.Stride);
|
||||||
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_TYPE:
|
||||||
|
*param = static_cast<GLint>(MG_Util::ConvertDataTypeToGLEnum(attr.Type));
|
||||||
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_NORMALIZED:
|
||||||
|
*param = attr.Normalized ? GL_TRUE : GL_FALSE;
|
||||||
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_INTEGER:
|
||||||
|
*param = attr.IsInteger ? GL_TRUE : GL_FALSE;
|
||||||
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_LONG:
|
||||||
|
*param = attr.IsLong ? GL_TRUE : GL_FALSE;
|
||||||
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_ARRAY_DIVISOR:
|
||||||
|
*param = static_cast<GLint>(attr.Divisor);
|
||||||
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_RELATIVE_OFFSET:
|
||||||
|
*param = static_cast<GLint>(vao->GetAttributeRelativeOffset(index));
|
||||||
|
return;
|
||||||
|
case GL_VERTEX_ATTRIB_BINDING:
|
||||||
|
*param = static_cast<GLint>(vao->GetAttributeBindingIndex(index));
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||||
|
"pname is not an accepted indexed vertex array query."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only GL_VERTEX_BINDING_OFFSET needs 64 bits. Its `index` names a vertex buffer binding
|
||||||
|
// point directly (GL 4.6 core 10.3.1), not an attribute - unlike every pname the 32-bit
|
||||||
|
// indexed query above accepts, which is why this one does not go through an attribute's
|
||||||
|
// binding index.
|
||||||
|
void GetVertexArrayIndexed64iv(GLuint vaobj, GLuint index, GLenum pname, GLint64* param) {
|
||||||
|
auto vao = GetNamedVertexArrayObject_State(vaobj, __func__);
|
||||||
|
if (!vao || !param) return;
|
||||||
|
if (!VertexArrayImpl::ValidateVertexAttributeIndex(index)) return;
|
||||||
|
if (pname != GL_VERTEX_BINDING_OFFSET) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pname must be GL_VERTEX_BINDING_OFFSET."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*param = static_cast<GLint64>(vao->GetBindingPoint(index).Offset);
|
||||||
|
}
|
||||||
|
|
||||||
void VertexArrayAttribFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized,
|
void VertexArrayAttribFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized,
|
||||||
GLuint relativeoffset) {
|
GLuint relativeoffset) {
|
||||||
VertexArrayAttribFormat_State(vaobj, attribindex, size, type, normalized, relativeoffset);
|
VertexArrayAttribFormat_State(vaobj, attribindex, size, type, normalized, relativeoffset);
|
||||||
@@ -1076,6 +1303,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const GLsizei* strides) {
|
const GLsizei* strides) {
|
||||||
auto vao = GetBoundVertexArrayOrError("BindVertexBuffers");
|
auto vao = GetBoundVertexArrayOrError("BindVertexBuffers");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
|
if (!ValidateVertexBindingRange(first, count, "BindVertexBuffers")) return;
|
||||||
for (GLsizei i = 0; i < count; ++i) {
|
for (GLsizei i = 0; i < count; ++i) {
|
||||||
if (!buffers) {
|
if (!buffers) {
|
||||||
VertexBufferBinding_State(vao, first + i, 0, 0, 16, "BindVertexBuffers");
|
VertexBufferBinding_State(vao, first + i, 0, 0, 16, "BindVertexBuffers");
|
||||||
@@ -1100,6 +1328,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
"VertexAttribIFormat");
|
"VertexAttribIFormat");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||||
|
auto vao = GetBoundVertexArrayOrError("VertexAttribLFormat");
|
||||||
|
if (!vao) return;
|
||||||
|
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VertexArrayAttribLFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset) {
|
||||||
|
auto vao = GetNamedVertexArrayObject_State(vaobj, "VertexArrayAttribLFormat");
|
||||||
|
if (!vao) return;
|
||||||
|
VertexAttribLFormatSeparate_State(vao, attribindex, size, type, relativeoffset);
|
||||||
|
}
|
||||||
|
|
||||||
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
|
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex) {
|
||||||
auto vao = GetBoundVertexArrayOrError("VertexAttribBinding");
|
auto vao = GetBoundVertexArrayOrError("VertexAttribBinding");
|
||||||
if (!vao) return;
|
if (!vao) return;
|
||||||
|
|||||||
@@ -92,9 +92,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
void EnableVertexArrayAttrib(GLuint vaobj, GLuint index);
|
void EnableVertexArrayAttrib(GLuint vaobj, GLuint index);
|
||||||
void VertexArrayElementBuffer(GLuint vaobj, GLuint buffer);
|
void VertexArrayElementBuffer(GLuint vaobj, GLuint buffer);
|
||||||
void VertexArrayVertexBuffer(GLuint vaobj, GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride);
|
void VertexArrayVertexBuffer(GLuint vaobj, GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride);
|
||||||
|
void GetVertexArrayiv(GLuint vaobj, GLenum pname, GLint* param);
|
||||||
|
void GetVertexArrayIndexediv(GLuint vaobj, GLuint index, GLenum pname, GLint* param);
|
||||||
|
void GetVertexArrayIndexed64iv(GLuint vaobj, GLuint index, GLenum pname, GLint64* param);
|
||||||
void VertexArrayAttribFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized,
|
void VertexArrayAttribFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized,
|
||||||
GLuint relativeoffset);
|
GLuint relativeoffset);
|
||||||
void VertexArrayAttribIFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
void VertexArrayAttribIFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
||||||
|
void VertexArrayAttribLFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
||||||
void VertexArrayAttribBinding(GLuint vaobj, GLuint attribindex, GLuint bindingindex);
|
void VertexArrayAttribBinding(GLuint vaobj, GLuint attribindex, GLuint bindingindex);
|
||||||
void VertexArrayBindingDivisor(GLuint vaobj, GLuint bindingindex, GLuint divisor);
|
void VertexArrayBindingDivisor(GLuint vaobj, GLuint bindingindex, GLuint divisor);
|
||||||
void VertexArrayVertexBuffers(GLuint vaobj, GLuint first, GLsizei count, const GLuint* buffers,
|
void VertexArrayVertexBuffers(GLuint vaobj, GLuint first, GLsizei count, const GLuint* buffers,
|
||||||
@@ -104,6 +108,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
const GLsizei* strides);
|
const GLsizei* strides);
|
||||||
void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset);
|
void VertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset);
|
||||||
void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
void VertexAttribIFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
||||||
|
void VertexAttribLFormat(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
|
||||||
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex);
|
void VertexAttribBinding(GLuint attribindex, GLuint bindingindex);
|
||||||
void VertexBindingDivisor(GLuint bindingindex, GLuint divisor);
|
void VertexBindingDivisor(GLuint bindingindex, GLuint divisor);
|
||||||
void VertexAttribDivisor(GLuint index, GLuint divisor);
|
void VertexAttribDivisor(GLuint index, GLuint divisor);
|
||||||
|
|||||||
@@ -23,6 +23,18 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
|||||||
return std::min(static_cast<Uint>(backendLimit), capacity);
|
return std::min(static_cast<Uint>(backendLimit), capacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint GetMaxVertexAttribBindings() {
|
||||||
|
return GetMaxVertexAttribs();
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint GetMaxVertexAttribRelativeOffset() {
|
||||||
|
return 2047;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint GetMaxVertexAttribStride() {
|
||||||
|
return 2048;
|
||||||
|
}
|
||||||
|
|
||||||
Bool ValidateVertexArrayName(Uint index) {
|
Bool ValidateVertexArrayName(Uint index) {
|
||||||
Bool isValid = MG_State::pGLContext->ValidateVertexArrayName(index);
|
Bool isValid = MG_State::pGLContext->ValidateVertexArrayName(index);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
@@ -90,9 +102,31 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, DataType type, Bool normalized, Int stride,
|
Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, GLenum glType, DataType type, Bool normalized,
|
||||||
Bool integerPath) {
|
Int stride, Bool integerPath) {
|
||||||
constexpr const char* fn = "ValidateVertexAttribFormat";
|
constexpr const char* fn = "ValidateVertexAttribFormat";
|
||||||
|
// GL_UNSIGNED_INT_10F_11F_11F_REV is a three-component float-path-only packing that has no
|
||||||
|
// DataType of its own, so it has to be recognised by name before the conversion below turns
|
||||||
|
// it into Unknown and reports the wrong error (GL 4.6 core 10.3.2).
|
||||||
|
if (glType == GL_UNSIGNED_INT_10F_11F_11F_REV) {
|
||||||
|
if (integerPath) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", fn,
|
||||||
|
std::format("GL_UNSIGNED_INT_10F_11F_11F_REV is not an integer-path type (attribute {}).",
|
||||||
|
index)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (sizeRaw != 3) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidOperation,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", fn,
|
||||||
|
std::format("GL_UNSIGNED_INT_10F_11F_11F_REV requires size 3 (attribute {}).", index)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (type == DataType::Unknown) {
|
if (type == DataType::Unknown) {
|
||||||
MG_State::pGLContext->RecordError(
|
MG_State::pGLContext->RecordError(
|
||||||
ErrorCode::InvalidEnum,
|
ErrorCode::InvalidEnum,
|
||||||
@@ -113,6 +147,30 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The integer path takes exactly the six signed/unsigned integer types (GL 4.6
|
||||||
|
// core 10.3.2): BYTE, UNSIGNED_BYTE, SHORT, UNSIGNED_SHORT, INT, UNSIGNED_INT.
|
||||||
|
// A blacklist could not express that: GL_FLOAT, GL_HALF_FLOAT,
|
||||||
|
// GL_DOUBLE and GL_FIXED all convert to a perfectly valid DataType, so they slipped
|
||||||
|
// through and were recorded as integer attributes.
|
||||||
|
if (integerPath) {
|
||||||
|
switch (type) {
|
||||||
|
case DataType::Int8:
|
||||||
|
case DataType::Uint8:
|
||||||
|
case DataType::Int16:
|
||||||
|
case DataType::Uint16:
|
||||||
|
case DataType::Int32:
|
||||||
|
case DataType::Uint32:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", fn,
|
||||||
|
std::format("Type is not an integer vertex attribute type (attribute {}).", index)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sizeRaw == static_cast<GLint>(GL_BGRA)) {
|
if (sizeRaw == static_cast<GLint>(GL_BGRA)) {
|
||||||
// GL_BGRA is a float-path-only size: it needs GL_UNSIGNED_BYTE or a 2_10_10_10 type and
|
// GL_BGRA is a float-path-only size: it needs GL_UNSIGNED_BYTE or a 2_10_10_10 type and
|
||||||
// normalized == GL_TRUE. On the integer path it is simply an out-of-range size.
|
// normalized == GL_TRUE. On the integer path it is simply an out-of-range size.
|
||||||
@@ -170,4 +228,40 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool ValidateVertexAttribLFormat(Uint index, GLint size, GLenum type) {
|
||||||
|
constexpr const char* fn = "ValidateVertexAttribLFormat";
|
||||||
|
if (size < 1 || size > 4) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", fn,
|
||||||
|
std::format("Invalid size {} for attribute {}. Must be 1-4.", size, index)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// GL 4.6 core 10.3.2: the long form takes GL_DOUBLE and nothing else.
|
||||||
|
if (type != GL_DOUBLE) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidEnum,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", fn,
|
||||||
|
std::format("Type 0x{:X} is not GL_DOUBLE (attribute {}).", type, index)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ValidateVertexAttribRelativeOffset(Uint relativeOffset) {
|
||||||
|
const Uint limit = GetMaxVertexAttribRelativeOffset();
|
||||||
|
if (relativeOffset > limit) {
|
||||||
|
MG_State::pGLContext->RecordError(
|
||||||
|
ErrorCode::InvalidValue,
|
||||||
|
MakeUnique<GenericErrorInfo>(
|
||||||
|
"MG_Impl/GLImpl", "ValidateVertexAttribRelativeOffset",
|
||||||
|
std::format("relativeoffset {} exceeds GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET ({}).", relativeOffset,
|
||||||
|
limit)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl
|
} // namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl
|
||||||
|
|||||||
@@ -15,6 +15,20 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
|||||||
// capacity). Falls back to the capacity when no backend is active (unit tests).
|
// capacity). Falls back to the capacity when no backend is active (unit tests).
|
||||||
Uint GetMaxVertexAttribs();
|
Uint GetMaxVertexAttribs();
|
||||||
|
|
||||||
|
// GL_MAX_VERTEX_ATTRIB_BINDINGS. The default attribute -> binding mapping is the identity, so a
|
||||||
|
// binding point that cannot also be an attribute index would resolve into an attribute the
|
||||||
|
// backend has to reject on every draw; real drivers report the two limits equal as well.
|
||||||
|
Uint GetMaxVertexAttribBindings();
|
||||||
|
|
||||||
|
// GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET. The relative offset is folded into the resolved
|
||||||
|
// attribute offset in the frontend and never reaches a backend limit, so this is the value the
|
||||||
|
// spec requires an implementation to support at minimum (GL 4.6 core table 23.63).
|
||||||
|
Uint GetMaxVertexAttribRelativeOffset();
|
||||||
|
|
||||||
|
// GL_MAX_VERTEX_ATTRIB_STRIDE. Like the relative offset above, the stride never reaches a
|
||||||
|
// backend limit of its own, so this is the spec minimum (GL 4.6 core table 23.63).
|
||||||
|
Uint GetMaxVertexAttribStride();
|
||||||
|
|
||||||
Bool ValidateVertexArrayName(Uint index);
|
Bool ValidateVertexArrayName(Uint index);
|
||||||
Bool ValidateVertexArrayObject(Uint index);
|
Bool ValidateVertexArrayObject(Uint index);
|
||||||
Bool ValidateVertexAttributeIndex(Uint index);
|
Bool ValidateVertexAttributeIndex(Uint index);
|
||||||
@@ -22,6 +36,13 @@ namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl {
|
|||||||
// Full glVertexAttribPointer / glVertexAttribIPointer format validation, including the packed
|
// Full glVertexAttribPointer / glVertexAttribIPointer format validation, including the packed
|
||||||
// 2_10_10_10 types and GL_BGRA size. sizeRaw is the untranslated GL size (possibly GL_BGRA);
|
// 2_10_10_10 types and GL_BGRA size. sizeRaw is the untranslated GL size (possibly GL_BGRA);
|
||||||
// integerPath selects the glVertexAttribIPointer rules.
|
// integerPath selects the glVertexAttribIPointer rules.
|
||||||
Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, DataType type, Bool normalized, Int stride,
|
Bool ValidateVertexAttribFormat(Uint index, GLint sizeRaw, GLenum glType, DataType type, Bool normalized,
|
||||||
Bool integerPath);
|
Int stride, Bool integerPath);
|
||||||
|
// glVertexAttribLFormat / glVertexArrayAttribLFormat: the only accepted type is GL_DOUBLE and
|
||||||
|
// the size range is 1-4 (GL_BGRA is a float-path size). Separate from the function above
|
||||||
|
// because the long path shares none of its type or size rules.
|
||||||
|
Bool ValidateVertexAttribLFormat(Uint index, GLint size, GLenum type);
|
||||||
|
// Shared by every *Format entry point: INVALID_VALUE once relativeoffset leaves the range the
|
||||||
|
// implementation advertises.
|
||||||
|
Bool ValidateVertexAttribRelativeOffset(Uint relativeOffset);
|
||||||
} // namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl
|
} // namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl
|
||||||
|
|||||||
@@ -15,4 +15,257 @@ MOBILEGL_GLX_API void* glXGetProcAddress(const char* name) {
|
|||||||
|
|
||||||
MOBILEGL_GLX_API void* glXGetProcAddressARB(const char* name) {
|
MOBILEGL_GLX_API void* glXGetProcAddressARB(const char* name) {
|
||||||
return MG_Impl::GLXImpl::GetProcAddressARB(name);
|
return MG_Impl::GLXImpl::GetProcAddressARB(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(__linux__) && !defined(__ANDROID__)
|
||||||
|
#include "../GLXImpl.h"
|
||||||
|
|
||||||
|
namespace GLXImpl = MobileGL::MG_Impl::GLXImpl;
|
||||||
|
|
||||||
|
// GLX handle/type spellings from GL/glx.h, expressed without including it:
|
||||||
|
// GLXContext/GLXFBConfig are opaque pointers, drawables are XIDs, Bool is int,
|
||||||
|
// and XVisualInfo* crosses as void*.
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXQueryExtension(Display* dpy, int* errorBase, int* eventBase) {
|
||||||
|
return GLXImpl::QueryExtension(dpy, errorBase, eventBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXQueryVersion(Display* dpy, int* major, int* minor) {
|
||||||
|
return GLXImpl::QueryVersion(dpy, major, minor);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API const char* glXQueryExtensionsString(Display* dpy, int screen) {
|
||||||
|
return GLXImpl::QueryExtensionsString(dpy, screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API const char* glXGetClientString(Display* dpy, int name) {
|
||||||
|
return GLXImpl::GetClientString(dpy, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API const char* glXQueryServerString(Display* dpy, int screen, int name) {
|
||||||
|
return GLXImpl::QueryServerString(dpy, screen, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void** glXGetFBConfigs(Display* dpy, int screen, int* nelements) {
|
||||||
|
return GLXImpl::GetFBConfigs(dpy, screen, nelements);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void** glXChooseFBConfig(Display* dpy, int screen, const int* attribList,
|
||||||
|
int* nelements) {
|
||||||
|
return GLXImpl::ChooseFBConfig(dpy, screen, attribList, nelements);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXGetFBConfigAttrib(Display* dpy, void* config, int attribute, int* value) {
|
||||||
|
return GLXImpl::GetFBConfigAttrib(dpy, config, attribute, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void* glXGetVisualFromFBConfig(Display* dpy, void* config) {
|
||||||
|
return GLXImpl::GetVisualFromFBConfig(dpy, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void* glXChooseVisual(Display* dpy, int screen, int* attribList) {
|
||||||
|
return GLXImpl::ChooseVisual(dpy, screen, attribList);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXGetConfig(Display* dpy, void* visualInfo, int attribute, int* value) {
|
||||||
|
return GLXImpl::GetConfig(dpy, visualInfo, attribute, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void* glXCreateContext(Display* dpy, void* visualInfo, void* shareList, int direct) {
|
||||||
|
return GLXImpl::CreateContext(dpy, visualInfo, shareList, direct);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void* glXCreateNewContext(Display* dpy, void* config, int renderType,
|
||||||
|
void* shareList, int direct) {
|
||||||
|
return GLXImpl::CreateNewContext(dpy, config, renderType, shareList, direct);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void* glXCreateContextAttribsARB(Display* dpy, void* config, void* shareContext,
|
||||||
|
int direct, const int* attribList) {
|
||||||
|
return GLXImpl::CreateContextAttribsARB(dpy, config, shareContext, direct, attribList);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXDestroyContext(Display* dpy, void* context) {
|
||||||
|
GLXImpl::DestroyContext(dpy, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXMakeCurrent(Display* dpy, unsigned long drawable, void* context) {
|
||||||
|
return GLXImpl::MakeCurrent(dpy, drawable, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXMakeContextCurrent(Display* dpy, unsigned long draw, unsigned long read,
|
||||||
|
void* context) {
|
||||||
|
return GLXImpl::MakeContextCurrent(dpy, draw, read, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXSwapBuffers(Display* dpy, unsigned long drawable) {
|
||||||
|
GLXImpl::SwapBuffers(dpy, drawable);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API unsigned long glXCreateWindow(Display* dpy, void* config, unsigned long window,
|
||||||
|
const int* attribList) {
|
||||||
|
return GLXImpl::CreateWindow(dpy, config, window, attribList);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXDestroyWindow(Display* dpy, unsigned long window) {
|
||||||
|
GLXImpl::DestroyWindow(dpy, window);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void* glXGetCurrentContext() {
|
||||||
|
return GLXImpl::GetCurrentContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API unsigned long glXGetCurrentDrawable() {
|
||||||
|
return GLXImpl::GetCurrentDrawable();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API unsigned long glXGetCurrentReadDrawable() {
|
||||||
|
return GLXImpl::GetCurrentReadDrawable();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API Display* glXGetCurrentDisplay() {
|
||||||
|
return GLXImpl::GetCurrentDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXIsDirect(Display* dpy, void* context) {
|
||||||
|
return GLXImpl::IsDirect(dpy, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXWaitGL() {
|
||||||
|
GLXImpl::WaitGL();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXWaitX() {
|
||||||
|
GLXImpl::WaitX();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXQueryContext(Display* dpy, void* context, int attribute, int* value) {
|
||||||
|
return GLXImpl::QueryContext(dpy, context, attribute, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXQueryDrawable(Display* dpy, unsigned long drawable, int attribute,
|
||||||
|
unsigned int* value) {
|
||||||
|
GLXImpl::QueryDrawable(dpy, drawable, attribute, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXSwapIntervalEXT(Display* dpy, unsigned long drawable, int interval) {
|
||||||
|
GLXImpl::SwapIntervalEXT(dpy, drawable, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXSwapIntervalMESA(unsigned int interval) {
|
||||||
|
return GLXImpl::SwapIntervalMESA(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXGetSwapIntervalMESA() {
|
||||||
|
return GLXImpl::GetSwapIntervalMESA();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API int glXSwapIntervalSGI(int interval) {
|
||||||
|
return GLXImpl::SwapIntervalSGI(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy entry points some loaders probe for; harmless no-op stubs.
|
||||||
|
MOBILEGL_GLX_API void glXCopyContext(Display*, void*, void*, unsigned long) {
|
||||||
|
MGLOG_W("glx: glXCopyContext is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API unsigned long glXCreateGLXPixmap(Display*, void*, unsigned long) {
|
||||||
|
MGLOG_W("glx: glXCreateGLXPixmap is not supported");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXDestroyGLXPixmap(Display*, unsigned long) {}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API unsigned long glXCreatePixmap(Display*, void*, unsigned long, const int*) {
|
||||||
|
MGLOG_W("glx: glXCreatePixmap is not supported");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXDestroyPixmap(Display*, unsigned long) {}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API unsigned long glXCreatePbuffer(Display*, void*, const int*) {
|
||||||
|
MGLOG_W("glx: glXCreatePbuffer is not supported");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXDestroyPbuffer(Display*, unsigned long) {}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXUseXFont(unsigned long, int, int, int) {
|
||||||
|
MGLOG_W("glx: glXUseXFont is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXSelectEvent(Display*, unsigned long, unsigned long) {}
|
||||||
|
|
||||||
|
MOBILEGL_GLX_API void glXGetSelectedEvent(Display*, unsigned long, unsigned long* eventMask) {
|
||||||
|
if (eventMask) {
|
||||||
|
*eventMask = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Impl::GLXImpl {
|
||||||
|
namespace {
|
||||||
|
struct GLXEntryPoint {
|
||||||
|
const char* Name;
|
||||||
|
void* Proc;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GLXEntryPoint kGLXEntryPoints[] = {
|
||||||
|
{"glXChooseFBConfig", reinterpret_cast<void*>(glXChooseFBConfig)},
|
||||||
|
{"glXChooseVisual", reinterpret_cast<void*>(glXChooseVisual)},
|
||||||
|
{"glXCopyContext", reinterpret_cast<void*>(glXCopyContext)},
|
||||||
|
{"glXCreateContext", reinterpret_cast<void*>(glXCreateContext)},
|
||||||
|
{"glXCreateContextAttribsARB", reinterpret_cast<void*>(glXCreateContextAttribsARB)},
|
||||||
|
{"glXCreateGLXPixmap", reinterpret_cast<void*>(glXCreateGLXPixmap)},
|
||||||
|
{"glXCreateNewContext", reinterpret_cast<void*>(glXCreateNewContext)},
|
||||||
|
{"glXCreatePbuffer", reinterpret_cast<void*>(glXCreatePbuffer)},
|
||||||
|
{"glXCreatePixmap", reinterpret_cast<void*>(glXCreatePixmap)},
|
||||||
|
{"glXCreateWindow", reinterpret_cast<void*>(glXCreateWindow)},
|
||||||
|
{"glXDestroyContext", reinterpret_cast<void*>(glXDestroyContext)},
|
||||||
|
{"glXDestroyGLXPixmap", reinterpret_cast<void*>(glXDestroyGLXPixmap)},
|
||||||
|
{"glXDestroyPbuffer", reinterpret_cast<void*>(glXDestroyPbuffer)},
|
||||||
|
{"glXDestroyPixmap", reinterpret_cast<void*>(glXDestroyPixmap)},
|
||||||
|
{"glXDestroyWindow", reinterpret_cast<void*>(glXDestroyWindow)},
|
||||||
|
{"glXGetClientString", reinterpret_cast<void*>(glXGetClientString)},
|
||||||
|
{"glXGetConfig", reinterpret_cast<void*>(glXGetConfig)},
|
||||||
|
{"glXGetCurrentContext", reinterpret_cast<void*>(glXGetCurrentContext)},
|
||||||
|
{"glXGetCurrentDisplay", reinterpret_cast<void*>(glXGetCurrentDisplay)},
|
||||||
|
{"glXGetCurrentDrawable", reinterpret_cast<void*>(glXGetCurrentDrawable)},
|
||||||
|
{"glXGetCurrentReadDrawable", reinterpret_cast<void*>(glXGetCurrentReadDrawable)},
|
||||||
|
{"glXGetFBConfigAttrib", reinterpret_cast<void*>(glXGetFBConfigAttrib)},
|
||||||
|
{"glXGetFBConfigs", reinterpret_cast<void*>(glXGetFBConfigs)},
|
||||||
|
{"glXGetProcAddress", reinterpret_cast<void*>(glXGetProcAddress)},
|
||||||
|
{"glXGetProcAddressARB", reinterpret_cast<void*>(glXGetProcAddressARB)},
|
||||||
|
{"glXGetSelectedEvent", reinterpret_cast<void*>(glXGetSelectedEvent)},
|
||||||
|
{"glXGetSwapIntervalMESA", reinterpret_cast<void*>(glXGetSwapIntervalMESA)},
|
||||||
|
{"glXGetVisualFromFBConfig", reinterpret_cast<void*>(glXGetVisualFromFBConfig)},
|
||||||
|
{"glXIsDirect", reinterpret_cast<void*>(glXIsDirect)},
|
||||||
|
{"glXMakeContextCurrent", reinterpret_cast<void*>(glXMakeContextCurrent)},
|
||||||
|
{"glXMakeCurrent", reinterpret_cast<void*>(glXMakeCurrent)},
|
||||||
|
{"glXQueryContext", reinterpret_cast<void*>(glXQueryContext)},
|
||||||
|
{"glXQueryDrawable", reinterpret_cast<void*>(glXQueryDrawable)},
|
||||||
|
{"glXQueryExtension", reinterpret_cast<void*>(glXQueryExtension)},
|
||||||
|
{"glXQueryExtensionsString", reinterpret_cast<void*>(glXQueryExtensionsString)},
|
||||||
|
{"glXQueryServerString", reinterpret_cast<void*>(glXQueryServerString)},
|
||||||
|
{"glXQueryVersion", reinterpret_cast<void*>(glXQueryVersion)},
|
||||||
|
{"glXSelectEvent", reinterpret_cast<void*>(glXSelectEvent)},
|
||||||
|
{"glXSwapBuffers", reinterpret_cast<void*>(glXSwapBuffers)},
|
||||||
|
{"glXSwapIntervalEXT", reinterpret_cast<void*>(glXSwapIntervalEXT)},
|
||||||
|
{"glXSwapIntervalMESA", reinterpret_cast<void*>(glXSwapIntervalMESA)},
|
||||||
|
{"glXSwapIntervalSGI", reinterpret_cast<void*>(glXSwapIntervalSGI)},
|
||||||
|
{"glXUseXFont", reinterpret_cast<void*>(glXUseXFont)},
|
||||||
|
{"glXWaitGL", reinterpret_cast<void*>(glXWaitGL)},
|
||||||
|
{"glXWaitX", reinterpret_cast<void*>(glXWaitX)},
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void* GetGLXEntryPoint(const char* name) {
|
||||||
|
for (const auto& entry : kGLXEntryPoints) {
|
||||||
|
if (std::strcmp(entry.Name, name) == 0) {
|
||||||
|
return entry.Proc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_Impl::GLXImpl
|
||||||
|
|
||||||
|
#endif // __linux__ && !__ANDROID__
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Impl/GLXImpl/GLXImpl.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#if defined(__linux__) && !defined(__ANDROID__)
|
||||||
|
|
||||||
|
namespace MobileGL::MG_Impl::GLXImpl {
|
||||||
|
// GLX layered on MobileGL's own EGL, mirroring WGLImpl/CGLImpl. Handles are
|
||||||
|
// opaque to callers; XVisualInfo crosses the ABI as void* so this header
|
||||||
|
// needs no Xlib includes (Includes.h forward-declares Display/XID/Window).
|
||||||
|
using GLXFBConfigHandle = void*;
|
||||||
|
using GLXContextHandle = void*;
|
||||||
|
using GLXDrawableHandle = unsigned long; // XID
|
||||||
|
|
||||||
|
int QueryExtension(Display* dpy, int* errorBase, int* eventBase);
|
||||||
|
int QueryVersion(Display* dpy, int* major, int* minor);
|
||||||
|
const char* QueryExtensionsString(Display* dpy, int screen);
|
||||||
|
const char* GetClientString(Display* dpy, int name);
|
||||||
|
const char* QueryServerString(Display* dpy, int screen, int name);
|
||||||
|
|
||||||
|
GLXFBConfigHandle* GetFBConfigs(Display* dpy, int screen, int* nelements);
|
||||||
|
GLXFBConfigHandle* ChooseFBConfig(Display* dpy, int screen, const int* attribList, int* nelements);
|
||||||
|
int GetFBConfigAttrib(Display* dpy, GLXFBConfigHandle config, int attribute, int* value);
|
||||||
|
void* GetVisualFromFBConfig(Display* dpy, GLXFBConfigHandle config);
|
||||||
|
void* ChooseVisual(Display* dpy, int screen, int* attribList);
|
||||||
|
int GetConfig(Display* dpy, void* visualInfo, int attribute, int* value);
|
||||||
|
|
||||||
|
GLXContextHandle CreateContext(Display* dpy, void* visualInfo, GLXContextHandle share, int direct);
|
||||||
|
GLXContextHandle CreateNewContext(Display* dpy, GLXFBConfigHandle config, int renderType,
|
||||||
|
GLXContextHandle share, int direct);
|
||||||
|
GLXContextHandle CreateContextAttribsARB(Display* dpy, GLXFBConfigHandle config, GLXContextHandle share,
|
||||||
|
int direct, const int* attribList);
|
||||||
|
void DestroyContext(Display* dpy, GLXContextHandle context);
|
||||||
|
int MakeCurrent(Display* dpy, GLXDrawableHandle drawable, GLXContextHandle context);
|
||||||
|
int MakeContextCurrent(Display* dpy, GLXDrawableHandle draw, GLXDrawableHandle read,
|
||||||
|
GLXContextHandle context);
|
||||||
|
void SwapBuffers(Display* dpy, GLXDrawableHandle drawable);
|
||||||
|
|
||||||
|
GLXDrawableHandle CreateWindow(Display* dpy, GLXFBConfigHandle config, GLXDrawableHandle window,
|
||||||
|
const int* attribList);
|
||||||
|
void DestroyWindow(Display* dpy, GLXDrawableHandle window);
|
||||||
|
|
||||||
|
GLXContextHandle GetCurrentContext();
|
||||||
|
GLXDrawableHandle GetCurrentDrawable();
|
||||||
|
GLXDrawableHandle GetCurrentReadDrawable();
|
||||||
|
Display* GetCurrentDisplay();
|
||||||
|
int IsDirect(Display* dpy, GLXContextHandle context);
|
||||||
|
void WaitGL();
|
||||||
|
void WaitX();
|
||||||
|
int QueryContext(Display* dpy, GLXContextHandle context, int attribute, int* value);
|
||||||
|
void QueryDrawable(Display* dpy, GLXDrawableHandle drawable, int attribute, unsigned int* value);
|
||||||
|
|
||||||
|
void SwapIntervalEXT(Display* dpy, GLXDrawableHandle drawable, int interval);
|
||||||
|
int SwapIntervalMESA(unsigned int interval);
|
||||||
|
int GetSwapIntervalMESA();
|
||||||
|
int SwapIntervalSGI(int interval);
|
||||||
|
|
||||||
|
// Name -> exported glX entry point (table lives with the exports).
|
||||||
|
void* GetGLXEntryPoint(const char* name);
|
||||||
|
} // namespace MobileGL::MG_Impl::GLXImpl
|
||||||
|
|
||||||
|
#endif // __linux__ && !__ANDROID__
|
||||||
@@ -8,11 +8,27 @@
|
|||||||
|
|
||||||
#include "LookUp.h"
|
#include "LookUp.h"
|
||||||
|
|
||||||
namespace MG_Impl::GLXImpl {
|
#if defined(__linux__) && !defined(__ANDROID__)
|
||||||
// TODO: implement complete GLX functionality
|
#include "../GLXImpl.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace MG_Impl::GLXImpl {
|
||||||
void* GetProcAddress(const char* name) {
|
void* GetProcAddress(const char* name) {
|
||||||
|
if (!name) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
MGLOG_D("glXGetProcAddress(\"%s\")", name);
|
MGLOG_D("glXGetProcAddress(\"%s\")", name);
|
||||||
|
#if defined(__linux__) && !defined(__ANDROID__)
|
||||||
|
if (name[0] == 'g' && name[1] == 'l' && name[2] == 'X') {
|
||||||
|
// glX entry points resolve from the GLX layer's own table; GL/EGL
|
||||||
|
// names fall through to the shared resolver below.
|
||||||
|
void* proc = MobileGL::MG_Impl::GLXImpl::GetGLXEntryPoint(name);
|
||||||
|
if (!proc) {
|
||||||
|
MGLOG_D("glXGetProcAddress: unknown glX entry point %s", name);
|
||||||
|
}
|
||||||
|
return proc;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
void* proc = MobileGL::MG_Impl::GetProcAddress(name);
|
void* proc = MobileGL::MG_Impl::GetProcAddress(name);
|
||||||
if (!proc) {
|
if (!proc) {
|
||||||
MGLOG_W("Failed to get function: %s", (const char*)name);
|
MGLOG_W("Failed to get function: %s", (const char*)name);
|
||||||
@@ -25,4 +41,4 @@ namespace MG_Impl::GLXImpl {
|
|||||||
void* GetProcAddressARB(const char* name) {
|
void* GetProcAddressARB(const char* name) {
|
||||||
return GetProcAddress(name);
|
return GetProcAddress(name);
|
||||||
}
|
}
|
||||||
} // namespace MG_Impl::GLXImpl
|
} // namespace MG_Impl::GLXImpl
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.24)
|
||||||
|
|
||||||
|
# MobileGL headless GPU integration tests.
|
||||||
|
#
|
||||||
|
# These are not unit tests: each scenario brings up a real EGL context on a
|
||||||
|
# pbuffer, renders real frames through a real backend and asserts on
|
||||||
|
# glReadPixels output. They need a GPU, so the module is OFF by default
|
||||||
|
# (MOBILEGL_BUILD_INTEGRATION_TEST) and every scenario skips cleanly - never
|
||||||
|
# fails, never hangs - on a machine without one. "Cleanly" is not a hope: the
|
||||||
|
# harness runs the whole bring-up in a forked child first, because MobileGL
|
||||||
|
# ABORTS rather than returning an error on an unusable platform (HeadlessGL.cpp).
|
||||||
|
#
|
||||||
|
# A clean skip is also indistinguishable from a pass, so set
|
||||||
|
# MOBILEGL_ITEST_REQUIRE_GPU wherever the machine is supposed to have a GPU.
|
||||||
|
#
|
||||||
|
# Backend selection is latched at initialization from MOBILEGL_BACKEND_TYPE, so
|
||||||
|
# one process is one backend: the same binary is registered twice, once per
|
||||||
|
# backend, under the `integration-gpu` label.
|
||||||
|
|
||||||
|
message(STATUS "Generating build files for MobileGL Integration Test...")
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
set(MGL_ITEST_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..)
|
||||||
|
|
||||||
|
# Only meaningful where MobileGL_s exists (i.e. not Android).
|
||||||
|
if (NOT TARGET MobileGL_s)
|
||||||
|
message(STATUS "MobileGL_s is not available; skipping the integration test module")
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# MG_Test already pulls googletest in when MOBILEGL_BUILD_TEST is ON. Stand on
|
||||||
|
# our own feet when it is not, so this module can be built by itself.
|
||||||
|
if (NOT TARGET GTest::gtest)
|
||||||
|
include(FetchContent)
|
||||||
|
FetchContent_Declare(
|
||||||
|
googletest
|
||||||
|
GIT_REPOSITORY https://github.com/google/googletest.git
|
||||||
|
GIT_TAG v1.17.0
|
||||||
|
)
|
||||||
|
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
|
||||||
|
FetchContent_MakeAvailable(googletest)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_executable(MobileGLIntegrationTest
|
||||||
|
Main.cpp
|
||||||
|
Harness/HeadlessGL.cpp
|
||||||
|
Scenarios/OrientationScenario.cpp
|
||||||
|
Scenarios/CrossFrameBufferScenario.cpp
|
||||||
|
Scenarios/ResidentIndexScenario.cpp
|
||||||
|
Scenarios/MultiDrawScenario.cpp
|
||||||
|
Scenarios/AsyncCompileScenario.cpp
|
||||||
|
Scenarios/XfbAfterClipDistanceScenario.cpp
|
||||||
|
Scenarios/ThreeChannelAttachmentScenario.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||||
|
${MGL_ITEST_ROOT}/include
|
||||||
|
${MGL_ITEST_ROOT}/MobileGL
|
||||||
|
)
|
||||||
|
|
||||||
|
# gtest, not gtest_main: Main.cpp installs the harness banner itself.
|
||||||
|
target_link_libraries(MobileGLIntegrationTest PRIVATE
|
||||||
|
GTest::gtest
|
||||||
|
MobileGL_s
|
||||||
|
)
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
# Same reason as MG_Test/Backend/DirectVulkan: the GLES headers declare gl*
|
||||||
|
# as dllimport on Windows, so the in-library GL entry-point definitions only
|
||||||
|
# resolve if the whole static library is part of the link.
|
||||||
|
target_link_options(MobileGLIntegrationTest PRIVATE /WHOLEARCHIVE:MobileGL_s)
|
||||||
|
endif()
|
||||||
|
target_compile_definitions(MobileGLIntegrationTest PRIVATE -DNOMINMAX)
|
||||||
|
|
||||||
|
# --- ctest wiring --------------------------------------------------------
|
||||||
|
# A bare libEGL on a glvnd box resolves to whatever vendor comes first, which is
|
||||||
|
# usually Mesa/llvmpipe - a software rasteriser silently replacing the GPU under
|
||||||
|
# a GPU test. Pin the vendor/ICD json the same way MG_Benchmark's
|
||||||
|
# run_driver_bench.sh does.
|
||||||
|
#
|
||||||
|
# Leaving these empty is not a neutral default, it is the failure mode: an
|
||||||
|
# unpinned libEGL lands on llvmpipe and the suite goes green having tested a
|
||||||
|
# software rasteriser. So they are DETECTED here rather than defaulted to empty,
|
||||||
|
# and an empty result is a loud warning.
|
||||||
|
#
|
||||||
|
# mgl_itest_find_driver_json(<outVar> <description> <glob> [<glob>...])
|
||||||
|
# Picks the first json a real hardware vendor owns, in preference order, and
|
||||||
|
# never picks a software rasteriser (llvmpipe / lavapipe / swrast) - landing on
|
||||||
|
# one of those silently is the exact accident this pinning exists to prevent.
|
||||||
|
function(mgl_itest_find_driver_json outVar)
|
||||||
|
set(candidates "")
|
||||||
|
foreach(pattern IN LISTS ARGN)
|
||||||
|
file(GLOB matches "${pattern}")
|
||||||
|
list(APPEND candidates ${matches})
|
||||||
|
endforeach()
|
||||||
|
list(SORT candidates)
|
||||||
|
# Vendors ship an i686 json beside the x86_64 one and it sorts first. Pinning
|
||||||
|
# the wrong word size is worse than not pinning at all - the loader finds no
|
||||||
|
# driver and the whole suite skips - so drop the mismatched ones outright.
|
||||||
|
if (CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||||
|
list(FILTER candidates EXCLUDE REGEX "i686|i386")
|
||||||
|
else()
|
||||||
|
list(FILTER candidates EXCLUDE REGEX "x86_64|aarch64")
|
||||||
|
endif()
|
||||||
|
set(software "")
|
||||||
|
foreach(vendor IN ITEMS nvidia amdgpu amd radeon intel_hasvk intel broadcom freedreno panfrost)
|
||||||
|
foreach(candidate IN LISTS candidates)
|
||||||
|
get_filename_component(leaf "${candidate}" NAME)
|
||||||
|
string(TOLOWER "${leaf}" leaf)
|
||||||
|
if (leaf MATCHES "${vendor}")
|
||||||
|
set(${outVar} "${candidate}" PARENT_SCOPE)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
endforeach()
|
||||||
|
# Nothing recognised as hardware. Report the first non-software entry if there
|
||||||
|
# is one; otherwise report nothing, so the warning below fires.
|
||||||
|
foreach(candidate IN LISTS candidates)
|
||||||
|
get_filename_component(leaf "${candidate}" NAME)
|
||||||
|
string(TOLOWER "${leaf}" leaf)
|
||||||
|
if (NOT leaf MATCHES "lvp|llvmpipe|lavapipe|swrast|softpipe")
|
||||||
|
set(${outVar} "${candidate}" PARENT_SCOPE)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
set(software "${candidate}")
|
||||||
|
endforeach()
|
||||||
|
set(${outVar} "" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
set(MGL_ITEST_DETECTED_EGL_VENDOR "")
|
||||||
|
set(MGL_ITEST_DETECTED_VK_ICD "")
|
||||||
|
if (UNIX AND NOT APPLE AND NOT ANDROID)
|
||||||
|
mgl_itest_find_driver_json(MGL_ITEST_DETECTED_EGL_VENDOR
|
||||||
|
"/usr/share/glvnd/egl_vendor.d/*.json"
|
||||||
|
"/etc/glvnd/egl_vendor.d/*.json")
|
||||||
|
mgl_itest_find_driver_json(MGL_ITEST_DETECTED_VK_ICD
|
||||||
|
"/usr/share/vulkan/icd.d/*.json"
|
||||||
|
"/etc/vulkan/icd.d/*.json")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(MOBILEGL_ITEST_EGL_VENDOR "${MGL_ITEST_DETECTED_EGL_VENDOR}" CACHE FILEPATH
|
||||||
|
"glvnd EGL vendor json to pin for the integration tests (empty: leave the loader alone)")
|
||||||
|
set(MOBILEGL_ITEST_VK_ICD "${MGL_ITEST_DETECTED_VK_ICD}" CACHE FILEPATH
|
||||||
|
"Vulkan ICD json to pin for the DirectVulkan integration tests (empty: leave the loader alone)")
|
||||||
|
|
||||||
|
if (MOBILEGL_ITEST_EGL_VENDOR)
|
||||||
|
message(STATUS "Integration tests: pinning EGL vendor ${MOBILEGL_ITEST_EGL_VENDOR}")
|
||||||
|
else()
|
||||||
|
message(WARNING
|
||||||
|
"Integration tests: no EGL vendor json found or configured (MOBILEGL_ITEST_EGL_VENDOR is empty). "
|
||||||
|
"An unpinned libEGL on a glvnd system resolves to whichever vendor comes first, which is usually "
|
||||||
|
"Mesa/llvmpipe - the scenarios would then go green against a software rasteriser instead of the GPU. "
|
||||||
|
"Set -DMOBILEGL_ITEST_EGL_VENDOR=/usr/share/glvnd/egl_vendor.d/<vendor>.json.")
|
||||||
|
endif()
|
||||||
|
if (MOBILEGL_ITEST_VK_ICD)
|
||||||
|
message(STATUS "Integration tests: pinning Vulkan ICD ${MOBILEGL_ITEST_VK_ICD}")
|
||||||
|
else()
|
||||||
|
message(WARNING
|
||||||
|
"Integration tests: no Vulkan ICD json found or configured (MOBILEGL_ITEST_VK_ICD is empty). "
|
||||||
|
"DirectVulkan would then load whichever ICD the loader enumerates first, quite possibly lavapipe. "
|
||||||
|
"Set -DMOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/<vendor>.json.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Turns "no usable GPU" from a clean skip into a failure - see ScenarioFixture.h.
|
||||||
|
# Without it the integration-gpu label is unfalsifiable: a run that skipped every
|
||||||
|
# scenario and a run that passed every scenario are the same green in ctest.
|
||||||
|
option(MOBILEGL_ITEST_REQUIRE_GPU
|
||||||
|
"Fail (rather than skip) the integration scenarios when the headless harness is unusable" OFF)
|
||||||
|
|
||||||
|
# DirectGLES asks the system EGL for a pbuffer config, and on Mesa the default
|
||||||
|
# platform is not X11 unless it is said out loud (run_driver_bench.sh sets the
|
||||||
|
# same variable). Wrong platform here is not a soft failure: eglCreatePbuffer
|
||||||
|
# fails and every scenario skips.
|
||||||
|
if (UNIX AND NOT APPLE AND NOT ANDROID)
|
||||||
|
set(MOBILEGL_ITEST_EGL_PLATFORM "x11" CACHE STRING
|
||||||
|
"EGL_PLATFORM for the integration tests (empty: leave the loader alone)")
|
||||||
|
else()
|
||||||
|
set(MOBILEGL_ITEST_EGL_PLATFORM "" CACHE STRING
|
||||||
|
"EGL_PLATFORM for the integration tests (empty: leave the loader alone)")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(MGL_ITEST_COMMON_ENV "")
|
||||||
|
if (MOBILEGL_ITEST_EGL_VENDOR)
|
||||||
|
list(APPEND MGL_ITEST_COMMON_ENV "__EGL_VENDOR_LIBRARY_FILENAMES=${MOBILEGL_ITEST_EGL_VENDOR}")
|
||||||
|
endif()
|
||||||
|
if (MOBILEGL_ITEST_EGL_PLATFORM)
|
||||||
|
list(APPEND MGL_ITEST_COMMON_ENV "EGL_PLATFORM=${MOBILEGL_ITEST_EGL_PLATFORM}")
|
||||||
|
endif()
|
||||||
|
if (MOBILEGL_ITEST_REQUIRE_GPU)
|
||||||
|
list(APPEND MGL_ITEST_COMMON_ENV "MOBILEGL_ITEST_REQUIRE_GPU=1")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(MGL_ITEST_VULKAN_ENV ${MGL_ITEST_COMMON_ENV})
|
||||||
|
if (MOBILEGL_ITEST_VK_ICD)
|
||||||
|
list(APPEND MGL_ITEST_VULKAN_ENV "VK_ICD_FILENAMES=${MOBILEGL_ITEST_VK_ICD}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The ENVIRONMENT test property is itself a `;`-list, and gtest_discover_tests
|
||||||
|
# forwards PROPERTIES as a flat list - so a plain `;`-joined value arrives as
|
||||||
|
# four separate arguments and everything after the first is silently read as
|
||||||
|
# another property name. Escaping the separators keeps the whole thing one list
|
||||||
|
# element until set_tests_properties expands it back. Without this only
|
||||||
|
# MOBILEGL_BACKEND_TYPE reaches the test and the vendor/ICD pinning is lost.
|
||||||
|
function(mgl_itest_join_environment outVar)
|
||||||
|
set(joined "")
|
||||||
|
foreach(entry IN LISTS ARGN)
|
||||||
|
if (joined)
|
||||||
|
string(APPEND joined "\\;${entry}")
|
||||||
|
else()
|
||||||
|
set(joined "${entry}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
set(${outVar} "${joined}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_GLES_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectGLES" ${MGL_ITEST_COMMON_ENV})
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_VULKAN_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectVulkan" ${MGL_ITEST_VULKAN_ENV})
|
||||||
|
mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT
|
||||||
|
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV})
|
||||||
|
|
||||||
|
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
||||||
|
set(MGL_ITEST_TIMEOUT 120)
|
||||||
|
|
||||||
|
include(GoogleTest)
|
||||||
|
|
||||||
|
# Discovery runs `--gtest_list_tests`, which does not construct the harness and
|
||||||
|
# so needs no GPU. One registration per backend; TEST_PREFIX keeps the two sets
|
||||||
|
# of ctest names apart.
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES."
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS integration-gpu
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectVulkan."
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS integration-gpu
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_VULKAN_ENVIRONMENT}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# A third registration, of ONE scenario, with asynchronous shader compilation
|
||||||
|
# pinned on. Not a second code path in the renderer: a second ALLOCATION pattern.
|
||||||
|
# The async pipeline's job objects change which of the freed blocks the capture
|
||||||
|
# phase is handed, and that is what decides whether the destroyed-VAO address is
|
||||||
|
# reached at all - on the ablated (pre-fix) tree async=1 reproduced 3 runs out of
|
||||||
|
# 3 where the ambient default reproduced 2 of 3. Pinning it here means the
|
||||||
|
# high-signal configuration runs whatever the shipped default becomes, instead of
|
||||||
|
# the suite quietly weakening the day that default flips. It must be process-wide
|
||||||
|
# (the ENVIRONMENT property), not an in-process scope: the compile pool and its
|
||||||
|
# threads are stood up at initialization, and their allocations are half the
|
||||||
|
# point. DirectVulkan only - the memo this pins is DirectVulkan's.
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectVulkan.AsyncCompile."
|
||||||
|
TEST_FILTER "XfbAfterClipDistanceScenario.*"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS integration-gpu
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT}"
|
||||||
|
)
|
||||||
@@ -0,0 +1,587 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
#include "HeadlessGL.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <ostream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// MobileGL_s, so every gl*/egl* below binds to MobileGL's implementation, not
|
||||||
|
// to a system loader.
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#include <EGL/egl.h>
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
// The pre-flight below runs the whole EGL bring-up in a forked child, which is
|
||||||
|
// the only construction that is actually predictive here: MobileGL ABORTS
|
||||||
|
// (MOBILEGL_ASSERT -> SIGTRAP) rather than returning an error on an unusable
|
||||||
|
// platform, so nothing the parent can call in-process is allowed to be wrong.
|
||||||
|
#if !defined(_WIN32) && !defined(__APPLE__) && __has_include(<sys/wait.h>)
|
||||||
|
#define MGITEST_HAVE_FORK_PREFLIGHT 1
|
||||||
|
#include <csignal>
|
||||||
|
#include <ctime>
|
||||||
|
#include <sys/resource.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#else
|
||||||
|
#define MGITEST_HAVE_FORK_PREFLIGHT 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Small enough that a readback is cheap, big enough that "top third" and
|
||||||
|
// "bottom third" are unambiguous. Non-square on purpose: a transposing
|
||||||
|
// bug cannot hide behind a square.
|
||||||
|
constexpr int kSurfaceWidth = 128;
|
||||||
|
constexpr int kSurfaceHeight = 96;
|
||||||
|
|
||||||
|
std::string EnvOr(const char* name, const char* fallback) {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
return (value != nullptr && value[0] != '\0') ? std::string(value) : std::string(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A skip reason is only useful if it says which call failed AND why, so
|
||||||
|
// every bring-up step reports the EGL error it left behind.
|
||||||
|
std::string WithEglError(const char* what) {
|
||||||
|
std::ostringstream out;
|
||||||
|
out << what << " (eglGetError=0x" << std::hex << eglGetError() << ")";
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The EGL objects one bring-up produces.
|
||||||
|
struct EglBringUp {
|
||||||
|
void* display = nullptr;
|
||||||
|
void* surface = nullptr;
|
||||||
|
void* context = nullptr;
|
||||||
|
std::string renderer;
|
||||||
|
};
|
||||||
|
|
||||||
|
// THE bring-up, in one function so the pre-flight child and the parent run
|
||||||
|
// literally the same sequence - a pre-flight that tests something narrower
|
||||||
|
// than what the parent will do is exactly the kind of "predictive" check
|
||||||
|
// that is not.
|
||||||
|
//
|
||||||
|
// Returns 0 on success, or the 1-based index of the step that failed, and
|
||||||
|
// fills outReason either way.
|
||||||
|
int RunEglBringUp(EglBringUp& out, std::string& outReason) {
|
||||||
|
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||||
|
if (display == EGL_NO_DISPLAY) {
|
||||||
|
outReason = WithEglError("eglGetDisplay(EGL_DEFAULT_DISPLAY) returned EGL_NO_DISPLAY");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
EGLint major = 0, minor = 0;
|
||||||
|
if (eglInitialize(display, &major, &minor) != EGL_TRUE) {
|
||||||
|
outReason = WithEglError("eglInitialize failed: no usable display/driver on this machine");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (eglBindAPI(EGL_OPENGL_API) != EGL_TRUE) {
|
||||||
|
outReason = WithEglError("eglBindAPI(EGL_OPENGL_API) failed");
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EGLint configAttribs[] = {EGL_SURFACE_TYPE,
|
||||||
|
EGL_PBUFFER_BIT,
|
||||||
|
EGL_RED_SIZE,
|
||||||
|
8,
|
||||||
|
EGL_GREEN_SIZE,
|
||||||
|
8,
|
||||||
|
EGL_BLUE_SIZE,
|
||||||
|
8,
|
||||||
|
EGL_ALPHA_SIZE,
|
||||||
|
8,
|
||||||
|
EGL_DEPTH_SIZE,
|
||||||
|
24,
|
||||||
|
EGL_RENDERABLE_TYPE,
|
||||||
|
EGL_OPENGL_BIT,
|
||||||
|
EGL_NONE};
|
||||||
|
EGLConfig config = nullptr;
|
||||||
|
EGLint configCount = 0;
|
||||||
|
if (eglChooseConfig(display, configAttribs, &config, 1, &configCount) != EGL_TRUE || configCount < 1) {
|
||||||
|
outReason = WithEglError("eglChooseConfig found no pbuffer-capable RGBA8/D24 config");
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_NONE};
|
||||||
|
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
|
||||||
|
if (context == EGL_NO_CONTEXT) {
|
||||||
|
context = eglCreateContext(display, config, EGL_NO_CONTEXT, nullptr);
|
||||||
|
}
|
||||||
|
if (context == EGL_NO_CONTEXT) {
|
||||||
|
outReason = WithEglError("eglCreateContext failed: no desktop-GL context available");
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE};
|
||||||
|
EGLSurface surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
|
||||||
|
if (surface == EGL_NO_SURFACE) {
|
||||||
|
outReason = WithEglError("eglCreatePbufferSurface failed");
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
// The step that brings the whole backend up (DirectVulkan creates its
|
||||||
|
// instance, device and surface in here) and therefore the step that
|
||||||
|
// aborts instead of returning an error on an unusable platform.
|
||||||
|
if (eglMakeCurrent(display, surface, surface, context) != EGL_TRUE) {
|
||||||
|
outReason = WithEglError("eglMakeCurrent failed");
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLubyte* renderer = glGetString(GL_RENDERER);
|
||||||
|
if (renderer == nullptr) {
|
||||||
|
outReason = "glGetString(GL_RENDERER) returned null after eglMakeCurrent";
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.display = display;
|
||||||
|
out.surface = surface;
|
||||||
|
out.context = context;
|
||||||
|
out.renderer = reinterpret_cast<const char*>(renderer);
|
||||||
|
outReason.clear();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platform pre-flight, and the reason this module can claim to skip
|
||||||
|
// cleanly rather than merely hope to.
|
||||||
|
//
|
||||||
|
// MobileGL does not return errors when the platform is unusable - it
|
||||||
|
// ABORTS. MOBILEGL_ASSERT raises SIGTRAP, and the DirectVulkan bring-up
|
||||||
|
// asserts its way through instance, physical-device and surface creation
|
||||||
|
// inside eglMakeCurrent. So there is no in-process question the harness
|
||||||
|
// can ask that is guaranteed to be survivable, and the old form (dlopen
|
||||||
|
// the Vulkan loader, count physical devices, look for
|
||||||
|
// VK_EXT_headless_surface) was a guess at the abort conditions rather
|
||||||
|
// than a test of them: it named three of the ways bring-up can die and
|
||||||
|
// was silent about every other one, including every DirectGLES one.
|
||||||
|
//
|
||||||
|
// What is actually predictive is to run the bring-up itself somewhere a
|
||||||
|
// SIGTRAP is a datum instead of a crash. fork() gives exactly that: the
|
||||||
|
// child performs the identical sequence and _exit(0)s on success, and
|
||||||
|
// ANY non-zero exit or ANY signal in the parent's waitpid() means "this
|
||||||
|
// platform is unusable" - whatever the reason, including reasons nobody
|
||||||
|
// has thought of. Only then does the parent do the real bring-up.
|
||||||
|
//
|
||||||
|
// Returns an empty string when the platform survived a full bring-up.
|
||||||
|
std::string PreflightBringUp() {
|
||||||
|
#if !MGITEST_HAVE_FORK_PREFLIGHT
|
||||||
|
// No fork(): let the in-process bring-up speak for itself, which is
|
||||||
|
// what this module did before. Windows/macOS are not CI targets for
|
||||||
|
// the headless scenarios.
|
||||||
|
return {};
|
||||||
|
#else
|
||||||
|
int channel[2] = {-1, -1};
|
||||||
|
if (pipe(channel) != 0) {
|
||||||
|
return {}; // cannot pre-flight; fall through to the in-process attempt
|
||||||
|
}
|
||||||
|
// The child inherits our stdio buffers; flush so nothing is printed twice.
|
||||||
|
std::fflush(nullptr);
|
||||||
|
const pid_t child = fork();
|
||||||
|
if (child < 0) {
|
||||||
|
close(channel[0]);
|
||||||
|
close(channel[1]);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (child == 0) {
|
||||||
|
close(channel[0]);
|
||||||
|
// The child is EXPECTED to die on a signal on an unusable
|
||||||
|
// platform; that is the measurement. Do not let each such
|
||||||
|
// measurement drop a core file next to the test binary.
|
||||||
|
const rlimit noCore{0, 0};
|
||||||
|
setrlimit(RLIMIT_CORE, &noCore);
|
||||||
|
std::fprintf(stderr, "[itest] pre-flight child: attempting a full EGL bring-up\n");
|
||||||
|
EglBringUp local;
|
||||||
|
std::string reason;
|
||||||
|
const int step = RunEglBringUp(local, reason);
|
||||||
|
if (!reason.empty()) {
|
||||||
|
const std::size_t bytes = std::min<std::size_t>(reason.size(), 480);
|
||||||
|
const ssize_t written = write(channel[1], reason.data(), bytes);
|
||||||
|
(void)written;
|
||||||
|
}
|
||||||
|
close(channel[1]);
|
||||||
|
// _exit, never exit(): every atexit handler and static destructor
|
||||||
|
// in this address space belongs to the parent's copy of the world,
|
||||||
|
// and the child is holding a live context it must not tear down.
|
||||||
|
_exit(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(channel[1]);
|
||||||
|
// Reap first, read after: the message is bounded well below the pipe
|
||||||
|
// buffer so the child can never block writing it, and polling the exit
|
||||||
|
// status is what lets a wedged child be killed instead of hanging the
|
||||||
|
// parent on a read that will never return.
|
||||||
|
constexpr int kPreflightTimeoutMs = 30000;
|
||||||
|
int status = 0;
|
||||||
|
int waitedMs = 0;
|
||||||
|
for (;;) {
|
||||||
|
const pid_t reaped = waitpid(child, &status, WNOHANG);
|
||||||
|
if (reaped == child) break;
|
||||||
|
if (reaped < 0) {
|
||||||
|
close(channel[0]);
|
||||||
|
return "waitpid on the EGL bring-up pre-flight child failed";
|
||||||
|
}
|
||||||
|
if (waitedMs >= kPreflightTimeoutMs) {
|
||||||
|
kill(child, SIGKILL);
|
||||||
|
(void)waitpid(child, &status, 0);
|
||||||
|
close(channel[0]);
|
||||||
|
std::ostringstream out;
|
||||||
|
out << "the EGL bring-up wedged: a forked pre-flight child made no progress in "
|
||||||
|
<< kPreflightTimeoutMs / 1000 << "s and was killed";
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
timespec nap{0, 10 * 1000 * 1000};
|
||||||
|
nanosleep(&nap, nullptr);
|
||||||
|
waitedMs += 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string childSays;
|
||||||
|
char buffer[512];
|
||||||
|
for (;;) {
|
||||||
|
const ssize_t got = read(channel[0], buffer, sizeof(buffer));
|
||||||
|
if (got <= 0) break;
|
||||||
|
childSays.append(buffer, static_cast<std::size_t>(got));
|
||||||
|
}
|
||||||
|
close(channel[0]);
|
||||||
|
|
||||||
|
if (WIFSIGNALED(status)) {
|
||||||
|
const int signalNumber = WTERMSIG(status);
|
||||||
|
const char* signalName = strsignal(signalNumber);
|
||||||
|
std::ostringstream out;
|
||||||
|
out << "the EGL bring-up ABORTS on this platform: a forked pre-flight child died on signal "
|
||||||
|
<< signalNumber << " (" << (signalName != nullptr ? signalName : "?") << ")";
|
||||||
|
if (!childSays.empty()) out << " after: " << childSays;
|
||||||
|
out << ". MobileGL asserts rather than returning an error here, so the scenarios would "
|
||||||
|
"have taken the whole test binary down with them";
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
if (!WIFEXITED(status)) {
|
||||||
|
return "the EGL bring-up pre-flight child neither exited nor was signalled";
|
||||||
|
}
|
||||||
|
const int exitStatus = WEXITSTATUS(status);
|
||||||
|
if (exitStatus != 0) {
|
||||||
|
std::ostringstream out;
|
||||||
|
out << (childSays.empty() ? "the EGL bring-up failed" : childSays)
|
||||||
|
<< " (forked pre-flight child exit status " << exitStatus << ")";
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool RequireGpu() {
|
||||||
|
const char* value = std::getenv("MOBILEGL_ITEST_REQUIRE_GPU");
|
||||||
|
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& os, const Rgba8& c) {
|
||||||
|
os << "rgba(" << int(c.r) << "," << int(c.g) << "," << int(c.b) << "," << int(c.a) << ")";
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rgba8 Image::At(int x, int y) const {
|
||||||
|
if (x < 0 || y < 0 || x >= m_width || y >= m_height) {
|
||||||
|
return Rgba8{};
|
||||||
|
}
|
||||||
|
const std::size_t index = (static_cast<std::size_t>(y) * m_width + x) * 4;
|
||||||
|
return Rgba8{m_pixels[index], m_pixels[index + 1], m_pixels[index + 2], m_pixels[index + 3]};
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* Image::ColorName(int x, int y) const {
|
||||||
|
const Rgba8 c = At(x, y);
|
||||||
|
const bool r = c.r > 160, g = c.g > 160, b = c.b > 160;
|
||||||
|
const bool nr = c.r < 96, ng = c.g < 96, nb = c.b < 96;
|
||||||
|
if (nr && ng && nb) return "black";
|
||||||
|
if (r && g && b) return "white";
|
||||||
|
if (r && ng && nb) return "red";
|
||||||
|
if (nr && g && nb) return "green";
|
||||||
|
if (nr && ng && b) return "blue";
|
||||||
|
if (r && g && nb) return "yellow";
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t Image::ByteDiffCount(const Image& other) const {
|
||||||
|
if (m_width != other.m_width || m_height != other.m_height) {
|
||||||
|
return std::max(m_pixels.size(), other.m_pixels.size());
|
||||||
|
}
|
||||||
|
std::size_t differing = 0;
|
||||||
|
for (std::size_t i = 0; i < m_pixels.size(); ++i) {
|
||||||
|
if (m_pixels[i] != other.m_pixels[i]) ++differing;
|
||||||
|
}
|
||||||
|
return differing;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Image::QuadrantSignature() const {
|
||||||
|
if (m_width < 2 || m_height < 2) return "<empty>";
|
||||||
|
// Quadrant CENTRES, so a one-pixel rounding difference at a quadrant edge
|
||||||
|
// never decides the answer. Order is fixed and load-bearing: bottom-left,
|
||||||
|
// bottom-right, top-left, top-right.
|
||||||
|
const int leftX = m_width / 4;
|
||||||
|
const int rightX = m_width * 3 / 4;
|
||||||
|
const int bottomY = m_height / 4;
|
||||||
|
const int topY = m_height * 3 / 4;
|
||||||
|
std::ostringstream out;
|
||||||
|
out << ColorName(leftX, bottomY) << "," << ColorName(rightX, bottomY) << "," << ColorName(leftX, topY) << ","
|
||||||
|
<< ColorName(rightX, topY);
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
RegionScan ScanRegion(const Image& image, int x0, int x1, int y0, int y1, const char* expectedColor) {
|
||||||
|
RegionScan scan;
|
||||||
|
x0 = std::max(x0, 0);
|
||||||
|
y0 = std::max(y0, 0);
|
||||||
|
x1 = std::min(x1, image.Width() - 1);
|
||||||
|
y1 = std::min(y1, image.Height() - 1);
|
||||||
|
for (int y = y0; y <= y1; ++y) {
|
||||||
|
for (int x = x0; x <= x1; ++x) {
|
||||||
|
++scan.total;
|
||||||
|
const char* name = image.ColorName(x, y);
|
||||||
|
if (std::strcmp(name, expectedColor) == 0) continue;
|
||||||
|
++scan.offenders;
|
||||||
|
if (scan.firstX < 0) {
|
||||||
|
scan.firstX = x;
|
||||||
|
scan.firstY = y;
|
||||||
|
scan.firstColor = image.At(x, y);
|
||||||
|
scan.firstColorName = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scan;
|
||||||
|
}
|
||||||
|
|
||||||
|
::testing::AssertionResult RegionIsMostly(const Image& image, int x0, int x1, int y0, int y1,
|
||||||
|
const char* expectedColor, double tolerance,
|
||||||
|
const std::string& when) {
|
||||||
|
const RegionScan scan = ScanRegion(image, x0, x1, y0, y1, expectedColor);
|
||||||
|
if (scan.total == 0) {
|
||||||
|
return ::testing::AssertionFailure()
|
||||||
|
<< when << ": region x[" << x0 << "," << x1 << "] y[" << y0 << "," << y1
|
||||||
|
<< "] is empty against a " << image.Width() << "x" << image.Height() << " readback";
|
||||||
|
}
|
||||||
|
const double offendingFraction = static_cast<double>(scan.offenders) / scan.total;
|
||||||
|
if (offendingFraction <= tolerance) {
|
||||||
|
return ::testing::AssertionSuccess();
|
||||||
|
}
|
||||||
|
return ::testing::AssertionFailure()
|
||||||
|
<< when << ": region x[" << x0 << "," << x1 << "] y[" << y0 << "," << y1 << "] should be all "
|
||||||
|
<< expectedColor << ", but " << scan.offenders << " of " << scan.total << " pixels ("
|
||||||
|
<< static_cast<int>(offendingFraction * 100.0 + 0.5) << "%) are not; first offender at (" << scan.firstX
|
||||||
|
<< "," << scan.firstY << ") is " << scan.firstColorName << " " << scan.firstColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
HeadlessGL& HeadlessGL::Get() {
|
||||||
|
static HeadlessGL instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
HeadlessGL::HeadlessGL() {
|
||||||
|
m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", "<unset>");
|
||||||
|
m_usable = BringUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HeadlessGL::BringUp() {
|
||||||
|
// Ask a disposable copy of this process first. Only if it survived does
|
||||||
|
// the real one try - see PreflightBringUp for why nothing weaker is
|
||||||
|
// predictive against a stack that aborts instead of returning errors.
|
||||||
|
const std::string preflightProblem = PreflightBringUp();
|
||||||
|
if (!preflightProblem.empty()) {
|
||||||
|
m_skipReason = preflightProblem;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same shape as DriverBench's boot_egl(), minus the dlopen: the provider
|
||||||
|
// is this binary. A pbuffer needs no window system, but MobileGL's own
|
||||||
|
// loader still has to reach a real driver underneath - and the child
|
||||||
|
// above just proved it can.
|
||||||
|
EglBringUp brought;
|
||||||
|
std::string reason;
|
||||||
|
if (RunEglBringUp(brought, reason) != 0) {
|
||||||
|
// The pre-flight passed and the parent's identical attempt did not.
|
||||||
|
// That is a real result, not a machine without a GPU, so say so: it
|
||||||
|
// means something is different between the two attempts (a leaked
|
||||||
|
// exclusive device, an environment the child did not have).
|
||||||
|
m_skipReason = reason + " - although an identical bring-up in a forked pre-flight child succeeded";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_display = brought.display;
|
||||||
|
m_surface = brought.surface;
|
||||||
|
m_context = brought.context;
|
||||||
|
m_width = kSurfaceWidth;
|
||||||
|
m_height = kSurfaceHeight;
|
||||||
|
m_renderer = std::move(brought.renderer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HeadlessGL::EndFrame() {
|
||||||
|
if (!m_usable) return;
|
||||||
|
eglSwapBuffers(static_cast<EGLDisplay>(m_display), static_cast<EGLSurface>(m_surface));
|
||||||
|
++m_frameIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HeadlessGL::ShutDown() {
|
||||||
|
if (!m_usable) return;
|
||||||
|
EGLDisplay display = static_cast<EGLDisplay>(m_display);
|
||||||
|
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||||
|
if (m_context != nullptr) eglDestroyContext(display, static_cast<EGLContext>(m_context));
|
||||||
|
if (m_surface != nullptr) eglDestroySurface(display, static_cast<EGLSurface>(m_surface));
|
||||||
|
eglTerminate(display);
|
||||||
|
m_context = nullptr;
|
||||||
|
m_surface = nullptr;
|
||||||
|
m_display = nullptr;
|
||||||
|
m_usable = false;
|
||||||
|
m_skipReason = "the headless context has already been torn down";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- scenario vocabulary ------------------------------------------------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
unsigned int CompileStage(GLenum stage, const char* source, std::string* outError) {
|
||||||
|
const GLuint shader = glCreateShader(stage);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compiled = 0;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||||
|
if (compiled == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
GLsizei length = 0;
|
||||||
|
glGetShaderInfoLog(shader, sizeof(log) - 1, &length, log);
|
||||||
|
if (outError != nullptr) {
|
||||||
|
*outError = std::string(stage == GL_VERTEX_SHADER ? "vertex" : "fragment") +
|
||||||
|
" shader failed to compile: " + log;
|
||||||
|
}
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
unsigned int CompileProgram(const char* vertexSource, const char* fragmentSource, std::string* outError) {
|
||||||
|
const GLuint vs = CompileStage(GL_VERTEX_SHADER, vertexSource, outError);
|
||||||
|
if (vs == 0) return 0;
|
||||||
|
const GLuint fs = CompileStage(GL_FRAGMENT_SHADER, fragmentSource, outError);
|
||||||
|
if (fs == 0) {
|
||||||
|
glDeleteShader(vs);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, vs);
|
||||||
|
glAttachShader(program, fs);
|
||||||
|
// Pinned rather than queried so the scenarios can set up a VAO without a
|
||||||
|
// round trip, and so a driver that reorders attributes cannot change what
|
||||||
|
// the test means.
|
||||||
|
glBindAttribLocation(program, 0, "aPos");
|
||||||
|
glBindAttribLocation(program, 1, "aColor");
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDeleteShader(vs);
|
||||||
|
glDeleteShader(fs);
|
||||||
|
GLint linked = 0;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
if (linked == GL_FALSE) {
|
||||||
|
char log[2048] = {};
|
||||||
|
GLsizei length = 0;
|
||||||
|
glGetProgramInfoLog(program, sizeof(log) - 1, &length, log);
|
||||||
|
if (outError != nullptr) *outError = std::string("program failed to link: ") + log;
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorFbo MakeColorFbo(int width, int height) {
|
||||||
|
ColorFbo target;
|
||||||
|
target.width = width;
|
||||||
|
target.height = height;
|
||||||
|
glGenTextures(1, &target.texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
glGenFramebuffers(1, &target.fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
|
||||||
|
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
DestroyColorFbo(target);
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DestroyColorFbo(ColorFbo& target) {
|
||||||
|
if (target.fbo != 0) glDeleteFramebuffers(1, &target.fbo);
|
||||||
|
if (target.texture != 0) glDeleteTextures(1, &target.texture);
|
||||||
|
target.fbo = 0;
|
||||||
|
target.texture = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BindDefaultFramebuffer() {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glViewport(0, 0, HeadlessGL::Get().Width(), HeadlessGL::Get().Height());
|
||||||
|
}
|
||||||
|
|
||||||
|
void BindFbo(const ColorFbo& target) {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
|
||||||
|
glViewport(0, 0, target.width, target.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearTo(float r, float g, float b, float a) {
|
||||||
|
glClearColor(r, g, b, a);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Image ReadPixels(int width, int height) {
|
||||||
|
Image image(width, height);
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||||
|
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, image.Data());
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int FirstGLError() {
|
||||||
|
const GLenum first = glGetError();
|
||||||
|
if (first == GL_NO_ERROR) return GL_NO_ERROR;
|
||||||
|
// Drain, bounded: a broken stack must not turn an error check into a hang.
|
||||||
|
for (int i = 0; i < 64 && glGetError() != GL_NO_ERROR; ++i) {}
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* GLErrorName(unsigned int error) {
|
||||||
|
switch (error) {
|
||||||
|
case GL_NO_ERROR:
|
||||||
|
return "GL_NO_ERROR";
|
||||||
|
case GL_INVALID_ENUM:
|
||||||
|
return "GL_INVALID_ENUM";
|
||||||
|
case GL_INVALID_VALUE:
|
||||||
|
return "GL_INVALID_VALUE";
|
||||||
|
case GL_INVALID_OPERATION:
|
||||||
|
return "GL_INVALID_OPERATION";
|
||||||
|
case GL_OUT_OF_MEMORY:
|
||||||
|
return "GL_OUT_OF_MEMORY";
|
||||||
|
case GL_INVALID_FRAMEBUFFER_OPERATION:
|
||||||
|
return "GL_INVALID_FRAMEBUFFER_OPERATION";
|
||||||
|
default:
|
||||||
|
return "GL_<unknown>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/HeadlessGL.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// A headless GL context and the small vocabulary the scenarios are written in.
|
||||||
|
//
|
||||||
|
// The scenarios in this module are end-to-end: they drive MobileGL's own GL and
|
||||||
|
// EGL entry points (this binary links MobileGL_s, so gl*/egl* resolve straight
|
||||||
|
// into the implementation) and assert on glReadPixels output. Nothing here
|
||||||
|
// inspects backend state - both bugs this module pins were invisible to
|
||||||
|
// state-level assertions and visible only in pixels.
|
||||||
|
//
|
||||||
|
// Headless by construction, following MG_Benchmark/Driver/DriverBench.c: an EGL
|
||||||
|
// context on a PBUFFER surface. No window, no window manager, no human. Unlike
|
||||||
|
// DriverBench the scenarios do draw to the DEFAULT framebuffer (that is where
|
||||||
|
// the Y-flip lives) and do call eglSwapBuffers (that is the frame boundary the
|
||||||
|
// cross-frame scenarios need to be real).
|
||||||
|
//
|
||||||
|
// One process is one backend: MOBILEGL_BACKEND_TYPE is latched at
|
||||||
|
// initialization, so the CMake wiring runs this binary once per backend rather
|
||||||
|
// than trying to switch in-process.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
// True when MOBILEGL_ITEST_REQUIRE_GPU is set in the environment: the runner
|
||||||
|
// is asserting that this machine HAS a usable GPU, so "no GPU" stops being a
|
||||||
|
// clean skip and becomes a failure. Without it the integration-gpu label is
|
||||||
|
// unfalsifiable - a CI job that ran nothing reports exactly the same green as
|
||||||
|
// a job that ran everything.
|
||||||
|
bool RequireGpu();
|
||||||
|
|
||||||
|
struct Rgba8 {
|
||||||
|
std::uint8_t r = 0, g = 0, b = 0, a = 0;
|
||||||
|
|
||||||
|
bool operator==(const Rgba8& other) const {
|
||||||
|
return r == other.r && g == other.g && b == other.b && a == other.a;
|
||||||
|
}
|
||||||
|
bool operator!=(const Rgba8& other) const { return !(*this == other); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prints as "rgba(255,0,0,255)" so a gtest failure names the colour it saw.
|
||||||
|
std::ostream& operator<<(std::ostream& os, const Rgba8& c);
|
||||||
|
|
||||||
|
// An RGBA8 readback. Row 0 is the BOTTOM row: that is GL's convention for
|
||||||
|
// glReadPixels and it is what "correctly oriented" means everywhere below.
|
||||||
|
class Image {
|
||||||
|
public:
|
||||||
|
Image() = default;
|
||||||
|
Image(int width, int height)
|
||||||
|
: m_width(width), m_height(height), m_pixels(static_cast<std::size_t>(width) * height * 4, 0) {}
|
||||||
|
|
||||||
|
int Width() const { return m_width; }
|
||||||
|
int Height() const { return m_height; }
|
||||||
|
bool Empty() const { return m_pixels.empty(); }
|
||||||
|
std::uint8_t* Data() { return m_pixels.data(); }
|
||||||
|
const std::uint8_t* Data() const { return m_pixels.data(); }
|
||||||
|
|
||||||
|
Rgba8 At(int x, int y) const;
|
||||||
|
// Nearest of {black, red, green, blue, white, other} - the scenarios only
|
||||||
|
// ever draw those, so this turns a pixel into something readable.
|
||||||
|
const char* ColorName(int x, int y) const;
|
||||||
|
|
||||||
|
bool operator==(const Image& other) const {
|
||||||
|
return m_width == other.m_width && m_height == other.m_height && m_pixels == other.m_pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count of differing bytes, for a failure message that says how wrong.
|
||||||
|
std::size_t ByteDiffCount(const Image& other) const;
|
||||||
|
|
||||||
|
// The four quadrant centres, in the fixed order
|
||||||
|
// bottom-left, bottom-right, top-left, top-right.
|
||||||
|
//
|
||||||
|
// This replaces the old VerticalSignature(bandCount), which read three
|
||||||
|
// full-width horizontal stripes down the centre line and was therefore
|
||||||
|
// blind to an X flip, to a transpose, and to a 180 rotation composed with
|
||||||
|
// a Y flip - all of those left the stripe order alone. Four quadrant
|
||||||
|
// colours are asymmetric in BOTH axes, so each of the eight square
|
||||||
|
// symmetries produces a different string (see OrientationScenario, which
|
||||||
|
// spells all eight out).
|
||||||
|
std::string QuadrantSignature() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_width = 0;
|
||||||
|
int m_height = 0;
|
||||||
|
std::vector<std::uint8_t> m_pixels;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The process-wide headless context. Brought up lazily on the first Get() so
|
||||||
|
// that `--gtest_list_tests` (which CMake runs at build time to discover the
|
||||||
|
// cases) never touches a GPU.
|
||||||
|
class HeadlessGL {
|
||||||
|
public:
|
||||||
|
static HeadlessGL& Get();
|
||||||
|
|
||||||
|
// False on a machine with no usable GPU/display/ICD. SkipReason() then
|
||||||
|
// says which step failed; every fixture turns that into GTEST_SKIP().
|
||||||
|
bool Usable() const { return m_usable; }
|
||||||
|
const std::string& SkipReason() const { return m_skipReason; }
|
||||||
|
|
||||||
|
// Backend actually in use, as reported by MOBILEGL_BACKEND_TYPE.
|
||||||
|
const std::string& BackendName() const { return m_backendName; }
|
||||||
|
const std::string& RendererString() const { return m_renderer; }
|
||||||
|
|
||||||
|
int Width() const { return m_width; }
|
||||||
|
int Height() const { return m_height; }
|
||||||
|
|
||||||
|
// THE frame boundary. eglSwapBuffers is what retires a frame in the
|
||||||
|
// renderer, and the cross-frame scenarios are meaningless without it.
|
||||||
|
void EndFrame();
|
||||||
|
|
||||||
|
// Frames completed so far, for failure messages.
|
||||||
|
int FrameIndex() const { return m_frameIndex; }
|
||||||
|
|
||||||
|
// Releases the context and surface and terminates the display. Called
|
||||||
|
// once, after the last scenario: MobileGL frees its backend objects
|
||||||
|
// through eglTerminate, and letting a process simply exit on top of a
|
||||||
|
// live context leaves those objects to be torn down from a static
|
||||||
|
// destructor with no driver left underneath.
|
||||||
|
void ShutDown();
|
||||||
|
|
||||||
|
private:
|
||||||
|
HeadlessGL();
|
||||||
|
HeadlessGL(const HeadlessGL&) = delete;
|
||||||
|
HeadlessGL& operator=(const HeadlessGL&) = delete;
|
||||||
|
|
||||||
|
bool BringUp();
|
||||||
|
|
||||||
|
bool m_usable = false;
|
||||||
|
std::string m_skipReason;
|
||||||
|
std::string m_backendName;
|
||||||
|
std::string m_renderer;
|
||||||
|
int m_width = 0;
|
||||||
|
int m_height = 0;
|
||||||
|
int m_frameIndex = 0;
|
||||||
|
void* m_display = nullptr;
|
||||||
|
void* m_surface = nullptr;
|
||||||
|
void* m_context = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- the scenario vocabulary -------------------------------------------
|
||||||
|
// Deliberately tiny. A scenario should read like a story; anything that
|
||||||
|
// needs a comment about GL mechanics belongs here instead.
|
||||||
|
|
||||||
|
// Compiles and links vs+fs, pinning attribute 0 to "aPos" and 1 to "aColor".
|
||||||
|
// Returns 0 and fills outError on failure.
|
||||||
|
unsigned int CompileProgram(const char* vertexSource, const char* fragmentSource, std::string* outError);
|
||||||
|
|
||||||
|
struct ColorFbo {
|
||||||
|
unsigned int fbo = 0;
|
||||||
|
unsigned int texture = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A complete RGBA8 render target. Returns fbo==0 on failure.
|
||||||
|
ColorFbo MakeColorFbo(int width, int height);
|
||||||
|
void DestroyColorFbo(ColorFbo& target);
|
||||||
|
|
||||||
|
// Binds a target and sets the viewport to match. Passing fbo 0 means the
|
||||||
|
// default (presentable) framebuffer.
|
||||||
|
void BindDefaultFramebuffer();
|
||||||
|
void BindFbo(const ColorFbo& target);
|
||||||
|
|
||||||
|
void ClearTo(float r, float g, float b, float a);
|
||||||
|
|
||||||
|
// Reads back the whole currently bound READ framebuffer. width/height must
|
||||||
|
// be the target's full size - DirectVulkan's default-framebuffer readback
|
||||||
|
// only re-orients a full-extent read.
|
||||||
|
Image ReadPixels(int width, int height);
|
||||||
|
|
||||||
|
// Drains any GL error queue and returns the first error, or 0.
|
||||||
|
unsigned int FirstGLError();
|
||||||
|
const char* GLErrorName(unsigned int error);
|
||||||
|
|
||||||
|
// ---- whole-region readback predicates ----------------------------------
|
||||||
|
// The scenarios used to assert on two or three individual pixels, which is
|
||||||
|
// provably too weak: a draw in which 3 of a quad's 4 vertices carry stale
|
||||||
|
// data still paints the sampled centre the expected colour (that exact case
|
||||||
|
// is a standing negative-control test - see CrossFrameBufferScenario). The
|
||||||
|
// readback is already fully in memory, so counting every pixel in a region
|
||||||
|
// costs nothing and turns "the middle looks right" into "all of it is right".
|
||||||
|
|
||||||
|
// Everything a caller needs to say what was wrong and where.
|
||||||
|
struct RegionScan {
|
||||||
|
int total = 0; // pixels examined
|
||||||
|
int offenders = 0; // pixels whose ColorName() != expected
|
||||||
|
int firstX = -1; // first offender in bottom-to-top, left-to-right order
|
||||||
|
int firstY = -1;
|
||||||
|
Rgba8 firstColor{};
|
||||||
|
std::string firstColorName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inclusive pixel bounds, clamped to the image. Row 0 is the bottom row.
|
||||||
|
RegionScan ScanRegion(const Image& image, int x0, int x1, int y0, int y1, const char* expectedColor);
|
||||||
|
|
||||||
|
// gtest predicate wrapper: EXPECT_TRUE(RegionIsMostly(...)) reports the
|
||||||
|
// offender count, the offender fraction and the FIRST offending pixel's
|
||||||
|
// coordinates and colour. `tolerance` is the fraction of the region allowed
|
||||||
|
// to disagree; pass 0.0 to demand every pixel (which is what the scenarios
|
||||||
|
// do - they inset their regions away from primitive edges so exactness is
|
||||||
|
// achievable).
|
||||||
|
::testing::AssertionResult RegionIsMostly(const Image& image, int x0, int x1, int y0, int y1,
|
||||||
|
const char* expectedColor, double tolerance,
|
||||||
|
const std::string& when);
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
//
|
||||||
|
// The base fixture every scenario derives from. Its only jobs are to bring the
|
||||||
|
// headless context up once per process and to decide what "this machine has no
|
||||||
|
// usable GPU" means.
|
||||||
|
//
|
||||||
|
// By default it means a clean GTEST_SKIP() - never a failure, never a hang -
|
||||||
|
// because a developer box or a container without a GPU should not fail a run it
|
||||||
|
// was never able to perform. But a skip is indistinguishable from a pass in
|
||||||
|
// every CI summary, so the `integration-gpu` label on its own is unfalsifiable:
|
||||||
|
// a runner whose driver pinning silently broke reports the same green as one
|
||||||
|
// that rendered every frame. MOBILEGL_ITEST_REQUIRE_GPU is the caller saying
|
||||||
|
// "this machine HAS a GPU and I am relying on these scenarios actually running";
|
||||||
|
// with it set, an unusable harness is a FAILURE carrying the pre-flight's reason.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "HeadlessGL.h"
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
|
||||||
|
class ScenarioTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
m_ready = false;
|
||||||
|
HeadlessGL& gl = HeadlessGL::Get();
|
||||||
|
if (!gl.Usable()) {
|
||||||
|
if (RequireGpu()) {
|
||||||
|
// FAIL() is a FATAL failure but does NOT mark the test skipped,
|
||||||
|
// so a derived SetUp that guards on IsSkipped() alone would run
|
||||||
|
// straight into GL calls with no current context and SIGSEGV -
|
||||||
|
// that exact crash shipped from the first version of this guard.
|
||||||
|
// Derived fixtures must gate on Ready() (below), which is false
|
||||||
|
// on BOTH the skip path and this failure path.
|
||||||
|
FAIL() << "MOBILEGL_ITEST_REQUIRE_GPU is set, so an unusable harness is a failure, not a skip. "
|
||||||
|
<< "Backend " << gl.BackendName() << " could not be brought up: " << gl.SkipReason();
|
||||||
|
}
|
||||||
|
GTEST_SKIP() << "no usable GPU/display/ICD for backend " << gl.BackendName() << ": " << gl.SkipReason();
|
||||||
|
}
|
||||||
|
if (RequireGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) {
|
||||||
|
// "Ran on llvmpipe" must not be able to pass as "ran on the GPU":
|
||||||
|
// a misconfigured vendor pin silently lands on the software
|
||||||
|
// rasterizer, and REQUIRE_GPU exists precisely to make that loud.
|
||||||
|
FAIL() << "MOBILEGL_ITEST_REQUIRE_GPU is set but the context landed on a software rasterizer: "
|
||||||
|
<< gl.RendererString();
|
||||||
|
}
|
||||||
|
// A scenario starts from a clean slate but shares the context (and so
|
||||||
|
// the renderer's memos) with every other scenario in this process -
|
||||||
|
// which is exactly the situation both shipped bugs needed.
|
||||||
|
RecordProperty("backend", gl.BackendName());
|
||||||
|
RecordProperty("renderer", gl.RendererString());
|
||||||
|
m_ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ONLY gate a derived SetUp/TearDown may use: `if (!Ready()) return;`.
|
||||||
|
// True only when the base SetUp brought the context up and neither skipped
|
||||||
|
// nor failed. IsSkipped() alone is WRONG here (see the comment at FAIL()).
|
||||||
|
bool Ready() const { return m_ready; }
|
||||||
|
|
||||||
|
static HeadlessGL& Gl() { return HeadlessGL::Get(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static bool LooksLikeSoftwareRasterizer(const std::string& renderer) {
|
||||||
|
static const char* kNames[] = {"llvmpipe", "lavapipe", "softpipe", "SwiftShader", "swrast"};
|
||||||
|
for (const char* name : kNames) {
|
||||||
|
if (renderer.find(name) != std::string::npos) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool m_ready = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Main.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
|
||||||
|
//
|
||||||
|
// Entry point for the headless GPU integration scenarios.
|
||||||
|
//
|
||||||
|
// The banner lives in a gtest Environment rather than in main() on purpose:
|
||||||
|
// Environment::SetUp does not run for `--gtest_list_tests`, which is what CMake
|
||||||
|
// invokes at build time to discover the cases. Discovery therefore never brings
|
||||||
|
// up EGL, never needs a GPU and cannot hang.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#include "Harness/HeadlessGL.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class HarnessBanner : public ::testing::Environment {
|
||||||
|
public:
|
||||||
|
void SetUp() override {
|
||||||
|
const MGITest::HeadlessGL& gl = MGITest::HeadlessGL::Get();
|
||||||
|
std::fprintf(stderr, "MobileGL integration scenarios: backend=%s\n", gl.BackendName().c_str());
|
||||||
|
if (gl.Usable()) {
|
||||||
|
std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless)\n",
|
||||||
|
gl.RendererString().c_str(), gl.Width(), gl.Height());
|
||||||
|
} else if (MGITest::RequireGpu()) {
|
||||||
|
std::fprintf(stderr,
|
||||||
|
" FAILING every scenario (MOBILEGL_ITEST_REQUIRE_GPU is set): %s\n",
|
||||||
|
gl.SkipReason().c_str());
|
||||||
|
} else {
|
||||||
|
std::fprintf(stderr,
|
||||||
|
" SKIPPING every scenario: %s\n"
|
||||||
|
" (set MOBILEGL_ITEST_REQUIRE_GPU=1 to make this a failure instead - a run that\n"
|
||||||
|
" skipped everything is otherwise indistinguishable from one that passed)\n",
|
||||||
|
gl.SkipReason().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override { MGITest::HeadlessGL::Get().ShutDown(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
::testing::AddGlobalTestEnvironment(new HarnessBanner());
|
||||||
|
return RUN_ALL_TESTS();
|
||||||
|
}
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/AsyncCompileScenario.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 E - asynchronous shader compilation and GL_KHR_parallel_shader_compile
|
||||||
|
// on a REAL driver.
|
||||||
|
//
|
||||||
|
// WHY THIS EXISTS ALONGSIDE THE UNIT SUITES. MG_Test/Program's async suites already
|
||||||
|
// drive the same GL entry points, but they stop at the frontend: nothing there ever
|
||||||
|
// reaches a driver, so nothing there can catch the failure this scenario is built for
|
||||||
|
// - artifacts produced on a worker thread that the BACKEND then rejects, mis-binds or
|
||||||
|
// renders differently from the ones the GL thread produced. The frontend cannot tell
|
||||||
|
// the two apart; a pixel can.
|
||||||
|
//
|
||||||
|
// The five things it pins, in order:
|
||||||
|
//
|
||||||
|
// (a) 64 heavy compiles are enqueued and polled through GL_COMPLETION_STATUS_KHR.
|
||||||
|
// At least one must be observed GL_FALSE - i.e. the query really answers while
|
||||||
|
// work is outstanding rather than silently joining. Skipped, never failed, when
|
||||||
|
// the machine drained the whole batch before the first poll: a fast box must not
|
||||||
|
// be able to turn this into a red.
|
||||||
|
// (b) Forcing the join afterwards produces the right answer for every one of them:
|
||||||
|
// GL_COMPILE_STATUS true, an empty info log, and a program that links.
|
||||||
|
// (c) The extension string matches the configuration. This is the half a recorded
|
||||||
|
// trace can never cover - Iris and Sodium change their submission schedule the
|
||||||
|
// moment they see the string - so it is asserted against a real backend's real
|
||||||
|
// GL_EXTENSIONS, through both glGetString and glGetStringi.
|
||||||
|
// (d) glMaxShaderCompilerThreadsKHR(0) leaves nothing in flight: every subsequent
|
||||||
|
// GL_COMPLETION_STATUS_KHR reads GL_TRUE immediately, and compilation after it
|
||||||
|
// is synchronous. That is what the extension requires of a zero count.
|
||||||
|
// (e) THE ONE THAT NEEDS A GPU: the same frame, drawn with programs compiled and
|
||||||
|
// linked asynchronously and then with programs compiled and linked inline, must
|
||||||
|
// come out byte-identical under glReadPixels. Anything the worker thread got
|
||||||
|
// wrong about the compile environment, the reflection or the SPIR-V shows up
|
||||||
|
// here as a pixel difference and nowhere else.
|
||||||
|
//
|
||||||
|
// Backend selection is the module's usual one process, one backend (MOBILEGL_BACKEND_TYPE),
|
||||||
|
// so this file runs twice per ctest invocation.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Harness/HeadlessGL.h"
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
|
||||||
|
#include "Config.h"
|
||||||
|
#include "MG_Util/Async/ShaderCompilePool.h"
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
// GL_KHR_parallel_shader_compile. Spelled out rather than relying on the host's
|
||||||
|
// glext.h: this module is built against whatever GL headers the machine has, and an
|
||||||
|
// older one has neither token. Both are also GL_*_ARB with identical values.
|
||||||
|
#ifndef GL_MAX_SHADER_COMPILER_THREADS_KHR
|
||||||
|
#define GL_MAX_SHADER_COMPILER_THREADS_KHR 0x91B0
|
||||||
|
#endif
|
||||||
|
#ifndef GL_COMPLETION_STATUS_KHR
|
||||||
|
#define GL_COMPLETION_STATUS_KHR 0x91B1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// The entry point under test, resolved by the linker straight into MobileGL_s like
|
||||||
|
// every other gl* call in this module. Declared here for the same reason as the
|
||||||
|
// tokens above.
|
||||||
|
extern "C" void glMaxShaderCompilerThreadsKHR(GLuint count);
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using MobileGL::MG_Config::QuirkOverride;
|
||||||
|
|
||||||
|
// Same shape as the other scenarios: a two-attribute pass-through, so the only
|
||||||
|
// thing that can differ between the two compilation modes is the compilation.
|
||||||
|
constexpr const char* kVertexSource = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
in vec3 aColor;
|
||||||
|
out vec3 vColor;
|
||||||
|
void main() {
|
||||||
|
vColor = aColor;
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFragmentSource = R"(#version 330 core
|
||||||
|
in vec3 vColor;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() {
|
||||||
|
oColor = vec4(vColor, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Asymmetric in both axes, so a mode difference that also happens to be a
|
||||||
|
// symmetry of the image cannot hide (the same reason OrientationScenario draws
|
||||||
|
// quadrants rather than stripes).
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
float r, g, b;
|
||||||
|
};
|
||||||
|
|
||||||
|
void AppendQuad(std::vector<Vertex>& out, float x0, float x1, float y0, float y1, float r, float g, float b) {
|
||||||
|
const Vertex bl{x0, y0, r, g, b};
|
||||||
|
const Vertex br{x1, y0, r, g, b};
|
||||||
|
const Vertex tr{x1, y1, r, g, b};
|
||||||
|
const Vertex tl{x0, y1, r, g, b};
|
||||||
|
out.insert(out.end(), {bl, br, tr, bl, tr, tl});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Vertex> QuadrantGeometry() {
|
||||||
|
std::vector<Vertex> vertices;
|
||||||
|
vertices.reserve(24);
|
||||||
|
AppendQuad(vertices, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f); // bottom-left: blue
|
||||||
|
AppendQuad(vertices, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f); // bottom-right: green
|
||||||
|
AppendQuad(vertices, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f); // top-left: red
|
||||||
|
AppendQuad(vertices, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f); // top-right: white
|
||||||
|
return vertices;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expensive enough that a compile is not instantaneous, and distinct per index so
|
||||||
|
// the source-hash memo never turns one into a no-op: without both properties the
|
||||||
|
// pool has no backlog and (a) has nothing to observe.
|
||||||
|
std::string BulkyFragmentSource(int index) {
|
||||||
|
std::string source = "#version 330 core\n";
|
||||||
|
source += "in vec3 vColor;\nout vec4 oColor;\n";
|
||||||
|
source += "uniform float uSeed" + std::to_string(index) + ";\n";
|
||||||
|
source += "void main() {\n float acc = uSeed" + std::to_string(index) + ";\n";
|
||||||
|
for (int i = 0; i < 320; ++i) {
|
||||||
|
source += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0) * cos(acc);\n";
|
||||||
|
}
|
||||||
|
source += " oColor = vec4(vColor * acc, 1.0);\n}\n";
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MOBILEGL_ASYNC_SHADER_COMPILE decides the ambient mode; a scenario that wants
|
||||||
|
// the other one says so here and gets the ambient one back on scope exit. Forcing
|
||||||
|
// it in-process is what lets ONE ctest run compare the two modes against each
|
||||||
|
// other - the whole point of (e).
|
||||||
|
class AsyncModeScope {
|
||||||
|
public:
|
||||||
|
explicit AsyncModeScope(bool async) : m_saved(MobileGL::MG_Config::Features.AsyncShaderCompile) {
|
||||||
|
MobileGL::MG_Config::Features.AsyncShaderCompile =
|
||||||
|
async ? QuirkOverride::ForceOn : QuirkOverride::ForceOff;
|
||||||
|
}
|
||||||
|
~AsyncModeScope() { MobileGL::MG_Config::Features.AsyncShaderCompile = m_saved; }
|
||||||
|
AsyncModeScope(const AsyncModeScope&) = delete;
|
||||||
|
AsyncModeScope& operator=(const AsyncModeScope&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const QuirkOverride m_saved;
|
||||||
|
};
|
||||||
|
|
||||||
|
// glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls
|
||||||
|
// it has to put the pool back or it changes how every scenario after it compiles.
|
||||||
|
class CompilerThreadScope {
|
||||||
|
public:
|
||||||
|
CompilerThreadScope() = default;
|
||||||
|
~CompilerThreadScope() {
|
||||||
|
MobileGL::MG_Util::Async::SetAsyncShaderCompileSuspended(false);
|
||||||
|
auto& pool = MobileGL::MG_Util::Async::ShaderCompilePool::Get();
|
||||||
|
pool.SetMaxConcurrency(pool.GetThreadCount());
|
||||||
|
}
|
||||||
|
CompilerThreadScope(const CompilerThreadScope&) = delete;
|
||||||
|
CompilerThreadScope& operator=(const CompilerThreadScope&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
GLint ShaderCompletion(GLuint shader) {
|
||||||
|
GLint status = -1;
|
||||||
|
glGetShaderiv(shader, GL_COMPLETION_STATUS_KHR, &status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint ShaderCompileStatus(GLuint shader) {
|
||||||
|
GLint status = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ShaderInfoLog(GLuint shader) {
|
||||||
|
GLint length = 0;
|
||||||
|
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||||
|
if (length <= 0) return std::string();
|
||||||
|
std::vector<char> buffer(static_cast<std::size_t>(length));
|
||||||
|
GLsizei written = 0;
|
||||||
|
glGetShaderInfoLog(shader, length, &written, buffer.data());
|
||||||
|
return std::string(buffer.data(), static_cast<std::size_t>(written));
|
||||||
|
}
|
||||||
|
|
||||||
|
class AsyncCompileScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
const std::vector<Vertex> vertices = QuadrantGeometry();
|
||||||
|
m_vertexCount = static_cast<int>(vertices.size());
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glGenBuffers(1, &m_vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data(),
|
||||||
|
GL_STATIC_DRAW);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<void*>(0));
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<void*>(8));
|
||||||
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "setup left a GL error behind";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A fresh program every time, compiled and linked in whatever mode is in
|
||||||
|
// force. Reusing one would defeat the comparison: the second mode would just
|
||||||
|
// read the first mode's artifacts back out of the memo.
|
||||||
|
GLuint BuildProgram() {
|
||||||
|
std::string error;
|
||||||
|
const GLuint program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||||
|
EXPECT_NE(program, 0u) << error;
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
Image DrawFrameWith(GLuint program) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glUseProgram(program);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, m_vertexCount);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
Gl().EndFrame();
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueues `count` distinct heavy compiles and returns their names WITHOUT
|
||||||
|
// reading anything back, so the pool is left with a real backlog.
|
||||||
|
std::vector<GLuint> EnqueueBacklog(int count, int seedBase) {
|
||||||
|
std::vector<GLuint> shaders;
|
||||||
|
shaders.reserve(static_cast<std::size_t>(count));
|
||||||
|
m_sources.reserve(m_sources.size() + static_cast<std::size_t>(count));
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
m_sources.push_back(BulkyFragmentSource(seedBase + i));
|
||||||
|
const char* text = m_sources.back().c_str();
|
||||||
|
const GLuint shader = glCreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
glShaderSource(shader, 1, &text, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
shaders.push_back(shader);
|
||||||
|
}
|
||||||
|
return shaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
GLuint m_vbo = 0;
|
||||||
|
int m_vertexCount = 0;
|
||||||
|
// Kept alive for the whole case: glShaderSource copies, but keeping the
|
||||||
|
// strings makes a failure message able to name the source it came from.
|
||||||
|
std::vector<std::string> m_sources;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- (a) + (b) ------------------------------------------------------------
|
||||||
|
// A backlog is enqueued, polled without joining, then forced to settle and
|
||||||
|
// checked for correctness. Both halves in one case on purpose: (b) is only
|
||||||
|
// interesting for shaders that (a) proved were genuinely still outstanding.
|
||||||
|
TEST_F(AsyncCompileScenario, CompletionStatusPollingThenForcedJoin) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const AsyncModeScope async(true);
|
||||||
|
const CompilerThreadScope threads;
|
||||||
|
// One worker, so the queue behind it is what the poll observes.
|
||||||
|
glMaxShaderCompilerThreadsKHR(1);
|
||||||
|
|
||||||
|
const std::vector<GLuint> shaders = EnqueueBacklog(64, 6000);
|
||||||
|
|
||||||
|
int outstanding = 0;
|
||||||
|
for (const GLuint shader : shaders) {
|
||||||
|
const GLint completion = ShaderCompletion(shader);
|
||||||
|
ASSERT_TRUE(completion == GL_TRUE || completion == GL_FALSE)
|
||||||
|
<< "GL_COMPLETION_STATUS_KHR returned " << completion;
|
||||||
|
if (completion == GL_FALSE) ++outstanding;
|
||||||
|
}
|
||||||
|
if (outstanding == 0) {
|
||||||
|
GTEST_SKIP() << "this machine drained 64 heavy compiles before the first poll; "
|
||||||
|
"nothing was outstanding to observe";
|
||||||
|
}
|
||||||
|
|
||||||
|
// (b) Forced join: every one of them is correct, and usable.
|
||||||
|
for (const GLuint shader : shaders) {
|
||||||
|
EXPECT_EQ(ShaderCompileStatus(shader), GL_TRUE) << ShaderInfoLog(shader);
|
||||||
|
EXPECT_TRUE(ShaderInfoLog(shader).empty());
|
||||||
|
EXPECT_EQ(ShaderCompletion(shader), GL_TRUE) << "GL_COMPILE_STATUS must have joined";
|
||||||
|
}
|
||||||
|
|
||||||
|
// And a link over one of them really produces a usable program on this driver.
|
||||||
|
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||||
|
glShaderSource(vs, 1, &kVertexSource, nullptr);
|
||||||
|
glCompileShader(vs);
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, vs);
|
||||||
|
glAttachShader(program, shaders.front());
|
||||||
|
glBindAttribLocation(program, 0, "aPos");
|
||||||
|
glBindAttribLocation(program, 1, "aColor");
|
||||||
|
glLinkProgram(program);
|
||||||
|
GLint linked = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||||
|
EXPECT_EQ(linked, GL_TRUE);
|
||||||
|
EXPECT_GE(glGetUniformLocation(program, "uSeed6000"), 0);
|
||||||
|
|
||||||
|
glDeleteProgram(program);
|
||||||
|
glDeleteShader(vs);
|
||||||
|
for (const GLuint shader : shaders) glDeleteShader(shader);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- (c) ------------------------------------------------------------------
|
||||||
|
// The extension string, read from a real backend that really brought a driver
|
||||||
|
// up. No mode forcing here: a backend builds its advertised list once, from the
|
||||||
|
// configuration in force at its first use, so the meaningful assertion is
|
||||||
|
// against the AMBIENT configuration - which is exactly what makes this case
|
||||||
|
// worth running in both of the suite's flag states.
|
||||||
|
TEST_F(AsyncCompileScenario, ExtensionStringMatchesTheConfiguration) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const bool expected = MobileGL::MG_Util::Async::AsyncShaderCompileEnabled();
|
||||||
|
|
||||||
|
const char* extensions = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
|
||||||
|
ASSERT_NE(extensions, nullptr);
|
||||||
|
const std::string extensionString(extensions);
|
||||||
|
const bool inString = extensionString.find("GL_KHR_parallel_shader_compile") != std::string::npos;
|
||||||
|
EXPECT_EQ(inString, expected)
|
||||||
|
<< "backend " << Gl().BackendName() << " GL_EXTENSIONS = " << extensionString;
|
||||||
|
|
||||||
|
// LWJGL builds GLCapabilities from the INDEXED form on a core profile, so the
|
||||||
|
// two spellings disagreeing would be invisible to the check above and fatal
|
||||||
|
// to a real application.
|
||||||
|
GLint count = 0;
|
||||||
|
glGetIntegerv(GL_NUM_EXTENSIONS, &count);
|
||||||
|
ASSERT_GT(count, 0);
|
||||||
|
bool inIndexed = false;
|
||||||
|
for (GLint i = 0; i < count; ++i) {
|
||||||
|
const char* name = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, GLuint(i)));
|
||||||
|
if (name != nullptr && std::string(name) == "GL_KHR_parallel_shader_compile") inIndexed = true;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(inIndexed, expected);
|
||||||
|
|
||||||
|
// The companion query, which an application reads right after the string.
|
||||||
|
GLint maxThreads = -1;
|
||||||
|
glGetIntegerv(GL_MAX_SHADER_COMPILER_THREADS_KHR, &maxThreads);
|
||||||
|
if (expected) {
|
||||||
|
EXPECT_GE(maxThreads, 1);
|
||||||
|
} else {
|
||||||
|
EXPECT_EQ(maxThreads, 0);
|
||||||
|
}
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- (d) ------------------------------------------------------------------
|
||||||
|
// A zero count must leave nothing in flight and keep it that way.
|
||||||
|
TEST_F(AsyncCompileScenario, ZeroCompilerThreadsSettlesEverythingImmediately) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const AsyncModeScope async(true);
|
||||||
|
const CompilerThreadScope threads;
|
||||||
|
glMaxShaderCompilerThreadsKHR(1);
|
||||||
|
|
||||||
|
const std::vector<GLuint> backlog = EnqueueBacklog(48, 6200);
|
||||||
|
glMaxShaderCompilerThreadsKHR(0);
|
||||||
|
|
||||||
|
for (const GLuint shader : backlog) {
|
||||||
|
EXPECT_EQ(ShaderCompletion(shader), GL_TRUE)
|
||||||
|
<< "glMaxShaderCompilerThreadsKHR(0) must join everything still in flight";
|
||||||
|
EXPECT_EQ(ShaderCompileStatus(shader), GL_TRUE) << ShaderInfoLog(shader);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compilation after the zero count is synchronous too.
|
||||||
|
const std::vector<GLuint> serial = EnqueueBacklog(6, 6300);
|
||||||
|
for (const GLuint shader : serial) {
|
||||||
|
EXPECT_EQ(ShaderCompletion(shader), GL_TRUE) << "a compile after a zero count must be synchronous";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const GLuint shader : backlog) glDeleteShader(shader);
|
||||||
|
for (const GLuint shader : serial) glDeleteShader(shader);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- (e) ------------------------------------------------------------------
|
||||||
|
// The one that needs the GPU. Two programs, identical source, one built with
|
||||||
|
// compilation and linking on worker threads and one built inline; the frames
|
||||||
|
// they draw must be byte-identical.
|
||||||
|
//
|
||||||
|
// Compared through the DEFAULT framebuffer deliberately: that is where the
|
||||||
|
// backend's orientation and present path live, so the comparison covers the
|
||||||
|
// whole pipeline rather than the reflection tables alone.
|
||||||
|
TEST_F(AsyncCompileScenario, AsyncAndSyncProgramsRenderIdenticalFrames) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
Image asyncImage;
|
||||||
|
{
|
||||||
|
const AsyncModeScope async(true);
|
||||||
|
const GLuint program = BuildProgram();
|
||||||
|
ASSERT_NE(program, 0u);
|
||||||
|
asyncImage = DrawFrameWith(program);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
Image syncImage;
|
||||||
|
{
|
||||||
|
const AsyncModeScope async(false);
|
||||||
|
const GLuint program = BuildProgram();
|
||||||
|
ASSERT_NE(program, 0u);
|
||||||
|
syncImage = DrawFrameWith(program);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_FALSE(asyncImage.Empty());
|
||||||
|
ASSERT_FALSE(syncImage.Empty());
|
||||||
|
// The frame is the expected one in the first place - two identically WRONG
|
||||||
|
// frames would otherwise pass.
|
||||||
|
EXPECT_EQ(asyncImage.QuadrantSignature(), "blue,green,red,white")
|
||||||
|
<< "the asynchronously compiled program did not draw the expected frame";
|
||||||
|
EXPECT_EQ(asyncImage, syncImage)
|
||||||
|
<< "asynchronous and synchronous compilation rendered different frames ("
|
||||||
|
<< asyncImage.ByteDiffCount(syncImage) << " bytes differ); backend " << Gl().BackendName();
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same comparison over a batch, which is the shape a shaderpack load has:
|
||||||
|
// many programs enqueued before any of them is read back, then each one drawn.
|
||||||
|
// A per-worker state leak (glslang's thread-local pools are the obvious
|
||||||
|
// candidate) shows up here and not in the single-program case above.
|
||||||
|
TEST_F(AsyncCompileScenario, ABatchOfAsyncProgramsAllRenderCorrectly) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
constexpr int kPrograms = 12;
|
||||||
|
|
||||||
|
std::vector<GLuint> programs;
|
||||||
|
{
|
||||||
|
const AsyncModeScope async(true);
|
||||||
|
const CompilerThreadScope threads;
|
||||||
|
glMaxShaderCompilerThreadsKHR(1);
|
||||||
|
// Everything enqueued before anything is read: the only shape in which
|
||||||
|
// more than one job is in flight at a time.
|
||||||
|
for (int i = 0; i < kPrograms; ++i) {
|
||||||
|
programs.push_back(BuildProgram());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < kPrograms; ++i) {
|
||||||
|
ASSERT_NE(programs[static_cast<std::size_t>(i)], 0u) << "program " << i;
|
||||||
|
const Image image = DrawFrameWith(programs[static_cast<std::size_t>(i)]);
|
||||||
|
EXPECT_EQ(image.QuadrantSignature(), "blue,green,red,white") << "program " << i;
|
||||||
|
}
|
||||||
|
for (const GLuint program : programs) glDeleteProgram(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,761 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CrossFrameBufferScenario.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 B - "the draw rendered last frame's buffer".
|
||||||
|
//
|
||||||
|
// The shipped bug (DirectVulkan, TryBindResolvedVertexBindings and the EBO
|
||||||
|
// memo in UploadAndBindIndexBuffer): both memos revalidated themselves ACROSS a
|
||||||
|
// frame boundary by comparing recorded per-buffer slice epochs, and on a match
|
||||||
|
// skipped the per-frame buffer acquire. The acquire is the frame's content-sync
|
||||||
|
// point; skipping it trusted the BumpSliceEpoch call-site inventory to cover
|
||||||
|
// every way a buffer's GPU copy can go stale, and at least one path escaped it.
|
||||||
|
// Result: a draw in a later frame renders from a STALE buffer slice - random
|
||||||
|
// triangles in Minecraft/Sodium on Adreno, corrupted journeymap and
|
||||||
|
// common-mods retraces.
|
||||||
|
//
|
||||||
|
// What pins it: mutate a buffer AFTER a frame boundary and BEFORE the next
|
||||||
|
// draw, then prove the pixels show the NEW content. Every mutation API gets its
|
||||||
|
// own test case, so a failure names the culprit rather than saying "buffers".
|
||||||
|
// The index buffer is covered too: the EBO memo had exactly the same hole.
|
||||||
|
//
|
||||||
|
// The scene is deliberately trivial and entirely buffer-driven:
|
||||||
|
//
|
||||||
|
// vertices 0..3 left half of the viewport, RED
|
||||||
|
// vertices 4..7 right half of the viewport, GREEN
|
||||||
|
// indices A {0,1,2, 0,2,3} -> the left, red quad
|
||||||
|
// indices B {4,5,6, 4,6,7} -> the right, green quad
|
||||||
|
//
|
||||||
|
// A vertex-buffer test rewrites the left quad's colour red -> green and expects
|
||||||
|
// the left half to turn green. An index-buffer test rewrites the indices
|
||||||
|
// A -> B and expects the picture to jump from a red left half to a green right
|
||||||
|
// half. Either way "stale" and "fresh" are different colours in different
|
||||||
|
// places; no thresholds, no interpretation.
|
||||||
|
//
|
||||||
|
// Two families of scenario live here, and they catch different halves of the
|
||||||
|
// same rule:
|
||||||
|
//
|
||||||
|
// CrossFrameBufferScenario - one case per buffer-mutation API. Every one of
|
||||||
|
// these APIs is supposed to retire the memo; today they all do (each notify
|
||||||
|
// path bumps the slice epoch), so these pass on the buggy revision too.
|
||||||
|
// They are the standing statement of the contract: whatever a future memo
|
||||||
|
// keys on, a write through ANY of these APIs must reach the next frame's
|
||||||
|
// draw. They are also where a coherent persistent write - the one shape
|
||||||
|
// that changes a buffer with no GL call at all - is pinned.
|
||||||
|
//
|
||||||
|
// StreamedArenaScenario - the case that actually caught the shipped bug. It
|
||||||
|
// attacks the other half of the rule: a buffer nobody wrote at all, whose
|
||||||
|
// GPU-side bytes moved out from under the memo anyway.
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <functional>
|
||||||
|
#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* kVertexSource = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
in vec3 aColor;
|
||||||
|
out vec3 vColor;
|
||||||
|
void main() {
|
||||||
|
vColor = aColor;
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFragmentSource = R"(#version 330 core
|
||||||
|
in vec3 vColor;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() {
|
||||||
|
oColor = vec4(vColor, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
float r, g, b;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr int kLeftQuadFirstVertex = 0;
|
||||||
|
constexpr int kLeftQuadVertexCount = 4;
|
||||||
|
constexpr int kIndexCount = 6;
|
||||||
|
|
||||||
|
// Enough consecutive frames drawing the same VAO that any per-(VAO, frame)
|
||||||
|
// memo is fully armed before the mutation lands.
|
||||||
|
constexpr int kWarmupFrames = 3;
|
||||||
|
|
||||||
|
std::vector<Vertex> SceneVertices(bool leftQuadIsGreen) {
|
||||||
|
const float lr = leftQuadIsGreen ? 0.0f : 1.0f;
|
||||||
|
const float lg = leftQuadIsGreen ? 1.0f : 0.0f;
|
||||||
|
return {
|
||||||
|
// 0..3: left half
|
||||||
|
{-1.0f, -1.0f, lr, lg, 0.0f},
|
||||||
|
{0.0f, -1.0f, lr, lg, 0.0f},
|
||||||
|
{0.0f, 1.0f, lr, lg, 0.0f},
|
||||||
|
{-1.0f, 1.0f, lr, lg, 0.0f},
|
||||||
|
// 4..7: right half
|
||||||
|
{0.0f, -1.0f, 0.0f, 1.0f, 0.0f},
|
||||||
|
{1.0f, -1.0f, 0.0f, 1.0f, 0.0f},
|
||||||
|
{1.0f, 1.0f, 0.0f, 1.0f, 0.0f},
|
||||||
|
{0.0f, 1.0f, 0.0f, 1.0f, 0.0f},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint kIndicesLeftQuad[kIndexCount] = {0, 1, 2, 0, 2, 3};
|
||||||
|
const GLuint kIndicesRightQuad[kIndexCount] = {4, 5, 6, 4, 6, 7};
|
||||||
|
|
||||||
|
// How far inside each half the whole-region checks start. The two quads
|
||||||
|
// meet on a pixel boundary, so a couple of pixels of margin makes "every
|
||||||
|
// single pixel in the region" an achievable demand.
|
||||||
|
constexpr int kHalfInset = 2;
|
||||||
|
|
||||||
|
// Asserts the left and right halves of the viewport, with a message that
|
||||||
|
// says what the app had asked GL to draw by then.
|
||||||
|
//
|
||||||
|
// This counts EVERY pixel in each half rather than sampling its centre.
|
||||||
|
// Sampling two pixels was demonstrably too weak: a draw in which three of
|
||||||
|
// the left quad's four vertices still carry stale data paints a centre
|
||||||
|
// pixel of exactly the expected colour and passed the old assertion. That
|
||||||
|
// case is now a standing negative control - see
|
||||||
|
// PartialStalenessIsCaughtByWholeRegionChecks below, which constructs it
|
||||||
|
// deliberately and proves the region scan reports it.
|
||||||
|
void ExpectHalves(const Image& image, const char* expectedLeft, const char* expectedRight,
|
||||||
|
const std::string& when) {
|
||||||
|
const int w = image.Width();
|
||||||
|
const int h = image.Height();
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, kHalfInset, w / 2 - kHalfInset, kHalfInset, h - kHalfInset, expectedLeft,
|
||||||
|
0.0, when + " [left half]"));
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, w / 2 + kHalfInset, w - kHalfInset, kHalfInset, h - kHalfInset,
|
||||||
|
expectedRight, 0.0, when + " [right half]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// How the app hands the new bytes to GL. Each is its own test case.
|
||||||
|
enum class Mutation {
|
||||||
|
SubData, // glBufferSubData
|
||||||
|
MapWriteUnmap, // glMapBufferRange(WRITE) + glUnmapBuffer
|
||||||
|
PersistentFlush, // write through a persistent map + glFlushMappedBufferRange
|
||||||
|
PersistentCoherent, // write through a COHERENT persistent map, no GL call at all
|
||||||
|
OrphanReupload, // glBufferData(NULL) then a full re-upload
|
||||||
|
CopySubData, // glCopyBufferSubData from a staging buffer
|
||||||
|
};
|
||||||
|
|
||||||
|
bool NeedsImmutableStorage(Mutation mutation) {
|
||||||
|
return mutation == Mutation::PersistentFlush || mutation == Mutation::PersistentCoherent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The coherent variant is the one shape in which an application changes a
|
||||||
|
// buffer's contents with NO GL call whatsoever - the write lands in the
|
||||||
|
// mapping and that is the end of it. Sodium's chunk streaming is written
|
||||||
|
// this way, and it is the case a per-buffer "has anything changed?" epoch
|
||||||
|
// cannot see on its own.
|
||||||
|
bool NeedsCoherentMapping(Mutation mutation) {
|
||||||
|
return mutation == Mutation::PersistentCoherent;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CrossFrameBufferScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "program setup left a GL error behind";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
ReleaseBuffers();
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds the VAO/VBO/EBO. `immutable` switches to glBufferStorage plus a
|
||||||
|
// persistent mapping of both buffers, which is the only shape in which the
|
||||||
|
// persistent-write mutation is legal.
|
||||||
|
void BuildScene(bool immutable, bool coherent = false) {
|
||||||
|
const std::vector<Vertex> vertices = SceneVertices(/*leftQuadIsGreen=*/false);
|
||||||
|
m_vertexBytes = GLsizeiptr(vertices.size() * sizeof(Vertex));
|
||||||
|
m_indexBytes = GLsizeiptr(sizeof(kIndicesLeftQuad));
|
||||||
|
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
|
||||||
|
glGenBuffers(1, &m_vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
glGenBuffers(1, &m_ebo);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||||
|
|
||||||
|
if (immutable) {
|
||||||
|
const GLbitfield storageFlags = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_DYNAMIC_STORAGE_BIT |
|
||||||
|
(coherent ? GL_MAP_COHERENT_BIT : 0);
|
||||||
|
glBufferStorage(GL_ARRAY_BUFFER, m_vertexBytes, vertices.data(), storageFlags);
|
||||||
|
glBufferStorage(GL_ELEMENT_ARRAY_BUFFER, m_indexBytes, kIndicesLeftQuad, storageFlags);
|
||||||
|
const GLenum storageError = FirstGLError();
|
||||||
|
if (storageError != GL_NO_ERROR) {
|
||||||
|
m_storageUnsupported = true;
|
||||||
|
m_storageError = storageError;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const GLbitfield mapFlags = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT |
|
||||||
|
(coherent ? GL_MAP_COHERENT_BIT : GL_MAP_FLUSH_EXPLICIT_BIT);
|
||||||
|
m_vertexMap =
|
||||||
|
static_cast<unsigned char*>(glMapBufferRange(GL_ARRAY_BUFFER, 0, m_vertexBytes, mapFlags));
|
||||||
|
m_indexMap = static_cast<unsigned char*>(
|
||||||
|
glMapBufferRange(GL_ELEMENT_ARRAY_BUFFER, 0, m_indexBytes, mapFlags));
|
||||||
|
if (m_vertexMap == nullptr || m_indexMap == nullptr) {
|
||||||
|
m_storageUnsupported = true;
|
||||||
|
m_storageError = FirstGLError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, m_vertexBytes, vertices.data(), GL_STATIC_DRAW);
|
||||||
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indexBytes, kIndicesLeftQuad, GL_STATIC_DRAW);
|
||||||
|
}
|
||||||
|
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<void*>(0));
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<void*>(8));
|
||||||
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
glGenBuffers(1, &m_staging);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReleaseBuffers() {
|
||||||
|
if (m_vertexMap != nullptr || m_indexMap != nullptr) {
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
if (m_vertexMap != nullptr) {
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
glUnmapBuffer(GL_ARRAY_BUFFER);
|
||||||
|
}
|
||||||
|
if (m_indexMap != nullptr) {
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||||
|
glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
|
||||||
|
}
|
||||||
|
glBindVertexArray(0);
|
||||||
|
m_vertexMap = nullptr;
|
||||||
|
m_indexMap = nullptr;
|
||||||
|
}
|
||||||
|
if (m_staging != 0) glDeleteBuffers(1, &m_staging);
|
||||||
|
if (m_ebo != 0) glDeleteBuffers(1, &m_ebo);
|
||||||
|
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
m_staging = m_ebo = m_vbo = m_vao = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawScene() {
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawElements(GL_TRIANGLES, kIndexCount, GL_UNSIGNED_INT, nullptr);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BeginFrame() {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
Image ReadFrame() { return ReadPixels(Gl().Width(), Gl().Height()); }
|
||||||
|
|
||||||
|
// ---- the mutations ---------------------------------------------
|
||||||
|
// Each writes `newBytes` over the first `rangeBytes` of `buffer`;
|
||||||
|
// `wholeBytes`/`wholeSize` are the full contents an orphan+re-upload
|
||||||
|
// needs. `target` is the binding point the buffer normally lives at.
|
||||||
|
void ApplyMutation(Mutation mutation, GLenum target, GLuint buffer, unsigned char* persistentMap,
|
||||||
|
const void* newBytes, GLsizeiptr rangeBytes, const void* wholeBytes,
|
||||||
|
GLsizeiptr wholeSize) {
|
||||||
|
// The element-array binding is VAO state, so mutating the EBO happens
|
||||||
|
// with the scene's VAO bound - exactly as an application would.
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
switch (mutation) {
|
||||||
|
case Mutation::SubData: {
|
||||||
|
glBindBuffer(target, buffer);
|
||||||
|
glBufferSubData(target, 0, rangeBytes, newBytes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Mutation::MapWriteUnmap: {
|
||||||
|
glBindBuffer(target, buffer);
|
||||||
|
void* mapped =
|
||||||
|
glMapBufferRange(target, 0, rangeBytes, GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT);
|
||||||
|
ASSERT_NE(mapped, nullptr) << "glMapBufferRange(WRITE) returned null";
|
||||||
|
std::memcpy(mapped, newBytes, std::size_t(rangeBytes));
|
||||||
|
ASSERT_EQ(glUnmapBuffer(target), GLboolean(GL_TRUE)) << "glUnmapBuffer reported data loss";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Mutation::PersistentFlush: {
|
||||||
|
ASSERT_NE(persistentMap, nullptr) << "no persistent mapping for this buffer";
|
||||||
|
std::memcpy(persistentMap, newBytes, std::size_t(rangeBytes));
|
||||||
|
glBindBuffer(target, buffer);
|
||||||
|
glFlushMappedBufferRange(target, 0, rangeBytes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Mutation::PersistentCoherent: {
|
||||||
|
// Deliberately no GL call: a coherent persistent mapping is a
|
||||||
|
// promise that the write alone is enough.
|
||||||
|
ASSERT_NE(persistentMap, nullptr) << "no persistent mapping for this buffer";
|
||||||
|
std::memcpy(persistentMap, newBytes, std::size_t(rangeBytes));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Mutation::OrphanReupload: {
|
||||||
|
glBindBuffer(target, buffer);
|
||||||
|
glBufferData(target, wholeSize, nullptr, GL_STATIC_DRAW);
|
||||||
|
glBufferSubData(target, 0, wholeSize, wholeBytes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Mutation::CopySubData: {
|
||||||
|
glBindBuffer(GL_COPY_READ_BUFFER, m_staging);
|
||||||
|
glBufferData(GL_COPY_READ_BUFFER, rangeBytes, newBytes, GL_STATIC_DRAW);
|
||||||
|
glBindBuffer(GL_COPY_WRITE_BUFFER, buffer);
|
||||||
|
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, rangeBytes);
|
||||||
|
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
|
||||||
|
glBindBuffer(GL_COPY_READ_BUFFER, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glBindVertexArray(0);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the mutation itself raised a GL error";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the story -------------------------------------------------
|
||||||
|
// Steady state for a few frames, one frame boundary, then the
|
||||||
|
// mutation, then the draw that must show the new content.
|
||||||
|
void RunAcrossFrameBoundary(Mutation mutation, const std::function<void()>& mutate,
|
||||||
|
const char* expectedLeftAfter, const char* expectedRightAfter) {
|
||||||
|
ASSERT_NO_FATAL_FAILURE(BuildScene(NeedsImmutableStorage(mutation), NeedsCoherentMapping(mutation)));
|
||||||
|
if (m_storageUnsupported) {
|
||||||
|
GTEST_SKIP() << "immutable/persistent buffer storage is unavailable on this stack ("
|
||||||
|
<< GLErrorName(m_storageError) << "); the persistent-map mutation cannot "
|
||||||
|
<< "be expressed here";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int frame = 0; frame < kWarmupFrames; ++frame) {
|
||||||
|
BeginFrame();
|
||||||
|
DrawScene();
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
BeginFrame();
|
||||||
|
DrawScene();
|
||||||
|
const Image before = ReadFrame();
|
||||||
|
ExpectHalves(before, "red", "black", "steady state before the mutation");
|
||||||
|
ASSERT_FALSE(::testing::Test::HasFailure())
|
||||||
|
<< "the scenario never reached its steady state, so nothing after this means anything";
|
||||||
|
|
||||||
|
// >>> a genuine frame boundary. Everything below happens in the NEXT
|
||||||
|
// frame, which is the whole point: a mutation inside one frame proves
|
||||||
|
// nothing about a memo that revalidates itself across frames.
|
||||||
|
Gl().EndFrame();
|
||||||
|
|
||||||
|
BeginFrame();
|
||||||
|
ASSERT_NO_FATAL_FAILURE(mutate());
|
||||||
|
DrawScene();
|
||||||
|
const Image after = ReadFrame();
|
||||||
|
Gl().EndFrame();
|
||||||
|
|
||||||
|
ExpectHalves(after, expectedLeftAfter, expectedRightAfter,
|
||||||
|
"the draw after the mutation drew STALE buffer content");
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The two things a scenario mutates.
|
||||||
|
void MutateVertexColorsToGreen(Mutation mutation) {
|
||||||
|
const std::vector<Vertex> updated = SceneVertices(/*leftQuadIsGreen=*/true);
|
||||||
|
const GLsizeiptr leftQuadBytes = GLsizeiptr(kLeftQuadVertexCount * sizeof(Vertex));
|
||||||
|
ApplyMutation(mutation, GL_ARRAY_BUFFER, m_vbo, m_vertexMap, updated.data() + kLeftQuadFirstVertex,
|
||||||
|
leftQuadBytes, updated.data(), m_vertexBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MutateIndicesToRightQuad(Mutation mutation) {
|
||||||
|
ApplyMutation(mutation, GL_ELEMENT_ARRAY_BUFFER, m_ebo, m_indexMap, kIndicesRightQuad, m_indexBytes,
|
||||||
|
kIndicesRightQuad, m_indexBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int m_program = 0;
|
||||||
|
unsigned int m_vao = 0;
|
||||||
|
unsigned int m_vbo = 0;
|
||||||
|
unsigned int m_ebo = 0;
|
||||||
|
unsigned int m_staging = 0;
|
||||||
|
GLsizeiptr m_vertexBytes = 0;
|
||||||
|
GLsizeiptr m_indexBytes = 0;
|
||||||
|
unsigned char* m_vertexMap = nullptr;
|
||||||
|
unsigned char* m_indexMap = nullptr;
|
||||||
|
bool m_storageUnsupported = false;
|
||||||
|
unsigned int m_storageError = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- vertex buffer: the left quad must turn green ------------------
|
||||||
|
|
||||||
|
TEST_F(CrossFrameBufferScenario, VertexBufferSubData) {
|
||||||
|
RunAcrossFrameBoundary(
|
||||||
|
Mutation::SubData, [&] { MutateVertexColorsToGreen(Mutation::SubData); }, "green", "black");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CrossFrameBufferScenario, VertexMapWriteUnmap) {
|
||||||
|
RunAcrossFrameBoundary(
|
||||||
|
Mutation::MapWriteUnmap, [&] { MutateVertexColorsToGreen(Mutation::MapWriteUnmap); }, "green", "black");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CrossFrameBufferScenario, VertexPersistentMapFlush) {
|
||||||
|
RunAcrossFrameBoundary(
|
||||||
|
Mutation::PersistentFlush, [&] { MutateVertexColorsToGreen(Mutation::PersistentFlush); }, "green",
|
||||||
|
"black");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CrossFrameBufferScenario, VertexPersistentCoherentWrite) {
|
||||||
|
RunAcrossFrameBoundary(
|
||||||
|
Mutation::PersistentCoherent, [&] { MutateVertexColorsToGreen(Mutation::PersistentCoherent); }, "green",
|
||||||
|
"black");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CrossFrameBufferScenario, VertexOrphanAndReupload) {
|
||||||
|
RunAcrossFrameBoundary(
|
||||||
|
Mutation::OrphanReupload, [&] { MutateVertexColorsToGreen(Mutation::OrphanReupload); }, "green",
|
||||||
|
"black");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CrossFrameBufferScenario, VertexCopyBufferSubData) {
|
||||||
|
RunAcrossFrameBoundary(
|
||||||
|
Mutation::CopySubData, [&] { MutateVertexColorsToGreen(Mutation::CopySubData); }, "green", "black");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- index buffer: the picture must jump to the right, green quad --
|
||||||
|
// The EBO memo had the same cross-frame hole as the vertex one, and no
|
||||||
|
// vertex-only test can see it.
|
||||||
|
|
||||||
|
TEST_F(CrossFrameBufferScenario, IndexBufferSubData) {
|
||||||
|
RunAcrossFrameBoundary(
|
||||||
|
Mutation::SubData, [&] { MutateIndicesToRightQuad(Mutation::SubData); }, "black", "green");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CrossFrameBufferScenario, IndexMapWriteUnmap) {
|
||||||
|
RunAcrossFrameBoundary(
|
||||||
|
Mutation::MapWriteUnmap, [&] { MutateIndicesToRightQuad(Mutation::MapWriteUnmap); }, "black", "green");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CrossFrameBufferScenario, IndexPersistentMapFlush) {
|
||||||
|
RunAcrossFrameBoundary(
|
||||||
|
Mutation::PersistentFlush, [&] { MutateIndicesToRightQuad(Mutation::PersistentFlush); }, "black",
|
||||||
|
"green");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kept, with its coverage stated exactly, because it is the one case in
|
||||||
|
// this file that is served a stale slice by the buggy revision and passes
|
||||||
|
// anyway - and a test that reads as coverage without being coverage is
|
||||||
|
// worse than no test.
|
||||||
|
//
|
||||||
|
// COVERS: the coherent-persistent index contract - a write into a coherent
|
||||||
|
// persistent mapping, with no GL call at all, must reach the next frame's
|
||||||
|
// draw. That is a real contract and this is the only case that states it
|
||||||
|
// for indices.
|
||||||
|
//
|
||||||
|
// DOES NOT COVER: the EBO cross-frame memo. Instrumented against the
|
||||||
|
// re-enabled buggy path, it enters the cross-frame branch 4 times and is
|
||||||
|
// served its recorded slice all 4 times - and still passes, because the
|
||||||
|
// backend adopted the persistent map into that very storage
|
||||||
|
// (AcquirePersistentMap succeeded), so the application's writes landed in
|
||||||
|
// the bytes the "stale" slice names. It would only discriminate on a stack
|
||||||
|
// where that adoption is declined and the CPU shadow stays authoritative;
|
||||||
|
// measured over this whole module, 50 of 50 coherent persistent write maps
|
||||||
|
// were adopted. See ResidentIndexScenario.cpp for the full account.
|
||||||
|
TEST_F(CrossFrameBufferScenario, IndexPersistentCoherentWrite) {
|
||||||
|
RunAcrossFrameBoundary(
|
||||||
|
Mutation::PersistentCoherent, [&] { MutateIndicesToRightQuad(Mutation::PersistentCoherent); }, "black",
|
||||||
|
"green");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CrossFrameBufferScenario, IndexOrphanAndReupload) {
|
||||||
|
RunAcrossFrameBoundary(
|
||||||
|
Mutation::OrphanReupload, [&] { MutateIndicesToRightQuad(Mutation::OrphanReupload); }, "black",
|
||||||
|
"green");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CrossFrameBufferScenario, IndexCopyBufferSubData) {
|
||||||
|
RunAcrossFrameBoundary(
|
||||||
|
Mutation::CopySubData, [&] { MutateIndicesToRightQuad(Mutation::CopySubData); }, "black", "green");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- a self-test of the assertions, not of MobileGL ------------------
|
||||||
|
//
|
||||||
|
// Every case above leans on ExpectHalves. ExpectHalves used to sample the
|
||||||
|
// centre pixel of each half - two pixels for a 12288-pixel readback - and
|
||||||
|
// that is measurably too weak to stand behind a claim about buffer
|
||||||
|
// freshness: a quad whose four vertices are only PARTLY updated still
|
||||||
|
// paints a sampled centre the expected colour, because the centre is a
|
||||||
|
// barycentric blend dominated by the vertices that DID update.
|
||||||
|
//
|
||||||
|
// So construct that case on purpose. Update the left quad's colour to
|
||||||
|
// green in the buffer but leave exactly one of its four vertices holding
|
||||||
|
// the old red, once for each vertex, and check two things:
|
||||||
|
//
|
||||||
|
// - the whole-region scan reports every one of the four (the tightening
|
||||||
|
// is real, and this test fails the moment someone loosens it back to
|
||||||
|
// sampling);
|
||||||
|
// - at least one of the four is invisible to a single centre sample
|
||||||
|
// (the blind spot was real, and this records which vertices it hid).
|
||||||
|
//
|
||||||
|
// Nothing here calls a memo path; it is the assertion itself under test.
|
||||||
|
TEST_F(CrossFrameBufferScenario, PartialStalenessIsCaughtByWholeRegionChecks) {
|
||||||
|
ASSERT_NO_FATAL_FAILURE(BuildScene(/*immutable=*/false));
|
||||||
|
|
||||||
|
const std::vector<Vertex> allGreen = SceneVertices(/*leftQuadIsGreen=*/true);
|
||||||
|
const std::vector<Vertex> allRed = SceneVertices(/*leftQuadIsGreen=*/false);
|
||||||
|
const GLsizeiptr leftQuadBytes = GLsizeiptr(kLeftQuadVertexCount * sizeof(Vertex));
|
||||||
|
|
||||||
|
int centreSampleMissed = 0;
|
||||||
|
std::string missedVertices;
|
||||||
|
for (int staleVertex = 0; staleVertex < kLeftQuadVertexCount; ++staleVertex) {
|
||||||
|
// Every left-quad vertex turns green except this one.
|
||||||
|
std::vector<Vertex> partial(allGreen.begin(), allGreen.begin() + kLeftQuadVertexCount);
|
||||||
|
partial[std::size_t(staleVertex)] = allRed[std::size_t(staleVertex)];
|
||||||
|
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, 0, leftQuadBytes, partial.data());
|
||||||
|
glBindVertexArray(0);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the partial update itself raised a GL error";
|
||||||
|
|
||||||
|
BeginFrame();
|
||||||
|
DrawScene();
|
||||||
|
const Image image = ReadFrame();
|
||||||
|
Gl().EndFrame();
|
||||||
|
|
||||||
|
const int w = image.Width();
|
||||||
|
const int h = image.Height();
|
||||||
|
const RegionScan scan =
|
||||||
|
ScanRegion(image, kHalfInset, w / 2 - kHalfInset, kHalfInset, h - kHalfInset, "green");
|
||||||
|
EXPECT_GT(scan.offenders, 0)
|
||||||
|
<< "vertex " << staleVertex << " of the left quad kept its stale red colour and the "
|
||||||
|
<< "whole-region scan saw nothing wrong across " << scan.total << " pixels - the assertion "
|
||||||
|
<< "is not tight enough to stand behind any freshness claim in this file";
|
||||||
|
|
||||||
|
// What the old two-pixel form of ExpectHalves would have concluded.
|
||||||
|
if (std::strcmp(image.ColorName(w / 4, h / 2), "green") == 0) {
|
||||||
|
++centreSampleMissed;
|
||||||
|
if (!missedVertices.empty()) missedVertices += ",";
|
||||||
|
missedVertices += std::to_string(staleVertex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_GT(centreSampleMissed, 0)
|
||||||
|
<< "no single-vertex staleness was invisible to a centre sample, so this negative control "
|
||||||
|
<< "is no longer demonstrating anything - re-derive it before trusting it";
|
||||||
|
if (centreSampleMissed > 0) {
|
||||||
|
RecordProperty("centre_sample_blind_to_stale_vertices", missedVertices);
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[itest] whole-region scan caught all %d single-stale-vertex cases; a centre "
|
||||||
|
"sample alone was blind to %d of them (vertices %s)\n",
|
||||||
|
kLeftQuadVertexCount, centreSampleMissed, missedVertices.c_str());
|
||||||
|
}
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the same bug, seen from the other side --------------------------
|
||||||
|
//
|
||||||
|
// The mutation cases above ask "did the new bytes reach the GPU?". This
|
||||||
|
// one asks the question a STREAMED buffer forces: "do the old bytes even
|
||||||
|
// still exist?".
|
||||||
|
//
|
||||||
|
// A GL_STREAM_DRAW / GL_DYNAMIC_DRAW buffer is not given permanent GPU
|
||||||
|
// storage. Every frame its contents are copied into that frame's
|
||||||
|
// transient upload arena, which is a bump allocator reset at the start of
|
||||||
|
// each frame slot - so a slice handed out in frame N names bytes that
|
||||||
|
// frame N+frames-in-flight hands to whoever uploads first. A memo that
|
||||||
|
// revalidates across a frame boundary and skips the acquire never
|
||||||
|
// re-uploads, so it keeps binding an offset the arena has since given
|
||||||
|
// away: the draw reads whatever the next tenant put there. That is the
|
||||||
|
// "random triangles" shape of this bug - the buffer nobody touched is the
|
||||||
|
// one that renders wrong.
|
||||||
|
//
|
||||||
|
// The scene makes the next tenant deterministic instead of arbitrary: a
|
||||||
|
// second streamed object of exactly the same size is uploaded and drawn
|
||||||
|
// FIRST in every frame, so it lands on precisely the bytes the memo still
|
||||||
|
// points at. A draw that renders the decoy's geometry instead of its own
|
||||||
|
// is unmissable.
|
||||||
|
|
||||||
|
class StreamedArenaScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
static constexpr int kQuietFrames = 2; // frames in which only the subject draws
|
||||||
|
static constexpr int kChurnFrames = 8; // > frames-in-flight, so the ring wraps
|
||||||
|
|
||||||
|
struct StreamedObject {
|
||||||
|
unsigned int vao = 0;
|
||||||
|
unsigned int vbo = 0;
|
||||||
|
unsigned int ebo = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
for (StreamedObject* object : {&m_subject, &m_decoy}) {
|
||||||
|
if (object->ebo != 0) glDeleteBuffers(1, &object->ebo);
|
||||||
|
if (object->vbo != 0) glDeleteBuffers(1, &object->vbo);
|
||||||
|
if (object->vao != 0) glDeleteVertexArrays(1, &object->vao);
|
||||||
|
*object = StreamedObject{};
|
||||||
|
}
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL_STREAM_DRAW is what puts a buffer on the transient arena
|
||||||
|
// (ShouldUseTransientVertexIndexBuffer) - and what Minecraft uses for
|
||||||
|
// exactly this kind of geometry.
|
||||||
|
void BuildStreamedObject(StreamedObject& object, const std::vector<Vertex>& vertices,
|
||||||
|
const GLuint (&indices)[kIndexCount]) {
|
||||||
|
glGenVertexArrays(1, &object.vao);
|
||||||
|
glBindVertexArray(object.vao);
|
||||||
|
glGenBuffers(1, &object.vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, object.vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data(),
|
||||||
|
GL_STREAM_DRAW);
|
||||||
|
glGenBuffers(1, &object.ebo);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, object.ebo);
|
||||||
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, GLsizeiptr(sizeof(indices)), indices, GL_STREAM_DRAW);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<void*>(0));
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<void*>(8));
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Draw(const StreamedObject& object) {
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glBindVertexArray(object.vao);
|
||||||
|
glDrawElements(GL_TRIANGLES, kIndexCount, GL_UNSIGNED_INT, nullptr);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-uploading the decoy is what forces it onto a fresh arena slice
|
||||||
|
// this frame - i.e. what makes it the arena's next tenant.
|
||||||
|
void RestreamDecoy(const std::vector<Vertex>& vertices, const GLuint (&indices)[kIndexCount]) {
|
||||||
|
glBindVertexArray(m_decoy.vao);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_decoy.vbo);
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, 0, GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data());
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_decoy.ebo);
|
||||||
|
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, GLsizeiptr(sizeof(indices)), indices);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int m_program = 0;
|
||||||
|
StreamedObject m_subject;
|
||||||
|
StreamedObject m_decoy;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vertex data. Subject and decoy differ in geometry AND colour, so a
|
||||||
|
// subject draw that reads the decoy's arena bytes paints the decoy's quad.
|
||||||
|
TEST_F(StreamedArenaScenario, StreamedVertexDataSurvivesArenaRecycling) {
|
||||||
|
const std::vector<Vertex> full = SceneVertices(/*leftQuadIsGreen=*/false);
|
||||||
|
const std::vector<Vertex> subjectVertices(full.begin(), full.begin() + 4); // left, red
|
||||||
|
const std::vector<Vertex> decoyVertices(full.begin() + 4, full.begin() + 8); // right, green
|
||||||
|
ASSERT_EQ(subjectVertices.size(), decoyVertices.size()); // same arena footprint
|
||||||
|
|
||||||
|
BuildStreamedObject(m_subject, subjectVertices, kIndicesLeftQuad);
|
||||||
|
BuildStreamedObject(m_decoy, decoyVertices, kIndicesLeftQuad);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
||||||
|
|
||||||
|
// Quiet frames: the subject is the only thing uploading, so its data
|
||||||
|
// sits at the head of the arena and its memo records that offset.
|
||||||
|
for (int frame = 0; frame < kQuietFrames; ++frame) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
Draw(m_subject);
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Churn frames: the decoy re-streams and draws first every frame. The
|
||||||
|
// subject is never touched again - it must still render itself.
|
||||||
|
for (int frame = 0; frame < kChurnFrames; ++frame) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
RestreamDecoy(decoyVertices, kIndicesLeftQuad);
|
||||||
|
Draw(m_decoy);
|
||||||
|
Draw(m_subject);
|
||||||
|
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
ExpectHalves(image, "red", "green",
|
||||||
|
"churn frame " + std::to_string(frame) +
|
||||||
|
": the untouched streamed vertex buffer rendered someone else's arena bytes");
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index data. Both objects carry the SAME eight vertices, so only the
|
||||||
|
// element buffer can decide which half is drawn - this isolates the EBO
|
||||||
|
// memo, which had its own copy of the cross-frame hole.
|
||||||
|
//
|
||||||
|
// COVERS: that an untouched streamed index buffer still renders its own
|
||||||
|
// geometry after the arena it lives in has been recycled by another
|
||||||
|
// object - the index-side statement of the invariant the vertex case
|
||||||
|
// above actually catches.
|
||||||
|
//
|
||||||
|
// DOES NOT COVER: the EBO cross-frame memo. Instrumented against the
|
||||||
|
// re-enabled buggy path this case reaches that branch ZERO times: the memo
|
||||||
|
// is recorded only on the RESIDENT index path (UploadAndBindIndexBuffer
|
||||||
|
// stores it in the arm after AcquireResidentSlice), and a streamed EBO
|
||||||
|
// never gets there. So it passes on the buggy revision exactly as it does
|
||||||
|
// on the fixed one, and it is not evidence about the fix.
|
||||||
|
//
|
||||||
|
// It stays because it is the tripwire for the change that would make the
|
||||||
|
// EBO memo dangerous: memoise the streamed index path - the obvious next
|
||||||
|
// step for the same optimisation - and the reach stops being zero and this
|
||||||
|
// test fails on the first churn frame. See ResidentIndexScenario.cpp.
|
||||||
|
TEST_F(StreamedArenaScenario, StreamedIndexDataSurvivesArenaRecycling) {
|
||||||
|
const std::vector<Vertex> shared = SceneVertices(/*leftQuadIsGreen=*/false);
|
||||||
|
|
||||||
|
BuildStreamedObject(m_subject, shared, kIndicesLeftQuad); // draws the left, red quad
|
||||||
|
BuildStreamedObject(m_decoy, shared, kIndicesRightQuad); // draws the right, green quad
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
||||||
|
|
||||||
|
for (int frame = 0; frame < kQuietFrames; ++frame) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
Draw(m_subject);
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int frame = 0; frame < kChurnFrames; ++frame) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
RestreamDecoy(shared, kIndicesRightQuad);
|
||||||
|
Draw(m_decoy);
|
||||||
|
Draw(m_subject);
|
||||||
|
const Image image = ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
ExpectHalves(image, "red", "green",
|
||||||
|
"churn frame " + std::to_string(frame) +
|
||||||
|
": the untouched streamed index buffer rendered someone else's arena bytes");
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,522 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/MultiDrawScenario.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 D - glMultiDrawElements(BaseVertex) against the draws it stands for.
|
||||||
|
//
|
||||||
|
// Neither entry point exists in OpenGL ES, so DirectGLES emulates both through a
|
||||||
|
// ladder of tiers (MG_Backend/DirectGLES/MultiDraw.cpp): a native
|
||||||
|
// glMultiDrawElementsBaseVertexEXT, synthesized indirect commands drawn one at a
|
||||||
|
// time or in one batch, a per-sub-draw replay, a CPU rewrite of the index stream,
|
||||||
|
// and a compute shader that flattens the whole batch into a single draw. They
|
||||||
|
// share nothing but their contract, which is the one thing asserted here:
|
||||||
|
//
|
||||||
|
// a multi-draw must paint exactly what the unrolled single draws paint.
|
||||||
|
//
|
||||||
|
// The reference side never enters the emulation - it is a loop of
|
||||||
|
// glDrawElementsBaseVertex / glDrawElements - so a tier cannot make itself look
|
||||||
|
// right by breaking both sides the same way.
|
||||||
|
//
|
||||||
|
// The Minecraft retraces already cover the common shape (GL_UNSIGNED_INT indices
|
||||||
|
// in a bound element array buffer, small base vertices, GL_TRIANGLES) on every
|
||||||
|
// tier. What they contain none of, and what these cases are for, is the set of
|
||||||
|
// shapes where a tier has to decline or compensate rather than replay:
|
||||||
|
//
|
||||||
|
// * narrow index types, where a rewritten stream has to widen (BYTE/SHORT);
|
||||||
|
// * a base vertex past the index type's range, where folding it into the
|
||||||
|
// indices at the source width silently wraps - GL adds base vertices at full
|
||||||
|
// precision, so `ushort index 10 + baseVertex 70000` is vertex 70010 and not
|
||||||
|
// vertex 4474;
|
||||||
|
// * primitive restart, where a rewritten stream must carry the sentinel across
|
||||||
|
// unrebased or the restart is lost and the strip welds shut;
|
||||||
|
// * client-memory index arrays, which have no buffer for the indirect tiers to
|
||||||
|
// address or for the compute tier to read;
|
||||||
|
// * a strip mode, which the flattening tier must decline outright because
|
||||||
|
// concatenation would weld one sub-draw's last primitive to the next
|
||||||
|
// sub-draw's first.
|
||||||
|
//
|
||||||
|
// One process is one tier (MOBILEGL_ESPRYT_MULTIDRAW_MODE is read once at
|
||||||
|
// startup), so a single run exercises whichever tier this driver resolved to.
|
||||||
|
// Running the binary once per mode is what covers the ladder; each run is a
|
||||||
|
// complete, self-contained proof for the tier it landed on.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#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/glext.h>
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr const char* kVertexSource = R"(#version 330 core
|
||||||
|
layout(location = 0) in vec2 aPos;
|
||||||
|
layout(location = 1) in vec3 aColor;
|
||||||
|
out vec3 vColor;
|
||||||
|
void main() {
|
||||||
|
vColor = aColor;
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFragmentSource = R"(#version 330 core
|
||||||
|
in vec3 vColor;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() {
|
||||||
|
oColor = vec4(vColor, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
float r, g, b;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Four column quads spanning the viewport left to right, in four colours,
|
||||||
|
// so a sub-draw that lands in the wrong place, draws the wrong vertices or
|
||||||
|
// does not draw at all changes the picture rather than hiding inside it.
|
||||||
|
constexpr int kColumns = 4;
|
||||||
|
|
||||||
|
const Rgba8 kColumnColors[kColumns] = {
|
||||||
|
{255, 0, 0, 255},
|
||||||
|
{0, 255, 0, 255},
|
||||||
|
{0, 0, 255, 255},
|
||||||
|
{255, 255, 255, 255},
|
||||||
|
};
|
||||||
|
|
||||||
|
// `padVertices` leading dummies force every sub-draw to need its own base
|
||||||
|
// vertex: without one applied, a draw reads the padding and paints black.
|
||||||
|
std::vector<Vertex> ColumnVertices(int padVertices) {
|
||||||
|
std::vector<Vertex> vertices(static_cast<std::size_t>(padVertices), Vertex{0.0f, 0.0f, 0.0f, 0.0f, 0.0f});
|
||||||
|
for (int column = 0; column < kColumns; ++column) {
|
||||||
|
const float x0 = -1.0f + 2.0f * static_cast<float>(column) / kColumns;
|
||||||
|
const float x1 = -1.0f + 2.0f * static_cast<float>(column + 1) / kColumns;
|
||||||
|
const Rgba8 color = kColumnColors[column];
|
||||||
|
const float r = color.r / 255.0f;
|
||||||
|
const float g = color.g / 255.0f;
|
||||||
|
const float b = color.b / 255.0f;
|
||||||
|
vertices.push_back({x0, -1.0f, r, g, b});
|
||||||
|
vertices.push_back({x1, -1.0f, r, g, b});
|
||||||
|
vertices.push_back({x1, 1.0f, r, g, b});
|
||||||
|
vertices.push_back({x0, 1.0f, r, g, b});
|
||||||
|
}
|
||||||
|
return vertices;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every sub-draw uses the SAME six indices, 0..3 relative to its own quad;
|
||||||
|
// only the base vertex tells the columns apart. That makes the base vertex
|
||||||
|
// the load-bearing part of the batch.
|
||||||
|
const std::uint32_t kQuadIndices[6] = {0, 1, 2, 0, 2, 3};
|
||||||
|
|
||||||
|
// One column, as a restart-separated pair of triangle strips. Two strips in
|
||||||
|
// one sub-draw means the sentinel is genuinely interior: drop it and the two
|
||||||
|
// halves weld into a single strip that paints across the gap between them.
|
||||||
|
// Indices are relative to the sub-draw's own quad, like kQuadIndices.
|
||||||
|
template <typename Index>
|
||||||
|
std::vector<Index> RestartStripIndices(Index restartSentinel) {
|
||||||
|
// 3,0,2,1 is the strip winding of the quad; splitting it around the
|
||||||
|
// sentinel gives two degenerate-free halves that redraw the same area.
|
||||||
|
return {Index{3}, Index{0}, Index{2}, restartSentinel, Index{0}, Index{2}, Index{1}};
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiDrawScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "program setup left a GL error behind";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
ReleaseBuffers();
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
}
|
||||||
|
|
||||||
|
// VAO + VBO, and an EBO only when `indexBytes` is non-null: a null one
|
||||||
|
// leaves GL_ELEMENT_ARRAY_BUFFER unbound so the sub-draws address client
|
||||||
|
// memory, which is the shape that forces the buffer-reading tiers out.
|
||||||
|
void BuildScene(int padVertices, const void* indexBytes, std::size_t indexByteCount) {
|
||||||
|
ReleaseBuffers();
|
||||||
|
const std::vector<Vertex> vertices = ColumnVertices(padVertices);
|
||||||
|
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glGenBuffers(1, &m_vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(vertices.size() * sizeof(Vertex)),
|
||||||
|
vertices.data(), GL_STATIC_DRAW);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<const void*>(0));
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
|
||||||
|
reinterpret_cast<const void*>(sizeof(float) * 2));
|
||||||
|
|
||||||
|
if (indexBytes != nullptr) {
|
||||||
|
glGenBuffers(1, &m_ebo);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
|
||||||
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, static_cast<GLsizeiptr>(indexByteCount), indexBytes,
|
||||||
|
GL_STATIC_DRAW);
|
||||||
|
}
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReleaseBuffers() {
|
||||||
|
if (m_ebo != 0) glDeleteBuffers(1, &m_ebo);
|
||||||
|
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
m_ebo = 0;
|
||||||
|
m_vbo = 0;
|
||||||
|
m_vao = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_program = 0;
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
GLuint m_vbo = 0;
|
||||||
|
GLuint m_ebo = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Runs `draw`, reads the default framebuffer back and returns the image.
|
||||||
|
template <typename DrawFn>
|
||||||
|
Image RenderPass(GLuint program, GLuint vao, DrawFn&& draw) {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
glViewport(0, 0, HeadlessGL::Get().Width(), HeadlessGL::Get().Height());
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glUseProgram(program);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
draw();
|
||||||
|
return ReadPixels(HeadlessGL::Get().Width(), HeadlessGL::Get().Height());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The whole point of the file: two renderings of the same geometry, one
|
||||||
|
// through the multi-draw emulation and one through the single-draw entry
|
||||||
|
// points it stands for, must be identical to the byte.
|
||||||
|
void ExpectSameImage(const Image& multiDraw, const Image& unrolled, const std::string& what) {
|
||||||
|
ASSERT_FALSE(multiDraw.Empty()) << what << ": multi-draw readback was empty";
|
||||||
|
ASSERT_FALSE(unrolled.Empty()) << what << ": reference readback was empty";
|
||||||
|
EXPECT_EQ(multiDraw, unrolled)
|
||||||
|
<< what << ": glMultiDraw* painted something else than the draws it stands for ("
|
||||||
|
<< multiDraw.ByteDiffCount(unrolled) << " bytes differ; multi-draw quadrants "
|
||||||
|
<< multiDraw.QuadrantSignature() << ", unrolled quadrants " << unrolled.QuadrantSignature() << ")";
|
||||||
|
// A pair of blank frames would satisfy the comparison above and prove
|
||||||
|
// nothing at all - the failure mode a multi-draw path most often has is
|
||||||
|
// drawing NOTHING (see the shipped glMultiDrawElementsBaseVertexEXT stub
|
||||||
|
// that silently dropped every draw). Demand the columns really landed.
|
||||||
|
EXPECT_NE(multiDraw.QuadrantSignature(), "black,black,black,black") << what << ": nothing was drawn at all";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- GL_UNSIGNED_INT indices in a buffer, per-sub-draw base vertices ----
|
||||||
|
|
||||||
|
TEST_F(MultiDrawScenario, BaseVertexBatchMatchesUnrolledDraws) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
constexpr int kPad = 5; // odd, so nothing lines up by accident
|
||||||
|
BuildScene(kPad, kQuadIndices, sizeof(kQuadIndices));
|
||||||
|
|
||||||
|
GLsizei counts[kColumns];
|
||||||
|
const void* offsets[kColumns];
|
||||||
|
GLint baseVertices[kColumns];
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
counts[i] = 6;
|
||||||
|
offsets[i] = reinterpret_cast<const void*>(0);
|
||||||
|
baseVertices[i] = kPad + i * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||||
|
glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets, kColumns, baseVertices);
|
||||||
|
});
|
||||||
|
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_INT, offsets[i], baseVertices[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
ExpectSameImage(batched, unrolled, "GL_UNSIGNED_INT indices, per-sub-draw base vertices");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- glMultiDrawElements: no base vertices, distinct index offsets ----
|
||||||
|
|
||||||
|
TEST_F(MultiDrawScenario, PlainBatchMatchesUnrolledDraws) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
// No padding and no base vertices: each sub-draw reaches its own column
|
||||||
|
// through its index offset instead.
|
||||||
|
std::vector<std::uint32_t> indices;
|
||||||
|
for (int column = 0; column < kColumns; ++column) {
|
||||||
|
for (const std::uint32_t index : kQuadIndices) {
|
||||||
|
indices.push_back(index + static_cast<std::uint32_t>(column * 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BuildScene(0, indices.data(), indices.size() * sizeof(std::uint32_t));
|
||||||
|
|
||||||
|
GLsizei counts[kColumns];
|
||||||
|
const void* offsets[kColumns];
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
counts[i] = 6;
|
||||||
|
offsets[i] = reinterpret_cast<const void*>(static_cast<std::uintptr_t>(i * 6 * sizeof(std::uint32_t)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||||
|
glMultiDrawElements(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets, kColumns);
|
||||||
|
});
|
||||||
|
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
glDrawElements(GL_TRIANGLES, counts[i], GL_UNSIGNED_INT, offsets[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
ExpectSameImage(batched, unrolled, "glMultiDrawElements with no base vertices");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- narrow index types ----
|
||||||
|
// A tier that rewrites the stream emits GL_UNSIGNED_INT whatever came in,
|
||||||
|
// so these two say the widening reproduces the original draw exactly.
|
||||||
|
|
||||||
|
TEST_F(MultiDrawScenario, UnsignedShortBatchMatchesUnrolledDraws) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
constexpr int kPad = 3;
|
||||||
|
std::uint16_t indices[6];
|
||||||
|
for (int i = 0; i < 6; ++i)
|
||||||
|
indices[i] = static_cast<std::uint16_t>(kQuadIndices[i]);
|
||||||
|
BuildScene(kPad, indices, sizeof(indices));
|
||||||
|
|
||||||
|
GLsizei counts[kColumns];
|
||||||
|
const void* offsets[kColumns];
|
||||||
|
GLint baseVertices[kColumns];
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
counts[i] = 6;
|
||||||
|
offsets[i] = reinterpret_cast<const void*>(0);
|
||||||
|
baseVertices[i] = kPad + i * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||||
|
glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_SHORT, offsets, kColumns, baseVertices);
|
||||||
|
});
|
||||||
|
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_SHORT, offsets[i], baseVertices[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
ExpectSameImage(batched, unrolled, "GL_UNSIGNED_SHORT indices");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(MultiDrawScenario, UnsignedByteBatchMatchesUnrolledDraws) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
constexpr int kPad = 3;
|
||||||
|
std::uint8_t indices[6];
|
||||||
|
for (int i = 0; i < 6; ++i)
|
||||||
|
indices[i] = static_cast<std::uint8_t>(kQuadIndices[i]);
|
||||||
|
// 24 bytes: a word multiple, which the compute tier needs of the source
|
||||||
|
// buffer when the index type is narrower than a word.
|
||||||
|
std::uint8_t padded[24] = {};
|
||||||
|
for (int i = 0; i < 6; ++i)
|
||||||
|
padded[i] = indices[i];
|
||||||
|
BuildScene(kPad, padded, sizeof(padded));
|
||||||
|
|
||||||
|
GLsizei counts[kColumns];
|
||||||
|
const void* offsets[kColumns];
|
||||||
|
GLint baseVertices[kColumns];
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
counts[i] = 6;
|
||||||
|
offsets[i] = reinterpret_cast<const void*>(0);
|
||||||
|
baseVertices[i] = kPad + i * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||||
|
glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_BYTE, offsets, kColumns, baseVertices);
|
||||||
|
});
|
||||||
|
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_BYTE, offsets[i], baseVertices[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
ExpectSameImage(batched, unrolled, "GL_UNSIGNED_BYTE indices");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- a base vertex the index type cannot spell ----
|
||||||
|
// GL adds the base vertex at full precision, so folding it into a
|
||||||
|
// GL_UNSIGNED_SHORT index stream at the source width wraps and addresses the
|
||||||
|
// wrong vertex. The columns here start past 65535, which no ushort index can
|
||||||
|
// reach on its own.
|
||||||
|
|
||||||
|
TEST_F(MultiDrawScenario, BaseVertexBeyondIndexTypeRangeMatchesUnrolledDraws) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
constexpr int kPad = 70000; // > 0xFFFF
|
||||||
|
std::uint16_t indices[6];
|
||||||
|
for (int i = 0; i < 6; ++i)
|
||||||
|
indices[i] = static_cast<std::uint16_t>(kQuadIndices[i]);
|
||||||
|
BuildScene(kPad, indices, sizeof(indices));
|
||||||
|
|
||||||
|
GLsizei counts[kColumns];
|
||||||
|
const void* offsets[kColumns];
|
||||||
|
GLint baseVertices[kColumns];
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
counts[i] = 6;
|
||||||
|
offsets[i] = reinterpret_cast<const void*>(0);
|
||||||
|
baseVertices[i] = kPad + i * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||||
|
glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_SHORT, offsets, kColumns, baseVertices);
|
||||||
|
});
|
||||||
|
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_SHORT, offsets[i], baseVertices[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
ExpectSameImage(batched, unrolled, "base vertex past the GL_UNSIGNED_SHORT range");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- client-memory index arrays ----
|
||||||
|
// No element array buffer, so the indirect tiers have nothing to address and
|
||||||
|
// the compute tier nothing to read; both must decline and hand the batch to
|
||||||
|
// a tier that can replay it.
|
||||||
|
|
||||||
|
TEST_F(MultiDrawScenario, ClientSideIndicesBatchMatchesUnrolledDraws) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
constexpr int kPad = 5;
|
||||||
|
BuildScene(kPad, nullptr, 0);
|
||||||
|
|
||||||
|
GLsizei counts[kColumns];
|
||||||
|
const void* offsets[kColumns];
|
||||||
|
GLint baseVertices[kColumns];
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
counts[i] = 6;
|
||||||
|
offsets[i] = kQuadIndices;
|
||||||
|
baseVertices[i] = kPad + i * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||||
|
glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets, kColumns, baseVertices);
|
||||||
|
});
|
||||||
|
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_INT, offsets[i], baseVertices[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
ExpectSameImage(batched, unrolled, "client-memory index arrays");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- primitive restart inside a strip ----
|
||||||
|
// Two things at once: a strip mode, which the flattening tier must decline
|
||||||
|
// because concatenation would weld sub-draws together, and a restart
|
||||||
|
// sentinel, which any tier that rewrites indices must carry across without
|
||||||
|
// adding the base vertex to it.
|
||||||
|
|
||||||
|
TEST_F(MultiDrawScenario, PrimitiveRestartStripBatchMatchesUnrolledDraws) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
constexpr int kPad = 5;
|
||||||
|
const std::vector<std::uint32_t> indices = RestartStripIndices<std::uint32_t>(0xFFFFFFFFu);
|
||||||
|
BuildScene(kPad, indices.data(), indices.size() * sizeof(std::uint32_t));
|
||||||
|
|
||||||
|
GLsizei counts[kColumns];
|
||||||
|
const void* offsets[kColumns];
|
||||||
|
GLint baseVertices[kColumns];
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
counts[i] = static_cast<GLsizei>(indices.size());
|
||||||
|
offsets[i] = reinterpret_cast<const void*>(0);
|
||||||
|
baseVertices[i] = kPad + i * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
||||||
|
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||||
|
glMultiDrawElementsBaseVertex(GL_TRIANGLE_STRIP, counts, GL_UNSIGNED_INT, offsets, kColumns,
|
||||||
|
baseVertices);
|
||||||
|
});
|
||||||
|
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
glDrawElementsBaseVertex(GL_TRIANGLE_STRIP, counts[i], GL_UNSIGNED_INT, offsets[i],
|
||||||
|
baseVertices[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
ExpectSameImage(batched, unrolled, "GL_TRIANGLE_STRIP with primitive restart");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same, with GL_UNSIGNED_SHORT: the sentinel a rewritten stream has to
|
||||||
|
// recognise is the index TYPE's all-ones value, not the rewritten stream's.
|
||||||
|
TEST_F(MultiDrawScenario, PrimitiveRestartUnsignedShortBatchMatchesUnrolledDraws) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
constexpr int kPad = 5;
|
||||||
|
const std::vector<std::uint16_t> indices = RestartStripIndices<std::uint16_t>(0xFFFFu);
|
||||||
|
BuildScene(kPad, indices.data(), indices.size() * sizeof(std::uint16_t));
|
||||||
|
|
||||||
|
GLsizei counts[kColumns];
|
||||||
|
const void* offsets[kColumns];
|
||||||
|
GLint baseVertices[kColumns];
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
counts[i] = static_cast<GLsizei>(indices.size());
|
||||||
|
offsets[i] = reinterpret_cast<const void*>(0);
|
||||||
|
baseVertices[i] = kPad + i * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
||||||
|
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||||
|
glMultiDrawElementsBaseVertex(GL_TRIANGLE_STRIP, counts, GL_UNSIGNED_SHORT, offsets, kColumns,
|
||||||
|
baseVertices);
|
||||||
|
});
|
||||||
|
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
glDrawElementsBaseVertex(GL_TRIANGLE_STRIP, counts[i], GL_UNSIGNED_SHORT, offsets[i],
|
||||||
|
baseVertices[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
ExpectSameImage(batched, unrolled, "GL_TRIANGLE_STRIP with GL_UNSIGNED_SHORT primitive restart");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- a batch with holes ----
|
||||||
|
// Zero-count sub-draws draw nothing. The flattening tier's binary search
|
||||||
|
// finds a sub-draw by prefix sum, and a zero-count entry repeats the
|
||||||
|
// previous sum - so a search that resolves ties the other way would attribute
|
||||||
|
// indices to the empty draw and paint the wrong column.
|
||||||
|
|
||||||
|
TEST_F(MultiDrawScenario, ZeroCountSubDrawsMatchUnrolledDraws) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
constexpr int kPad = 5;
|
||||||
|
BuildScene(kPad, kQuadIndices, sizeof(kQuadIndices));
|
||||||
|
|
||||||
|
GLsizei counts[kColumns];
|
||||||
|
const void* offsets[kColumns];
|
||||||
|
GLint baseVertices[kColumns];
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
// Columns 1 and 2 are skipped, leaving the outer two painted.
|
||||||
|
counts[i] = (i == 1 || i == 2) ? 0 : 6;
|
||||||
|
offsets[i] = reinterpret_cast<const void*>(0);
|
||||||
|
baseVertices[i] = kPad + i * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image batched = RenderPass(m_program, m_vao, [&] {
|
||||||
|
glMultiDrawElementsBaseVertex(GL_TRIANGLES, counts, GL_UNSIGNED_INT, offsets, kColumns, baseVertices);
|
||||||
|
});
|
||||||
|
const Image unrolled = RenderPass(m_program, m_vao, [&] {
|
||||||
|
for (int i = 0; i < kColumns; ++i) {
|
||||||
|
if (counts[i] == 0) continue;
|
||||||
|
glDrawElementsBaseVertex(GL_TRIANGLES, counts[i], GL_UNSIGNED_INT, offsets[i], baseVertices[i]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
ExpectSameImage(batched, unrolled, "a batch with zero-count sub-draws");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/OrientationScenario.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 - "the frame came out upside down".
|
||||||
|
//
|
||||||
|
// The shipped bug (DirectVulkan, GetBaseTransformFlagsRaw): the shader
|
||||||
|
// transform flags - the Y-flip and surface-rotation bits that apply ONLY when
|
||||||
|
// the bound draw framebuffer is the default one - were memoized on the
|
||||||
|
// swapchain pre-transform alone. The is-default-framebuffer input was not part
|
||||||
|
// of the key, so whichever kind of pass evaluated the memo first decided the
|
||||||
|
// orientation of every pass after it. In a real frame that meant: after any
|
||||||
|
// render-to-texture pass, the next default-framebuffer pass inherited the FBO's
|
||||||
|
// unflipped flags and the whole frame rendered upside down (retrace SSIM 0.052,
|
||||||
|
// deterministic; flickering clouds on device).
|
||||||
|
//
|
||||||
|
// What pins it: a pattern asymmetric in BOTH axes - four quadrants, coloured
|
||||||
|
//
|
||||||
|
// top-left RED | WHITE top-right
|
||||||
|
// bottom-left BLUE | GREEN bottom-right
|
||||||
|
//
|
||||||
|
// - drawn to a target, read back with glReadPixels, and reduced to the four
|
||||||
|
// quadrant-centre colours in the fixed order bottom-left, bottom-right,
|
||||||
|
// top-left, top-right.
|
||||||
|
//
|
||||||
|
// Four quadrants rather than the three horizontal stripes this scenario used to
|
||||||
|
// draw, because stripes only pin ONE axis. Stripes read down the centre line
|
||||||
|
// are unchanged by an X flip, by a transpose, and by a 180 rotation composed
|
||||||
|
// with a Y flip: all three of those bugs would have rendered a green stripe
|
||||||
|
// between a blue one and a red one and passed. Every one of the eight
|
||||||
|
// symmetries of the square now produces a different string:
|
||||||
|
//
|
||||||
|
// identity blue,green,red,white <- correct
|
||||||
|
// Y flip red,white,blue,green <- the shipped bug
|
||||||
|
// X flip green,blue,white,red
|
||||||
|
// 180 rotation white,red,green,blue
|
||||||
|
// transpose blue,red,green,white
|
||||||
|
// anti-transpose white,green,red,blue
|
||||||
|
// rotate 90 CCW red,blue,white,green
|
||||||
|
// rotate 90 CW green,white,blue,red
|
||||||
|
//
|
||||||
|
// The assertions then go further than the signature: every quadrant is checked
|
||||||
|
// pixel by pixel over its whole area (RegionIsMostly), so a partial or torn
|
||||||
|
// draw cannot pass by having the four sampled centres come out right.
|
||||||
|
//
|
||||||
|
// Both orderings are covered, because the memo is poisoned by whichever pass
|
||||||
|
// runs first and these tests share one process:
|
||||||
|
// - default -> FBO -> default (the FBO pass inherits the default's flip)
|
||||||
|
// - FBO -> default (the shipped symptom: the default pass
|
||||||
|
// inherits the FBO's lack of flip)
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#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* kVertexSource = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
in vec3 aColor;
|
||||||
|
out vec3 vColor;
|
||||||
|
void main() {
|
||||||
|
vColor = aColor;
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr const char* kFragmentSource = R"(#version 330 core
|
||||||
|
in vec3 vColor;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() {
|
||||||
|
oColor = vec4(vColor, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The correctly-oriented answer, in glReadPixels order (row 0 is the
|
||||||
|
// bottom row) and in QuadrantSignature's order: bottom-left, bottom-right,
|
||||||
|
// top-left, top-right. Plain GL semantics; holds for every framebuffer,
|
||||||
|
// default or not.
|
||||||
|
constexpr const char* kUprightSignature = "blue,green,red,white";
|
||||||
|
|
||||||
|
// How far inside each quadrant the whole-region checks start. The quadrant
|
||||||
|
// seam sits on a pixel boundary, so one pixel of margin is enough to make
|
||||||
|
// "every single pixel" an achievable (and therefore useful) demand.
|
||||||
|
constexpr int kQuadrantInset = 2;
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
float x, y;
|
||||||
|
float r, g, b;
|
||||||
|
};
|
||||||
|
|
||||||
|
void AppendQuad(std::vector<Vertex>& out, float x0, float x1, float y0, float y1, float r, float g, float b) {
|
||||||
|
const Vertex bl{x0, y0, r, g, b};
|
||||||
|
const Vertex br{x1, y0, r, g, b};
|
||||||
|
const Vertex tr{x1, y1, r, g, b};
|
||||||
|
const Vertex tl{x0, y1, r, g, b};
|
||||||
|
out.insert(out.end(), {bl, br, tr, bl, tr, tl});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Vertex> QuadrantGeometry() {
|
||||||
|
std::vector<Vertex> vertices;
|
||||||
|
vertices.reserve(24);
|
||||||
|
AppendQuad(vertices, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f); // bottom-left: blue
|
||||||
|
AppendQuad(vertices, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f); // bottom-right: green
|
||||||
|
AppendQuad(vertices, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f); // top-left: red
|
||||||
|
AppendQuad(vertices, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f); // top-right: white
|
||||||
|
return vertices;
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrientationScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
|
||||||
|
ASSERT_NE(m_program, 0u) << error;
|
||||||
|
|
||||||
|
const std::vector<Vertex> vertices = QuadrantGeometry();
|
||||||
|
m_vertexCount = static_cast<int>(vertices.size());
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glGenBuffers(1, &m_vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(vertices.size() * sizeof(Vertex)), vertices.data(),
|
||||||
|
GL_STATIC_DRAW);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<void*>(0));
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<void*>(8));
|
||||||
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
m_offscreen = MakeColorFbo(Gl().Width(), Gl().Height());
|
||||||
|
ASSERT_NE(m_offscreen.fbo, 0u) << "offscreen FBO is not framebuffer-complete";
|
||||||
|
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "setup left a GL error behind";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
DestroyColorFbo(m_offscreen);
|
||||||
|
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawQuadrants() {
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, m_vertexCount);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One pass to the default (presentable) framebuffer.
|
||||||
|
Image DefaultFramebufferPass() {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
DrawQuadrants();
|
||||||
|
return ReadPixels(Gl().Width(), Gl().Height());
|
||||||
|
}
|
||||||
|
|
||||||
|
// One render-to-texture pass. Real frames do this constantly
|
||||||
|
// (shadow maps, post-processing, Minecraft's main render target).
|
||||||
|
Image OffscreenPass() {
|
||||||
|
BindFbo(m_offscreen);
|
||||||
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
DrawQuadrants();
|
||||||
|
return ReadPixels(m_offscreen.width, m_offscreen.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The signature says WHICH transform went wrong; this says the whole
|
||||||
|
// image is right, not merely its four sampled centres.
|
||||||
|
void ExpectUprightQuadrants(const Image& image, const std::string& when) {
|
||||||
|
const int w = image.Width();
|
||||||
|
const int h = image.Height();
|
||||||
|
const int inset = kQuadrantInset;
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, inset, w / 2 - inset, inset, h / 2 - inset, "blue", 0.0, when));
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, w / 2 + inset, w - inset, inset, h / 2 - inset, "green", 0.0, when));
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, inset, w / 2 - inset, h / 2 + inset, h - inset, "red", 0.0, when));
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, w / 2 + inset, w - inset, h / 2 + inset, h - inset, "white", 0.0,
|
||||||
|
when));
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int m_program = 0;
|
||||||
|
unsigned int m_vao = 0;
|
||||||
|
unsigned int m_vbo = 0;
|
||||||
|
int m_vertexCount = 0;
|
||||||
|
ColorFbo m_offscreen;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The plain statement of GL semantics that everything else leans on: an
|
||||||
|
// FBO pass is never flipped.
|
||||||
|
TEST_F(OrientationScenario, OffscreenPassRendersUpright) {
|
||||||
|
const Image offscreen = OffscreenPass();
|
||||||
|
EXPECT_EQ(offscreen.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "a render-to-texture pass must render unflipped";
|
||||||
|
ExpectUprightQuadrants(offscreen, "render-to-texture pass");
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same for the default framebuffer: whatever the backend does with
|
||||||
|
// the swapchain internally, glReadPixels owes the caller GL orientation.
|
||||||
|
TEST_F(OrientationScenario, DefaultFramebufferPassRendersUpright) {
|
||||||
|
const Image presented = DefaultFramebufferPass();
|
||||||
|
EXPECT_EQ(presented.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "a default-framebuffer pass must read back in GL orientation";
|
||||||
|
ExpectUprightQuadrants(presented, "default-framebuffer pass");
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scenario A proper: default -> FBO -> default in one frame. The third
|
||||||
|
// pass must be pixel-identical to the first; the FBO pass in between
|
||||||
|
// must not have moved anything.
|
||||||
|
TEST_F(OrientationScenario, DefaultFramebufferSurvivesAnOffscreenPass) {
|
||||||
|
const Image before = DefaultFramebufferPass();
|
||||||
|
const Image offscreen = OffscreenPass();
|
||||||
|
const Image after = DefaultFramebufferPass();
|
||||||
|
|
||||||
|
EXPECT_EQ(before.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "first default-framebuffer pass is already misoriented";
|
||||||
|
EXPECT_EQ(offscreen.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "the render-to-texture pass in the middle rendered flipped - the "
|
||||||
|
"default framebuffer's transform flags leaked into it";
|
||||||
|
EXPECT_EQ(after.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "the default-framebuffer pass AFTER a render-to-texture pass is "
|
||||||
|
"misoriented - it inherited the FBO's transform flags";
|
||||||
|
ExpectUprightQuadrants(after, "default-framebuffer pass after a render-to-texture pass");
|
||||||
|
EXPECT_TRUE(after == before) << "the third pass differs from the first in " << after.ByteDiffCount(before)
|
||||||
|
<< " bytes; first=" << before.QuadrantSignature()
|
||||||
|
<< " third=" << after.QuadrantSignature();
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The shipped symptom, in its shipped order: an FBO pass, then the
|
||||||
|
// default framebuffer. This is the one that flipped whole Minecraft
|
||||||
|
// frames.
|
||||||
|
TEST_F(OrientationScenario, DefaultFramebufferAfterOffscreenIsNotFlipped) {
|
||||||
|
const Image offscreen = OffscreenPass();
|
||||||
|
const Image presented = DefaultFramebufferPass();
|
||||||
|
|
||||||
|
EXPECT_EQ(offscreen.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "render-to-texture pass rendered flipped";
|
||||||
|
EXPECT_EQ(presented.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "the default-framebuffer pass that follows a render-to-texture pass "
|
||||||
|
"rendered upside down";
|
||||||
|
ExpectUprightQuadrants(presented, "default-framebuffer pass following a render-to-texture pass");
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// And across a real frame boundary, which is how a game actually
|
||||||
|
// alternates the two kinds of pass.
|
||||||
|
TEST_F(OrientationScenario, OrientationIsStableAcrossFrames) {
|
||||||
|
const Image firstFrame = DefaultFramebufferPass();
|
||||||
|
ExpectUprightQuadrants(firstFrame, "frame 0");
|
||||||
|
Gl().EndFrame();
|
||||||
|
|
||||||
|
for (int frame = 0; frame < 3; ++frame) {
|
||||||
|
const Image offscreen = OffscreenPass();
|
||||||
|
EXPECT_EQ(offscreen.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "frame " << frame + 1 << "'s render-to-texture pass is misoriented";
|
||||||
|
const Image presented = DefaultFramebufferPass();
|
||||||
|
EXPECT_EQ(presented.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< "frame " << frame + 1 << " of the alternating FBO/default loop is misoriented";
|
||||||
|
ExpectUprightQuadrants(presented, "frame " + std::to_string(frame + 1));
|
||||||
|
EXPECT_TRUE(presented == firstFrame) << "frame " << frame + 1 << " differs from frame 0 in "
|
||||||
|
<< presented.ByteDiffCount(firstFrame) << " bytes";
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A standing self-test of the signature, not of MobileGL: it proves the
|
||||||
|
// four-quadrant reduction really does separate all eight symmetries of
|
||||||
|
// the square, so a future "simplify the pattern" change cannot quietly
|
||||||
|
// reintroduce the blind spot the three-stripe version had (X flip,
|
||||||
|
// transpose and 180+Y-flip all left the stripe signature alone).
|
||||||
|
TEST_F(OrientationScenario, QuadrantSignatureSeparatesEverySquareSymmetry) {
|
||||||
|
const Image upright = OffscreenPass();
|
||||||
|
ASSERT_EQ(upright.QuadrantSignature(), kUprightSignature) << "the reference image is not upright";
|
||||||
|
|
||||||
|
const int w = upright.Width();
|
||||||
|
const int h = upright.Height();
|
||||||
|
// Transposes are expressed on the largest centred square the readback
|
||||||
|
// contains, which is enough for the four quadrant centres to move.
|
||||||
|
const int side = std::min(w, h);
|
||||||
|
const int ox = (w - side) / 2;
|
||||||
|
const int oy = (h - side) / 2;
|
||||||
|
|
||||||
|
struct Symmetry {
|
||||||
|
const char* name;
|
||||||
|
const char* expected;
|
||||||
|
int (*mapX)(int x, int y, int w, int h);
|
||||||
|
int (*mapY)(int x, int y, int w, int h);
|
||||||
|
};
|
||||||
|
const Symmetry symmetries[] = {
|
||||||
|
{"Y flip", "red,white,blue,green", [](int x, int, int, int) { return x; },
|
||||||
|
[](int, int y, int, int hh) { return hh - 1 - y; }},
|
||||||
|
{"X flip", "green,blue,white,red", [](int x, int, int ww, int) { return ww - 1 - x; },
|
||||||
|
[](int, int y, int, int) { return y; }},
|
||||||
|
{"180 rotation", "white,red,green,blue", [](int x, int, int ww, int) { return ww - 1 - x; },
|
||||||
|
[](int, int y, int, int hh) { return hh - 1 - y; }},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const Symmetry& symmetry : symmetries) {
|
||||||
|
Image transformed(w, h);
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
const Rgba8 source = upright.At(symmetry.mapX(x, y, w, h), symmetry.mapY(x, y, w, h));
|
||||||
|
std::uint8_t* out = transformed.Data() + (std::size_t(y) * w + x) * 4;
|
||||||
|
out[0] = source.r;
|
||||||
|
out[1] = source.g;
|
||||||
|
out[2] = source.b;
|
||||||
|
out[3] = source.a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_EQ(transformed.QuadrantSignature(), symmetry.expected)
|
||||||
|
<< symmetry.name << " must produce its own signature, or the pattern cannot see it";
|
||||||
|
EXPECT_NE(transformed.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< symmetry.name << " is INDISTINGUISHABLE from an upright frame - the pattern is too symmetric";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The four symmetries that move the axes into each other. They only
|
||||||
|
// make sense on a square, so they run on the largest centred one.
|
||||||
|
struct SquareSymmetry {
|
||||||
|
const char* name;
|
||||||
|
const char* expected;
|
||||||
|
int (*sourceX)(int x, int y, int side);
|
||||||
|
int (*sourceY)(int x, int y, int side);
|
||||||
|
};
|
||||||
|
const SquareSymmetry squareSymmetries[] = {
|
||||||
|
{"transpose", "blue,red,green,white", [](int, int y, int) { return y; },
|
||||||
|
[](int x, int, int) { return x; }},
|
||||||
|
{"anti-transpose", "white,green,red,blue", [](int, int y, int s) { return s - 1 - y; },
|
||||||
|
[](int x, int, int s) { return s - 1 - x; }},
|
||||||
|
{"rotate 90 CCW", "red,blue,white,green", [](int, int y, int) { return y; },
|
||||||
|
[](int x, int, int s) { return s - 1 - x; }},
|
||||||
|
{"rotate 90 CW", "green,white,blue,red", [](int, int y, int s) { return s - 1 - y; },
|
||||||
|
[](int x, int, int) { return x; }},
|
||||||
|
};
|
||||||
|
for (const SquareSymmetry& symmetry : squareSymmetries) {
|
||||||
|
Image square(side, side);
|
||||||
|
for (int y = 0; y < side; ++y) {
|
||||||
|
for (int x = 0; x < side; ++x) {
|
||||||
|
const Rgba8 source =
|
||||||
|
upright.At(ox + symmetry.sourceX(x, y, side), oy + symmetry.sourceY(x, y, side));
|
||||||
|
std::uint8_t* out = square.Data() + (std::size_t(y) * side + x) * 4;
|
||||||
|
out[0] = source.r;
|
||||||
|
out[1] = source.g;
|
||||||
|
out[2] = source.b;
|
||||||
|
out[3] = source.a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_EQ(square.QuadrantSignature(), symmetry.expected)
|
||||||
|
<< symmetry.name << " must produce its own signature, or the pattern cannot see it";
|
||||||
|
EXPECT_NE(square.QuadrantSignature(), kUprightSignature)
|
||||||
|
<< symmetry.name << " is INDISTINGUISHABLE from an upright frame";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ResidentIndexScenario.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 C - RESIDENT index buffers across frame boundaries.
|
||||||
|
//
|
||||||
|
// WHAT THIS FILE DOES AND DOES NOT COVER, stated plainly because the answer is
|
||||||
|
// not the one it was written to find.
|
||||||
|
//
|
||||||
|
// The shipped fix (d7976326) removed cross-frame slice trust from TWO memos: the
|
||||||
|
// vertex-binding one and the EBO one. StreamedArenaScenario pins the vertex
|
||||||
|
// half - re-enable that half alone and it fails. Nothing pinned the EBO half,
|
||||||
|
// and these cases are the result of trying to build something that does.
|
||||||
|
//
|
||||||
|
// The EBO memo lives in UploadAndBindIndexBuffer and is recorded ONLY on the
|
||||||
|
// resident branch, keyed on (BufferObject*, VkBufferResource::sliceEpoch,
|
||||||
|
// frame serial). To fail with only the EBO revalidation re-enabled, a scenario
|
||||||
|
// needs a RESIDENT index buffer whose recorded slice stops describing the right
|
||||||
|
// bytes while the pointer and the epoch still match. Every case below is an
|
||||||
|
// attempt at that, run against the re-enabled buggy path with the branch
|
||||||
|
// instrumented to count reaches, acceptances, and - critically - what the
|
||||||
|
// skipped AcquireResidentSlice WOULD have done. The measurement, over this file
|
||||||
|
// plus every other scenario in the module:
|
||||||
|
//
|
||||||
|
// reached=89 accepted=81 sliceMoved=0 bytesChanged=0 epochBumped=0
|
||||||
|
//
|
||||||
|
// The buggy branch is entered 89 times and serves its recorded slice 81 times,
|
||||||
|
// and in NOT ONE of those 81 would the acquire have moved the slice, changed a
|
||||||
|
// byte of it, or bumped the epoch. The skipped work was a no-op every time.
|
||||||
|
//
|
||||||
|
// That is not luck, it is the shape of the code. A resident slice is
|
||||||
|
// `resource->buffer.GetSlice(0, size)` of a dedicated VkBuffer, so it can only
|
||||||
|
// move when CreateResidentStorage mints new storage - which bumps the epoch. Its
|
||||||
|
// bytes can only change through Respecify / SubData / FlushMappedRange - each of
|
||||||
|
// which bumps the epoch as its first act - or through
|
||||||
|
// BufferObject::SyncPersistentMappedRange, which the acquire calls and the memo
|
||||||
|
// skips. That last one is the real escape, and it is dead here: it early-outs
|
||||||
|
// when the backend has adopted the map into coherent GPU storage, and
|
||||||
|
// AcquirePersistentMap only declines when a host-visible coherent allocation
|
||||||
|
// FAILS. Instrumented across the whole module: 50 persistent coherent write
|
||||||
|
// maps, 50 adopted, 0 dispatches. A 96 MiB EBO did not change that either.
|
||||||
|
//
|
||||||
|
// So on DirectVulkan as it stands, the EBO half of the fix is not reachable from
|
||||||
|
// a GL-level test - not because the guard is sound in principle (it is the same
|
||||||
|
// unsound idea the vertex half shipped corruption with) but because the two
|
||||||
|
// mechanisms that made the vertex half observable are both absent for indices:
|
||||||
|
//
|
||||||
|
// 1. ARENA RELOCATION. The vertex memo records STREAMED slices too, and a
|
||||||
|
// streamed slice moves to a new arena block every frame BY DESIGN - the
|
||||||
|
// epoch that catches it is bumped inside the very acquire the memo skips.
|
||||||
|
// That is what StreamedVertexDataSurvivesArenaRecycling exploits. The index
|
||||||
|
// memo is never recorded on the streamed branch, so no index memo ever
|
||||||
|
// names an arena offset. Measured: StreamedIndexDataSurvivesArenaRecycling
|
||||||
|
// reaches the branch 0 times, and so does PromotedDynamicEbo below (a
|
||||||
|
// promoted DYNAMIC_DRAW buffer is SERVED by AcquireResidentSlice but still
|
||||||
|
// ROUTED as streamed, so it is not memoised either).
|
||||||
|
// 2. HOST-MAP SYNC. Dead, as above.
|
||||||
|
//
|
||||||
|
// These cases therefore stay as what they honestly are: end-to-end regression
|
||||||
|
// tests for resident index-buffer freshness across frame boundaries, and the
|
||||||
|
// standing tripwire for change (1). The moment anyone memoises the streamed or
|
||||||
|
// promoted index path - the natural next step for the same optimisation - these
|
||||||
|
// stop being redundant and start failing. Each case says below what it covers.
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#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 const char* kVS = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
in vec3 aColor;
|
||||||
|
out vec3 vColor;
|
||||||
|
void main() { vColor = aColor; gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
constexpr const char* kFS = R"(#version 330 core
|
||||||
|
in vec3 vColor;
|
||||||
|
out vec4 oColor;
|
||||||
|
void main() { oColor = vec4(vColor, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
struct V {
|
||||||
|
float x, y, r, g, b;
|
||||||
|
};
|
||||||
|
constexpr int kIdx = 6;
|
||||||
|
const GLuint kLeft[kIdx] = {0, 1, 2, 0, 2, 3};
|
||||||
|
const GLuint kRight[kIdx] = {4, 5, 6, 4, 6, 7};
|
||||||
|
|
||||||
|
std::vector<V> Scene() {
|
||||||
|
return {{-1, -1, 1, 0, 0}, {0, -1, 1, 0, 0}, {0, 1, 1, 0, 0}, {-1, 1, 1, 0, 0},
|
||||||
|
{0, -1, 0, 1, 0}, {1, -1, 0, 1, 0}, {1, 1, 0, 1, 0}, {0, 1, 0, 1, 0}};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResidentIndexScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
std::string err;
|
||||||
|
m_program = CompileProgram(kVS, kFS, &err);
|
||||||
|
ASSERT_NE(m_program, 0u) << err;
|
||||||
|
}
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A VAO whose VBO is STATIC_DRAW (so it resolves resident and the
|
||||||
|
// vertex memo is recorded) and whose EBO is `eboName`.
|
||||||
|
unsigned int MakeVao(unsigned int vbo, unsigned int ebo) {
|
||||||
|
unsigned int vao = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(V), reinterpret_cast<void*>(0));
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(V), reinterpret_cast<void*>(8));
|
||||||
|
glBindVertexArray(0);
|
||||||
|
return vao;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int MakeStaticVbo() {
|
||||||
|
const std::vector<V> vertices = Scene();
|
||||||
|
unsigned int vbo = 0;
|
||||||
|
glGenBuffers(1, &vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(vertices.size() * sizeof(V)), vertices.data(),
|
||||||
|
GL_STATIC_DRAW);
|
||||||
|
return vbo;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Draw(unsigned int vao) {
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glDrawElements(GL_TRIANGLES, kIdx, GL_UNSIGNED_INT, nullptr);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
void Begin() {
|
||||||
|
BindDefaultFramebuffer();
|
||||||
|
ClearTo(0, 0, 0, 1);
|
||||||
|
}
|
||||||
|
Image Read() { return ReadPixels(Gl().Width(), Gl().Height()); }
|
||||||
|
void Halves(const Image& image, const char* left, const char* right, const std::string& when) {
|
||||||
|
const int w = image.Width(), h = image.Height();
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, 2, w / 2 - 2, 2, h - 2, left, 0.0, when + " [left]"));
|
||||||
|
EXPECT_TRUE(RegionIsMostly(image, w / 2 + 2, w - 2, 2, h - 2, right, 0.0, when + " [right]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int m_program = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A: a coherent persistent EBO rewritten on EVERY frame, with no GL call
|
||||||
|
// between the write and the draw. This is the only shape in which an
|
||||||
|
// application changes index data with nothing for the backend to notice.
|
||||||
|
//
|
||||||
|
// COVERS: the coherent-persistent index contract end to end.
|
||||||
|
// DOES NOT COVER: the EBO memo. Instrumented it reaches the cross-frame
|
||||||
|
// branch 11 times and is served its recorded slice all 11 - but the
|
||||||
|
// backend adopted the map into that same storage, so the "stale" slice IS
|
||||||
|
// where the application's writes landed. It would only discriminate on a
|
||||||
|
// stack where AcquirePersistentMap declines (see the file header). A
|
||||||
|
// 96 MiB variant was tried to force that and did not: it cost 40s and
|
||||||
|
// measured the same zero, so it is not kept.
|
||||||
|
TEST_F(ResidentIndexScenario, PersistentCoherentEboWrittenEveryFrame) {
|
||||||
|
const unsigned int vbo = MakeStaticVbo();
|
||||||
|
unsigned int ebo = 0;
|
||||||
|
glGenBuffers(1, &ebo);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||||
|
const GLbitfield storageFlags =
|
||||||
|
GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT | GL_DYNAMIC_STORAGE_BIT;
|
||||||
|
glBufferStorage(GL_ELEMENT_ARRAY_BUFFER, GLsizeiptr(sizeof(kLeft)), kLeft, storageFlags);
|
||||||
|
if (FirstGLError() != GL_NO_ERROR) GTEST_SKIP() << "no immutable storage";
|
||||||
|
auto* map = static_cast<unsigned char*>(glMapBufferRange(
|
||||||
|
GL_ELEMENT_ARRAY_BUFFER, 0, GLsizeiptr(sizeof(kLeft)),
|
||||||
|
GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT));
|
||||||
|
ASSERT_NE(map, nullptr);
|
||||||
|
const unsigned int vao = MakeVao(vbo, ebo);
|
||||||
|
|
||||||
|
for (int frame = 0; frame < 12; ++frame) {
|
||||||
|
Begin();
|
||||||
|
const bool wantRight = (frame % 2) == 1;
|
||||||
|
std::memcpy(map, wantRight ? kRight : kLeft, sizeof(kLeft));
|
||||||
|
Draw(vao);
|
||||||
|
const Image image = Read();
|
||||||
|
Halves(image, wantRight ? "black" : "red", wantRight ? "green" : "black",
|
||||||
|
"frame " + std::to_string(frame) + " of a per-frame coherent EBO rewrite");
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||||
|
glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(1, &ebo);
|
||||||
|
glDeleteBuffers(1, &vbo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// B: usage escalation. The EBO is memoised as an index buffer, then bound
|
||||||
|
// as a VERTEX buffer in a later frame, which forces the backend to
|
||||||
|
// recreate its resident storage carrying the extra usage bit. A memo that
|
||||||
|
// survived that recreate would name a destroyed VkBuffer.
|
||||||
|
//
|
||||||
|
// COVERS: that a storage recreate driven by a DIFFERENT binding point
|
||||||
|
// retires the index memo. Reaches the branch 5 times.
|
||||||
|
TEST_F(ResidentIndexScenario, EboAlsoBoundAsVertexBufferLater) {
|
||||||
|
const unsigned int vbo = MakeStaticVbo();
|
||||||
|
unsigned int ebo = 0;
|
||||||
|
glGenBuffers(1, &ebo);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||||
|
// Big enough to be a legal (if nonsensical) vertex source too.
|
||||||
|
std::vector<GLuint> indices(64, 0);
|
||||||
|
std::memcpy(indices.data(), kLeft, sizeof(kLeft));
|
||||||
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, GLsizeiptr(indices.size() * 4), indices.data(), GL_STATIC_DRAW);
|
||||||
|
const unsigned int vao = MakeVao(vbo, ebo);
|
||||||
|
|
||||||
|
unsigned int vertexUseVao = 0;
|
||||||
|
glGenVertexArrays(1, &vertexUseVao);
|
||||||
|
glBindVertexArray(vertexUseVao);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, ebo); // the EBO, as a vertex source
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(V), reinterpret_cast<void*>(0));
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(V), reinterpret_cast<void*>(8));
|
||||||
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
for (int frame = 0; frame < 6; ++frame) {
|
||||||
|
Begin();
|
||||||
|
Draw(vao);
|
||||||
|
if (frame == 2) Draw(vertexUseVao); // forces the usage escalation
|
||||||
|
const Image image = Read();
|
||||||
|
if (frame != 2) {
|
||||||
|
Halves(image, "red", "black", "frame " + std::to_string(frame) + " around a usage escalation");
|
||||||
|
}
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
glDeleteVertexArrays(1, &vertexUseVao);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(1, &ebo);
|
||||||
|
glDeleteBuffers(1, &vbo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// C: delete the EBO and immediately recreate it, so the frontend
|
||||||
|
// BufferObject may well land at the same address - which is all the memo's
|
||||||
|
// identity check compares. What stops it is that a fresh resource cannot
|
||||||
|
// reproduce an epoch from the process-lifetime counter; this is the test
|
||||||
|
// that says so out loud.
|
||||||
|
//
|
||||||
|
// COVERS: address reuse of a deleted index buffer. Reaches 7, accepts 6 -
|
||||||
|
// the one decline is the post-recreate draw.
|
||||||
|
TEST_F(ResidentIndexScenario, EboDeletedAndRecreatedAtTheSameName) {
|
||||||
|
const unsigned int vbo = MakeStaticVbo();
|
||||||
|
unsigned int ebo = 0;
|
||||||
|
glGenBuffers(1, &ebo);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||||
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, GLsizeiptr(sizeof(kLeft)), kLeft, GL_STATIC_DRAW);
|
||||||
|
unsigned int vao = MakeVao(vbo, ebo);
|
||||||
|
|
||||||
|
for (int frame = 0; frame < 4; ++frame) {
|
||||||
|
Begin();
|
||||||
|
Draw(vao);
|
||||||
|
Halves(Read(), "red", "black", "warmup frame " + std::to_string(frame));
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same VAO, same GL name, different contents.
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(1, &ebo);
|
||||||
|
glGenBuffers(1, &ebo);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||||
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, GLsizeiptr(sizeof(kRight)), kRight, GL_STATIC_DRAW);
|
||||||
|
vao = MakeVao(vbo, ebo);
|
||||||
|
|
||||||
|
for (int frame = 0; frame < 4; ++frame) {
|
||||||
|
Begin();
|
||||||
|
Draw(vao);
|
||||||
|
Halves(Read(), "black", "green", "post-recreate frame " + std::to_string(frame));
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(1, &ebo);
|
||||||
|
glDeleteBuffers(1, &vbo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// D: one resident EBO shared by two VAOs, so two independent memo entries
|
||||||
|
// hold the same recorded slice, mutated through one of them and drawn
|
||||||
|
// through both across frames.
|
||||||
|
//
|
||||||
|
// COVERS: that a mutation retires EVERY memo naming the buffer, not just
|
||||||
|
// the one whose VAO issued it. Reaches 8, accepts 6.
|
||||||
|
TEST_F(ResidentIndexScenario, OneEboTwoVaosMutatedAcrossFrames) {
|
||||||
|
const unsigned int vbo = MakeStaticVbo();
|
||||||
|
unsigned int ebo = 0;
|
||||||
|
glGenBuffers(1, &ebo);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||||
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, GLsizeiptr(sizeof(kLeft)), kLeft, GL_STATIC_DRAW);
|
||||||
|
const unsigned int vaoA = MakeVao(vbo, ebo);
|
||||||
|
const unsigned int vaoB = MakeVao(vbo, ebo);
|
||||||
|
|
||||||
|
for (int frame = 0; frame < 10; ++frame) {
|
||||||
|
Begin();
|
||||||
|
const bool wantRight = frame >= 5;
|
||||||
|
if (frame == 5) {
|
||||||
|
glBindVertexArray(vaoA);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||||
|
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, GLsizeiptr(sizeof(kRight)), kRight);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
Draw((frame % 2) == 0 ? vaoA : vaoB);
|
||||||
|
Halves(Read(), wantRight ? "black" : "red", wantRight ? "green" : "black",
|
||||||
|
"shared-EBO frame " + std::to_string(frame));
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
glDeleteVertexArrays(1, &vaoB);
|
||||||
|
glDeleteVertexArrays(1, &vaoA);
|
||||||
|
glDeleteBuffers(1, &ebo);
|
||||||
|
glDeleteBuffers(1, &vbo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// E: a DYNAMIC_DRAW EBO left untouched long enough for the streaming path
|
||||||
|
// to PROMOTE it onto resident storage, then mutated.
|
||||||
|
//
|
||||||
|
// COVERS: promoted-buffer index freshness across a frame boundary.
|
||||||
|
// DOES NOT COVER: the EBO memo, and this is the useful part - instrumented,
|
||||||
|
// it reaches the cross-frame branch ZERO times. A promoted buffer is SERVED
|
||||||
|
// by AcquireResidentSlice but still ROUTED through the streamed branch of
|
||||||
|
// UploadAndBindIndexBuffer, which never records a memo. That asymmetry is
|
||||||
|
// exactly what makes the EBO half of the shipped fix unobservable, and this
|
||||||
|
// case is the tripwire: memoise the streamed/promoted index path and the
|
||||||
|
// reach stops being zero.
|
||||||
|
TEST_F(ResidentIndexScenario, PromotedDynamicEbo) {
|
||||||
|
const unsigned int vbo = MakeStaticVbo();
|
||||||
|
unsigned int ebo = 0;
|
||||||
|
glGenBuffers(1, &ebo);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||||
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, GLsizeiptr(sizeof(kLeft)), kLeft, GL_DYNAMIC_DRAW);
|
||||||
|
const unsigned int vao = MakeVao(vbo, ebo);
|
||||||
|
|
||||||
|
for (int frame = 0; frame < 10; ++frame) {
|
||||||
|
Begin();
|
||||||
|
Draw(vao);
|
||||||
|
Halves(Read(), "red", "black", "promotion warmup frame " + std::to_string(frame));
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
for (int frame = 0; frame < 6; ++frame) {
|
||||||
|
Begin();
|
||||||
|
if (frame == 0) {
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||||
|
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, GLsizeiptr(sizeof(kRight)), kRight);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
Draw(vao);
|
||||||
|
Halves(Read(), "black", "green", "post-promotion frame " + std::to_string(frame));
|
||||||
|
Gl().EndFrame();
|
||||||
|
}
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteBuffers(1, &ebo);
|
||||||
|
glDeleteBuffers(1, &vbo);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ThreeChannelAttachmentScenario.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 - THREE-CHANNEL COLOUR ATTACHMENTS, on a live driver.
|
||||||
|
//
|
||||||
|
// The bug: no OpenGL ES driver renders to a three-channel image. EXT_render_snorm covers
|
||||||
|
// R/RG/RGBA only, EXT_color_buffer_float excludes RGB16F, and RGB integer formats are not
|
||||||
|
// colour-renderable anywhere. Complementary Reimagined declares colortex1 = RGB8_SNORM and
|
||||||
|
// colortex2 = RGB16F, so every framebuffer Iris built from them answered
|
||||||
|
// GL_FRAMEBUFFER_UNSUPPORTED and Iris refused to load the shaderpack. DirectGLES now stores such
|
||||||
|
// an attachment in its four-channel sibling (GL_RGB8_SNORM -> GL_RGBA16F) and reports the
|
||||||
|
// substitution as a caveat capability, which is what makes glCheckFramebufferStatus say COMPLETE.
|
||||||
|
//
|
||||||
|
// WHY THIS SCENARIO EXISTS RATHER THAN A UNIT TEST. The unit tests in
|
||||||
|
// MG_Test/Framebuffer/FramebufferTest.cpp drive a HAND-BUILT capability cache: they prove the
|
||||||
|
// frontend accepts a caveat capability, and prove the colour-mask/clear discipline that keeps a
|
||||||
|
// widened attachment's stored alpha at 1.0, but they cannot prove that a real driver's probe
|
||||||
|
// actually PRODUCES that caveat. Only a live glCheckFramebufferStatus can, and the answer is
|
||||||
|
// per-driver, not per-platform:
|
||||||
|
//
|
||||||
|
// Mesa llvmpipe (the headless CI driver), ES 3.2, GL_TEXTURE_2D colour attachment:
|
||||||
|
// COMPLETE GL_RGB8, GL_RGB16F, GL_R11F_G11F_B10F, every RGBA*
|
||||||
|
// INCOMPLETE_ATTACHMENT GL_RGB8_SNORM, GL_SRGB8, every RGB integer format
|
||||||
|
// UNSUPPORTED GL_RGB32F
|
||||||
|
//
|
||||||
|
// So the widening is LIVE on llvmpipe - "the desktop build is unaffected" was simply wrong, and
|
||||||
|
// the CI retraces were green before the fix only because retrace ignores what
|
||||||
|
// glCheckFramebufferStatus returns. This scenario is the gate that actually looks.
|
||||||
|
//
|
||||||
|
// DirectGLES only. DirectVulkan's format story is its own (Vulkan exposes R8G8B8_SNORM on almost
|
||||||
|
// nothing, and Magma substitutes on different terms); asserting Espryt's answers there would
|
||||||
|
// only pin a coincidence.
|
||||||
|
|
||||||
|
#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 const char* kVS = R"(#version 330 core
|
||||||
|
in vec2 aPos;
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Two outputs so the mixed case is covered: draw buffer 0 is a natively renderable
|
||||||
|
// four-channel format whose alpha the application owns, draw buffer 1 is the widened
|
||||||
|
// three-channel one whose alpha the format says is 1.0. Both alphas are deliberately
|
||||||
|
// NOT 1.0 in the shader, so an implementation that simply passed the value through would
|
||||||
|
// fail the second assertion.
|
||||||
|
constexpr const char* kFS = R"(#version 330 core
|
||||||
|
layout(location = 0) out vec4 oNative;
|
||||||
|
layout(location = 1) out vec4 oWidened;
|
||||||
|
void main() {
|
||||||
|
oNative = vec4(1.0, 0.0, 0.0, 0.25);
|
||||||
|
oWidened = vec4(0.0, 1.0, 0.0, 0.75);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
constexpr int kSize = 16;
|
||||||
|
|
||||||
|
class ThreeChannelAttachmentScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
if (Gl().BackendName() != "DirectGLES") {
|
||||||
|
GTEST_SKIP() << "three-channel widening is a DirectGLES substitution; 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// THE regression gate for the frontend's answer: this is the exact call Iris makes, and
|
||||||
|
// GL_FRAMEBUFFER_UNSUPPORTED here is the whole shaderpack load failure.
|
||||||
|
TEST_F(ThreeChannelAttachmentScenario, ThreeChannelColorAttachmentsReportComplete) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
// GL_RGB8 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_RGB8), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||||
|
<< "GL_RGB8 is ES-core colour-renderable";
|
||||||
|
|
||||||
|
// Complementary Reimagined's colortex1 and colortex2.
|
||||||
|
EXPECT_EQ(SingleAttachmentStatus(GL_RGB8_SNORM), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||||
|
<< "colortex1 (RGB8_SNORM) must be renderable through the four-channel widening";
|
||||||
|
EXPECT_EQ(SingleAttachmentStatus(GL_RGB16F), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
|
||||||
|
<< "colortex2 (RGB16F) must be renderable, natively or through the widening";
|
||||||
|
|
||||||
|
// The other formats the widening covers. GL_RGB32F only reaches a renderable
|
||||||
|
// four-channel form when EXT_color_buffer_float is present, so a half-float-only
|
||||||
|
// driver legitimately answers UNSUPPORTED for it - see the POST's per-format row.
|
||||||
|
EXPECT_EQ(SingleAttachmentStatus(GL_SRGB8), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
EXPECT_EQ(SingleAttachmentStatus(GL_RGB8UI), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other half: the substitution has to be INVISIBLE. A three-channel format has no
|
||||||
|
// alpha, so GL answers 1.0 for it - and that answer has to hold after a draw that wrote
|
||||||
|
// something else into the widened storage's real alpha channel, which is what the
|
||||||
|
// colour-mask discipline in SyncRenderState is for. GL_DST_ALPHA blending and
|
||||||
|
// glBlitFramebuffer read that stored alpha inside the driver, where no readback fixup can
|
||||||
|
// reach it, so "the storage really holds 1.0" is the only workable invariant.
|
||||||
|
TEST_F(ThreeChannelAttachmentScenario, WidenedAttachmentReadsBackOpaqueWhileItsNeighbourKeepsItsAlpha) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const GLuint program = CompileProgram(kVS, kFS, &error);
|
||||||
|
ASSERT_NE(program, 0u) << error;
|
||||||
|
|
||||||
|
const GLuint nativeTexture = MakeTexture(GL_RGBA16F);
|
||||||
|
const GLuint widenedTexture = MakeTexture(GL_RGB8_SNORM);
|
||||||
|
ASSERT_NE(nativeTexture, 0u);
|
||||||
|
ASSERT_NE(widenedTexture, 0u);
|
||||||
|
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, nativeTexture, 0);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, widenedTexture, 0);
|
||||||
|
const GLenum drawBuffers[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
|
||||||
|
glDrawBuffers(2, drawBuffers);
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
|
||||||
|
|
||||||
|
glViewport(0, 0, kSize, kSize);
|
||||||
|
// Alpha 0.0 on purpose: the widened attachment must come back 1.0 anyway, and the
|
||||||
|
// native one must come back 0.0 where the draw does not cover it.
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
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);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
std::vector<float> pixels(static_cast<std::size_t>(kSize) * kSize * 4, -1.0f);
|
||||||
|
|
||||||
|
glReadBuffer(GL_COLOR_ATTACHMENT1);
|
||||||
|
glReadPixels(0, 0, kSize, kSize, GL_RGBA, GL_FLOAT, pixels.data());
|
||||||
|
EXPECT_NEAR(pixels[0], 0.0f, 0.02f) << "widened attachment red";
|
||||||
|
EXPECT_NEAR(pixels[1], 1.0f, 0.02f) << "widened attachment green";
|
||||||
|
EXPECT_NEAR(pixels[2], 0.0f, 0.02f) << "widened attachment blue";
|
||||||
|
EXPECT_NEAR(pixels[3], 1.0f, 0.001f)
|
||||||
|
<< "a three-channel format has no alpha channel, so GL must report 1.0 for it";
|
||||||
|
|
||||||
|
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||||
|
glReadPixels(0, 0, kSize, kSize, GL_RGBA, GL_FLOAT, pixels.data());
|
||||||
|
EXPECT_NEAR(pixels[0], 1.0f, 0.02f) << "native attachment red";
|
||||||
|
EXPECT_NEAR(pixels[3], 0.25f, 0.02f)
|
||||||
|
<< "the alpha discipline must not leak onto a natively renderable attachment";
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteBuffers(1, &vbo);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteTextures(1, &nativeTexture);
|
||||||
|
glDeleteTextures(1, &widenedTexture);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The case above can be satisfied by the readback fixup alone (ForceWideReadAlphaToOne
|
||||||
|
// rewrites glReadPixels' alpha), so it does NOT prove the STORED alpha is 1.0. This one
|
||||||
|
// does, by asking the driver to read that alpha itself: GL_DST_ALPHA blending multiplies
|
||||||
|
// by the destination alpha inside the raster pipeline, where nothing MobileGL does can
|
||||||
|
// intervene. Same reason GL_ONE_MINUS_DST_ALPHA and glBlitFramebuffer are covered for
|
||||||
|
// free once this holds - and the reason the discipline is a write mask rather than a
|
||||||
|
// readback patch.
|
||||||
|
//
|
||||||
|
// Ablation-checked on llvmpipe, each half separately: disable the alpha doctoring in
|
||||||
|
// SyncRenderState and the opaque draw leaves 0.25 in the stored alpha; disable the clear
|
||||||
|
// substitution in Clear() and it stays at the application's 0.0. Either way this case
|
||||||
|
// reads back the wrong number, which is what makes it a gate rather than a description.
|
||||||
|
TEST_F(ThreeChannelAttachmentScenario, DstAlphaBlendingSeesOneInAWidenedAttachment) {
|
||||||
|
if (!Ready() || IsSkipped()) return;
|
||||||
|
|
||||||
|
static constexpr const char* kSingleOutFS = R"(#version 330 core
|
||||||
|
out vec4 oColor;
|
||||||
|
uniform vec4 uColor;
|
||||||
|
void main() { oColor = uColor; }
|
||||||
|
)";
|
||||||
|
std::string error;
|
||||||
|
const GLuint program = CompileProgram(kVS, kSingleOutFS, &error);
|
||||||
|
ASSERT_NE(program, 0u) << error;
|
||||||
|
const GLint colorLocation = glGetUniformLocation(program, "uColor");
|
||||||
|
ASSERT_GE(colorLocation, 0);
|
||||||
|
|
||||||
|
const GLuint widenedTexture = MakeTexture(GL_RGB8_SNORM);
|
||||||
|
ASSERT_NE(widenedTexture, 0u);
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, widenedTexture, 0);
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_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);
|
||||||
|
glViewport(0, 0, kSize, kSize);
|
||||||
|
|
||||||
|
// The clear's alpha is 0.0 and the draw's is 0.25 - neither is the 1.0 the format
|
||||||
|
// implies, so both halves of the discipline have to fire for the blend below to see
|
||||||
|
// 1.0: the clear substitutes it, and the draw is masked away from it.
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glUniform4f(colorLocation, 0.0f, 1.0f, 0.0f, 0.25f);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
// dst = stored alpha; src factor GL_DST_ALPHA, dst factor GL_ZERO, source white
|
||||||
|
// => the destination colour becomes (storedAlpha, storedAlpha, storedAlpha).
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_DST_ALPHA, GL_ZERO);
|
||||||
|
glUniform4f(colorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
|
||||||
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
|
||||||
|
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());
|
||||||
|
EXPECT_NEAR(pixels[0], 1.0f, 0.02f)
|
||||||
|
<< "GL_DST_ALPHA read the stored alpha of a three-channel attachment; it must be 1.0";
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteBuffers(1, &vbo);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteTextures(1, &widenedTexture);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
EXPECT_EQ(FirstGLError(), 0u) << GLErrorName(FirstGLError());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,584 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/XfbAfterClipDistanceScenario.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 F - a draw must never read a destroyed object's memoised state.
|
||||||
|
//
|
||||||
|
// Distilled from the order-triggered CTS failure: on DirectVulkan, once
|
||||||
|
// KHR-GLxx.clip_distance.functional had run in the same process, every later
|
||||||
|
// transform_feedback CAPTURE case failed. It looked like a transform feedback
|
||||||
|
// bug and is not one. The capture works; the DRAW being captured fetched its
|
||||||
|
// vertices from the WRONG BUFFER - the one the clip workload had just deleted.
|
||||||
|
//
|
||||||
|
// The mechanism, and why the sequence matters. DirectVulkan memoises a VAO's
|
||||||
|
// resolved Vulkan vertex bindings in a table keyed on the VertexArrayObject's
|
||||||
|
// heap ADDRESS, validated by a content hash that folds in the bound
|
||||||
|
// BufferObject's heap ADDRESS. Both are recycled by the allocator, so when the
|
||||||
|
// workload's VAO and vertex buffer are destroyed and the capture phase's own
|
||||||
|
// VAO and vertex buffer are allocated onto their addresses under a
|
||||||
|
// byte-identical attribute layout (one vec4 float array at location 0 - which
|
||||||
|
// is what both phases use), the key matches, the hash matches, and the memo
|
||||||
|
// hands the new draw the dead buffer's GPU slice. Nothing about transform
|
||||||
|
// feedback is involved: capture just makes the wrong vertices legible, because
|
||||||
|
// the captured record IS the vertex data. The fix gives VertexArrayObject and
|
||||||
|
// BufferObject never-reused lifetime ids and keys the memo on those.
|
||||||
|
//
|
||||||
|
// MOBILEGL_ASYNC_SHADER_COMPILE is not part of the defect. It shifts the
|
||||||
|
// allocation pattern, so it changes WHICH stop points below land on a recycled
|
||||||
|
// address - which is why the CTS saw ~100% incidence with it on and ~2% with it
|
||||||
|
// off, and why the sweep case matters more than any single stop point.
|
||||||
|
//
|
||||||
|
// The shapes are the two CTS cases verbatim in structure:
|
||||||
|
// * the workload is glcClipDistance.cpp FunctionalTest's inner loop (a program
|
||||||
|
// per (redeclaration, clip count), glEnable(GL_CLIP_DISTANCEi), an FBO per
|
||||||
|
// primitive type, a draw and a readback), including its early-return
|
||||||
|
// behaviour: on failure the test returns WITHOUT running its "clip clean"
|
||||||
|
// loop, so GL_CLIP_DISTANCE0..N-1 stay enabled for the rest of the process.
|
||||||
|
// That leftover enable state is NOT the carrier (one of the cases below pins
|
||||||
|
// that); the object churn is.
|
||||||
|
// * the victim is gl3cTransformFeedback3Tests.cpp's skip_components: a
|
||||||
|
// gl_SkipComponents capture layout under GL_RASTERIZER_DISCARD, read back
|
||||||
|
// out of a buffer pre-filled with -1-i so that "captured nothing" is
|
||||||
|
// distinguishable from "captured the wrong thing".
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#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
|
||||||
|
|
||||||
|
#ifndef GL_CLIP_DISTANCE0
|
||||||
|
#define GL_CLIP_DISTANCE0 0x3000
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links a vertex/fragment pair, optionally declaring transform feedback
|
||||||
|
// varyings first (glTransformFeedbackVaryings takes effect at the next link,
|
||||||
|
// exactly as the CTS uses it).
|
||||||
|
GLuint BuildProgram(const std::string& vertexSource, const std::string& fragmentSource,
|
||||||
|
const std::vector<const char*>& xfbVaryings, GLenum bufferMode, std::string* log) {
|
||||||
|
const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log);
|
||||||
|
if (vertexShader == 0) return 0;
|
||||||
|
const GLuint fragmentShader = CompileShader(GL_FRAGMENT_SHADER, fragmentSource, log);
|
||||||
|
if (fragmentShader == 0) {
|
||||||
|
glDeleteShader(vertexShader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, vertexShader);
|
||||||
|
glAttachShader(program, fragmentShader);
|
||||||
|
if (!xfbVaryings.empty()) {
|
||||||
|
glTransformFeedbackVaryings(program, static_cast<GLsizei>(xfbVaryings.size()), xfbVaryings.data(),
|
||||||
|
bufferMode);
|
||||||
|
}
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDeleteShader(vertexShader);
|
||||||
|
glDeleteShader(fragmentShader);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- poison
|
||||||
|
|
||||||
|
// glcClipDistance.cpp FunctionalTest::m_vertex_shader_code with the same
|
||||||
|
// three substitutions (redeclaration, clip function, array setter).
|
||||||
|
std::string ClipVertexSource(bool redeclaration, unsigned clipCount, unsigned clipFunction,
|
||||||
|
unsigned vertexCount) {
|
||||||
|
const std::string count = std::to_string(clipCount);
|
||||||
|
std::string source = "#version 400 core\n\n";
|
||||||
|
if (redeclaration) {
|
||||||
|
source += "out float gl_ClipDistance[" + count + "];\n";
|
||||||
|
}
|
||||||
|
source += "\n";
|
||||||
|
switch (clipFunction) {
|
||||||
|
case 0:
|
||||||
|
source += "float f(int i)\n{\n return 0.0;\n}\n";
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
source += "float f(int i)\n{\n return 0.25 + 0.75 * (float(i) + 1.0) * (float(gl_VertexID) + 1.0)"
|
||||||
|
" / (float(" + count + ") * float(" + std::to_string(vertexCount) + "));\n}\n";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
source += "float f(int i)\n{\n return - 0.25 - 0.75 * (float(i) + 1.0) * (float(gl_VertexID) + 1.0)"
|
||||||
|
" / (float(" + count + ") * float(" + std::to_string(vertexCount) + "));\n}\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
source += "\nin vec4 position;\n\nvoid main()\n{\n";
|
||||||
|
if (redeclaration) {
|
||||||
|
// Dynamic array setter.
|
||||||
|
source += " for(int i = 0; i < " + count + "; i++)\n {\n"
|
||||||
|
" gl_ClipDistance[i] = f(i);\n }\n";
|
||||||
|
} else {
|
||||||
|
// Static array setter, at the highest index this iteration enables.
|
||||||
|
const std::string index = std::to_string(clipCount - 1);
|
||||||
|
source += " gl_ClipDistance[" + index + "] = f(" + index + ");\n";
|
||||||
|
}
|
||||||
|
source += "\n gl_Position = position;\n}\n";
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* kClipFragmentSource = R"(#version 400 core
|
||||||
|
|
||||||
|
out vec4 color;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
color = vec4(1.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// How far into FunctionalTest's loop nest to get before bailing out the way
|
||||||
|
// the CTS does on a failed check: return immediately, skipping the "clip
|
||||||
|
// clean" loop that would have disabled GL_CLIP_DISTANCEi again.
|
||||||
|
struct ClipStopPoint {
|
||||||
|
unsigned primitiveIndex = 0; // 0 = POINTS, 1 = LINES, 2 = TRIANGLES
|
||||||
|
unsigned clipFunction = 0;
|
||||||
|
bool redeclaration = false;
|
||||||
|
unsigned clipCount = 1; // 1..8, the iteration that "fails"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Runs FunctionalTest's loop nest up to and including `stop`, then returns
|
||||||
|
// leaving exactly the state the CTS leaves behind on a failure.
|
||||||
|
void RunClipDistanceWorkload(const ClipStopPoint& stop) {
|
||||||
|
static const GLenum kPrimitiveTypes[] = {GL_POINTS, GL_LINES, GL_TRIANGLES};
|
||||||
|
static const GLsizei kPrimitiveIndices[] = {1, 2, 3};
|
||||||
|
static const float kPositions[3][12] = {
|
||||||
|
{0.0f, 0.0f, 0.0f, 1.0f},
|
||||||
|
{-1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f},
|
||||||
|
{-1.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (unsigned primitiveIndex = 0; primitiveIndex <= stop.primitiveIndex; ++primitiveIndex) {
|
||||||
|
const GLenum primitiveType = kPrimitiveTypes[primitiveIndex];
|
||||||
|
const GLsizei vertexCount = kPrimitiveIndices[primitiveIndex];
|
||||||
|
const GLsizei framebufferSize = (primitiveType == GL_POINTS) ? 1 : 32;
|
||||||
|
|
||||||
|
GLuint colorBuffer = 0;
|
||||||
|
GLuint framebuffer = 0;
|
||||||
|
glGenRenderbuffers(1, &colorBuffer);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, colorBuffer);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, framebufferSize, framebufferSize);
|
||||||
|
glGenFramebuffers(1, &framebuffer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorBuffer);
|
||||||
|
glViewport(0, 0, framebufferSize, framebufferSize);
|
||||||
|
|
||||||
|
const unsigned lastFunction =
|
||||||
|
(primitiveIndex == stop.primitiveIndex) ? stop.clipFunction : 2u;
|
||||||
|
for (unsigned clipFunction = 0; clipFunction <= lastFunction; ++clipFunction) {
|
||||||
|
const bool atStopFunction =
|
||||||
|
primitiveIndex == stop.primitiveIndex && clipFunction == stop.clipFunction;
|
||||||
|
for (unsigned redeclaration = 0; redeclaration < 2; ++redeclaration) {
|
||||||
|
const bool atStopRedeclaration =
|
||||||
|
atStopFunction && (redeclaration != 0) == stop.redeclaration;
|
||||||
|
const unsigned lastCount = atStopRedeclaration ? stop.clipCount : 8u;
|
||||||
|
for (unsigned clipCount = 1; clipCount <= lastCount; ++clipCount) {
|
||||||
|
std::string log;
|
||||||
|
const GLuint program =
|
||||||
|
BuildProgram(ClipVertexSource(redeclaration != 0, clipCount, clipFunction,
|
||||||
|
static_cast<unsigned>(vertexCount)),
|
||||||
|
kClipFragmentSource, {}, GL_INTERLEAVED_ATTRIBS, &log);
|
||||||
|
if (program == 0) continue;
|
||||||
|
glUseProgram(program);
|
||||||
|
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
glEnable(GL_CLIP_DISTANCE0 + clipCount - 1);
|
||||||
|
|
||||||
|
GLuint vao = 0;
|
||||||
|
GLuint vbo = 0;
|
||||||
|
glGenVertexArrays(1, &vao);
|
||||||
|
glBindVertexArray(vao);
|
||||||
|
glGenBuffers(1, &vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER,
|
||||||
|
static_cast<GLsizeiptr>(sizeof(float) * 4 * vertexCount),
|
||||||
|
kPositions[primitiveIndex], GL_STATIC_DRAW);
|
||||||
|
const GLint location = glGetAttribLocation(program, "position");
|
||||||
|
if (location >= 0) {
|
||||||
|
glEnableVertexAttribArray(static_cast<GLuint>(location));
|
||||||
|
glVertexAttribPointer(static_cast<GLuint>(location), 4, GL_FLOAT, GL_FALSE, 0,
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
glDrawArrays(primitiveType, 0, vertexCount);
|
||||||
|
|
||||||
|
std::vector<unsigned char> pixels(
|
||||||
|
static_cast<std::size_t>(framebufferSize) * framebufferSize * 4, 0);
|
||||||
|
glReadPixels(0, 0, framebufferSize, framebufferSize, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
pixels.data());
|
||||||
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glUseProgram(0);
|
||||||
|
// MGL_REPRO_KEEPCLIPOBJ leaks the per-iteration objects so
|
||||||
|
// no GL name and no heap address can be recycled into the
|
||||||
|
// capture phase.
|
||||||
|
// Deleting all three is load-bearing, not tidiness: the defect
|
||||||
|
// this scenario pins needs the VAO's AND its vertex buffer's heap
|
||||||
|
// addresses to be freed here so the capture phase's own objects
|
||||||
|
// can be handed the same ones back.
|
||||||
|
glDeleteBuffers(1, &vbo);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
|
||||||
|
if (atStopRedeclaration && clipCount == stop.clipCount) {
|
||||||
|
// The CTS's early return: the "clip clean" loop below
|
||||||
|
// never runs, so the enables survive.
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &framebuffer);
|
||||||
|
glDeleteRenderbuffers(1, &colorBuffer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (unsigned i = 0; i < 8; ++i) {
|
||||||
|
glDisable(GL_CLIP_DISTANCE0 + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &framebuffer);
|
||||||
|
glDeleteRenderbuffers(1, &colorBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- victim
|
||||||
|
|
||||||
|
// gl3cTransformFeedback3Tests.cpp TransformFeedbackBaseTestCase::m_shader_vert.
|
||||||
|
const char* kXfbVertexSource = R"(#version 400 core
|
||||||
|
in vec4 vertex;
|
||||||
|
out vec4 value1;
|
||||||
|
out vec4 value2;
|
||||||
|
out vec4 value3;
|
||||||
|
out vec4 value4;
|
||||||
|
|
||||||
|
void main (void)
|
||||||
|
{
|
||||||
|
vec4 temp = vertex;
|
||||||
|
|
||||||
|
gl_Position = temp;
|
||||||
|
|
||||||
|
value1 = abs(temp) * 1.0;
|
||||||
|
value2 = abs(temp) * 2.0;
|
||||||
|
value3 = abs(temp) * 3.0;
|
||||||
|
value4 = abs(temp) * 4.0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kXfbFragmentSource = R"(#version 400 core
|
||||||
|
out vec4 color;
|
||||||
|
void main (void)
|
||||||
|
{
|
||||||
|
color = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The skip_components capture layout, verbatim.
|
||||||
|
std::vector<const char*> SkipComponentsVaryings() {
|
||||||
|
return {"gl_SkipComponents1", "value1", "gl_SkipComponents2", "gl_SkipComponents1", "value2",
|
||||||
|
"gl_SkipComponents3", "gl_SkipComponents2", "value3", "gl_SkipComponents4", "value4"};
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr unsigned kSkipComponentCount = 4 * 4 + (1 + 2 + 3 + 4 + 1 + 2); // 16 values + 13 skipped
|
||||||
|
constexpr unsigned kSkipVertexCount = 6;
|
||||||
|
|
||||||
|
// Runs skip_components and reports what came back. `outCaptured` is the raw
|
||||||
|
// readback so a failure can say whether anything was written at all.
|
||||||
|
void RunSkipComponentsCapture(std::vector<float>& outCaptured, std::string* buildLog) {
|
||||||
|
outCaptured.clear();
|
||||||
|
|
||||||
|
const GLuint program = BuildProgram(kXfbVertexSource, kXfbFragmentSource, SkipComponentsVaryings(),
|
||||||
|
GL_INTERLEAVED_ATTRIBS, buildLog);
|
||||||
|
ASSERT_NE(program, 0u) << "skip_components program failed to link: " << (buildLog ? *buildLog : "");
|
||||||
|
glUseProgram(program);
|
||||||
|
|
||||||
|
const std::vector<float> vertices = {
|
||||||
|
-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -2.0f, 1.0f, -1.0f, 1.0f, -3.0f, 1.0f,
|
||||||
|
1.0f, 1.0f, 4.0f, 1.0f, -1.0f, 1.0f, 5.0f, 1.0f, 1.0f, -1.0f, 6.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, static_cast<GLsizeiptr>(sizeof(float) * vertices.size()), vertices.data(),
|
||||||
|
GL_STATIC_DRAW);
|
||||||
|
const GLint location = glGetAttribLocation(program, "vertex");
|
||||||
|
if (location >= 0) {
|
||||||
|
glEnableVertexAttribArray(static_cast<GLuint>(location));
|
||||||
|
glVertexAttribPointer(static_cast<GLuint>(location), 4, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned floatCount = kSkipVertexCount * kSkipComponentCount;
|
||||||
|
const GLsizeiptr byteSize = static_cast<GLsizeiptr>(sizeof(float) * floatCount);
|
||||||
|
|
||||||
|
GLuint captureBuffer = 0;
|
||||||
|
glGenBuffers(1, &captureBuffer);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, captureBuffer);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, byteSize, nullptr, GL_STATIC_READ);
|
||||||
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
|
||||||
|
// The pre-fill that makes "nothing was captured" recognisable.
|
||||||
|
std::vector<float> prefill(floatCount);
|
||||||
|
for (unsigned i = 0; i < floatCount; ++i) {
|
||||||
|
prefill[i] = -1.0f - static_cast<float>(i);
|
||||||
|
}
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, captureBuffer);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, byteSize, prefill.data(), GL_STATIC_DRAW);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
|
||||||
|
glEnable(GL_RASTERIZER_DISCARD);
|
||||||
|
glClearColor(0.1f, 0.0f, 0.5f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer);
|
||||||
|
glBeginTransformFeedback(GL_TRIANGLES);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(kSkipVertexCount));
|
||||||
|
glEndTransformFeedback();
|
||||||
|
glDisable(GL_RASTERIZER_DISCARD);
|
||||||
|
|
||||||
|
outCaptured.resize(floatCount);
|
||||||
|
glBindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, captureBuffer, 0, byteSize);
|
||||||
|
const void* mapped = glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, byteSize, GL_MAP_READ_BIT);
|
||||||
|
if (mapped != nullptr) {
|
||||||
|
std::memcpy(outCaptured.data(), mapped, static_cast<std::size_t>(byteSize));
|
||||||
|
glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);
|
||||||
|
}
|
||||||
|
|
||||||
|
glDisableVertexAttribArray(0);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
glDeleteBuffers(1, &vbo);
|
||||||
|
glDeleteBuffers(1, &captureBuffer);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glDeleteVertexArrays(1, &vao);
|
||||||
|
glUseProgram(0);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip_components' expected buffer: the 13 skipped components keep their
|
||||||
|
// pre-fill, the 16 captured ones carry |vertex| * n.
|
||||||
|
std::vector<float> SkipComponentsExpected() {
|
||||||
|
const std::vector<float> vertices = {
|
||||||
|
-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -2.0f, 1.0f, -1.0f, 1.0f, -3.0f, 1.0f,
|
||||||
|
1.0f, 1.0f, 4.0f, 1.0f, -1.0f, 1.0f, 5.0f, 1.0f, 1.0f, -1.0f, 6.0f, 1.0f,
|
||||||
|
};
|
||||||
|
const unsigned floatCount = kSkipVertexCount * kSkipComponentCount;
|
||||||
|
std::vector<float> expected(floatCount);
|
||||||
|
for (unsigned i = 0; i < floatCount; ++i) {
|
||||||
|
expected[i] = -1.0f - static_cast<float>(i);
|
||||||
|
}
|
||||||
|
// Record layout, in floats:
|
||||||
|
// [0] skip1
|
||||||
|
// [1..4] value1
|
||||||
|
// [5..7] skip2 + skip1
|
||||||
|
// [8..11] value2
|
||||||
|
// [12..16] skip3 + skip2
|
||||||
|
// [17..20] value3
|
||||||
|
// [21..24] skip4
|
||||||
|
// [25..28] value4
|
||||||
|
static const unsigned kValueOffsets[4] = {1, 8, 17, 25};
|
||||||
|
for (unsigned v = 0; v < kSkipVertexCount; ++v) {
|
||||||
|
const unsigned base = v * kSkipComponentCount;
|
||||||
|
for (unsigned value = 0; value < 4; ++value) {
|
||||||
|
for (unsigned component = 0; component < 4; ++component) {
|
||||||
|
const float source = vertices[v * 4 + component];
|
||||||
|
expected[base + kValueOffsets[value] + component] =
|
||||||
|
std::fabs(source) * static_cast<float>(value + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reports the first mismatch, and whether the readback is byte-for-byte the
|
||||||
|
// pre-fill (i.e. the capture never happened).
|
||||||
|
::testing::AssertionResult CheckSkipComponents(const std::vector<float>& captured) {
|
||||||
|
const std::vector<float> expected = SkipComponentsExpected();
|
||||||
|
if (captured.size() != expected.size()) {
|
||||||
|
return ::testing::AssertionFailure()
|
||||||
|
<< "readback size " << captured.size() << " != " << expected.size();
|
||||||
|
}
|
||||||
|
bool anyWritten = false;
|
||||||
|
for (std::size_t i = 0; i < captured.size(); ++i) {
|
||||||
|
if (captured[i] != -1.0f - static_cast<float>(i)) {
|
||||||
|
anyWritten = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < expected.size(); ++i) {
|
||||||
|
if (std::fabs(captured[i] - expected[i]) > 0.0125f) {
|
||||||
|
return ::testing::AssertionFailure()
|
||||||
|
<< "capture mismatch at index " << i << ": got " << captured[i] << ", expected "
|
||||||
|
<< expected[i] << (anyWritten ? "" : " (the whole buffer is still the pre-fill: "
|
||||||
|
"NOTHING was captured)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ::testing::AssertionSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The harness turns "no context came up" into a clean skip, and a skip is
|
||||||
|
// indistinguishable from a pass in a ctest summary. For this scenario that
|
||||||
|
// is a hole rather than a courtesy: the defect it pins is DirectVulkan's
|
||||||
|
// alone, and DirectVulkan now comes up headless on any machine at all - a
|
||||||
|
// surfaceless EGL platform over a software ICD (lavapipe) is enough. So
|
||||||
|
// "DirectVulkan did not initialise" here means the run is MISCONFIGURED,
|
||||||
|
// not that the machine has no GPU, and it must not report green.
|
||||||
|
//
|
||||||
|
// Local on purpose: the harness-wide skip semantics are deliberate
|
||||||
|
// (ScenarioFixture.h states the reasoning), and MOBILEGL_ITEST_REQUIRE_GPU
|
||||||
|
// is the harness-wide lever for the same intent - but that lever also
|
||||||
|
// demands a HARDWARE renderer, which is exactly what a lavapipe-only box
|
||||||
|
// cannot offer. This overrides nothing else: only this scenario, only for
|
||||||
|
// the backend that can regress, and only for the unusable-harness case.
|
||||||
|
class XfbAfterClipDistanceScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
// Ready() is false on the base's skip path AND on its REQUIRE_GPU
|
||||||
|
// failure path; the second one has already failed, so leave it alone
|
||||||
|
// rather than burying its reason under a second message.
|
||||||
|
if (Ready() || HasFatalFailure()) return;
|
||||||
|
if (Gl().BackendName() == "DirectVulkan") {
|
||||||
|
FAIL() << "DirectVulkan could not be brought up, so the regression this scenario guards - a "
|
||||||
|
"draw served a destroyed VAO's memoised vertex bindings - was never exercised, and "
|
||||||
|
"that must be a failure rather than a silent skip. Headless bring-up needs only a "
|
||||||
|
"Vulkan ICD and a surfaceless EGL platform (a software ICD such as lavapipe "
|
||||||
|
"qualifies: VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json with "
|
||||||
|
"EGL_PLATFORM=surfaceless). Harness reason: "
|
||||||
|
<< Gl().SkipReason();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Control: the capture on its own must work.
|
||||||
|
TEST_F(XfbAfterClipDistanceScenario, SkipComponentsCaptureAlone) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
std::vector<float> captured;
|
||||||
|
std::string log;
|
||||||
|
RunSkipComponentsCapture(captured, &log);
|
||||||
|
EXPECT_TRUE(CheckSkipComponents(captured));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bisection step 1: only the leftover GL_CLIP_DISTANCEi enables.
|
||||||
|
TEST_F(XfbAfterClipDistanceScenario, SkipComponentsCaptureAfterClipDistanceEnables) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
for (unsigned i = 0; i < 8; ++i) {
|
||||||
|
glEnable(GL_CLIP_DISTANCE0 + i);
|
||||||
|
}
|
||||||
|
std::vector<float> captured;
|
||||||
|
std::string log;
|
||||||
|
RunSkipComponentsCapture(captured, &log);
|
||||||
|
for (unsigned i = 0; i < 8; ++i) {
|
||||||
|
glDisable(GL_CLIP_DISTANCE0 + i);
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(CheckSkipComponents(captured));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bisection step 2: the whole clip_distance.functional workload, stopped
|
||||||
|
// where the CTS stopped in the runs that went on to break the capture.
|
||||||
|
TEST_F(XfbAfterClipDistanceScenario, SkipComponentsCaptureAfterClipDistanceWorkloadLines8) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
RunClipDistanceWorkload({.primitiveIndex = 1, .clipFunction = 0, .redeclaration = false, .clipCount = 8});
|
||||||
|
std::vector<float> captured;
|
||||||
|
std::string log;
|
||||||
|
RunSkipComponentsCapture(captured, &log);
|
||||||
|
for (unsigned i = 0; i < 8; ++i) {
|
||||||
|
glDisable(GL_CLIP_DISTANCE0 + i);
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(CheckSkipComponents(captured));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(XfbAfterClipDistanceScenario, SkipComponentsCaptureAfterClipDistanceWorkloadPoints1) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
RunClipDistanceWorkload({.primitiveIndex = 0, .clipFunction = 0, .redeclaration = true, .clipCount = 1});
|
||||||
|
std::vector<float> captured;
|
||||||
|
std::string log;
|
||||||
|
RunSkipComponentsCapture(captured, &log);
|
||||||
|
for (unsigned i = 0; i < 8; ++i) {
|
||||||
|
glDisable(GL_CLIP_DISTANCE0 + i);
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(CheckSkipComponents(captured));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single stop point is not a regression test for this defect: whether the
|
||||||
|
// capture phase's VAO and vertex buffer land on the addresses the workload just
|
||||||
|
// freed is a function of how much the workload allocated, so the two cases above
|
||||||
|
// pin two draws of a lottery. Sweep the grid instead - before the fix, roughly a
|
||||||
|
// third of these stop points came back holding the workload's vertex data.
|
||||||
|
TEST_F(XfbAfterClipDistanceScenario, SkipComponentsCaptureSurvivesEveryClipWorkloadStopPoint) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
for (unsigned primitiveIndex = 0; primitiveIndex < 3; ++primitiveIndex) {
|
||||||
|
for (unsigned redeclaration = 0; redeclaration < 2; ++redeclaration) {
|
||||||
|
for (const unsigned clipCount : {1u, 4u, 8u}) {
|
||||||
|
RunClipDistanceWorkload({.primitiveIndex = primitiveIndex,
|
||||||
|
.clipFunction = 0,
|
||||||
|
.redeclaration = redeclaration != 0,
|
||||||
|
.clipCount = clipCount});
|
||||||
|
std::vector<float> captured;
|
||||||
|
std::string log;
|
||||||
|
RunSkipComponentsCapture(captured, &log);
|
||||||
|
for (unsigned i = 0; i < 8; ++i) {
|
||||||
|
glDisable(GL_CLIP_DISTANCE0 + i);
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(CheckSkipComponents(captured))
|
||||||
|
<< " (stop point: primitive " << primitiveIndex << ", redeclaration " << redeclaration
|
||||||
|
<< ", clip count " << clipCount << ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Run the headless MobileGL integration scenarios on one backend:
|
||||||
|
# ./run_integration_test.sh espryt [gtest args...] -> DirectGLES
|
||||||
|
# ./run_integration_test.sh magma [gtest args...] -> DirectVulkan
|
||||||
|
#
|
||||||
|
# The backend is latched at initialization from MOBILEGL_BACKEND_TYPE, so one
|
||||||
|
# process is one backend; this script is the dev-box equivalent of the two ctest
|
||||||
|
# registrations in CMakeLists.txt.
|
||||||
|
#
|
||||||
|
# Pin the vendor libraries explicitly, for the same reason
|
||||||
|
# MG_Benchmark/Driver/run_driver_bench.sh does: a bare libEGL on a glvnd system
|
||||||
|
# resolves to whatever vendor comes first, which is usually Mesa/llvmpipe - a
|
||||||
|
# software rasteriser silently replacing the GPU under a GPU test. Override
|
||||||
|
# MGL_EGL_VENDOR / MGL_VK_ICD to test another driver.
|
||||||
|
#
|
||||||
|
# Set MOBILEGL_ITEST_REQUIRE_GPU=1 to turn "the harness is unusable" from a clean
|
||||||
|
# skip into a failure. Do that anywhere the machine is supposed to have a GPU: a
|
||||||
|
# run that skipped everything and a run that passed everything are otherwise the
|
||||||
|
# same green, so without it a broken driver pinning is invisible.
|
||||||
|
set -eu
|
||||||
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
BIN=${MOBILEGL_ITEST_BIN:-$HERE/MobileGLIntegrationTest}
|
||||||
|
EGL_VENDOR=${MGL_EGL_VENDOR:-/usr/share/glvnd/egl_vendor.d/10_nvidia.json}
|
||||||
|
VK_ICD=${MGL_VK_ICD:-/usr/share/vulkan/icd.d/nvidia_icd.x86_64.json}
|
||||||
|
MODE=$1; shift
|
||||||
|
|
||||||
|
if [ ! -x "$BIN" ]; then
|
||||||
|
echo "MobileGLIntegrationTest not found at $BIN"
|
||||||
|
echo "configure with -DMOBILEGL_BUILD_INTEGRATION_TEST=ON and set MOBILEGL_ITEST_BIN"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -r "$EGL_VENDOR" ] && export __EGL_VENDOR_LIBRARY_FILENAMES=$EGL_VENDOR
|
||||||
|
export EGL_PLATFORM=${EGL_PLATFORM:-x11}
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
espryt|DirectGLES)
|
||||||
|
export MOBILEGL_BACKEND_TYPE=DirectGLES
|
||||||
|
;;
|
||||||
|
magma|DirectVulkan)
|
||||||
|
export MOBILEGL_BACKEND_TYPE=DirectVulkan
|
||||||
|
[ -r "$VK_ICD" ] && export VK_ICD_FILENAMES=$VK_ICD
|
||||||
|
;;
|
||||||
|
*) echo "unknown mode: $MODE (espryt|magma)"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
export MOBILEGL_ITEST_REQUIRE_GPU=${MOBILEGL_ITEST_REQUIRE_GPU:-}
|
||||||
|
exec "$BIN" "$@"
|
||||||
@@ -8,9 +8,17 @@
|
|||||||
|
|
||||||
#include "BufferObject.h"
|
#include "BufferObject.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
namespace MobileGL::MG_State::GLState {
|
namespace MobileGL::MG_State::GLState {
|
||||||
namespace {
|
namespace {
|
||||||
const BufferBackendOps* g_bufferBackendOps = nullptr;
|
const BufferBackendOps* g_bufferBackendOps = nullptr;
|
||||||
|
// Starts at 1 so a zero-initialized cache slot can never carry a live buffer's id.
|
||||||
|
std::atomic<Uint64> g_nextBufferLifetimeId{1};
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 BufferObject::AllocateLifetimeId() {
|
||||||
|
return g_nextBufferLifetimeId.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetBufferBackendOps(const BufferBackendOps* ops) {
|
void SetBufferBackendOps(const BufferBackendOps* ops) {
|
||||||
@@ -41,6 +49,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
void BufferObject::NotifySubData(SizeT offset, SizeT size) {
|
void BufferObject::NotifySubData(SizeT offset, SizeT size) {
|
||||||
++m_changeSerial;
|
++m_changeSerial;
|
||||||
if (size == 0) return;
|
if (size == 0) return;
|
||||||
|
m_hasDefinedContent = true;
|
||||||
if (g_bufferBackendOps && g_bufferBackendOps->SubData) {
|
if (g_bufferBackendOps && g_bufferBackendOps->SubData) {
|
||||||
g_bufferBackendOps->SubData(*this, offset, size);
|
g_bufferBackendOps->SubData(*this, offset, size);
|
||||||
}
|
}
|
||||||
@@ -49,12 +58,14 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
void BufferObject::NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess) {
|
void BufferObject::NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess) {
|
||||||
++m_changeSerial;
|
++m_changeSerial;
|
||||||
if (range.start >= range.end) return;
|
if (range.start >= range.end) return;
|
||||||
|
m_hasDefinedContent = true;
|
||||||
if (g_bufferBackendOps && g_bufferBackendOps->FlushMappedRange) {
|
if (g_bufferBackendOps && g_bufferBackendOps->FlushMappedRange) {
|
||||||
g_bufferBackendOps->FlushMappedRange(*this, range, appAccess);
|
g_bufferBackendOps->FlushMappedRange(*this, range, appAccess);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BufferObject::NotifyContentWrite(SizeT offset, SizeT size) {
|
void BufferObject::NotifyContentWrite(SizeT offset, SizeT size) {
|
||||||
|
m_hasDefinedContent = true;
|
||||||
if (m_resource.IsGpuResident()) {
|
if (m_resource.IsGpuResident()) {
|
||||||
// The write already landed in coherent GPU memory; the backend has no separate
|
// The write already landed in coherent GPU memory; the backend has no separate
|
||||||
// copy to sync. Only bump the serial so cached transient slices invalidate.
|
// copy to sync. Only bump the serial so cached transient slices invalidate.
|
||||||
@@ -71,6 +82,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
if (data && size > 0) {
|
if (data && size > 0) {
|
||||||
Memcpy(m_resource.Bytes(), data, size);
|
Memcpy(m_resource.Bytes(), data, size);
|
||||||
}
|
}
|
||||||
|
// A NULL-data respecify (the orphaning idiom) leaves the store undefined;
|
||||||
|
// record that so backends skip uploading the stale shadow bytes.
|
||||||
|
m_hasDefinedContent = (data != nullptr) || size == 0;
|
||||||
m_isImmutableStorage = false;
|
m_isImmutableStorage = false;
|
||||||
m_storageFlags = 0;
|
m_storageFlags = 0;
|
||||||
NotifyRespecify();
|
NotifyRespecify();
|
||||||
@@ -89,6 +103,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
} else if (size > 0) {
|
} else if (size > 0) {
|
||||||
Memset(m_resource.Bytes(), 0, size);
|
Memset(m_resource.Bytes(), 0, size);
|
||||||
}
|
}
|
||||||
|
m_hasDefinedContent = true;
|
||||||
m_isImmutableStorage = true;
|
m_isImmutableStorage = true;
|
||||||
m_storageFlags = storageFlags;
|
m_storageFlags = storageFlags;
|
||||||
NotifyRespecify();
|
NotifyRespecify();
|
||||||
@@ -174,9 +189,28 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
++m_changeSerial;
|
++m_changeSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BufferObject::MarkGpuWritten() {
|
||||||
|
m_hasDefinedContent = true;
|
||||||
|
m_gpuWritePending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BufferObject::SyncGpuWrites() {
|
||||||
|
if (!m_gpuWritePending) return;
|
||||||
|
// Cleared unconditionally: without a readback op the shadow can never catch up,
|
||||||
|
// and retrying on every subsequent read would only repeat the same no-op.
|
||||||
|
m_gpuWritePending = false;
|
||||||
|
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->ReadbackFromGpu == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_bufferBackendOps->ReadbackFromGpu(*this);
|
||||||
|
}
|
||||||
|
|
||||||
void BufferObject::UploadSubData(DataPtr data, SizeT atOffset) {
|
void BufferObject::UploadSubData(DataPtr data, SizeT atOffset) {
|
||||||
MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent),
|
// GL 4.6 core 6.5 forbids only the OVERLAPPING write: a glBufferSubData that stays
|
||||||
"Cannot upload sub data while buffer is non-persistently mapped.");
|
// clear of a non-persistent mapping is legal, and the frontend lets it through.
|
||||||
|
MOBILEGL_ASSERT(!m_isMapped || (m_mappingAccess & BufferMappingAccessBit::Persistent) ||
|
||||||
|
atOffset >= m_mappedRange.end || atOffset + data.size <= m_mappedRange.start,
|
||||||
|
"Cannot upload sub data overlapping a non-persistent mapping.");
|
||||||
MOBILEGL_ASSERT(atOffset + data.size <= m_size,
|
MOBILEGL_ASSERT(atOffset + data.size <= m_size,
|
||||||
"UploadSubData out of bounds: atOffset (%zu) + data.size (%zu) > m_size (%zu)", atOffset,
|
"UploadSubData out of bounds: atOffset (%zu) + data.size (%zu) > m_size (%zu)", atOffset,
|
||||||
data.size, m_size);
|
data.size, m_size);
|
||||||
@@ -204,11 +238,13 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
"Destination buffer copy out of bounds: dstOffset (%zu) + size (%zu) > m_size (%zu)", dstOffset,
|
"Destination buffer copy out of bounds: dstOffset (%zu) + size (%zu) > m_size (%zu)", dstOffset,
|
||||||
size, m_size);
|
size, m_size);
|
||||||
|
|
||||||
|
src->SyncGpuWrites();
|
||||||
Memcpy(m_resource.Bytes() + dstOffset, src->m_resource.Bytes() + srcOffset, size);
|
Memcpy(m_resource.Bytes() + dstOffset, src->m_resource.Bytes() + srcOffset, size);
|
||||||
NotifyContentWrite(dstOffset, size);
|
NotifyContentWrite(dstOffset, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
void* BufferObject::AcquireMemory(Bool markMapped, Bool read, Bool write) {
|
void* BufferObject::AcquireMemory(Bool markMapped, Bool read, Bool write) {
|
||||||
|
SyncGpuWrites();
|
||||||
if (markMapped) {
|
if (markMapped) {
|
||||||
m_isMapped = true;
|
m_isMapped = true;
|
||||||
m_mappingAccess = (read ? BufferMappingAccessBit::Read : BufferMappingAccessBit::Null) |
|
m_mappingAccess = (read ? BufferMappingAccessBit::Read : BufferMappingAccessBit::Null) |
|
||||||
@@ -250,6 +286,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
MOBILEGL_ASSERT(range.end <= m_size && range.start <= range.end,
|
MOBILEGL_ASSERT(range.end <= m_size && range.start <= range.end,
|
||||||
"AcquireMemoryRange out of bounds: range (%zu, %zu) exceeds m_size (%zu)", range.start,
|
"AcquireMemoryRange out of bounds: range (%zu, %zu) exceeds m_size (%zu)", range.start,
|
||||||
range.end, m_size);
|
range.end, m_size);
|
||||||
|
// The app is about to look at the bytes; a shader may have rewritten them since
|
||||||
|
// the shadow was last authoritative. Also needed for a write map without an
|
||||||
|
// invalidate bit, whose staging copy is seeded from the shadow.
|
||||||
|
SyncGpuWrites();
|
||||||
m_isMapped = true;
|
m_isMapped = true;
|
||||||
m_mappingAccess = access;
|
m_mappingAccess = access;
|
||||||
m_mappedRange = range;
|
m_mappedRange = range;
|
||||||
@@ -311,6 +351,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return m_changeSerial;
|
return m_changeSerial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bool BufferObject::HasDefinedContent() const {
|
||||||
|
return m_hasDefinedContent;
|
||||||
|
}
|
||||||
|
|
||||||
const SharedPtr<BackendBufferResource>& BufferObject::GetBackendResource() const {
|
const SharedPtr<BackendBufferResource>& BufferObject::GetBackendResource() const {
|
||||||
return m_resource.Backend();
|
return m_resource.Backend();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,13 @@ namespace MobileGL {
|
|||||||
// Must be idempotent: a second call for an already-backed buffer returns the
|
// Must be idempotent: a second call for an already-backed buffer returns the
|
||||||
// same base pointer.
|
// same base pointer.
|
||||||
void* (*AcquirePersistentMap)(BufferObject& bufferObject) = nullptr;
|
void* (*AcquirePersistentMap)(BufferObject& bufferObject) = nullptr;
|
||||||
|
// Pulls the backend's current contents for the whole buffer into the shadow
|
||||||
|
// (through WritebackFromBackend). Only ever called for a buffer the GPU may
|
||||||
|
// have written behind the frontend's back - a shader storage or atomic counter
|
||||||
|
// binding of a draw or dispatch - because nothing else can desynchronise the
|
||||||
|
// shadow. Backends that cannot read their storage back leave this null; the
|
||||||
|
// shadow then keeps its pre-dispatch bytes, which is the old behaviour.
|
||||||
|
void (*ReadbackFromGpu)(BufferObject& bufferObject) = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Registered by the active backend at init, cleared at shutdown.
|
// Registered by the active backend at init, cleared at shutdown.
|
||||||
@@ -149,6 +156,16 @@ namespace MobileGL {
|
|||||||
// backend op: the backend storage already holds these bytes.
|
// backend op: the backend storage already holds these bytes.
|
||||||
void WritebackFromBackend(DataPtr data, SizeT atOffset);
|
void WritebackFromBackend(DataPtr data, SizeT atOffset);
|
||||||
|
|
||||||
|
// A draw or dispatch just ran with this buffer bound where a shader can write
|
||||||
|
// it (shader storage / atomic counter). The next read has to reconcile with
|
||||||
|
// that: pull the bytes back, or - when the shadow already IS coherent GPU
|
||||||
|
// memory - wait for the work that wrote them to retire. Which of the two is
|
||||||
|
// the backend's business; the flag only says a GPU write is outstanding.
|
||||||
|
void MarkGpuWritten();
|
||||||
|
// Refreshes the shadow from the backend when a GPU write is outstanding. Called
|
||||||
|
// from every path that reads the shadow on the app's behalf.
|
||||||
|
void SyncGpuWrites();
|
||||||
|
|
||||||
Bool IsMapped() const;
|
Bool IsMapped() const;
|
||||||
Bool IsImmutableStorage() const;
|
Bool IsImmutableStorage() const;
|
||||||
SizeT GetSize() const;
|
SizeT GetSize() const;
|
||||||
@@ -168,9 +185,21 @@ namespace MobileGL {
|
|||||||
Flags<BufferMappingAccessBit> GetMappingAccess() const;
|
Flags<BufferMappingAccessBit> GetMappingAccess() const;
|
||||||
GLbitfield GetStorageFlags() const;
|
GLbitfield GetStorageFlags() const;
|
||||||
Uint GetExternalIndex() const;
|
Uint GetExternalIndex() const;
|
||||||
|
// Globally-unique, never-reused id for THIS object's lifetime - same contract
|
||||||
|
// and same motivation as ProgramObject::GetLifetimeId() and
|
||||||
|
// VertexArrayObject::GetLifetimeId(). A backend that folds a buffer's IDENTITY
|
||||||
|
// into a cache key must use this, never the GL name (LIFO-recycled by
|
||||||
|
// glGenBuffers) and never the heap address (recycled by the allocator): both
|
||||||
|
// let a deleted-and-recreated buffer answer to a dead one's cache entry.
|
||||||
|
Uint64 GetLifetimeId() const { return m_lifetimeId; }
|
||||||
// Monotonic counter bumped on every shadow mutation; backends use it to
|
// Monotonic counter bumped on every shadow mutation; backends use it to
|
||||||
// validate cached transient slices.
|
// validate cached transient slices.
|
||||||
Uint64 GetChangeSerial() const;
|
Uint64 GetChangeSerial() const;
|
||||||
|
// False after a NULL-data (re)specification until the first content
|
||||||
|
// write: the app's orphaning idiom (glBufferData with nullptr) leaves
|
||||||
|
// the store undefined, so backends may (re)allocate GPU storage without
|
||||||
|
// uploading the stale CPU shadow.
|
||||||
|
Bool HasDefinedContent() const;
|
||||||
|
|
||||||
const SharedPtr<BackendBufferResource>& GetBackendResource() const;
|
const SharedPtr<BackendBufferResource>& GetBackendResource() const;
|
||||||
void SetBackendResource(SharedPtr<BackendBufferResource> resource);
|
void SetBackendResource(SharedPtr<BackendBufferResource> resource);
|
||||||
@@ -185,7 +214,10 @@ namespace MobileGL {
|
|||||||
// SubData transfer to sync the backend's separate GPU copy.
|
// SubData transfer to sync the backend's separate GPU copy.
|
||||||
void NotifyContentWrite(SizeT offset, SizeT size);
|
void NotifyContentWrite(SizeT offset, SizeT size);
|
||||||
|
|
||||||
|
static Uint64 AllocateLifetimeId();
|
||||||
|
|
||||||
const Uint m_externalIndex = 0;
|
const Uint m_externalIndex = 0;
|
||||||
|
const Uint64 m_lifetimeId = AllocateLifetimeId();
|
||||||
SizeT m_size = 0;
|
SizeT m_size = 0;
|
||||||
BufferUsage m_usage = BufferUsage::StaticDraw;
|
BufferUsage m_usage = BufferUsage::StaticDraw;
|
||||||
// Owns the buffer's bytes (CPU shadow or backend persistent GPU map) and
|
// Owns the buffer's bytes (CPU shadow or backend persistent GPU map) and
|
||||||
@@ -196,6 +228,10 @@ namespace MobileGL {
|
|||||||
Bool m_isImmutableStorage = false;
|
Bool m_isImmutableStorage = false;
|
||||||
GLbitfield m_storageFlags = 0;
|
GLbitfield m_storageFlags = 0;
|
||||||
Uint64 m_changeSerial = 0;
|
Uint64 m_changeSerial = 0;
|
||||||
|
// See HasDefinedContent().
|
||||||
|
Bool m_hasDefinedContent = true;
|
||||||
|
// Set by MarkGpuWritten, cleared by SyncGpuWrites once the shadow is refreshed.
|
||||||
|
Bool m_gpuWritePending = false;
|
||||||
Range1D m_mappedRange;
|
Range1D m_mappedRange;
|
||||||
Vector<Uint8> m_stagingData;
|
Vector<Uint8> m_stagingData;
|
||||||
Bool m_ownsStagingData;
|
Bool m_ownsStagingData;
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
BindingSlot<BufferObject>& GetBindingSlot(BufferTarget target);
|
BindingSlot<BufferObject>& GetBindingSlot(BufferTarget target);
|
||||||
// For glBindBufferBase / glBindBufferRange
|
// For glBindBufferBase / glBindBufferRange
|
||||||
BindingSlotRange1D<BufferObject>& GetBindingPoint(BufferTarget target, Uint index);
|
BindingSlotRange1D<BufferObject>& GetBindingPoint(BufferTarget target, Uint index);
|
||||||
|
const BindingSlotRange1D<BufferObject>& GetBindingPoint(BufferTarget target, Uint index) const {
|
||||||
|
return const_cast<BufferState*>(this)->GetBindingPoint(target, index);
|
||||||
|
}
|
||||||
constexpr SizeT GetBindingPointCount(const BufferTarget target) const {
|
constexpr SizeT GetBindingPointCount(const BufferTarget target) const {
|
||||||
auto it = std::find(BufferBindPointTargets.begin(), BufferBindPointTargets.end(), target);
|
auto it = std::find(BufferBindPointTargets.begin(), BufferBindPointTargets.end(), target);
|
||||||
auto index = std::distance(BufferBindPointTargets.begin(), it);
|
auto index = std::distance(BufferBindPointTargets.begin(), it);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user